Как реализовать шаблон единицы работы с Dapper?

В настоящее время я пытаюсь использовать Dapper ORM с Unit Of Work + Repository Pattern.

Я хочу использовать Unit of Work, а не простой репозиторий, потому что мои вставки и обновления требуют определенной степени обработки транзакций. Мне не удалось найти каких-либо полезных примеров, так как большинство из них, похоже, используют Entity Framework и имеют проблемы с утечками в единице работы.

Может ли кто-нибудь, пожалуйста, указать мне в правильном направлении?

7 ответов

Этот проект 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. Я выбираю Инъекцию зависимостей с помощью Конструктора.

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.

Более подробную информацию о репозитории в приведенном выше коде можно найти здесь.

Я уже разместил этот код здесь. Но этот вопрос выглядит более актуальным для меня для этого кода; поэтому я пишу снова вместо ссылки на оригинальный ответ.

Edit 2018-08-03: комментарий Амита действительно заставил меня задуматься и заставил меня понять, что хранилище на самом деле НЕ ДОЛЖНО быть свойствами самого контекста. Скорее, хранилища могут зависеть от контекста. Вместо того, чтобы продолжать вносить постепенные изменения в примеры кода ниже. Я просто ссылаюсь на репозиторий Git, который я собрал, чтобы содержать эту концепцию.

Стоя на плечах других здесь.

Учитывая, что этот ответ является лучшим в большинстве поисковых запросов 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);
}

Обратите внимание, как ни IDbContext или же IUnitOfWorkFactory реализует IDisposable. Это сделано специально, чтобы избежать утечки абстракции. Вместо этого опора на Commit()/Rollback() заботиться о уборке и утилизации.

Пара моментов, прежде чем делиться реализациями.

  • IUnitOfWorkFactory несет ответственность за создание экземпляров UnitOfWork и посредничество подключения к базе данных.
  • IDbContext является основой хранилища.
  • IUnitOfWork это инкапсуляция IDbTransactionи гарантирует, что при работе с несколькими репозиториями они совместно используют один контекст базы данных.

Реализация 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 по вашему выбору (я лично использую контейнер IoC, предоставленный .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();
}

Явная необходимость Commit() для этой простой операции только для чтения кажется чрезмерной, но платит дивиденды по мере роста системы. И, по-видимому, предлагает незначительное преимущество в производительности, по словам Сэма Шафрана. Вы можете также опустить db.Commit() на простых операциях чтения, выполняя это, оставляя соединение открытым и оставляя бремя очистки на сборщике мусора. Так что это не рекомендуется.

Я обычно приношу DbContext в склад на уровне сервиса, где он работает в унисон с другими сервисами для формирования "ServiceContext". Затем я ссылаюсь на этот ServiceContext в фактическом слое MVC. В качестве еще одного упоминания рекомендуется использовать async по всему стеку, если вы можете. Это опущено здесь для простоты.

Хорошо, прошло полдесятилетия с тех пор, как ОП задал вопрос, но поскольку я постоянно сталкиваюсь с этим вопросом, когда разрабатываю с помощью Dapper (или что-то еще, на самом деле, это не очень специфично для Dapper). Вот мои два цента.

Сначала поговорим о других ответах:

ответ пимбровера IDbContextуправляет единицей работы аналогично тому, как это делает entity framework. Это совершенно разумно и легко понять. Но главный недостаток заключается в том, что вы проходитеIDbContextко всему вашему бизнес-коду. Это что-то вроде объекта бога. Прямо как в EF. Я предпочитаю внедрять отдельные репозитории и четко указывать, какие элементы базы данных я собираюсь делать, вместо того, чтобы все в моей модели домена всегда было только одним.далеко. Однако, если вы не согласны с моим возражением "объект бога", ответ Пима будет для вас правильным.

Ответ Амита Джоши имеетMyRepositoryвозьмите единицу работы в качестве параметра конструктора. Это означает, что вы больше не можете вводить репозитории. Это можно решить, вместо этого внедрив фабрики репозиториев, но это, безусловно, собственный уровень хлопот.

Небольшое отступление: в некоторых из этих ответов слова "транзакция" и "единица работы" используются как синонимы. На практике у них отношения 1:1, но это не одно и то же. "Транзакция" - это реализация db, "единица работы" - это более концептуальная вещь более высокого уровня. Если бы у нас было больше устойчивости, чем у одной базы данных, была бы разница, и 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 });
    }
}

И, наконец, вот DI. Сделайте настройку. Вот пример однопоточного консольного приложения. Я полагаю, было бы разумно сделать его одиночным или по запросу. Реализация UnitOfWorkContext в любом случае может быть изменена в соответствии с вашими вариантами потоковой передачи (например, с использованием UnitOfWorkContext со статическим 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();
    }
}

Полная версия на Github: https://github.com/NathanLBCooper/unit-of-work-example

Анализ:

Мы устранили объекты богов, и нам не нужно создавать фабрики для всех наших репозиториев. цена заключается в том, что у нас есть немного более тонкая неочевидная связь между нашими репозиториями и материалом Unit of Work. Здесь нет шаблонной панели, но нам нужно быть осторожными с тем, какое время жизни мы даем нашему объекту контекста, особенно при многопоточности.

Я думаю, что это стоит того, но это я.

PS

Добавлю одно. Возможно, вы искали этот ответ, потому что начали использовать dapper. Сейчас все методы вашего репозитория представляют собой отдельные атомарные операции, и вы пока не чувствуете необходимости объединять их в транзакции. Тогда пока ничего из этого делать не нужно. Закройте это окно браузера, напишите свои репозитории самым простым и очевидным способом и будьте счастливы.

Для этого нет необходимости в скрученном вручную растворе. То, что вы хотите, может быть достигнуто очень просто, используя классы, уже имеющиеся в структуре.

/// <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 для нескольких 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 не может обрабатывать такие классы как есть. Он не знает, как сопоставить объекты значений со столбцами базы данных, и когда вы пытаетесь выбрать какое-либо значение из базы данных, он выдает ошибку и говорит, что не может создать экземпляр объекта сущности. Одним из вариантов является создание частного конструктора с параметрами, соответствующими именам и типам столбцов вашей базы данных, но это очень плохое решение, потому что ваш уровень домена не должен ничего знать о вашей базе данных.

Итак, я разделил сущности на разные типы:

  • Сущность домена : содержит логику вашего домена, используется другими частями приложения. Вы можете использовать здесь все, что хотите, включая приватные сеттеры и объекты-значения.
  • Постоянный объект : содержит все свойства, соответствующие столбцам вашей базы данных, используется только в реализации репозитория. Все свойства являются общедоступными

Идея состоит в том, что репозиторий работает с 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);
    }
}

Я заметил, что в вашем репозитории 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 будет отличаться при каждой инъекции, и это не сработает.

Я создал простую реализацию единицы работы поверх Dapper с некоторыми базовыми CQS. https://github.com/giangcoi48k/Dapper.CQS. Пожалуйста, взгляните и посмотрите, можно ли это применить к вашему проекту.

Использовать IUnitOfWorkвыполнить соответствующие Queryили же Command, определенный 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()
            };
        }
    }
Другие вопросы по тегам