When working with Entity Framework Core, combining the Generic Repository Pattern with the Unit of Work Pattern helps keep data access clean, testable, and transactional.
This guide walks you step-by-step with code snippets.
🔑 Interfaces
We start by defining two interfaces:
- IGenericRepository → Generic data access contract
- IUnitOfWork → Manages repositories and transactions
public interface IGenericRepository<T> where T : class
{
Task<T> GetByIdAsync(object id);
Task<IEnumerable<T>> GetAllAsync();
Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate);
Task AddAsync(T entity);
void Update(T entity);
void Remove(T entity);
IQueryable<T> Query();
}
public interface IUnitOfWork : IDisposable
{
IGenericRepository<T> Repository<T>() where T : class;
Task<int> SaveChangesAsync();
Task BeginTransactionAsync();
Task CommitAsync();
Task RollbackAsync();
}
⚙️ Implementations
🔹 Generic Repository
public class GenericRepository<T> : IGenericRepository<T> where T : class
{
protected readonly DbContext _context;
protected readonly DbSet<T> _dbSet;
public GenericRepository(DbContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_dbSet = _context.Set<T>();
}
public virtual async Task<T> GetByIdAsync(object id) =>
await _dbSet.FindAsync(id);
public virtual async Task<IEnumerable<T>> GetAllAsync() =>
await _dbSet.ToListAsync();
public virtual async Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate) =>
await _dbSet.Where(predicate).ToListAsync();
public virtual async Task AddAsync(T entity) =>
await _dbSet.AddAsync(entity);
public virtual void Update(T entity)
{
_dbSet.Attach(entity);
_context.Entry(entity).State = EntityState.Modified;
}
public virtual void Remove(T entity) =>
_dbSet.Remove(entity);
public virtual IQueryable<T> Query() =>
_dbSet.AsQueryable();
}
🔹 Unit of Work
public class UnitOfWork : IUnitOfWork
{
private readonly DbContext _context;
private readonly Dictionary<Type, object> _repositories;
private IDbContextTransaction _transaction;
private bool _disposed;
public UnitOfWork(DbContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_repositories = new Dictionary<Type, object>();
}
public IGenericRepository<T> Repository<T>() where T : class
{
var type = typeof(T);
if (_repositories.ContainsKey(type))
return (IGenericRepository<T>)_repositories[type];
var repo = new GenericRepository<T>(_context);
_repositories[type] = repo;
return repo;
}
public async Task<int> SaveChangesAsync() =>
await _context.SaveChangesAsync();
public async Task BeginTransactionAsync()
{
if (_transaction == null)
_transaction = await _context.Database.BeginTransactionAsync();
}
public async Task CommitAsync()
{
try
{
await _context.SaveChangesAsync();
if (_transaction != null)
{
await _transaction.CommitAsync();
await _transaction.DisposeAsync();
_transaction = null;
}
}
catch
{
await RollbackAsync();
throw;
}
}
public async Task RollbackAsync()
{
if (_transaction != null)
{
await _transaction.RollbackAsync();
await _transaction.DisposeAsync();
_transaction = null;
}
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_transaction?.Dispose();
_context.Dispose();
}
_disposed = true;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
🗂️ Example DbContext & Entities
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Product> Products { get; set; }
public DbSet<Order> Orders { get; set; }
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Order
{
public int Id { get; set; }
public DateTime CreatedAt { get; set; }
}
🔌 Registering with Dependency Injection
Add to Program.cs / Startup.cs:
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")));
services.AddScoped<IUnitOfWork, UnitOfWork>();
// Optional: If you want to expose repositories directly
// services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>));
🚀 Usage Example (Service Layer)
public class OrderService
{
private readonly IUnitOfWork _uow;
public OrderService(IUnitOfWork uow)
{
_uow = uow;
}
public async Task CreateOrderAsync(Order order, Product productToAdd)
{
await _uow.BeginTransactionAsync();
try
{
var orderRepo = _uow.Repository<Order>();
var productRepo = _uow.Repository<Product>();
await productRepo.AddAsync(productToAdd);
await orderRepo.AddAsync(order);
await _uow.CommitAsync(); // Save + commit transaction
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
}
✅ Notes & Best Practices
-
Why Unit of Work + Generic Repository?
- Centralizes
SaveChanges
in one place - Groups multiple repository operations into a single transaction
- Centralizes
-
Async-first → Use EF Core async APIs to avoid blocking
-
Don’t over-abstract → For complex queries, create custom repositories (e.g.,
IProductRepository
) -
Transactions → Use
BeginTransactionAsync
,CommitAsync
,RollbackAsync
for multi-step atomic operations -
Lifetime → Register Unit of Work as Scoped (per HTTP request)
-
Testing → Swap DbContext with InMemory provider or mock repositories for unit tests
👉 With this setup, you get clean repository access, transaction control, and easy testing. Perfect for medium-to-large .NET applications using EF Core.
Here’s a blogger-friendly, structured article version of your Unit of Work + Generic Repository example 👇
🏗️ Unit of Work + Generic Repository Pattern in .NET (EF Core)
When working with Entity Framework Core, combining the Generic Repository Pattern with the Unit of Work Pattern helps keep data access clean, testable, and transactional.
This guide walks you step-by-step with code snippets.
🔑 Interfaces
We start by defining two interfaces:
- IGenericRepository → Generic data access contract
- IUnitOfWork → Manages repositories and transactions
public interface IGenericRepository<T> where T : class
{
Task<T> GetByIdAsync(object id);
Task<IEnumerable<T>> GetAllAsync();
Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate);
Task AddAsync(T entity);
void Update(T entity);
void Remove(T entity);
IQueryable<T> Query();
}
public interface IUnitOfWork : IDisposable
{
IGenericRepository<T> Repository<T>() where T : class;
Task<int> SaveChangesAsync();
Task BeginTransactionAsync();
Task CommitAsync();
Task RollbackAsync();
}
⚙️ Implementations
🔹 Generic Repository
public class GenericRepository<T> : IGenericRepository<T> where T : class
{
protected readonly DbContext _context;
protected readonly DbSet<T> _dbSet;
public GenericRepository(DbContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_dbSet = _context.Set<T>();
}
public virtual async Task<T> GetByIdAsync(object id) =>
await _dbSet.FindAsync(id);
public virtual async Task<IEnumerable<T>> GetAllAsync() =>
await _dbSet.ToListAsync();
public virtual async Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate) =>
await _dbSet.Where(predicate).ToListAsync();
public virtual async Task AddAsync(T entity) =>
await _dbSet.AddAsync(entity);
public virtual void Update(T entity)
{
_dbSet.Attach(entity);
_context.Entry(entity).State = EntityState.Modified;
}
public virtual void Remove(T entity) =>
_dbSet.Remove(entity);
public virtual IQueryable<T> Query() =>
_dbSet.AsQueryable();
}
🔹 Unit of Work
public class UnitOfWork : IUnitOfWork
{
private readonly DbContext _context;
private readonly Dictionary<Type, object> _repositories;
private IDbContextTransaction _transaction;
private bool _disposed;
public UnitOfWork(DbContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_repositories = new Dictionary<Type, object>();
}
public IGenericRepository<T> Repository<T>() where T : class
{
var type = typeof(T);
if (_repositories.ContainsKey(type))
return (IGenericRepository<T>)_repositories[type];
var repo = new GenericRepository<T>(_context);
_repositories[type] = repo;
return repo;
}
public async Task<int> SaveChangesAsync() =>
await _context.SaveChangesAsync();
public async Task BeginTransactionAsync()
{
if (_transaction == null)
_transaction = await _context.Database.BeginTransactionAsync();
}
public async Task CommitAsync()
{
try
{
await _context.SaveChangesAsync();
if (_transaction != null)
{
await _transaction.CommitAsync();
await _transaction.DisposeAsync();
_transaction = null;
}
}
catch
{
await RollbackAsync();
throw;
}
}
public async Task RollbackAsync()
{
if (_transaction != null)
{
await _transaction.RollbackAsync();
await _transaction.DisposeAsync();
_transaction = null;
}
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
_transaction?.Dispose();
_context.Dispose();
}
_disposed = true;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
🗂️ Example DbContext & Entities
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Product> Products { get; set; }
public DbSet<Order> Orders { get; set; }
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Order
{
public int Id { get; set; }
public DateTime CreatedAt { get; set; }
}
🔌 Registering with Dependency Injection
Add to Program.cs / Startup.cs:
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")));
services.AddScoped<IUnitOfWork, UnitOfWork>();
// Optional: If you want to expose repositories directly
// services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>));
🚀 Usage Example (Service Layer)
public class OrderService
{
private readonly IUnitOfWork _uow;
public OrderService(IUnitOfWork uow)
{
_uow = uow;
}
public async Task CreateOrderAsync(Order order, Product productToAdd)
{
await _uow.BeginTransactionAsync();
try
{
var orderRepo = _uow.Repository<Order>();
var productRepo = _uow.Repository<Product>();
await productRepo.AddAsync(productToAdd);
await orderRepo.AddAsync(order);
await _uow.CommitAsync(); // Save + commit transaction
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
}
✅ Notes & Best Practices
-
Why Unit of Work + Generic Repository?
- Centralizes
SaveChanges
in one place - Groups multiple repository operations into a single transaction
- Centralizes
-
Async-first → Use EF Core async APIs to avoid blocking
-
Don’t over-abstract → For complex queries, create custom repositories (e.g.,
IProductRepository
) -
Transactions → Use
BeginTransactionAsync
,CommitAsync
,RollbackAsync
for multi-step atomic operations -
Lifetime → Register Unit of Work as Scoped (per HTTP request)
-
Testing → Swap DbContext with InMemory provider or mock repositories for unit tests
👉 With this setup, you get clean repository access, transaction control, and easy testing. Perfect for medium-to-large .NET applications using EF Core.
Comments
Post a Comment