Dekopon is a repository-style data access layer implementation, supports handy transaction management and batch insert/update.
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.
Install-Package Dekopon.Repository
[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; }
}
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-genericRepositoryBase
without entities.
There're 3 TransactionManager
provided.
DtcTransactionManager
simply wrapsTransactionScope
, has to be singleton and will affect all resources;TransactionManager
usesThreadLocal
to hold transaction info, multiple instances can be created with different resources;FlowableTransactionManager
usesAsyncLocal
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();
}
}
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();
}
}
}
- Sql Server limit 2100 parameters in one command, so batch methods like
AddAll
andUpdateAll
accepts chunk size as a parameter, suggested value is2100 / fieldCountInEntityT
. - If you need any wrap over connections, for example working with
ProfiledDbConnection
fromMiniProfiler
, simply deriveDatabaseManager
orSqlConnectionManager
. - Fell free to implement
IEntityQueryBuilder
for other databases, any PR are welcome. - See
Dekopon.Samples
for full example.
[TableAttribute]
marks entities and suggests database table name;[KeyAttribute]
marks key columns which will be used inFind
,Update
andDelete
methods;[KeyAttribute].IsIdentity
indicates the column is identity and will be updated afterInsert
(notInsertAll
), one entity can only has oneIsIdentity=true
property and its type must belong
;[GeneratedAttribute]
marks the key is ignored fromInsert
;[WhereAttribute]
affectsFind
methods;[DeleteAttribute].Set
indicates the entity will be UPDATEd with theSet
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.
[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:
select [Users].[Id], [Users].[Username], [Users].[Deleted], [Users].[CreateTime] from [Users] where deleted = 0;
select [Users].[Id], [Users].[Username], [Users].[Deleted], [Users].[CreateTime] from [Users] join (values (@Id_0)) as data([Id]) on [Users].[Id] = data.[Id];
select [Users].[Id], [Users].[Username], [Users].[Deleted], [Users].[CreateTime] from [Users] where deleted = 0 and [Id] = @Id;
insert into [Users] ([Username], [Deleted], [CreateTime]) values (@Username, @Deleted, @CreateTime);
select convert(bigint, scope_identity()) as [identity];
insert into [Users] ([Username], [Deleted], [CreateTime]) values (@Username_0, @Deleted_0, @CreateTime_0);
update [Users] set [Username] = @Username, [Deleted] = @Deleted, [CreateTime] = @CreateTime where [Id] = @Id;
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];
update [Users] set deleted = 1 where [Id] = @Id;
update [Users] set deleted = 1 join (values (@Id_0)) as data([Id]) on [Users].[Id] = data.[Id];
- Write usages
- Write tests (more tests needed)
- Publish to NuGet (pre-release ver)