Задать вопрос
@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() ?
  • Вопрос задан
  • 1200 просмотров
Подписаться 3 Оценить Комментировать
Пригласить эксперта
Ответы на вопрос 1
w1ld
@w1ld
Программирую
2 вопрос. Смотря, что вы делаете. Обычно он для транзакции создается. А транзакция по идее как раз должна бы укладываться в веб запрос.

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

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

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

Похожие вопросы