Как реализовать шаблон единицы работы с 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()
};
}
}