xkinepa / Dekopon

REPOSITORY-style data access layer implementation.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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.

Full docs

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 (Breaking changed)

There're 3 TransactionManager provided.

  • DtcTransactionManager simply wraps TransactionScope, has to be singleton and will affect all resources;
  • TransactionManager uses ThreadLocal to hold transaction info, multiple instances can be created with different resources;
  • FlowableTransactionManager uses AsyncLocal to hold transaction info, so async methods are allowed to use within your code;

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:

  • If no transaction exists, a new connection will be opened/reused and live along with the IDatabaseManager;
  • If transaction exists, a new connection will be opened/reused and live along with the txSupport.

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:

  • Create a shared FlowableTransactionManager;
  • Create each DatabaseManager per http request and let it disposed at the end of the request;
  • Create repositories and acquire transactions per usage, in your business logic layer.
    // 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

  • Sql Server limit 2100 parameters in one command, so batch methods like AddAll and UpdateAll accepts chunk size as a parameter, suggested value is 2100 / fieldCountInEntityT.
  • If you need any wrap over connections, for example working with ProfiledDbConnection from MiniProfiler, simply derive DatabaseManager or SqlConnectionManager.
  • Fell free to implement IEntityQueryBuilder for other databases, any PR are welcome.
  • See Dekopon.Samples for full example.

Entities and Queries

Attributes on entities

  • [TableAttribute] marks entities and suggests database table name;
  • [KeyAttribute] marks key columns which will be used in Find, Update and Delete methods;
  • [KeyAttribute].IsIdentity indicates the column is identity and will be updated after Insert (not InsertAll), one entity can only has one IsIdentity=true property and its type must be long;
  • [GeneratedAttribute] marks the key is ignored from Insert;
  • [WhereAttribute] affects Find methods;
  • [DeleteAttribute].Set indicates the entity will be UPDATEd with the Set clause rather than hard DELETE.
  • [ConvertAttribute] indicates when the column is also marked as [Key], the [ConvertAttribute].Pattern will be applied to this column when join and compare, which is useful when the database column is ntext (cannot be used to =) or datetime (loses timezone info hence cannot compare to DateTimeOffset) or in other circumstances. Be aware this will invalidate database indices.

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];

TODO

  • Write usages
  • Write tests (more tests needed)
  • Publish to NuGet (pre-release ver)

About

REPOSITORY-style data access layer implementation.

License:MIT License


Languages

Language:C# 100.0%