-1

The query I have is a little more complex than the title lets on. Perhaps this is a foolish question, but I couldn't find any certain answer when searching online. Currently, I'm implementing the Repository/Unit-of-Work pattern in my own flavor and it looks a little like this:

// Note: methods are async for conventions, not because
// they're truly async
public interface IUnitOfWork
{
    Task Begin();

    // The Task<int> is the numbers of rows affected by this commit
    Task<int> Commit();

    Task Rollback();
}

The repository can more or less be expressed as such:

public interface IWriteableRepository<T>
    where T : class
{
     EntityEntry<T> Insert(T item);

     // Other CRUD methods removed for brevity; they're
     // of similar signatures
}

The idea is that an IUnitOfWork will hold some TransactionScope instance internally and handle the respective logic.

I then have two concerns. First, if each IUnitOfWork and IWriteableRepository<T> instance is injected with different instances of a DbContext (I'm using EntityFrameworkCore for the time being), will calling DbContext.BeginTransactionAsync() produce a transaction scope for both in the following code?

await this.UnitOfWork.Begin();

this.Repository.Insert(someEntity);

var rows = await this.UnitOfWork.Commit();

In other words, does the repository only operate on the transaction created in the call to Begin(), or will it operate completely independently?

The second concern I have is in relation to implementing the IUnitOfWork interface. My approach thus far has been roughly

public class UnitOfWork : IUnitOfWork
{
    public UnitOfWork(DbContext context)
    {
        this.Context = context;
    }

    private DbContext Context { get; set; }

    private TransactionScope Transaction { get; set; }

    public async Task Begin()
    {
        if (this.Scope == null)
        {
            this.Transaction = await this.Context
                .Database
                .BeginTransactionAsync();
        }
    }

    public async Task<int> Commit()
    {
        if (this.Scope != null)
        {
            var rows = await this.Context.SaveChangesAsync(false);

            this.Scope.Commit();

            this.Context.AcceptAllChanges();

            return rows;
        }
    }

    public Task Rollback()
    {
        if (this.Scope != null)
        {
            this.Scope.Rollback();
            this.Scope.Dispose();

            this.Scope = null;
        }

        return Task.CompletedTask;
    }
}

I'm mostly unsure whether the Rollback() method could be improved. I feel like disposing the object explicitly isn't correct. Is there any other way that I should go about handling getting rid of a TransactionScope?

Liam Mueller
  • 1,360
  • 2
  • 10
  • 17
  • 4
    If you are using entity framework, my honest opinion is you should ditch the repository pattern and Unit of work all together, save your self a maintenance nightmare hiding simple logic behind mystical abstraction layers. EF already implements these designs and patterns for you. Additionally if you are just wanting to roll back DB transactions, use the more modern TransactionBegin in EF. There is really no good going to come form your approach other than having to redesign it all in the future. – TheGeneral Dec 12 '18 at 04:36
  • May be helpful: https://stackoverflow.com/a/51781877/5779732 – Amit Joshi Dec 13 '18 at 08:56

1 Answers1

0

In my case, this is the solution I've come up with - I would definitely not recommend following it and I'm sure there are catches that my team will have to solve when they come up...

Because we require multiple database engines (Mongo and EF/SQL), we've wrapped our interactions with the database in the repository and unit-of-work pattern. All our repositories are implemented per-database-engine, e.g., IMongoRepository<T> : IWriteableRepository<T>, and the methods that cannot be abstracted by IWriteableRepository<T> implemented by IMongoRepository<T>. This works out rather well and I don't mind using this pattern.

IUnitOfWork is also implemented per-database-engine because Mongo, SQL, etc. will handle transactions differently. The concern about contexts being shared while maintaining injectable objects has been solved by using a factory, i.e., something like

public class FooService
{
    public FooService(
        IUnitOfWorkFactory<EntityFrameworkUnitOfWork> factory,
        IRepositoryContext context,
        IWriteableRepository<Bar> repository)
    {
        this.UnitOfWorkFactory = factory;
        this.Context = context;
        this.Repository = repository;
    }

    private IUnitOfWorkFactory<EntityFrameworkUnitOfWork> UnitOfWorkFactory { get; set; }

    private IRepositoryContext Context { get; set; }

    private IWriteableRepository<Bar> Repository { get; set; }

    public bool RemoveBar(int baz)
    {
        // IUnitOfWorkFactory<T>.Begin(IRepositoryContext)
        //     where T : IUnitOfWork, new()
        // 
        // 1) Creates a new IUnitOfWork instance by calling parameterless constructor
        // 2) Call UseContext(IRepositoryContext) on UoW, passing in the context;
        //        This causes the UoW to use the passed-in context
        // 3) Calls .Begin() on the UoW
        // 4) Returns the UoW
        using (var unitOfWork = this.UnitOfWorkFactory.Begin(this.Context))
        {
            var bar = this.Repository
                .Query()
                .First(x => x.Baz == baz);

            this.Repository.Remove(bar);

            var (success, rows) = unitOfWork.Commit();

            return success && rows > 0;
        }
    }
}

The EntityFrameworkUnitOfWork (or any IUnitOfWork) is allowed to implement Begin, Commit, and Rollback however it wants. IUnitOfWork also implements IDisposable to ensure the underlying transaction objects get cleaned up. Using the same context also ensures that transactions will surely apply to the repositories using that context.

Additionally, an IUnitOfWork instead of a factory could be passed in if it has checks to ensure that it only has one transaction open at a time; but, to remove this coupling to the implementation, we've created a factory instead. Not only does this ensure that we have only one transaction each using block, we're given the ability to have a using block without touching the constructor for an IUnitOfWork in our consuming code.

As a disclaimer, I whole-heartedly agree that you should not wrap ORM's in repositories. It will muddy up your DB code and add unnecessary complications. We are using these repositories in an effort to make our database interactions agnostic when doing simple things. Otherwise we get more specific in our repository implementations, removing a lot of the magic that other repository pattern implementations suffer from. Generally, once methods get beyond individual-record operations, the database engines and drivers have different ideas how to go about it.

A final note: it may be obvious that if you inject a repository that doesn't fit with the context you inject (e.g., IMongoContext and a IEntityFrameworkRepository<Bar>) your code won't not be run in transactions with the database. The reason this isn't a concern is

  1. using a different context than the one in your repository is already nonsensical in most cases
  2. you must manage the context and repository in the consuming code, and thus will be aware of conflicting contexts
Liam Mueller
  • 1,360
  • 2
  • 10
  • 17