Dekopon

NuGet NuGet

License MIT

GitHub stars GitHub forks GitHub watchers

Dekopon is a repository-style data access layer implementation, supports handy transaction management and batch insert/update.

Basics

Dekopon currently only targets netstandard2.0, and only SqlServerEntityQueryBuilder is supported for now.

IEntityQueryBuilder generates queries for:

    (string, IDictionary<string, object>) FindAll(EntityDefinition entityDefinition);
    (string, IDictionary<string, object>) Find(EntityDefinition entityDefinition, object entity);
    (string, IDictionary<string, object>) FindAll(EntityDefinition entityDefinition, IEnumerable entities);
    (string, IDictionary<string, object>) Insert(EntityDefinition entityDefinition, object entity);
    (string, IDictionary<string, object>) InsertAll(EntityDefinition entityDefinition, IEnumerable entities);
    (string, IDictionary<string, object>) Update(EntityDefinition entityDefinition, object entity);
    (string, IDictionary<string, object>) UpdateAll(EntityDefinition entityDefinition, IEnumerable entities);
    (string, IDictionary<string, object>) Delete(EntityDefinition entityDefinition, object entity);
    (string, IDictionary<string, object>) DeleteAll(EntityDefinition entityDefinition, IEnumerable entities);

The return tuple contains query and parameters, which can be passed to Dapper query and execution methods directly.

Usages

NuGet

Install-Package Dekopon.Repository

Entity

    [Table("Users")]
    public class UserEntity
    {
        [Key(IsIdentity = true), Generated]
        public long Id { get; set; }
        public string Username { get; set; }
        //...
        public int Deleted { get; set; }
        public DateTimeOffset CreateTime { get; set; }
    }

Repository

    public class UserRepository : CrudRepositoryBase<UserEntity>, IUserRepository
    {
        public UserRepository(IDatabaseManager databaseManager) : base(databaseManager)
        {
        }

        // you can add other queries below
        public long CountAll()
        {
            return Conn.ExecuteScalar<long>($"select count(0) from {TableName} where deleted = 0");
        }
    }

    using (var dbManager = new DatabaseManager(new DbContextOptionsBuilder()
        .UseSqlServer(connectionString)
        .Options, entityQueryBuilder: new SqlServerEntityQueryBuilder()))
    {
        var userRepository = new UserRepository(dbManager);
        userRepository.Add(new UserEntity());
    }

ICrudRepository<T> contains below methods:

    public interface ICrudRepository<T> : IRepository<T>
    {
        IList<T> FindAll(IList<T> entities);
        T Get(T entity);

        long Add(T entity);
        int AddAll(IList<T> entities, int chunk = 100);

        int Update(T entity);
        int UpdateAll(IList<T> entities, int chunk = 100);

        int Delete(T entity);
        int DeleteAll(IList<T> entities);

        IList<T> FindByIdIn(IList<long> ids);
        T GetById(long id);
        int DeleteById(long id);
        int DeleteByIdIn(IList<long> ids);
    }

You can create XyzRepository from non-generic RepositoryBase without entities.

Transaction and Connection lifecycle

There’re 3 TransactionManager provided.

DtcTransactionManager and FlowableTransactionManager are suggested to use.

When RepositoryBase.Conn or IDatabaseManager.GetConnection() is invoked, Dekopon will check if any transaction info held in context:

You can create nested txSupport with different isolation and propagation parameters. See TransactionAwareResourceManager for details.

So, the order of new UserRepository() and txManager.Begin() doesn’t matter, you can define your custom [TransactionalAttribute] with AOP frameworks.

    using (var txManager = new FlowableTransactionManager())
    using (var dbManager = new DatabaseManager(new DbContextOptionsBuilder()
        .UseSqlServer(connectionString)
        .Options, txManager, entityQueryBuilder: new SqlServerEntityQueryBuilder()))
    {
        var userRepository = new UserRepository(dbManager);
        using (var txSupport = txManager.Begin())
        {
            Console.WriteLine(userRepository.CountAll());

            txSupport.Complete();
        }
    }

IoC

If you use IoC containers like Microsoft.Extensions.DependencyInjection or Autofac, here’s the best practise:

    // Autofac
    builder.RegisterType<FlowableTransactionManager>().AsSelf().AsImplementedInterfaces().SingleInstance();
    builder.Register(c => new DatabaseManager(new DbContextOptionsBuilder()
            .UseSqlServer(ConnectionString)
            .Options, c.Resolve<ITransactionManager>(), c.Resolve<SqlServerEntityQueryBuilder>()
    )).AsSelf().AsImplementedInterfaces().InstancePerLifetimeScope();

    builder.RegisterType<SqlServerEntityQueryBuilder>().AsSelf().AsImplementedInterfaces().InstancePerDependency();
    builder.RegisterType<UserRepository>().AsSelf().AsImplementedInterfaces().InstancePerDependency();

    builder.RegisterType<UserService>().AsSelf().AsImplementedInterfaces().InstancePerDependency();

    //
    public class UserService : IUserService
    {
        private readonly IUserRepository _userRepository;
        private readonly ITransactionManager _txManager;

        public UserService(IUserRepository userRepository, ITransactionManager txManager)
        {
            _userRepository = userRepository;
            _txManager = txManager;
        }

        public void CreateUser()
        {
            using (var txSupport = _txManager.Begin(TransactionScopeOption.Required, IsolationLevel.ReadCommitted))
            {
                userRepository.Add(new UserEntity { });

                txSupport.Complete();
            }
        }
    }

Suggestions

Entities and Queries

Attributes on entities

Queries

    [Table("Users")]
    [Where(Clause = "deleted = 0")]
    [Delete(Set = "deleted = 1")]
    public class UserEntity
    {
        [Key(IsIdentity = true), Generated]
        public long Id { get; set; }
        
        public string Username { get; set; }
        
        public int Deleted { get; set; }
        
        //[Column("CreateTime"), Convert("TODATETIMEOFFSET({0}, DATEPART(tz, SYSDATETIMEOFFSET()))")]
        public DateTimeOffset CreateTime { get; set; }
    }

For UserEntity defined above, all queries are generated as:

SqlServerEntityQueryBuilder

FindAll
select [Users].[Id], [Users].[Username], [Users].[Deleted], [Users].[CreateTime] from [Users] where deleted = 0;
FindAll
select [Users].[Id], [Users].[Username], [Users].[Deleted], [Users].[CreateTime] from [Users] join (values (@Id_0)) as data([Id]) on [Users].[Id] = data.[Id];
Get
select [Users].[Id], [Users].[Username], [Users].[Deleted], [Users].[CreateTime] from [Users] where deleted = 0 and [Id] = @Id;
Add
insert into [Users] ([Username], [Deleted], [CreateTime]) values (@Username, @Deleted, @CreateTime);
select convert(bigint, scope_identity()) as [identity];
AddAll
insert into [Users] ([Username], [Deleted], [CreateTime]) values (@Username_0, @Deleted_0, @CreateTime_0);
Update
update [Users] set [Username] = @Username, [Deleted] = @Deleted, [CreateTime] = @CreateTime where [Id] = @Id;
UpdateAll
update [Users] set [Username] = data.[Username], [Deleted] = data.[Deleted], [CreateTime] = data.[CreateTime] from [Users] join (values (@Id_0, @Username_0, @Deleted_0, @CreateTime_0)) as data([Id], [Username], [Deleted], [CreateTime]) on [Users].[Id] = data.[Id];
Delete
update [Users] set deleted = 1 where [Id] = @Id;
DeleteAll
update [Users] set deleted = 1 join (values (@Id_0)) as data([Id]) on [Users].[Id] = data.[Id];