Unit of Work
This page contains the recommended way of implementing a Unit of Work (UOW) object when using this library. The consolidated output of this page can be found here.
Interface
Create an inteface that would enable your UOW class to be dependency injectable.
public interface IUnitOfWork<TDbConnection>
{
TDbConnection Connection { get; }
DbTransaction Transaction { get ; }
void Begin();
void Rollback();
void Commit();
}
Ensure that this interface is accepting a generic type of the connection object.
Then, add the following properties that will hold the state of transaction.
Connection
- will hold the instance of the connection object.Transaction
- will hold the instance of the active transaction.
In addition to this, create the 3 basic transactional methods.
Begin
- is used to begin a new transaction.Commit
- is used to commit an existing transaction.Rollback
- is used to rollback an existing transaction.
Class
Create a class that implements the newly created interface.
public class RepoDbUnitOfWork : IUnitOfWork<SqlConnection>
{
private AppSettings settings;
private SqlConnection connection;
private DbTransaction transaction;
public RepoDbUnitOfWork(IOptions<AppSettings> settings)
{
this.settings = settings.Value;
}
public SqlConnection Connection { get { return connection; } }
public DbTransaction Transaction { get { return transaction; } }
}
Notice the private variable
transaction
. It is used to hold the instance of active transaction.
Create a private
method that ensures a connection instance.
private SqlConnection EnsureConnection() =>
connection ?? connection = new SqlConnection(settings.ConnectionString);
Implement the interface method that starts a new transaction.
public void Start()
{
if (transaction != null)
{
throw new InvalidOperationException("Cannot start a new transaction while the existing other one is still open.");
}
var connection = EnsureConnection();
transaction = connection.BeginTransaction();
}
Implement the interface method that commits the existing transaction.
public void Commit()
{
if (transaction == null)
{
throw new InvalidOperationException("There is no active transaction to commit.");
}
using (transaction)
{
transaction.Commit();
}
transaction = null;
}
Implement the interface method that rollbacks the existing transaction.
public void Rollback()
{
if (transaction == null)
{
throw new InvalidOperationException("There is no active transaction to rollback.");
}
using (transaction)
{
transaction.Rollback();
}
transaction = null;
}
Please note that the corresponding asynchronous methods were not implemented on this sample. You have to introduce it yourself if you wish to have it covered.
Repositories
First, implement the base interface that contains the necessary methods.
public interface IRepository<TEntity> where TEntity : class
{
void Attach(IUnitOfWork unitOfWork);
TResult Add<TResult>(TEntity entity);
int AddAll<TResult>(IEnumerable<TEntity> entities);
int Delete(object id);
int Delete(Entity entity);
TResult Merge<TResult>(TEntity entity);
TEntity Query(object id);
int Update(TEntity entity);
}
Take note of the method
Attach()
, it accepts an instance ofIUnitOfWork
object. This method will be used in the business logic to passed the currently injected (in-used) UOW class.
Then, implement the entity level interface repository.
public interface IOrderRepository : IRepository<Order>
{
...
}
Then, create a repository class that inherits either the DbRepository or BaseRepository. On this class, implement the newly created IOrderRepository
interface, the one that implements the IRepository<TEntity>
interface.
public class OrderRepository : BaseRepository<Order, SqlConnection>, IOrderRepository
{
private UnitOfWork unitOfWork;
public OrderRepository(IOptions<Settings> settings)
: base(settings.Value.ConnectionString)
{ }
}
Notice the variable
unitOfWork
. It will hold the instance ofIUnitOfWork
interface if theAttach()
method is called.
Then, implement the method Attach()
.
public void Attach(IUnitOfWork unitOfWork)
{
this.unitOfWork = unitOfWork;
}
Then, implement the methods of the interface. In each method, ensure to pass the instance of the Transaction
object that is being held by the IUnitOfWork
interface.
Add
public TResult Add<TResult>(Order entity)
{
return Insert<Order, TResult>(entity,
transaction: unitOfWork?.Transaction);
}
AddAll
public int AddAll(IEnumerable<Order> entities)
{
return InsertAll<Order>(entities,
transaction: unitOfWork?.Transaction);
}
Delete
public int Delete(object id)
{
return Delete<Order>(id,
transaction: unitOfWork?.Transaction);
}
public int Delete(Order entity)
{
return Delete<Order>(entity,
transaction: unitOfWork?.Transaction);
}
Merge
public TResult Merge<TResult>(Order entity)
{
return Merge<Order, TResult>(entity,
transaction: unitOfWork?.Transaction);
}
Query
public TResult Query(object id)
{
return Query<Order>(id,
transaction: unitOfWork?.Transaction);
}
Update
public int Update(Order entity)
{
return Update<Order>(entity,
transaction: unitOfWork?.Transaction);
}
Then, implement the same strategy when implementing the other repositories. Let us assumed, you also had created the corresponding repository for the OrderItem
entity model.
OrderItemRepository
- the repository for theOrderItem
.
Please note that the corresponding asynchronous methods were not implemented on this sample. You have to introduce it yourself if you wish to have it covered.
Dependency Injection
Register the UOW interface and class via service registration. Ensure it is on transient level.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// Registration
services.AddTransient<IUnitOfWork, RepoDbUnitOfWork>();
}
Also, register the repositories as singleton.
services.AddSingleton<IOrderRepository, OrderRepository>();
services.AddSingleton<IOrderItemRepository, OrderItemRepository>();
/* Do the same for the other repositories */
Business Logic
In your business logic, make sure that the IUnitOfWork
is being dependency injected. Let us assumed your business logic class is named SalesManager
that implements the ISalesManager
interface.
public interface ISalesManager
{
void SaveOrder(Order order,
IEnumerable<OrderItem> orderItems);
/* More business logic methods */
}
public class SalesManager : ISalesManager
{
private IUnitOfWork unitOfWork;
private IOrderRepository orderRepository;
private IOrderItemRepository orderItemRepository;
public SalesManager(IUnitOfWork unitOfWork,
IOrderRepository orderRepository,
IOrderItemRepository orderItemRepository,
/* Other repositories here */)
{
// Attach the UOW
orderRepository.Attach(unitOfWork);
orderItemRepository.Attach(unitOfWork);
/* Do the same for the other repositories */
// Set the variables
this.unitOfWork = unitOfWork;
this.orderRepository = orderRepository;
this.orderItemRepository = orderItemRepository;
/* Do the same for the other repositories */
}
}
In any of your business logic methods, ensure that you properly call the Begin()
, Commit()
and Rollback()
methods of the unitOfWork
variable.
Below is a sample method that saves the order and its order item.
public void SaveOrders(Order order,
IEnumerable<OrderItem> orderItems)
{
// Start the UOW
unitOfWork.Begin();
try
{
// Call the repository methods
var orderId = orderRepository.Save(order);
orderItems = orderItems
.AsList()
.ForEach(e => e.OrderId = orderId);
orderItemRepository.SaveAll(orderItems);
// Commit
unitOfWork.Commit();
}
catch
{
// Rollback
unitOfWork.Rollback();
throw;
}
}
Lastly, do not forget to as well inject the business logic as transient.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// Registration
services.AddTransient<ISalesManager, SalesManager>();
}
After the registration of the business logic, you can always pass it on the constructor of your controllers.
When working with UOW, you are obligated to always implement your own repository methods and call the repository methods underneath and explicitly pass the
transaction
object from the UOW object. You should avoid calling the normal operations of the repository directly as they are not a part of the transaction context. We in RepoDB are considering implementing the UOW in the near future versions, specifically theAttach()
method mentioned on this page.