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
?
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