Reputation: 11990
I am trying to utilize the repository design pattern in my application for 2 reasons
I like to de-couple my application from Entity in case I decide to not use Entity Framework at some point
I want to be able reuse the logic that interacts with the model
I successfully setup and used the repository pattern. However, I have one complexity to deal with which is a transaction.
I want to be able to use transaction so that I can make multiple calls to the repository and then commit or rollback.
Here is my repository interface
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace Support.Repositories.Contracts
{
public interface IRepository<TModel> where TModel : class
{
// Get records by it's primary key
TModel Get(int id);
// Get all records
IEnumerable<TModel> GetAll();
// Get all records matching a lambda expression
IEnumerable<TModel> Find(Expression<Func<TModel, bool>> predicate);
// Get the a single matching record or null
TModel SingleOrDefault(Expression<Func<TModel, bool>> predicate);
// Add single record
void Add(TModel entity);
// Add multiple records
void AddRange(IEnumerable<TModel> entities);
// Remove records
void Remove(TModel entity);
// remove multiple records
void RemoveRange(IEnumerable<TModel> entities);
}
}
Then I create an implementation for Entity Framework like so
using Support.Repositories.Contracts;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Linq.Expressions;
namespace Support.Repositories
{
public class EntityRepository<TEntity> : IRepository<TEntity>
where TEntity : class
{
protected readonly DbContext Context;
protected readonly DbSet<TEntity> DbSet;
public EntityRepository(DbContext context)
{
Context = context;
DbSet = context.Set<TEntity>();
}
public TEntity Get(int id)
{
return DbSet.Find(id);
}
public IEnumerable<TEntity> GetAll()
{
return DbSet.ToList();
}
public IEnumerable<TEntity> Find(Expression<Func<TEntity, bool>> predicate)
{
return DbSet.Where(predicate);
}
public TEntity SingleOrDefault(Expression<Func<TEntity, bool>> predicate)
{
return DbSet.SingleOrDefault(predicate);
}
public void Add(TEntity entity)
{
DbSet.Add(entity);
}
public void AddRange(IEnumerable<TEntity> entities)
{
DbSet.AddRange(entities);
}
public void Remove(TEntity entity)
{
DbSet.Remove(entity);
}
public void RemoveRange(IEnumerable<TEntity> entities)
{
DbSet.RemoveRange(entities);
}
}
}
Now, I create a IUnitOfWork
to interact with repository like so
using System;
namespace App.Repositories.Contracts
{
public interface IUnitOfWork : IDisposable
{
IUserRepository Users { get; }
IAddressRepository Addresses { get; }
}
}
Then I implemented this interface for Entity Framework like this:
using App.Contexts;
using App.Repositories.Contracts;
using App.Repositories.Entity;
namespace App.Repositories
{
public class UnitOfWork : IUnitOfWork
{
private readonly AppContext _context;
public IUserRepository Users { get; private set; }
public IAddressRepository Addresses { get; private set; }
public UnitOfWork(AppContext context)
{
_context = context;
Users = new UserRepository(_context);
Addresses = new AddressRepository(_context);
}
public UnitOfWork() : this(new AppContext())
{
}
public int Save()
{
return _context.SaveChanges();
}
public void Dispose()
{
_context.Dispose();
}
}
}
I am able to use the repository like this
using(var repository = new UnitOfWork())
{
repository.Users.Add(new User(... User One ...))
repository.Save();
repository.Addresses(new Address(... Address For User One ...))
repository.Save();
repository.Users.Add(new User(... User Two...))
repository.Save();
repository.Addresses(new Address(... Address For User Two...))
repository.Save();
}
Now, I want to be able to use database transaction so only when everything is good then commit otherwise rollback.
My first take is to add a new method called BeginTransaction()
to my UnitOfWork
class. But will couple my code to Entity Framework only.
Now, I am thinking to create a new interface that provides BeginTransaction()
, Commit()
and Rollback()
method which will allow me to write an implementation for any ORM.
i.e.
namespace Support.Contracts
{
public IRepositoryDatabase
{
SomethingToReturn BeginTransaction();
void Commit();
void Rollback();
}
}
The question is how would I tie IRepositoryDatabase
back to my UnitOfWork so I can implement correctly? And what would BeginTransaction()
needs to return?
Upvotes: 19
Views: 34618
Reputation: 1
Aside all the solutions above I did it the easy way I know most of the sollutions are handy but I'm sharing it maybe it can help somebody. First in IUnitOfWork I set a rule as:
DatabaseFacade DbTransaction();
And in the implementation I just used:
public DatabaseFacade DbTransaction()
{
return _myContext.Database;
}
Now anywhere in my code I can use it easily and I can call it and simplicity enable me to keep track of things and remember what I have coded. Calling process:
var transaction = _unitOfWork.DbTransaction().BeginTransaction();
Try{
//Any operations you like
transaction.Commit();
}catch(Exception E){
//handling Exception
}
Upvotes: 0
Reputation: 11990
I think I figured out the way to do it. (I hope I did it the right way)
Here is what I have done, I hope this helps someone looking to do the same thing.
I created a new Interface like so
using System;
namespace Support.Repositories.Contracts
{
public interface IDatabaseTransaction : IDisposable
{
void Commit();
void Rollback();
}
}
Then I implemented IDatabaseTransaction
for Entity framework like so
using Support.Repositories.Contracts;
using System.Data.Entity;
namespace Support.Entity.Repositories
{
public class EntityDatabaseTransaction : IDatabaseTransaction
{
private DbContextTransaction _transaction;
public EntityDatabaseTransaction(DbContext context)
{
_transaction = context.Database.BeginTransaction();
}
public void Commit()
{
_transaction.Commit();
}
public void Rollback()
{
_transaction.Rollback();
}
public void Dispose()
{
_transaction.Dispose();
}
}
}
Then, I added a new method called BeginTransaction()
to my IUnitOfWork
contract like so
using System;
namespace App.Repositories.Contracts
{
public interface IUnitOfWork : IDisposable
{
IDatabaseTransaction BeginTransaction();
IUserRepository Users { get; }
IAddressRepository Addresses { get; }
}
}
Finally, following is my UnitOfwork
implementation for Entity
using App.Contexts;
using App.Repositories.Contracts;
using App.Repositories.Entity;
using Support.Repositories;
namespace App.Repositories
{
public class UnitOfWork : IUnitOfWork
{
private readonly AppContext _context;
public IUserRepository Users { get; private set; }
public IAddressRepository Addresses { get; private set; }
public UnitOfWork(AppContext context)
{
_context = context;
Users = new UserRepository(_context);
Addresses = new AddressRepository(_context);
}
public UnitOfWork() : this(new AppContext())
{
}
public int Save()
{
return _context.SaveChanges();
}
public void Dispose()
{
_context.Dispose();
}
public IDatabaseTransaction BeginTransaction()
{
return new EntityDatabaseTransaction(_context);
}
}
}
And here is how I consume the UnitOfWork implementation from my controller
using(var unitOfWork = new UnitOfWork())
using(var transaction = new unitOfWork.BeginTransaction())
{
try
{
unitOfWork.Users.Add(new User(... User One ...))
unitOfWork.Save();
unitOfWork.Addresses(new Address(... Address For User One ...))
unitOfWork.Save();
unitOfWork.Users.Add(new User(... User Two...))
unitOfWork.Save();
unitOfWork.Addresses(new Address(... Address For User Two...))
unitOfWork.Save();
transaction.Commit();
}
catch(Exception)
{
transaction.Rollback();
}
}
Upvotes: 32
Reputation: 880
In EF Core, Although UnitOfWork pattern is implemented internally, you can simply use IDbContextTransaction interface as follow (supposing that you use Dependency Injection):
public interface IUnitOfWork
{
int SaveChanges();
Task<int> SaveChangesAsync();
IDbContextTransaction BeginTransaction();
Task<IDbContextTransaction> BeginTransactionAsync();
IUserRepository Users { get; }
IAddressRepository Addresses { get; }
}
and the implementation:
public class UnitOfWork : IUnitOfWork, IDisposable
{
private bool _disposed;
private readonly AppDbContext _context;
public UnitOfWork(AppDbContext context,
IUserRepository userRepositpry, IAddressRepository addressRepository)
{
_context = context;
Users = userRepositpry;
Addresses = addressRepository;
}
public IUserRepository Users { get; }
public IAddressRepository Addresses { get; }
public int SaveChanges()
{
return _context.SaveChanges();
}
public async Task<int> SaveChangesAsync()
{
return await _context.SaveChangesAsync();
}
public IDbContextTransaction BeginTransaction()
{
return _context.Database.BeginTransaction();
}
public async Task<IDbContextTransaction> BeginTransactionAsync()
{
return await _context.Database.BeginTransactionAsync();
}
protected void Dispose(bool disposing)
{
if (!this._disposed)
{
if (disposing)
{
_context.Dispose();
}
}
this._disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
Usage:
public class FooService
{
private readonly IUnitOfWork _unitOfWork;
public FooService(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public void Bar()
{
using (var transaction = _unitOfWork.BeginTransaction())
{
try
{
_unitOfWork.Users.Add(new UserModel("dummy username"));
_unitOfWork.SaveChanges();
_unitOfWork.Addresses.Add(new AddressModel("dummy address"));
_unitOfWork.SaveChanges();
transaction.Commit();
}
catch (Exception)
{
transaction.Rollback();
}
}
}
}
Upvotes: 14
Reputation: 3331
While comments by Sir Rufo are correct, you did said that wanted an EF independent solution and although usually abstracting away from the ORM is an overkill, if you are still set on handling the transaction yourself you can use TransactionScope
(which was apparently the way to achieve control over the transaction before having BeginTransaction
in the context.Database
).
Please see the following article for details: https://msdn.microsoft.com/en-us/data/dn456843.aspx
Relevant bits are that you can enclose all the calls in a TransactionScope
(this will actually work out of the box in other ORMs as well):
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;
namespace TransactionsExamples
{
class TransactionsExample
{
static void UsingTransactionScope()
{
using (var scope = new TransactionScope(TransactionScopeOption.Required))
{
using (var conn = new SqlConnection("..."))
{
conn.Open();
var sqlCommand = new SqlCommand();
sqlCommand.Connection = conn;
sqlCommand.CommandText =
@"UPDATE Blogs SET Rating = 5" +
" WHERE Name LIKE '%Entity Framework%'";
sqlCommand.ExecuteNonQuery();
using (var context =
new BloggingContext(conn, contextOwnsConnection: false))
{
var query = context.Posts.Where(p => p.Blog.Rating > 5);
foreach (var post in query)
{
post.Title += "[Cool Blog]";
}
context.SaveChanges();
}
}
scope.Complete();
}
}
}
}
But do need to mind the following caveats:
There are still some limitations to the TransactionScope
approach:
Database.UseTransaction()
approach of the previous sections Upvotes: 1