如何使用 Dapper 实现工作单元模式?

目前,我正在尝试使用 Dapper ORM 和 Unit Of Work + Repository 模式。

我想使用 Unit of Work 而不是简单的 dapper Repository,因为插入和更新需要一定程度的事务处理。我无法找到任何有用的例子,因为大多数似乎都在使用实体框架,而且工作组内部存在泄漏问题。

有人能给我指一下正确的方向吗?

71938 次浏览

这个 Git 项目是非常有帮助的。我从相同的开始,并根据我的需要做了一些改变。

public sealed class DalSession : IDisposable
{
public DalSession()
{
_connection = new OleDbConnection(DalCommon.ConnectionString);
_connection.Open();
_unitOfWork = new UnitOfWork(_connection);
}


IDbConnection _connection = null;
UnitOfWork _unitOfWork = null;


public UnitOfWork UnitOfWork
{
get { return _unitOfWork; }
}


public void Dispose()
{
_unitOfWork.Dispose();
_connection.Dispose();
}
}


public sealed class UnitOfWork : IUnitOfWork
{
internal UnitOfWork(IDbConnection connection)
{
_id = Guid.NewGuid();
_connection = connection;
}


IDbConnection _connection = null;
IDbTransaction _transaction = null;
Guid _id = Guid.Empty;


IDbConnection IUnitOfWork.Connection
{
get { return _connection; }
}
IDbTransaction IUnitOfWork.Transaction
{
get { return _transaction; }
}
Guid IUnitOfWork.Id
{
get { return _id; }
}


public void Begin()
{
_transaction = _connection.BeginTransaction();
}


public void Commit()
{
_transaction.Commit();
Dispose();
}


public void Rollback()
{
_transaction.Rollback();
Dispose();
}


public void Dispose()
{
if(_transaction != null)
_transaction.Dispose();
_transaction = null;
}
}


interface IUnitOfWork : IDisposable
{
Guid Id { get; }
IDbConnection Connection { get; }
IDbTransaction Transaction { get; }
void Begin();
void Commit();
void Rollback();
}

现在,您的存储库应该以某种方式接受这个 unitOfWork,我选择用 structor 依赖注入。

public sealed class MyRepository
{
public MyRepository(IUnitOfWork unitOfWork)
{
this.unitOfWork = unitOfWork;
}


IUnitOfWork unitOfWork = null;


//You also need to handle other parameters like 'sql', 'param' ect. This is out of scope of this answer.
public MyPoco Get()
{
return unitOfWork.Connection.Query(sql, param, unitOfWork.Transaction, .......);
}


public void Insert(MyPoco poco)
{
return unitOfWork.Connection.Execute(sql, param, unitOfWork.Transaction, .........);
}
}

然后你这样说:

交易:

using(DalSession dalSession = new DalSession())
{
UnitOfWork unitOfWork = dalSession.UnitOfWork;
unitOfWork.Begin();
try
{
//Your database code here
MyRepository myRepository = new MyRepository(unitOfWork);
myRepository.Insert(myPoco);
//You may create other repositories in similar way in same scope of UoW.


unitOfWork.Commit();
}
catch
{
unitOfWork.Rollback();
throw;
}
}

无交易:

using(DalSession dalSession = new DalSession())
{
//Your database code here
MyRepository myRepository = new MyRepository(dalSession.UnitOfWork);//UoW have no effect here as Begin() is not called.
myRepository.Insert(myPoco);
}

请注意,UnitOfWork 是 更多而不是 DBTransaction。

在上面的代码中可以找到更多关于 Repository 的详细信息 给你

我已经发布了这个代码 给你。但是这个问题看起来更适合我的这个代码,所以我再次张贴,而不是只是链接到原来的答案。

编辑2018-08-03: Amit 的评论真的让我思考,并且让我意识到存储库实际上并不需要是上下文本身的属性。相反,存储库可能依赖于上下文。而不是继续对下面的代码示例进行增量更改。我将简单地引用一个我已经放在一起的 回收来包含这个概念。

站在别人的肩膀上。

考虑到这个答案在大多数关于“整洁”和“工作单元”的谷歌搜索中位居榜首。我想提供我的方法,我已经使用了很多次,效果很好。

用一个虚构的(过于简化的)例子:

public interface IUnitOfWorkFactory
{
UnitOfWork Create();
}


public interface IDbContext
{
IProductRepository Product { get; set; }


void Commit();
void Rollback();
}


public interface IUnitOfWork
{
IDbTransaction Transaction { get;set; }


void Commit();
void Rollback();
}




public interface IProductRepository
{
Product Read(int id);
}

请注意,IDbContextIUnitOfWorkFactory都没有实现 IDisposable。这样做是有目的的,以避免 抽象漏洞定律。取而代之的是依靠 Commit()/Rollback()来进行清理和处置。

共享实现之前的几点。

  • IUnitOfWorkFactory负责实例化 UnitOfWork并代理数据库连接。
  • IDbContext是存储库的主干。
  • IUnitOfWorkIDbTransaction的封装,确保在使用多个存储库时,它们共享单个数据库上下文。

IUnitOfWorkFactory的实施

public class UnitOfWorkFactory<TConnection> : IUnitOfWorkFactory where TConnection : IDbConnection, new()
{
private string connectionString;


public UnitOfWorkFactory(string connectionString)
{
if (string.IsNullOrWhiteSpace(connectionString))
{
throw new ArgumentNullException("connectionString cannot be null");
}


this.connectionString = connectionString;
}


public UnitOfWork Create()
{
return new UnitOfWork(CreateOpenConnection());
}


private IDbConnection CreateOpenConnection()
{
var conn = new TConnection();
conn.ConnectionString = connectionString;


try
{
if (conn.State != ConnectionState.Open)
{
conn.Open();
}
}
catch (Exception exception)
{
throw new Exception("An error occured while connecting to the database. See innerException for details.", exception);
}


return conn;
}
}

IDbContext的实施

public class DbContext : IDbContext
{
private IUnitOfWorkFactory unitOfWorkFactory;


private UnitOfWork unitOfWork;


private IProductRepository product;


public DbContext(IUnitOfWorkFactory unitOfWorkFactory)
{
this.unitOfWorkFactory = unitOfWorkFactory;
}


public ProductRepository Product =>
product ?? (product = new ProductRepository(UnitOfWork));


protected UnitOfWork UnitOfWork =>
unitOfWork ?? (unitOfWork = unitOfWorkFactory.Create());


public void Commit()
{
try
{
UnitOfWork.Commit();
}
finally
{
Reset();
}
}


public void Rollback()
{
try
{
UnitOfWork.Rollback();
}
finally
{
Reset();
}
}


private void Reset()
{
unitOfWork = null;
product = null;
}
}

IUnitOfWork的实施

public class UnitOfWork : IUnitOfWork
{
private IDbTransaction transaction;


public UnitOfWork(IDbConnection connection)
{
transaction = connection.BeginTransaction();
}


public IDbTransaction Transaction =>
transaction;


public void Commit()
{
try
{
transaction.Commit();
transaction.Connection?.Close();
}
catch
{
transaction.Rollback();
throw;
}
finally
{
transaction?.Dispose();
transaction.Connection?.Dispose();
transaction = null;
}
}


public void Rollback()
{
try
{
transaction.Rollback();
transaction.Connection?.Close();
}
catch
{
throw;
}
finally
{
transaction?.Dispose();
transaction.Connection?.Dispose();
transaction = null;
}
}
}

IProductRepository的实施

public class ProductRepository : IProductRepository
{
protected readonly IDbConnection connection;
protected readonly IDbTransaction transaction;


public ProductRepository(UnitOfWork unitOfWork)
{
connection = unitOfWork.Transaction.Connection;
transaction = unitOfWork.Transaction;
}


public Product Read(int id)
{
return connection.QuerySingleOrDefault<Product>("select * from dbo.Product where Id = @id", new { id }, transaction: Transaction);
}
}

要访问数据库,只需使用您选择的 IoC 容器实例化 DbContext或注入(我个人使用 .NET 核心提供的 IoC 容器)。

var unitOfWorkFactory = new UnitOfWorkFactory<SqlConnection>("your connection string");
var db = new DbContext(unitOfWorkFactory);


Product product = null;


try
{
product = db.Product.Read(1);
db.Commit();
}
catch (SqlException ex)
{
//log exception
db.Rollback();
}

对于这种简单的只读操作,明确需要 Commit()似乎有些过分,但随着系统的增长,这种需求会得到回报。而且显然,根据 Sam Saffron提供了一个小的性能优势。您“可以”在简单的读操作中省略 db.Commit(),通过这样做,您可以保持连接处于打开状态,并将清理工作的责任交给垃圾收集器。所以不建议这么做。

我通常将 DbContext放在服务层,它与其他服务协同工作,形成“ ServiceContext”。然后在实际的 MVC 层中引用这个 ServiceContext。

另外值得一提的是,如果可以的话,建议在整个堆栈中使用 async。为了简单起见,这里省略了它。

我注意到在您的 github 回购中,您删除了 UnitOfWorkFactory,而是在访问 Connection 时实例化它

这种方法的问题是,我想不通的是。

设想下面的场景,如果我将 DBContext 注册为 Scoped,将 Repository 注册为瞬态

1. UserService CreateUserProfile
a. UserRepositoryGetByEmail("some@email.com")
b. UserRepository.Add(user)
c. AddressRepository.Add(new address)
2. UserService Commit?

在这种情况下,上面的(1.)都是一个事务,然后我想在(2.)中提交

对于使用相同范围的 dbcontext 实例的具有多个服务的大型业务层,我可以看到事务重叠

现在我可以将 dbcontext 设置为瞬态,但是 UnitOfWork 在每个注入上都是不同的,它不会工作。

好吧,距离 OP 提出这个问题已经过去了五年,但是当我使用 Dapper (或者其他任何东西)开发时,我不断遇到这个问题(或者其他任何东西,这并不是非常具体的 Dapper)。这是我的建议。

首先让我们谈谈其他的答案:

Pinbrouwer 的回答是: IDbContext管理工作单元的方式与实体框架非常相似。这完全合情合理,也很容易理解。但是主要的缺点是您最终将 IDbContext传递给所有业务代码。有点像上帝的物品。就像在 EF 里一样。我更喜欢注入单独的存储库,并明确我要做什么数据库的东西,而不是在我的领域模型中的所有东西总是只有一个 .。然而,如果你不同意我的“上帝对象”的反对意见,那么皮姆的回答听起来对你来说是正确的。

Amit Joshi 的回答 使得 MyRepository将工作单元作为构造函数参数。这意味着你不能再注入仓库了。可以通过注入存储库工厂来解决这个问题,但这肯定是它自己的麻烦级别。

插一句: 在其中一些答案中,“事务”和“工作单元”是可以互换使用的。实际上,它们是1:1的关系,但它们不是一回事。“事务”是数据库的实现,“工作单元”是更高层次的概念性事物。如果我们拥有比一个数据库更多的持久性,那么就会有所不同,UOW 将包含不止一个事务。因此,为了避免混淆,在我们的 UOW 界面中使用“事务”可能不是一个很好的词。

所以这是我的方法 :

我从用法开始

// Business code. I'm going to write a method, but a class with dependencies is more realistic
static async Task MyBusinessCode(IUnitOfWorkContext context, EntityRepoitory repo)
{
var expectedEntity = new Entity {Id = null, Value = 10};


using (var uow = context.Create())
{
expectedEntity.Id = await repo.CreateAsync(expectedEntity.Value);
await uow.CommitAsync();
}


using (context.Create())
{
var entity = await repo.GetOrDefaultAsync(expectedEntity.Id.Value);
entity.Should().NotBeNull();
entity.Value.Should().Be(expectedEntity.Value);
}
}

工作单元只包装一个事务,并且是短期的:

public class UnitOfWork : IDisposable
{


private readonly SQLiteTransaction _transaction;
public SQLiteConnection Connection { get; }


public bool IsDisposed { get; private set; } = false;


public UnitOfWork(SQLiteConnection connection)
{
Connection = connection;
_transaction = Connection.BeginTransaction();
}


public async Task RollBackAsync()
{
await _transaction.RollbackAsync();
}


public async Task CommitAsync()
{
await _transaction.CommitAsync();
}


public void Dispose()
{
_transaction?.Dispose();


IsDisposed = true;
}
}

背景更有趣。这是回购协议和工作单元在幕后交流的方式。

有一个接口用于业务代码管理工作单元,另一个接口用于回购遵守该工作单元。

public class UnitOfWorkContext : IUnitOfWorkContext, IConnectionContext
{
private readonly SQLiteConnection _connection;
private UnitOfWork _unitOfWork;


private bool IsUnitOfWorkOpen => !(_unitOfWork == null || _unitOfWork.IsDisposed);


public UnitOfWorkContext(SQLiteConnection connection)
{
_connection = connection;
}


public SQLiteConnection GetConnection()
{
if (!IsUnitOfWorkOpen)
{
throw new InvalidOperationException(
"There is not current unit of work from which to get a connection. Call BeginTransaction first");
}


return _unitOfWork.Connection;
}


public UnitOfWork Create()
{
if (IsUnitOfWorkOpen)
{
throw new InvalidOperationException(
"Cannot begin a transaction before the unit of work from the last one is disposed");
}


_unitOfWork = new UnitOfWork(_connection);
return _unitOfWork;
}
}


public interface IConnectionContext
{
SQLiteConnection GetConnection();
}


public interface IUnitOfWorkContext
{
UnitOfWork Create();
}

回购是这样做的:

public class EntityRepository
{
private readonly IConnectionContext _context;


public EntityRepository(IConnectionContext context)
{
_context = context;
}


public async Task<int> CreateAsync(int value)
{
return await _context.GetConnection().QuerySingleAsync<int>(
@"
insert into Entity (Value) values (@value);
select last_insert_rowid();
", new { value });
}


public async Task<Entity> GetOrDefaultAsync(int id)
{
return await _context.GetConnection().QuerySingleOrDefaultAsync<Entity>(
@"
select * from Entity where Id = @id
", new { id });
}
}

最后是督察。准备好。下面是一个单线程控制台应用示例。我认为将其设置为单例或每个请求是明智的。可以更改 UnitOfWorkContext 的实现以匹配您的线程选择(例如,使用带有线程静态 UOW 的 UnitOfWorkContext)。

public static void Main(string[] args)
{
using (var connection = new SQLiteConnection("Data Source=:memory:"))
{
connection.Open();
Setup(connection);
var context = new UnitOfWorkContextContext(connection);
var repo = new EntityRepository(context);


MyBusinessCode(repo, context).ConfigureAwait(false).GetAwaiter().GetResult();
}
}

Github 的完整版本: https://github.com/NathanLBCooper/unit-of-work-example

分析 :

我们已经消除了上帝对象,不需要为我们所有的存储库创建工厂。代价是我们在回购和工作单元之间有一点微妙的不明显的联系。虽然没有提前说明,但是我们确实需要注意我们给上下文对象的生存期,特别是在多线程时。

我觉得这是个值得的交易,但这就是我。

附言

我要补充一件事。也许你查找这个答案是因为你已经开始使用整洁。现在,您的所有存储库方法都是独立的原子操作,您觉得还不需要将它们组合成事务。那你暂时不需要做这些了。关闭此浏览器窗口,以最简单明了的方式编写您的存储库,并感到高兴。

没有必要为此提供手工卷制的解决方案。使用框架中已有的类可以非常简单地实现您想要的结果。

/// <summary>
/// Register a single instance using whatever DI system you like.
/// </summary>
class ConnectionFactory
{
private string _connectionString;


public ConnectionFactory(string connectionString)
{
_connectionString = connectionString;
}


public IDbConnection CreateConnection()
{
return new SqlConnection(_connectionString);
}
}




/// <summary>
/// Generally, in a properly normalized database, your repos wouldn't map to a single table,
/// but be an aggregate of data from several tables.
/// </summary>
class ProductRepo
{
private ConnectionFactory _connectionFactory;


public ProductRepo(ConnectionFactory connectionFactory)
{
_connectionFactory = connectionFactory;
}


public Product Get(int id)
{
// Allow connection pooling to worry about connection lifetime, that's its job.
using (var con = _connectionFactory.CreateConnection())
{
return con.Get<Product>(id);
}
}


// ...
}


class OrderRepo
{
// As above.
// ...
}


class ProductController : ControllerBase
{
private ProductRepo _productRepo;
private OrderRepo _orderRepo;


public ProductController(ProductRepo productRepo, OrderRepo orderRepo)
{
_productRepo = productRepo;
_orderRepo = orderRepo;
}


[HttpGet]
public Task<IAsyncResult> Get(int id)
{
// This establishes your transaction.
// Default isolation level is 'serializable' which is generally desirable and is configurable.
// Enable async flow option in case subordinate async code results in a thread continuation switch.
// If you don't need this transaction here, don't use it, or put it where it is needed.
using (var trn = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
Product product = _productRepo.Get(id);


// Use additional repositories and do something that actually requires an explicit transaction.
// A single SQL statement does not require a transaction on SQL Server due to default autocommit mode.
// ...


return Ok(product);
}
}
}

我在 Dapper 之上创建了一个简单的工作实现单元,其中考虑了一些基本的 CQS。https://github.com/giangcoi48k/Dapper.CQS.请看看它是否适用于您的项目。

使用 IUnitOfWork在该查询或命令中执行相应的 QueryCommand、定义的 SQL 查询或存储过程名称。

例如,这里有一个简单的控制器:

namespace Dapper.CQS.Example.Controllers
{
[ApiController]
[Route("[controller]/[action]")]
public class PropertyController : ControllerBase
{
private readonly IUnitOfWork _unitOfWork;


public PropertyController(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}


[HttpGet]
public async Task<ActionResult<Property>> GetById([FromQuery] int id)
{
var property = await _unitOfWork.QueryAsync(new PropertyGetByIdQuery(id));
return property == null ? NoContent() : Ok(property);
}


[HttpGet]
public async Task<ActionResult<List<Property>>> Filter([FromQuery] string? name)
{
var properties = await _unitOfWork.QueryAsync(new PropertyFilterQuery(name));
return Ok(properties);
}


[HttpGet]
public async Task<ActionResult<PagedList<Property>>> PagedFilter([FromQuery] string? name, int page = 1, int pageSize = 5)
{
var properties = await _unitOfWork.QueryAsync(new PropertyPagedFilterQuery(name, page, pageSize));
return Ok(properties);
}


[HttpPost]
public async Task<ActionResult<Property>> Create([FromBody] Property property)
{
var createdId = await _unitOfWork.ExecuteAsync(new PropertyCreateCommand(property));
await _unitOfWork.CommitAsync();
property.Id = createdId;
return Ok(property);
}


[HttpDelete]
public async Task<ActionResult> Delete([FromQuery] int id)
{
await _unitOfWork.ExecuteAsync(new PropertyDeleteCommand(id));
await _unitOfWork.CommitAsync();
return Ok();
}
}
}

这里有一个问题:

namespace Dapper.CQS.Example.CommandQueries
{
public class PropertyPagedFilterQuery : QueryPagedBase<Property>
{
[Parameter]
public string? Name { get; set; }
protected override CommandType CommandType => CommandType.Text;
protected override string Procedure => @"
SELECT *, COUNT(*) OVER() [COUNT]
FROM Properties WHERE Name = @Name OR @Name IS NULL
ORDER BY [Name]
OFFSET (@page -1 ) * @pageSize ROWS
FETCH NEXT @pageSize ROWS ONLY
";


public PropertyPagedFilterQuery(string? name, int page, int pageSize)
{
Name = name;
Page = page;
PageSize = pageSize;
}
}
}

QueryBase 将使用 Dapper

public abstract class QueryPagedBase<T> : CommandQuery, IQuery<PagedList<T>>, IQueryAsync<PagedList<T>>
{
[Parameter]
public int Page { get; set; }


[Parameter]
public int PageSize { get; set; }


protected virtual string FieldCount => "COUNT";


public virtual PagedList<T> Query(IDbConnection connection, IDbTransaction? transaction)
{
var result = connection.Query<T, int, (T Item, int Count)>(Procedure, (a, b) => (a, b), GetParams(), transaction, commandType: CommandType, splitOn: FieldCount);
return ToPagedList(result);
}


public virtual async Task<PagedList<T>?> QueryAsync(IDbConnection connection, IDbTransaction? transaction, CancellationToken cancellationToken = default)
{
var result = await connection.QueryAsync<T, int, (T Item, int Count)>(Procedure, (a, b) => (a, b), GetParams(), transaction, commandType: CommandType, splitOn: FieldCount);
return ToPagedList(result!);
}


private PagedList<T> ToPagedList(IEnumerable<(T Item, int Count)> result)
{
return new PagedList<T>
{
PageSize = PageSize,
Page = Page,
TotalRecords = result.Select(t => t.Count).FirstOrDefault(),
Items = result.Select(t => t.Item).ToList()
};
}
}

我想分享我的解决方案。我正在试验 UnitOfWork 在多个 ORM 中的实现,包括 Dapper。这里是完整的项目: https://github.com/pkirilin/UnitOfWorkExample

基本工作单元和存储库抽象:

public interface IUnitOfWork
{
Task SaveChangesAsync(CancellationToken cancellationToken);
}
public interface IRepository<TEntity, in TId> where TEntity : EntityBase<TId> where TId : IComparable<TId>
{
Task<TEntity> GetByIdAsync(TId id, CancellationToken cancellationToken);
    

TEntity Add(TEntity entity);


void Update(TEntity entity);


void Remove(TEntity entity);
}

域名模型:

public abstract class EntityBase<TId> where TId : IComparable<TId>
{
public TId Id { get; }


protected EntityBase()
{
}


protected EntityBase(TId id)
{
Id = id;
}
}


public class WeatherForecast : EntityBase<int>
{
// ...
}

具体的存储库接口:

public interface IWeatherForecastsRepository : IRepository<WeatherForecast, int>
{
Task<List<WeatherForecast>> GetForecastsAsync(CancellationToken cancellationToken);
}

具体的工作接口单元:

public interface IAppUnitOfWork : IUnitOfWork
{
IWeatherForecastsRepository WeatherForecasts { get; }
}

您的应用程序中可以有多个数据上下文,因此创建具有强边界的特定工作单元对我来说似乎是合理的。

工作单元的执行情况如下:

internal class AppUnitOfWork : IAppUnitOfWork, IDisposable
{
private readonly IDbConnection _connection;
private IDbTransaction _transaction;
    

public IWeatherForecastsRepository WeatherForecasts { get; private set; }


// Example for using in ASP.NET Core
// IAppUnitOfWork should be registered as scoped in DI container
public AppUnitOfWork(IConfiguration configuration)
{
// I was using MySql in my project, the connection will be different for different DBMS
_connection = new MySqlConnection(configuration["ConnectionStrings:MySql"]);
_connection.Open();
_transaction = _connection.BeginTransaction();
WeatherForecasts = new WeatherForecastsRepository(_connection, _transaction);
}
    

public Task SaveChangesAsync(CancellationToken cancellationToken)
{
try
{
_transaction.Commit();
}
catch
{
_transaction.Rollback();
throw;
}
finally
{
_transaction.Dispose();
_transaction = _connection.BeginTransaction();
WeatherForecasts = new WeatherForecastsRepository(_connection, _transaction);
}
        

return Task.CompletedTask;
}


public void Dispose()
{
_transaction.Dispose();
_connection.Dispose();
}
}

很简单。但是当我试图实现特定的存储库接口时,我遇到了一个问题。我的域模型很丰富(没有公共设置器,一些属性包装在值对象中等等)。Dapper 无法处理这样的类。它不知道如何将值对象映射到 db 列,当您尝试从 db 中选择某个值时,它会抛出错误并表示无法实例化实体对象。一种选择是创建私有构造函数,其参数与数据库列名称和类型相匹配,但这是一个非常糟糕的决策,因为域层不应该了解数据库的任何信息。

所以我把实体分成了不同的类型:

  • 域实体 : 包含您的域逻辑,由应用程序的其他部分使用。您可以在这里使用所有您想要的东西,包括私有设置器和值对象
  • 持久实体 : 包含与数据库列匹配的所有属性,仅在存储库实现中使用

其思想是,存储库仅通过持久化实体与 Dapper 一起工作,并且在必要时将持久化实体映射到域实体或从域实体映射到 Dapper。

还有一个名为 Dapper.Contrib的官方库,它可以为您构造基本(CRUD) SQL 查询,我正在实现中使用它,因为它确实使生活变得更加容易。

因此,我最后的仓库实现是:

// Dapper.Contrib annotations for SQL query generation
[Table("WeatherForecasts")]
public class WeatherForecastPersistentEntity
{
[Key]
public int Id { get; set; }


public DateTime Date { get; set; }


public int TemperatureC { get; set; }


public string? Summary { get; set; }
}


internal abstract class Repository<TDomainEntity, TPersistentEntity, TId> : IRepository<TDomainEntity, TId>
where TDomainEntity : EntityBase<TId>
where TPersistentEntity : class
where TId : IComparable<TId>
{
protected readonly IDbConnection Connection;
protected readonly IDbTransaction Transaction;


// Helper that looks for [Table(...)] annotation in persistent entity and gets table name to use it in custom SQL queries
protected static readonly string TableName = ReflectionHelper.GetTableName<TPersistentEntity>();


protected Repository(IDbConnection connection, IDbTransaction transaction)
{
Connection = connection;
Transaction = transaction;
}
    

public async Task<TDomainEntity> GetByIdAsync(TId id, CancellationToken cancellationToken)
{
var persistentEntity = await Connection.GetAsync<TPersistentEntity>(id, transaction: Transaction);
return (persistentEntity == null ? null : MapToDomainEntity(persistentEntity))!;
}


public TDomainEntity Add(TDomainEntity entity)
{
var persistentEntity = MapToPersistentEntity(entity);
Connection.Insert(persistentEntity, transaction: Transaction);
var id = Connection.ExecuteScalar<TId>("select LAST_INSERT_ID()", transaction: Transaction);
SetPersistentEntityId(persistentEntity, id);
return MapToDomainEntity(persistentEntity);
}


public void Update(TDomainEntity entity)
{
var persistentEntity = MapToPersistentEntity(entity);
Connection.Update(persistentEntity, transaction: Transaction);
}


public void Remove(TDomainEntity entity)
{
var persistentEntity = MapToPersistentEntity(entity);
Connection.Delete(persistentEntity, transaction: Transaction);
}


protected abstract TPersistentEntity MapToPersistentEntity(TDomainEntity entity);
    

protected abstract TDomainEntity MapToDomainEntity(TPersistentEntity entity);


protected abstract void SetPersistentEntityId(TPersistentEntity entity, TId id);
}


internal class WeatherForecastsRepository : Repository<WeatherForecast, WeatherForecastPersistentEntity, int>, IWeatherForecastsRepository
{
public WeatherForecastsRepository(IDbConnection connection, IDbTransaction transaction)
: base(connection, transaction)
{
}


public async Task<List<WeatherForecast>> GetForecastsAsync(CancellationToken cancellationToken)
{
var cmd = new CommandDefinition($"select * from {TableName} limit 100",
transaction: Transaction,
cancellationToken: cancellationToken);


var forecasts = await Connection.QueryAsync<WeatherForecastPersistentEntity>(cmd);


return forecasts
.Select(MapToDomainEntity)
.ToList();
}


protected override WeatherForecastPersistentEntity MapToPersistentEntity(WeatherForecast entity)
{
return new WeatherForecastPersistentEntity
{
Id = entity.Id,
Date = entity.Date,
Summary = entity.Summary.Text,
TemperatureC = entity.TemperatureC
};
}


protected override WeatherForecast MapToDomainEntity(WeatherForecastPersistentEntity entity)
{
return new WeatherForecast(entity.Id)
.SetDate(entity.Date)
.SetSummary(entity.Summary)
.SetCelciusTemperature(entity.TemperatureC);
}


protected override void SetPersistentEntityId(WeatherForecastPersistentEntity entity, int id)
{
entity.Id = id;
}
}


internal static class ReflectionHelper
{
public static string GetTableName<TPersistentEntity>()
{
var persistentEntityType = typeof(TPersistentEntity);
var tableAttributeType = typeof(TableAttribute);
var tableAttribute = persistentEntityType.CustomAttributes
.FirstOrDefault(a => a.AttributeType == tableAttributeType);


if (tableAttribute == null)
{
throw new InvalidOperationException(
$"Could not find attribute '{tableAttributeType.FullName}' " +
$"with table name for entity type '{persistentEntityType.FullName}'. " +
"Table attribute is required for all entity types");
}


return tableAttribute.ConstructorArguments
.First()
.Value
.ToString();
}
}

示例用法:

class SomeService
{
private readonly IAppUnitOfWork _unitOfWork;


public SomeService(IAppUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}


public async Task DoSomethingAsync(CancellationToken cancellationToken)
{
var entity = await _unitOfWork.WeatherForecasts.GetByIdAsync(..., cancellationToken);
_unitOfWork.WeatherForecasts.Delete(entity);


var newEntity = new WeatherForecast(...);
_unitOfWork.WeatherForecasts.Add(newEntity);


await _unitOfWork.SaveChangesAsync(cancellationToken);
}
}