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

Posted

技术标签:

【中文标题】如何使用 Dapper 实现工作单元模式?【英文标题】:How to implement Unit Of Work pattern with Dapper? 【发布时间】:2015-09-26 16:36:46 【问题描述】:

目前,我正在尝试将 Dapper ORM 与工作单元 + 存储库模式一起使用。

我想使用工作单元而不是简单的简洁存储库,因为我的插入和更新需要一定程度的事务处理。我找不到任何有用的示例,因为大多数示例似乎使用实体框架并且在工作单元中存在泄漏问题。

有人可以指点我正确的方向吗?

【问题讨论】:

Dapper 不是 ORM。 UoW 是数据库事务。存储库将使用 dapper 来处理数据库。 Dapper 是一个微型 ORM,我不知道 UOW 是事务,但我正在寻找一个使用 UOW 模式的 Dapper 的好例子。 micro ORM 用词不当,它基本上是一个数据映射器。使用 uow 模式的 dapper 的一个很好的例子是任何 db 事务。并且不要使用你还不理解的模式,你只会让你的生活复杂化。了解存储库是什么,了解 Uow 是什么(阅读实际定义),然后如果遇到这些特定问题时使用它们。许多开发人员以非常错误的方式使用 UoW/repository 组合。不要像他们一样。 这正是我想使用 UOW 模式的原因,因为我的插入和更新确实需要事务处理。查看我当前对 UOW 的实现,该模式需要在处理 UOW 时关闭数据库连接。这感觉不太对劲。 UnitOfWork 不仅仅是交易。 ***.com/q/39909985/5779732 【参考方案1】:

我想分享我的解决方案。我正在试验多个 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);
    

【讨论】:

【参考方案2】:

我在 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()
            ;
        
    

【讨论】:

你没有创建任何东西,只是复制了我们的库并把事情搞砸了一点【参考方案3】:

对此无需手动解决方案。使用框架中已有的类可以非常简单地实现您想要的。

/// <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);
        
    

【讨论】:

问题是关于 UnitOfWork 模式,而不是如何使用 TransactionScope 我的意思是,除非您希望向 UnitOfWork 抽象添加其他功能,例如更改跟踪,而这里的其他答案都没有,而是只需使用提供简单事务的 UnitOfWork 抽象,那么无需手动处理自定义的 UnitOfWork 抽象就可以非常简单地实现,因为框架提供的 TransactionScope 类型已经提供了这一点。 我还要补充一点,如果您确实需要更改跟踪,那么除非您已经知道您将需要大量优化以提高性能,否则您不妨咬一口子弹并使用实体框架或其他开箱即用的库,否则您最终将维护、测试和调试大量(可能不必要的)定制框架代码。 这可以在 .NET Framework 中工作,但不能在 .NET Core 中工作,直到解决 github.com/dotnet/runtime/issues/715 返回之前不需要调用 trn.Complete() 吗?【参考方案4】:

好的,自从 OP 提出问题以来已经过去了五年,但是当我使用 Dapper 开发时,我不断遇到这个问题(或者其他任何东西,这并不是非常具体的 Dapper)。这是我的两分钱。

首先说一下其他答案:

pimbrouwers' answer IDbContext 管理工作单元的方式与实体框架的管理方式非常相似。这是完全明智且易于理解的。但主要缺点是您最终会将IDbContext 传递给您的所有业务代码。这有点像上帝的对象。就像在 EF 中一样。我更喜欢注入单独的存储库并明确说明我将要做什么数据库内容,而不是让我的域模型中的所有内容始终只有一个.。但是,如果您不同意我的“上帝反对”反对意见,那么 pim 的回答听起来很适合您。

Amit Joshi's answer 具有 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;
    

上下文更有趣。这是 repos 和工作单元在幕后通信的方式。

有一个接口供业务代码管理一个工作单元,一个供 repo 遵守该工作单元。

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();

这里是 repo 是如何做到的:

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

最后是 DI。进行设置。这是一个单线程控制台应用程序示例。我想将其设置为单例或按请求是明智的。无论如何,可以更改 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

分析

我们已经消除了上帝对象,并且不需要为我们所有的存储库创建工厂。代价是我们在我们的回购和工作单元之间有了更多的微妙的非显而易见的联系。没有样板,但我们确实需要注意我们赋予上下文对象的生命周期,尤其是在多线程时。

我认为这是一个值得的权衡,但这就是我。

附言

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

【讨论】:

“我会添加一件事......” - 很好的建议。很多人在没有真正了解他们需要/正在做什么的情况下过度设计。 @内森 这里查询中使用的事务如何?现在它给出了错误,很可能是因为在构造函数中调用了 Begin Transaction,但没有在 dapper 查询中使用。我错过了什么吗?这是错误 - 当分配给命令的连接处于挂起的本地事务中时,ExecuteReader 要求命令具有事务。 @Jay 您使用的是 sql server 和 System.Data.SqlClient 对吗?虽然仅将连接传递给 dapper 对于 sqlite(此处)或 postgres 就足够了,但它不适用于 sql server。当您从GetConnection() 获得连接时,您也需要交易。那是你的问题吧? 我会尽快更新 SQL 的答案。但同时,把GetConnection()的签名改成(IDbConnection connection, IDbTransaction transaction) GetConnection();,这样就可以传入Dapper,在UnitOfWork上公开交易 @NathanCooper 是的,我正在使用 SqlClient。【参考方案5】:

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

在这里站在别人的肩膀上。

在大多数与“dapper”和“unit of work”相关的 Google 搜索中,考虑到这个答案是最重要的。我想提供我的方法,我已经用了好几次了。

使用虚构(且过于简化)的示例:

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。这样做是为了避免leaky abstraction。相反,依赖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);
    

要访问数据库,只需实例化DbContext 或使用您选择的IoC 容器进行注入(我个人使用.NET Core 提供的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。为简单起见,此处省略。

【讨论】:

我需要在DbContext 类中实例化我的所有存储库吗?如果是这样,那么它就违反了 SRP。每次引入新的存储库时,我都必须更改此类。 没有什么可以阻止您将 IServiceProvider(用于 .net 核心)注入到类中并维护存储库的哈希表。但是当你添加一个新的存储库时,你仍然需要更新一些东西,在本例中是 IoC 容器。我已经做到了两种方式。为了简单起见,我在这里做了前者。你也可以使用反射。但这可能无法很好地扩展。 @pimbrouwers 如何在这里使用 async 和 await? @pimbrouwers 感谢您提供代码。它的结构非常好。您能否提供 MVC5 和 Unity IoC 的使用示例。 有什么原因 IUnitOfWorkFactory.Create 不返回 IUnitOfWork 接口而不是 UnitOfWork 类?【参考方案6】:

我在您的 github 存储库中注意到您删除了 UnitOfWorkFactory,而是在访问连接时实例化它

这种方法的问题是我无法理解。

想象以下场景,如果我将 DBContext 注册为 Scoped 并将 Repositories 注册为 Transient

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

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

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

现在我可以将 dbcontext 设置为 Transient,但 UnitOfWork 在每次注入时都会有所不同,并且无法正常工作。

【讨论】:

【参考方案7】:

这个Git project 很有帮助。我从相同开始,并根据需要进行了一些更改。

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。我选择使用构造函数进行依赖注入。

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 是 more 而不是 DBTransaction。

以上代码中关于 Repository 的更多细节可以在here找到。

我已经发布了此代码here。但是对于这段代码,这个问题对我来说更相关;所以我再次发布,而不仅仅是链接到原始答案。

【讨论】:

如果我们要对 UnitOfWork 类进行单元测试,那将如何工作?我们是否必须模拟ConnectionTransaction?例如,我们将如何模拟 unitOfWork.Connection.Execute() @kkuilla:我对与数据访问层相关的类执行集成测试。这些测试适用于实际连接;我不需要嘲笑它。执行数据库操作是 DAL 的主要目的。通过模拟依赖项对其进行单元测试并不能达到主要目的。看看this 的帖子。 using 语句代码只是举例说明如何使用它。我没有任何理由不能将它注入服务中;它可以做得很好。实际上,我在我的现场项目中做同样的事情。关于 UoW;如果只是 IDbConnection 满足您的需求,那么这也不应该是一个问题。只需绕过 UoW 并直接注入连接。 好的,这是基本的 CRUD 示例,如果我有一个场景需要在应用程序代码中移动现有的复杂存储过程,那么使用这个存储库模式是否仍然相关?我的意思是,我们很可能会在应用程序代码中实现相同的查询。 @Sherlock:我不知道你的完整场景;但是在这个答案的上下文中,使用具有类似设计的复杂查询应该没有任何问题。 Dapper 不会为您生成查询;您编写查询。因此,查询的同谋并不重要。关于存储库本身的相关性?对不起,但我不能说。这取决于很多因素,当然也取决于个人选择。

以上是关于如何使用 Dapper 实现工作单元模式?的主要内容,如果未能解决你的问题,请参考以下文章

工作单元模式 - 管理父子关系

如何在 Dapper.NET 中使用事务?

如何在 ASP MVC 中实现工作单元、存储库和业务逻辑?

如何运用领域驱动设计 - 工作单元

如何在 Dapper 中使用“Where In”

如何使用 asp.net 核心中的依赖注入在工作单元模式中延迟注入存储库