.NET Core 6: Dependency Injection, Repository Pattern and Unit of Work
A repository pattern exists to add another layer of abstraction between the application logic and the data layer, using dependency injection to decouple the two. It's not my preferred way of doing things, because of the effort and complexity involved, and I'm certainly not the only person who initially struggled to understand the concepts behind it. Plus, it's apparent that Entity Framework makes heavy use of that dependency injection already. But a Repository Pattern is widely considered - by software engineers far more experienced than I am - best practice for engineering services that deal with critical data.
The repository pattern is implemented generally the same way dependency injection is in .NET Core. What I've ended up with is:
An interface that's called when a service is required.
Classes that implement the service, basically performing the data access functions. These will be generic repository classes that are extended elsewhere by repository classes specific to the Entity Framework model.
Code in Program.cs to register the service.
Constructor for the service in the HomeController class.
The repository should decouple the business logic from Entity Framework and the data access layer, and prevent partial writes that affect the integrity of the database. It is also useful for testing, as we can swap one data access layer with another.
In my project, all the repository and Unit of Work code is within the DotNetCore6.Data namespace.
Add IGeneric Repository Interface
This is essentially a boilerplate interface, and will be exposed to the application controller methods. It points to methods in the Generic class that implement fetch and write operations.
public interface IGenericRepository<T> where T : class
{
T GetById(int id);
IEnumerable<T> GetAll();
IEnumerable<T> Find(Expression<Func<T, bool>> expression);
void Add(T entity);
void AddRange(IEnumerable<T> entities);
void Remove(T entity);
void RemoveRange(IEnumerable<T> entities);
}
Add a Generic Repository Class
The GenericRepository class is where generic services exposed by the interface are implemented, and it uses the ApplicationDbContext directly. It passes ApplicationDbContext to a class and constructs it as '_context', just as I did in the HomeController referred to in an earlier post.
Notice that this is a generic repository, which isn't specific to anything in the Entity Framework model. This means I'm not duplicating the same data access code for each model class - though the code would likely be easier to follow if I did. Instead we'll declare and extend this class elsewhere, as needed.
public class GenericRepository<T> : IGenericRepository<T> where T : class
{
protected readonly ApplicationDbContext _context;
public GenericRepository(ApplicationDbContext context)
{
_context = context;
}
...
public void Add(T entity)
{
_context.Set<T>().Add(entity);
}
public IEnumerable<T> GetAll()
{
return _context.Set<T>().ToList();
}
public T GetById(int id)
{
return _context.Set<T>().Find(id);
}
...
}
The above sections can be considered (and used as) boilerplate code, though I'll likely be adding several other methods for other read/write/update functions later on. From this point, we need to extend this generic repository for working with the Entity Framework model classes.
Extend the Generic Repository for the Entity Framework Classes
In this section of code (ComputersRepository.cs), we create a non-generic repository for each model class. In this case, IComputersRepository will extend IGenericRepository. As we can see, I've also placed its implementation, ComputersRepository, in the same file.
public interface IComputersRepository : IGenericRepository<Computer>
{
IEnumerable<Computer> GetComputers(int count);
}
public class ComputersRepository : GenericRepository<Computer>, IComputersRepository
{
public ComputersRepository(ApplicationDbContext context) : base(context)
{
}
public IEnumerable<Computer> GetComputers(int count)
{
return _context.Computers.ToList();
}
}
Unit of Work Classes
Next I wanted to add a Unit of Work pattern to the project. Instead of using _context.Computers or _context.Labs, as I did originally, I'm defining them as interfaces within IUnitOfWork, containing an interface for each model class.
public interface IUnitOfWork : IDisposable
{
IComputersRepository Computers { get; }
ILabsRepository Labs { get; }
IUsersRepository Users { get; }
int Complete();
}
Only when whichever operation is completed is Complete() called, which in turn saves changes to the DbContext. This should prevent partial updates that might corrupt the source data.
public class UnitOfWork : IUnitOfWork
{
private readonly ApplicationDbContext _context;
public UnitOfWork(ApplicationDbContext context)
{
_context = context;
Computers = new ComputersRepository(_context);
Labs = new LabsRepository(_context);
Users = new UsersRepository(_context);
}
public IComputersRepository Computers { get; private set; }
public ILabsRepository Labs { get; private set; }
public IUsersRepository Users { get; private set; }
public int Complete()
{
return _context.SaveChanges();
}
public void Dispose()
{
_context.Dispose();
}
}
At this point, the project will include something like the following for the Repository Pattern, Unit of Work and Entity Framework code:
|-- Data
|-- dbcontext.cs
|-- UnitOfWork.cs
|-- Models
|-- Computer.cs
|-- Lab.cs
|-- User.cs
|-- Repository
|-- ComputersRepository.cs
|-- GenericRepository.cs
|-- IGenericRepository.cs
|-- LabsRepository.cs
|-- UsersRepository.cs
Registering the Services
When registering the interfaces in Program.cs in .NET Core 6, we must use 'builder.Services' instead of just the 'services' namespace:
builder.Services.AddTransient(typeof(IGenericRepository<>), typeof(GenericRepository<>));
builder.Services.AddTransient<IComputersRepository, ComputersRepository>();
builder.Services.AddTransient<ILabsRepository, LabsRepository>();
builder.Services.AddTransient<IUsersRepository, UsersRepository>();
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
Using the Repository Pattern in the Controller
Swap ApplicationDbContext with a constructor for the Unit of Work service:
private readonly IUnitOfWork _unitOfWork;
public HomeController(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
Accessing data with LINQ:
var model = _unitOfWork.Users.GetById(id);
var model = _unitOfWork.Labs.GetAll().Where(m => m.Name.Contains(searchTerm)).ToList();