Dekopon
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
XyzRepositoryfrom non-genericRepositoryBasewithout entities.
Transaction and Connection lifecycle
There’re 3 TransactionManager provided.
DtcTransactionManagersimply wrapsTransactionScope, has to be singleton and will affect all resources;TransactionManagerusesThreadLocalto hold transaction info, multiple instances can be created with different resources;FlowableTransactionManagerusesAsyncLocalto 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
DatabaseManagerper 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
AddAllandUpdateAllaccepts chunk size as a parameter, suggested value is2100 / fieldCountInEntityT. - If you need any wrap over connections, for example working with
ProfiledDbConnectionfromMiniProfiler, simply deriveDatabaseManagerorSqlConnectionManager. - Fell free to implement
IEntityQueryBuilderfor other databases, any PR are welcome. - See
Dekopon.Samplesfor full example.
Entities and Queries
Attributes on entities
[TableAttribute]marks entities and suggests database table name;[KeyAttribute]marks key columns which will be used inFind,UpdateandDeletemethods;[KeyAttribute].IsIdentityindicates the column is identity and will be updated afterInsert(notInsertAll), one entity can only has oneIsIdentity=trueproperty and its type must belong;[GeneratedAttribute]marks the key is ignored fromInsert;[WhereAttribute]affectsFindmethods;[DeleteAttribute].Setindicates the entity will be UPDATEd with theSetclause rather than hard DELETE.[ConvertAttribute]indicates when the column is also marked as[Key], the[ConvertAttribute].Patternwill 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];