- The repository and unit of work patterns are intended to create an abstraction layer between the data access layer and the business logic layer of an application.
- Implementing these patterns can help insulate your application from changes in the data store and can facilitate automated unit testing.
- When implementing repository classes, create a class and an interface per each entity type.
- When instantiating the repository in controller, use the interface so that the controller will accept a reference to any object that implements the repository interface.
- When the controller runs under a web server, it receives a repository that works with the EF.
- When the controller runs under a unit test, it receives a repository that works with data stored in a way that you can easily manipulate for testing.
- The Unit of Work pattern maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems.
- The Unit of Work class coordinates the work of multiple repositories by creating a single database context class shared by all of them.
- There are many ways to implement the repository and unit of work patterns:
- You can use repository classes with or without a unit of work class.
- You can implement a single repository for all entities, or one for each type.
- In this case, you can use separate classes, a generic base class and derived classes, or an abstract base class and derived classes.
- You can include business logic in your repository or restrict it to data access logic.
- You can also build an abstraction layer into your database context class by using
IDbSet
interfaces there instead of DbSet
types for your entity sets.
public interface IStudentRepository : IDisposable
{
IEnumerable<Student> GetStudents();
Student GetStudentByID(int studentId);
void InsertStudent(Student student);
void DeleteStudent(int studentID);
void UpdateStudent(Student student);
void Save();
}
Generic Repository & Unit of Work
- Creating a repository class for each entity type could result in a lot of redundant code, and it could result in partial updates.
- One way to minimize redundant code is to use a generic repository.
- To ensure that all repositories use the same database context (and thus coordinate all updates) you can use a unit of work class.
public class GenericRepository<TEntity>
where TEntity : class
{
internal SchoolContext context;
internal DbSet<TEntity> dbSet;
public GenericRepository(SchoolContext context)
{
this.context = context;
this.dbSet = context.Set<TEntity>();
}
public virtual IEnumerable<TEntity> Get(
Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
string includeProperties = "")
{
IQueryable<TEntity> query = dbSet;
if (filter != null)
query = query.Where(filter);
foreach (var includeProperty in includeProperties.Split
(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
query = query.Include(includeProperty);
if (orderBy != null)
return orderBy(query).ToList();
else return query.ToList();
}
public virtual TEntity GetByID(object id) => return dbSet.Find(id);
public virtual void Insert(TEntity entity) => dbSet.Add(entity);
public virtual void Delete(object id)
{
TEntity entityToDelete = dbSet.Find(id);
Delete(entityToDelete);
}
public virtual void Delete(TEntity entityToDelete)
{
if (context.Entry(entityToDelete).State == EntityState.Detached)
dbSet.Attach(entityToDelete);
dbSet.Remove(entityToDelete);
}
public virtual void Update(TEntity entityToUpdate)
{
dbSet.Attach(entityToUpdate);
context.Entry(entityToUpdate).State = EntityState.Modified;
}
}
- The unit of work class serves one purpose: to make sure that when you use multiple repositories, they share a single database context.
- That way, when a unit of work is complete you can call the
SaveChanges
method on that instance of the context
- Therefore, you can be assured that all related changes will be coordinated.
- All that the class needs is a
Save
method and a property for each repository.
- Each repository property returns a repository instance that has been instantiated using the same database context instance as the other repository instances.
public class UnitOfWork : IDisposable
{
private DbContext _Context;
private GenericRepository<Department> _DepartmentRepository;
public GenericRepository<Department> DepartmentRepository =>
_DepartmentRepository ?? new GenericRepository<Department>(_Context);
private GenericRepository<Course> _CourseRepository;
public GenericRepository<Course> CourseRepository =>
_CourseRepository ?? new GenericRepository<Course>(_Context);
public UnitOfWork(DbContext context) => _Context = context;
public void Save() => _Context.SaveChanges();
private bool _Disposed = false;
protected virtual void Dispose(bool disposing)
{
if (!_Disposed && disposing)
_Context.Dispose();
_Disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
- Asynchronous Repositories
Implement Query Objects
- One problem with repository interfaces is that they violate Interface Segregation Principle.
- They expose full set of CRUD operations even for entities for which e.g. deleting does not make sense.
- The idea behind repositories is that they should be used as a base interfaces for the custom repositories.
- Therefore, the command handlers and services should decide what methods are needed for each business domain.
- Instead of using repositories, you consume query objects in your services.
- Usually too big repositories are accompanied by huge services and overgrown entities.
- You can slim down both your repos and services by using CQRS-light.
- It differs from full-blown CQRS by using exactly the same database tables for both reads and writes.
- When doing CQRS-light we can use the same ORM framework for both reading and writing data.
- You can slowly migrate to real CQRS only in the part of application that need it.
- The key principles of CQRS-light are:
- Split all user actions into two categories.
- In the first put all actions that can modify the state of the system (like creating a new order in an e-commerce app).
- This category represents commands (writes).
- In the second category put all actions that do not modify state of the system (like viewing an order details).
- This represents one queries (reads).
- Only commands can change state of the system.
- Query handlers do not use repositories to access data.
- They access DB using whatever technology they want.
- Usual configurations include a single ORM on both reads and writes, ORM for writes and micro-ORM (Dapper) for reads, or ORM for writes and raw SQL for reads.
- Command handlers can only use repositories to access and modify data.
- Command handlers should not call query handlers to fetch data from database.
- If a command handler needs to execute a complex query and this query can be answered by a query handler you should duplicate this query logic and put it in both query handler and in a repository method (read and write sides must be separated).
- Query handlers are tested only using integration tests.
- For command handlers you will have unit and optionally integration tests.
- Repositories will be tested using integration tests.
- CQRS even in the “light” version is a huge topic and deserves a blog post of it’s own.
- MediatR library is a good starting point if you want to find out more about CQRS-light approach.
- The following code contains a piece of business logic that describes what it means for an order to be active.
- Usually ORM’s prevent us from encapsulating such pieces of logic into a separate properties like
IsActive
.
public IEnumerable<Order> FindActiveOrders() => base.FindAll()
.Where(order => order.State != OrderState.Closed && order.State != OrderState.Canceled)
.ToList();
- What we need here is the specification pattern.
- Our query method when we use the specification pattern should look similar to:
public IEnumerable<Order> FindActiveOrders() =>
base.FindBySpec(new ActiveOrders()).ToList();