After working again on codebase where Entity Framework Core was used through repository and unit of work patterns I decided to write eye-opener post for next (and maybe even current and previous) generations about what Entity Framework has to offer in the light of these to patterns. In many cases we don’t have to move away from database context approach but stick with it even more than we first planned. Here’s how many useless repositories and units of work born and here’s also how to avoid these and go with implementations offered by Entity Framework Core.
Typical repository implementation on Entity Framework Core
It was one of those days when project with legacy code landed on my table and for sure there were load of repository interfaces and classes. At first sight the code looked all okay – well organized, pretty clean, no single mess. But repositories warned me – there are landmines under the carpet.
Let’s take one repository from the code and dissect it a little bit. First let’s see how things were wired up.
Usually there’s generic interface and abstract base class for repositories.
public interface IRepository<T> where T : Entity
{
T Get(Guid id);
IList<T> List();
IList<T> List(Expression<Func<T, bool>> expression);
void Insert(T entity);
void Update(T entity);
void Delete(T entity);
}
public abstract class BaseRepository<T> : IRepository<T> where T : Entity
{
private readonly MyDbContext _dataContext;
public BaseRepository(MyDbContext dataContext)
{
_dataContext = dataContext;
}
public void Delete(T entity)
{
_dataContext.Set<T>().Remove(entity);
_dataContext.SaveChanges();
}
public T Get(Guid id)
{
return _dataContext.Set<T>().Find(id);
}
public void Insert(T entity)
{
_dataContext.Set<T>().Add(entity);
_dataContext.SaveChanges();
}
public IList<T> List()
{
return _dataContext.Set<T>().ToList();
}
public IList<T> List(Expression<Func<T, bool>> expression)
{
return _dataContext.Set<T>().Where(expression).ToList();
}
public void Update(T entity)
{
_dataContext.Entry<T>(entity).State = EntityState.Modified;
_dataContext.SaveChanges();
}
}
Every repository has its own interface. It’s used with dependency injection so it’s easier to change implementation if it’s (ever) needed.
public interface IInvoiceRepository : IRepository<Invoice>
{
List<Invoice> GetOverDueInvoice();
List<Invoice> GetCreditInvoices();
}
public class InvoiceRepository : BaseRepository<Invoice>, IInvoiceRepository
{
public List<Invoice> GetCreditInvoices()
{
// return list of credit invoices
}
public List<Invoice> GetOverDueInvoice()
{
// return list of over-due invoices
}
}
Those familiar with repository pattern find it probably all okay. Nice and clean generalization, code reusing, dependency injection etc – like a good arhictecture of code should be.
Finding a landmine under carpet
Now let’s take a good look at how operations with repositories work. Let’s take update and delete methods.
public void Delete(T entity)
{
_dataContext.Set<T>().Remove(entity);
_dataContext.SaveChanges();
}
public void Update(T entity)
{
_dataContext.Entry<T>(entity).State = EntityState.Modified;
_dataContext.SaveChanges();
}
What’s wrong here or why do I want to whine about this code? Well.. SaveChanges() method sends all changes to database. Not only changes related to given entity are saved but also changes to all other entities. If we have service class using this repository and it implements some more complex use case where update and delete are called multiple times we are screwed. If one update or delete fails then there’s no way to roll back all changes done to database before.
Unit of Work to help?
We can move SaveChanges() method out to unit of work container and this way we can make changes we need and save them when we commit unit of work.
public class UnitOfWork
{
private readonly MyDbContext _dataContext;
public UnitOfWork(MyDbContext dataContext)
{
_dataContext = dataContext;
}
public void Save()
{
_dataContext.SaveChanges();
}
}
This is how we can use our unit of work in imaginary service class.
public class InvoiceService
{
private readonly IInvoiceRepository _invoiceRepository;
private readonly UnitOfWork _unitOfWork;
public InvoiceService(IInvoiceRepository invoiceRepository, UnitOfWork unitOfWork)
{
_invoiceRepository = invoiceRepository;
_unitOfWork = unitOfWork;
}
public void CreditInvoice(Guid id)
{
var invoice = _invoiceRepository.Get(id);
Invoice creditInvoice;
// create credit invoice
_invoiceRepository.Insert(creditInvoice);
_unitOfWork.Save();
}
}
But still things doesn’t get better. We have now additional instance to inject to service classes and our repositories are just dummy wrappers to Entity Framework database context.
Disposing custom repositories
After thinking hard if there’s any reason to keep unit of work and repositories in code I found out that it’s not worth it. DbContext class provides us with mix of unit of work and repositories. We don’t need repository classes that are just containers to DbSet<Something> properties of DbContext. Also we don’t need new class to wrap call to SaveChanges();
Even better – custom database context is class written by us. We extend DbContext of EF Core which means we have pretty much free hands on building up the custom context. It can work as unit of context too. And if we need it can work as one-shot-repository too.
public class LasteDbContext : DbContext
{
public LasteDbContext(DbContextOptions<LasteDbContext> options)
: base(options)
{
}
public DbSet<Comment> Comments { get; set; }
public DbSet<Invoice> Invoices { get; set; }
public DbSet<InvoiceLine> InvoiceLines { get; set; }
private IDbContextTransaction _transaction;
public void BeginTransaction()
{
_transaction = Database.BeginTransaction();
}
public void Commit()
{
try
{
SaveChanges();
_transaction.Commit();
}
finally
{
_transaction.Dispose();
}
}
public void Rollback()
{
_transaction.Rollback();
_transaction.Dispose();
}
}
One can point out now that we are operating on concrete type and it’s not good for dependency injection. Okay, let’s apply interface and call it as IDataContext.
public interface IDataContext
{
DbSet<Comment> Comments { get; set; }
DbSet<Invoice> Invoices { get; set; }
DbSet<InvoiceLine> InvoiceLines { get; set; }
void BeginTransaction();
void Commit();
void Rollback();
}
So, no blasphemies of technical design anymore. We can inject instance of IDataContext to our services classes, commands or controllers and use just this one class with simple interface.
It’s not hard to use IDataContext with other object-relational mappers like NHibernate. My blog post NHibernate on ASP.NET Core demonstrates how to imitate DbContext-like mapper interface for NHibernate.
But what about custom queries?
One benefit of repository classes was keeping custom querying methods there. It was ideal place for this as those methods were implemented close to where data is handled. Not a perfect approach if there are many queries but still something.
public class InvoiceRepository : BaseRepository<Invoice>, IInvoiceRepository
{
public List<Invoice> GetCreditInvoices()
{
// return list of credit invoices
}
public List<Invoice> GetOverDueInvoice()
{
// return list of over-due invoices
}
}
Adding such methods to custom database context will grow it huge. So huge that we need some better solution. We have some options to consider:
- Query classes – classes where stored queries are implemented. Downside is the same as with repositories – the more querying methods you have the bigger the class grows. In one point you still need some better solution. Of course, in C# we can always go with partial classes and group queries to separate files.
- Query object – pattern introduced by Martin Fowler in his famous Patterns of Enterprise Application Architecture book. Query object is object hierarchy to build query that is transformed to SQL (or whatever the back-end data store is). It works if we don’t have too many conditions to consider.
- Extension methods – it’s also possible to have a static class per entity type where queries are defined as extension methods to their corresponding DbSet. It can be also something general like my Entity Framework paging example.
- Applying specification pattern – I have seen some solution where specifications to include child objects and collection and specifications to where-clause and order-by clause were defined. It’s not a good solution in long run as over time also queries tend to grow and we will easily end up with specification hell.
In practice we often run sprint with growing complexities in database, amounts of data and expenses to host database. Sooner or later we want to optimize queries to ask only the data we need. It means our LINQ queries get more complex because of calls to Include() method. Sometimes we need change tracking on EF Core query and sometimes we don’t.
I have seen it happening. In one project after all small optimizations and tweaks to LINQ queries we had some considerable wins on database performance but we ended up with lengthy queries. Queries were mostly long and ugly enough to not pile these together in one class. As most of queries were model specific we added factory methods to models. For some models we created factory class to not bloat model itself with details about how to build it. In both cases the queries were directly hosted in factories because there was no point to add some additional layer to host it all. We had also shared queries in implemented as extension methods in data access layer. But we had no single custom repository or unit of work class. Still we managed to have nice and understandable code structure.
Wrapping up
It’s tempting to create unit of work and repository interfaces and classes for Entity Framework Core because everybody is doing so. Still we have database specific implementations for both of these available with DbContext class.
- Unit of Work – use database transactions to avoid queries accidentally modify data before transaction is committed and be careful with SaveChanges() method of database context.
- CRUD – use DBSet<Something> properties of database context. These sets have all methods needed to modify and query the data in database.
- Stored queries – general ones can be part of database context and specialized ones can live in query classes, factories or extension methods.
There’s no actual need to implement your own unit of work and repositories if they just wrap DbContext functionalities without adding any new value. Simple idea – use what you already have.
No comments:
Post a Comment