@WhiteNinja

Реализация шаблона UnitOfWork при работе с транзакциями EntityFramework?

Добрый день!

Есть 3 интерефейса (слой инфрастуктуры SomeCompany.SomeProduct.Infrastructure):

public interface IRepository<TEntity> where TEntity : Entity
{
    TEntity Insert(TEntity entity);
    // ... other CRUD methods 
}

public interface IUnitOfWork
{
    void Commit();
    void Rollback();
}

public interface IUnitOfWorkFactory
{
    IUnitOfWork Create(Isolationlevel isolationLevel);
    IUnitOfWork Create(); // For default isolationLevel = Isolationlevel.ReadCommited
}


И их реализация (SomeCompany.SomeProduct.DataAccess.EntityFramework):

public class RepositoryBase<TEntity> : IRepository<TEntity> where TEntity : Entity
{
    protected readonly IDbContext _dbContext;

    public RepositoryBase(IDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public virtual TEntity Insert(TEntity entity)
    {
        if (entity == null)
            throw new ArgumentNullException("entity");

        return _dbContext.Set<TEntity>().Add(entity);
    }   

    // other methods
}

public class UnitOfWork : IUnitOfWork
{
    private readonly IDbContext _dbContext;
    private readonly IsolationLevel _isolationLevel;

    protected DbContextTransaction _transaction;

    private bool _isTransactionBeginer = false;
    private bool _commited = false;
    protected bool _disposed = false;

    public UnitOfWork(IDbContext dbContext, IsolationLevel isolationLevel)
    {
        if (dbContext == null)
            throw new ArgumentNullException("dbContext");

        _dbContext = dbContext;
        _isolationLevel = isolationLevel;

        if (_dbContext.Database.CurrentTransaction == null)
        {
            _transaction = _dbContext.Database.BeginTransaction(_isolationLevel);
            _isTransactionBeginer = true;
        }
    }

    // Dispose ...

    public void Commit()
    {
        _dbContext.SaveChanges();

        if (_isTransactionBeginer && _transaction != null)
        {
            _transaction.Commit();
            _transaction = _dbContext.Database.BeginTransaction(_isolationLevel);
            _commited = true;
        }
    }
}

public class UnitOfWorkFactory<TDbContext> : IUnitOfWorkFactory<TDbContext> where TDbContext : IDbContext
{
    private readonly TDbContext _dbContext;

    public UnitOfWorkFactory(TDbContext dbContext)
    {
        if (dbContext == null)
            throw new ArgumentNullException("dbContext");

        _dbContext = dbContext;
    }

    public IUnitOfWork Create(IsolationLevel isolationLevel)
    {
        return new UnitOfWork(_dbContext, isolationLevel);
    }

    public IUnitOfWork Create()
    {
        return Create(IsolationLevel.ReadCommitted);
    }
}


И самое главное работа со всем этим делом на уровне бизнес-логики (SomeCompany.SomeProduct.BusinessLogic):

public class CustomerService  : ICustomerService
{
    private readonly IUnitOfWorkFactory<IApplicationDbContext> _uowFactory;
    private readonly ICustomerRepository _customerRepository;

    public CustomerService(IUnitOfWorkFactory<IApplicationDbContext> uowFactory, ICustomerRepository _customerRepository)
    {
        _uowFactory = uowFactory;
        _customerRepository = _customerRepository;
    }

    public void EditCustomer(int customerId, string customerName)
    {
        Customer customer = _customerRepository.FindbyId(customerId); // working with IApplicationDbContext (ApplicationDbContext) instance. Without transaction.

        customer.ChangeName(customerName);
        // some validation...

        using(var uow = _uowFactory.Create()) // use same ApplicationDbContext instance, but start new transaction if not nested uow
        {
            _customerRepository.Update(customer);
            uow.Commit(); // Transaction of ApplicationDbContext commited;
        } // Close transaction if not nested uow
    }
}


Остается самое главное - регистрация контекста в IoC контейнере:

builder.RegisterType<ApplicationDbContext>()
    .As<IApplicationDbContext>()
    .InstancePerRequest();


Таким образом, идея заключается в том, что на каждый запрос создается контекст данных, в данном случае ApplicationDbContext, и он расшаривается в соответствующие репозитории и фабрику UoW. Т.е. если необходимо выполнить запрос на чтение, то можно инжектировать ICustomerRepository и в сервисе просто выполнить _customerRepository.Find/Get/GetPaged. Без использования UnitOfWorkFactory.

Если же нужно записать что-то в базу, то Uow обеспечивает транзакцию и сохранение данных в БД (uow.Commit()).

Вопросы:
1) Является ли это решение потокобезопасным (thread safe)?
2) В рекомендациях использования DbContext (EF) - выполнить Dispose() как можно быстрее. В моем же примере получается, что экземпляр контекста создается и держится до выполнения HttpRequest'a. Это нормально?
3) При регистрации контекста в IoC нужно использовать InstancePerRequest() или InstancePerLifetimeScope() ?
  • Вопрос задан
  • 1163 просмотра
Пригласить эксперта
Ответы на вопрос 1
w1ld
@w1ld
Программирую
2 вопрос. Смотря, что вы делаете. Обычно он для транзакции создается. А транзакция по идее как раз должна бы укладываться в веб запрос.

3 вопрос. Вы про IApplicationDbContext ? Не понятно что это такое. Это IDbContext ? Если так, то странно его создавать на Lifetimescope у Autofac, ведь он тогда, должно быть, будет жить в этом контейнере до конца жизни домена. EF контекст же легкий и предполагает частое создание и удаление.

Вообще, как-то вы странно обходитесь с UoW: обязываете бизнес логику заботиться об изоляции транзакций. Это "попахивает" тем, что data access логика в бизнес модели. Возможно, было бы лучше передать обязоность UoW репозиторию. Я имею в виду то, что у репозитория был бы метод Commit и не атомарные методы, а ожидающие завершения транзакции. Тогда не надо думать в бизнес логике об этом. Еще, может быть TransactionScope для реализации изоляции транзакций помог бы.
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы