W
W
WhiteNinja2017-04-18 13:06:36
C++ / C#
WhiteNinja, 2017-04-18 13:06:36

Implementing the UnitOfWork pattern when dealing with EntityFramework transactions?

Good afternoon!
There are 3 interfaces (infrastructure layer 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
}

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

And most importantly, work with all this business at the business logic level (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
    }
}

The most important thing remains - registering the context in the IoC container:
builder.RegisterType<ApplicationDbContext>()
    .As<IApplicationDbContext>()
    .InstancePerRequest();

Thus, the idea is that for each request a data context is created, in this case ApplicationDbContext , and it is shared to the appropriate repositories and the UoW factory. Those. if you need to execute a read request, then you can inject ICustomerRepository and simply execute _customerRepository.Find/Get/GetPaged in the service. Without using UnitOfWorkFactory.
If you need to write something to the database, then Uow provides a transaction and saving data to the database (uow.Commit()).
Questions:
1) Is this solution thread safe?
2) The DbContext (EF) recommendation is to execute Dispose() as quickly as possible. In my example, it turns out that the context instance is created and held until HttpRequest is executed. This is fine?
3) When registering a context in IoC, should I use InstancePerRequest() or InstancePerLifetimeScope() ?

Answer the question

In order to leave comments, you need to log in

1 answer(s)
W
w1ld, 2017-04-27
@w1ld

2 question. Watching what you do. Usually it is created for a transaction. A transaction, in theory, should just fit into a web request.
3 question. Are you talking about IApplicationDbContext ? It is not clear what it is. Is it IDbContext ? If so, then it's strange to create it on Autofac's Lifetimescope, because then it will probably live in this container until the end of the domain's life. EF context is lightweight and involves frequent creation and deletion.
In general, somehow you treat UoW strangely: you oblige business logic to take care of transaction isolation. This "smells" like data access logic in a business model. Perhaps it would be better to pass the responsibility of UoW to the repository. I mean that the repository would have a Commit method and not atomic methods, but waiting for the transaction to complete. Then you don't have to think about it in business logic. Also, maybe TransactionScope would help to implement transaction isolation.

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question