76

Currently, I am trying to use Dapper ORM with Unit Of Work + Repository Pattern.

I want to use Unit of Work as opposed to a simple dapper Repository due to the fact that my insert and updates require a degree of transaction processing. I have been unable to find any useful examples as most seem to use Entity Framework and have leakage issue within the Unit of Work.

Could someone please point me in the right direction?

Amit Joshi
  • 15,448
  • 21
  • 77
  • 141
Stig
  • 1,169
  • 1
  • 9
  • 12
  • 2
    Dapper is not an ORM. The UoW is the db transaction. The repository will use dapper to work with the db. – MikeSW Jul 09 '15 at 13:40
  • 5
    Dapper is a micro ORM, I undserstand that the UOW is the transaction, but I am looking for a good example of Dapper utilising the UOW pattern. – Stig Jul 10 '15 at 07:32
  • 1
    The micro ORM is a misnomer, it's basically a data mapper. A good example of dapper using uow pattern is any db transaction. And don't use patterns you don't understand yet, you'll only complicate your life. Understand what the repository is, understand what the Uow is (read the actual definitions) and then use them **if** you have those specific problems. Many devs are using the UoW/repository combo in a very wrong way. Don't be like them. – MikeSW Jul 10 '15 at 13:17
  • That is exactly why I want to use UOW pattern as my Inserts and updates do require transaction processing. Looking my current implementation of the UOW the pattern requires the closing of the db connection when the UOW is disposed. This just doesn’t feel quite right. – Stig Jul 10 '15 at 14:21
  • 1
    UnitOfWork is more than just transaction. https://stackoverflow.com/q/39909985/5779732 – Amit Joshi Jul 11 '17 at 08:48

7 Answers7

57

This Git project is very helpful. I started from the same and did some changes as per my need.

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

Now, your repositories should accept this UnitOfWork in some way. I choose Dependency Injection with Constructor.

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, .........);
    }
}

And then you call it like this:

With 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;
    }
}

Without Transaction:

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

Please note that, UnitOfWork is more than DBTransaction.

More details about Repository in above code could be found here.

I have already post this code here. But this question looks more relevant to me for this code; so I am posting again instead of just link to original answer.

Amit Joshi
  • 15,448
  • 21
  • 77
  • 141
  • If we are going to unit test the class `UnitOfWork`, how would that work? Do we have to mock `Connection` and `Transaction`? How would we mock `unitOfWork.Connection.Execute()` for example? – kkuilla Mar 14 '18 at 14:23
  • 2
    @kkuilla: I perform Integration Tests on classes related to Data Access Layer. Those tests work on actual Connection; I do not need to mock it. Performing DB operations is primary purpose of DAL. Unit testing it by mocking the dependencies does not serve primary purpose. Have a look at [this](https://stackoverflow.com/a/15049341/5779732) post. – Amit Joshi Mar 14 '18 at 14:40
  • The `using` statement code is just for example how you can use it. I do not any reason why you cannot inject it in Service; it could be done very well. Actually, I am doing same thing in my live project. About UoW; if just `IDbConnection` suits your needs, then that should not be an issue either. Just bypass UoW and inject connection directly. – Amit Joshi Apr 03 '18 at 17:02
  • Okay this is basic CRUD sample, if I have a scenario wherein I need to move existing complex stored procedure in application code, is it still relevant to use this repository pattern? I mean, chances are we are going to implement the same query in application code. – Anonymous Duck Oct 17 '19 at 13:01
  • 1
    @Sherlock: I am not aware about your complete scenario; but in context of this answer, there should not be any problem using complex queries with similar design. Dapper does not generate queries for you; you write the query. So, complicity of query does not matter. About relevance of repository itself? Sorry but I cannot say. It depends on so many factors and of-course a personal choice as well. – Amit Joshi Oct 17 '19 at 13:18
25

Edit 2018-08-03: Amit's comment really got me thinking, and made me realize that the repository's don't in fact NEED to be properties on the context itself. But rather, repositories could have a dependency on the context. Rather than continue to make incremental changes to the code samples below. I will simply reference a git repo I've put together to contain this concept.

Standing on the shoulders of others here.

Considering this answer is top in most Google searches pertaining to "dapper" and "unit of work". I wanted to provide my approach, which I've used to great effect several times now.

Using a ficitious (and overly simplified) example:

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

Note how neither IDbContext or IUnitOfWorkFactory implements IDisposable. This is purposefully done to avoid a leaky abstraction. Instead the reliance is on Commit()/Rollback() to take care of cleanup and disposal.

A couple of points before sharing implementations.

  • IUnitOfWorkFactory is responsible for instantiating the UnitOfWork and brokering the database connection.
  • IDbContext is the repository backbone.
  • IUnitOfWork is an encapsulation of IDbTransaction, and ensures that when working with multiple repositories, they share a single database context.

Implementation of 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;
    }
}

Implementation of 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;
    }
}

Implementation of 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;
        }
    }
}

Implementation of 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);
    }
}

To access the database, simply instantiate DbContext or inject using the IoC container of your choice (I personnally use the IoC container provided by .NET Core).

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

The explicit need for Commit() for this simple read-only operation seems excessive, but pays dividends as the system grows. And apparently, offers a minor performance benefit according to Sam Saffron. You "can" also omit the db.Commit() on simple read operations, by doing this though you leaving the connection hanging open and put the onus of cleaning things up onto the garbage collector. So this isn't recommended.

I typically bring the DbContext into the fold at the service-tier, where it works in unison with other services to form the "ServiceContext". I then reference this ServiceContext in the actual MVC layer.

As another point of mention, it's recommended to use async throughout the stack if you can. It is omitted here for simplicity.

pim
  • 12,019
  • 6
  • 66
  • 69
  • 2
    Do I need to instantiate all my repositories in `DbContext` class? If it is so, it violates SRP then. I have to change this class every time new repository is introduced. – Amit Joshi Jun 01 '18 at 08:13
  • There’s nothing stoppng you from injecting IServiceProvider (for .net core) into the class and maintaining a hashtable of repos. But you still need to update something when you add a new repository, in this case the IoC container. Ive done it both ways. I did the former here for simplicity. You could also use reflection. But that may not scale well. – pim Jun 01 '18 at 10:29
  • @pimbrouwers how to use async and await here? – Hameed Syed Oct 27 '18 at 12:14
  • @pimbrouwers thank you for the code. It is very well structured. Can you please provide a usage sample for MVC5 and Unity IoC. – user1770849 Nov 07 '18 at 21:28
  • Is there any reason IUnitOfWorkFactory.Create does not return the IUnitOfWork interface instead of the UnitOfWork class? – Søren Pedersen Sep 11 '19 at 07:29
  • I'm very confused by this. What does this do that could not be achieved equally well by injecting your connection string into each repository, having each repository instantiate its own IDbConnection instance (relying on connection pooling), and using TransactionScope as your unit of work wherever you need it? – Neutrino Apr 16 '20 at 13:43
  • You would then be coupling to a specific implementation of IDbConnection. Thus, making your solution more coupled than it needs to be since the connection resource is really the only unique, vendor specific aspect to ADO. Injecting a string literal likely isn't a good idea, unless it's a specific domain's service, wrapping SendGrid for example and injecting the API key. But when the underlying resource is variable, this pattern doesn't work. – pim Apr 16 '20 at 14:55
  • I don't see how. Assuming your connection string comes from a configuration source of some kind, and the implementation of IDbConnection is defined by whatever you've registered with whatever IOC container you are using, then the controller is independent of both the format of the connection string and the IDbConnection implementation. Besides, you could just configure your IOC container to provide a per-request IDbConnection implementation for the same effect, you don't actually hae to inject the string, that's just an implementation detail. – Neutrino Jul 23 '20 at 19:11
  • The implementation of `IUnitOfWork` doesn't implements a `Transaction` setter, as defined in the interface. In my code I removed the `set;` from the interface. – MFedatto Aug 12 '21 at 10:56
13

Okay, it's been half a decade since the OP asked, but as I keep coming across this question when I develop with Dapper (or anything really, this isn't really very Dapper specific). Here's my two cents.

First Let's talk about the other answers:

pimbrouwers' answer IDbContext manages Unit of Work in a very similar way to how entity framework does it. It's perfectly sensible and easy to understand. But the major drawback is that you end up passing a IDbContext to all your business code. It's a bit of a god object. Just like in EF. I prefer to inject individual repositories and make it explicit what database stuff I'm going to be doing, instead of having everything in my domain model always just one . away. However, if you don't agree with my 'god object' objection, pim's answer sounds like the right one for you.

Amit Joshi's answer has the MyRepository take the unit of work as a constructor parameter. This means you can't inject Repositories anymore. This can be solved by injecting repository factories instead, but this is certainly its own level of hassle.

A quick aside: In some of these answers the word "transaction" and "unit of work" are used interchangeably. In practice here they have a 1:1 relationship, but they aren't the same thing. The "transaction" is the db implementation, the "unit of work" is more of a higher level conceptual thing. If we had more persistence that just one database, there would be a difference, and the UOW would contain more than just one transaction. So, to avoid confusion, "Transaction" is probably not a great word to use in our UOW interface.

So here's my way:

I'll start with the Usage

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

The unit of work just wraps a transaction and is shortlived:

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

The Context is more interesting. It's the way in which the repos and the unit of works communicate behind the scenes.

There's one interface for the business code to manage a unit of work, and one for the repo to abide by that unit of work.

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

Here's how the repo does that:

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

And finally here's DI. Do the setup. Here's a single threaded console application Example. I imagine it would be sensible to make it a singleton or per request. The implementation of UnitOfWorkContext can be changed to match your threading choices anyway (Eg by using a UnitOfWorkContext with a thread static UOW).

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

Full version on Github: https://github.com/NathanLBCooper/unit-of-work-example

Analysis:

We've eliminated god objects and don't need to create factories for all our repositories. the cost is that we've got a little bit more of a subtle non-obvious link between our repos and the Unit of Work stuff. There's no boiler plate, but we do need to be careful about what lifetime we give our context object, especially when multithreading.

I think this is a trade-off that's worth it, but that's me.

PS

I'll add one thing. Maybe you've looked up this answer because you've started using dapper. Right now all your repository methods are separate atomic operations and you feel no need to combine them into transactions yet. Then for the time being you don't need to do any of this. Close this browser window, write your repositories in the most simple and obvious way and be happy.

Nathan Cooper
  • 6,262
  • 4
  • 36
  • 75
  • 4
    "I'll add one thing..." - excellent advice. Lots of people overengineer without really understanding what they need/are doing. @nathan – rdelgado-incinc Apr 29 '21 at 16:30
  • How is the transaction being used in query here? Right now it's giving error, most probably because Begin Transaction was called in constructor but not used in dapper query. Am I missing something? This is the error - ExecuteReader requires the command to have a transaction when the connection assigned to the command is in a pending local transaction. – Jay Dec 13 '21 at 15:57
  • @Jay You're using sql server and System.Data.SqlClient right? While passing just the connection into dapper is sufficient for sqlite (here) or postgres, it doesn't work like that with sql server. When you get the connection from `GetConnection()`, you need the transaction as well. That's your problem right? – Nathan Cooper Dec 13 '21 at 18:32
  • I'll update the answer for SQL soon. But in the meantime, change the signature of `GetConnection()` to `(IDbConnection connection, IDbTransaction transaction) GetConnection();`, so you can pass it into Dapper, and make the transaction public on the `UnitOfWork` – Nathan Cooper Dec 13 '21 at 18:33
  • @NathanCooper Yes, I am using SqlClient. – Jay Dec 13 '21 at 19:15
  • @NathanCooper Can you please let me know how you would access transaction to dapper query. Also, How would you register the service in .net core startup. – Jay Dec 14 '21 at 06:28
  • Hey. I added a DI example to the github repository, if you were still wondering. And there is a mssql example in there as well. – Nathan Cooper Feb 19 '22 at 17:31
10

There is no need for a hand-rolled solution for this. What you want can be achieved very simply using the classes already in the framework.

/// <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);
        }
    }
}
Neutrino
  • 8,496
  • 4
  • 57
  • 83
  • 4
    The question was about UnitOfWork pattern, not how to use TransactionScope – Moisés Alexander Salazar Vila Jul 11 '20 at 22:12
  • 2
    And my point is, that unless you wish to add additional features to the UnitOfWork abstraction, like change tracking, which none of the other answers here do, and instead just use a UnitOfWork abstraction that provides simple transactioning, then that can be achieved quite simply without handrolling a custom UnitOfWork abstraction since the framework provided TransactionScope type provides that already. – Neutrino Jul 13 '20 at 10:34
  • 2
    I'd also add, that if you _do_ need change tracking, then unless you already know you are going to need to optimize heavily for performance you might as well bite the bullet and use Entity Framework or some other library that provides it out of the box, otherwise you are going to end up maintaining, testing and debugging a lot of (probably unnecessary) bespoke framework code. – Neutrino Jul 13 '20 at 10:39
  • This could work in .NET Framework, but not .NET Core until there is a resolution to https://github.com/dotnet/runtime/issues/715 – Rob Johnston Jul 23 '20 at 17:39
  • Nice that even now the official API documentation makes no mention of this regression https://learn.microsoft.com/en-us/dotnet/api/system.transactions.transactionscope?view=netcore-2.1 – Neutrino Jul 23 '20 at 19:35
  • As best I can make out it does work though for access to a single database, at least using the SqlClient provider for SQL Server. – Neutrino Jul 23 '20 at 19:36
  • 1
    Don't you need to call trn.Complete() before you return? – Ε Г И І И О May 22 '21 at 08:29
  • Technically you are correct, I should call trn.Complete. Although in the case where all that is being done in the transaction is readng data instead of committing anything to the database it's not clear whether calling Complete makes any difference. – Neutrino May 23 '21 at 09:45
  • 1
    @RobJohnston Good news: that issue was marked fixed (for .NET 7) in August 2022. – MarredCheese Oct 27 '22 at 20:26
3

I'd like to share my solution. I was experimenting with UnitOfWork implementation for multiple ORMs, including Dapper. Here's full project: https://github.com/pkirilin/UnitOfWorkExample

Base unit of work and repository abstractions:

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

Domain model:

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>
{
    // ...
}

Specific repository interface:

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

Specific unit of work interface:

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

You can have multiple data contexts in your application, so creating specific unit of works with strong boundary seems reasonable to me.

The implementation of unit of work will look like this:

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

Quite simple. But when I tried to implement specific repository interface, I faced a problem. My domain model was rich (no public setters, some properties were wrapped in value objects etc.). Dapper is unable to handle such classes as-is. It doesn't know how to map value objects to db columns and when you try to select some value from db, it throws error and says it can't instantiate entity object. One option is to create private constructor with parameters matching your db column names and types, but it's very bad decision, because your domain layer shouldn't know anything about your database.

So I've splitted entities into different types:

  • Domain entity: contains your domain logic, is used by other parts of application. You can use everything you want here, including private setters and value objects
  • Persistent entity: contains all properties matching your database columns, is used only in repository implementation. All properties are public

The idea is that repository works with Dapper only via persistent entity and, when nessesary, maps persistent entity to or from domain entity.

There is also an official library called Dapper.Contrib, which can construct basic (CRUD) SQL queries for you, and I'm using it in my implementation, because it really makes life easier.

So, my final repository implementation:

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

Example usage:

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);
    }
}
pkirilin
  • 332
  • 2
  • 8
1

I noticed in your github repo you removed the UnitOfWorkFactory and instead instantiate it when accessing the Connection

Problem with this approach that i can't wrap my head around is.

Imagine the following scenario, if i register DBContext as Scoped and Repositories as Transient

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

In this case all of (1.) above is a single transaction and then I want to commit in (2.)

With a large business layer with multiple services using the same scoped instance of dbcontext I can see transactions overlapping

Now I could set dbcontext as Transient but then UnitOfWork would be different on each injection and it wouldn't work.

drowhunter
  • 371
  • 2
  • 12
-2

I've created a simple unit of work implementation on top of Dapper, with some basic CQS in mind. https://github.com/giangcoi48k/Dapper.CQS. Please take a look and see if it can apply to your project.

Use IUnitOfWork to execute the corresponding Query or Command, defined SQL query, or Stored Procedure name in that Query or Command.

For example, here is a simple controller:

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

And here is a Query:

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 will use 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()
            };
        }
    }
Minh Giang
  • 631
  • 9
  • 28