我想分享我的解决方案。我正在试验多个 ORM 的 UnitOfWork 实现,包括 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 中选择某个值时,它会抛出错误并说它无法实例化实体对象。一种选择是使用与您的数据库列名称和类型匹配的参数创建私有构造函数,但这是一个非常糟糕的决定,因为您的域层不应该知道您的数据库的任何信息。
所以我将实体分为不同的类型:
-
域实体:包含您的域逻辑,由应用程序的其他部分使用。您可以在这里使用任何您想要的东西,包括私有 setter 和值对象
-
持久实体:包含与您的数据库列匹配的所有属性,仅用于存储库实现。所有属性都是公开的
这个想法是存储库仅通过持久实体与 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);
}
}