
The unit of work and generic repository pattern are essential tools that can be seamlessly integrated into any ASP.NET Core project. Their versatility and applicability make them invaluable for us as developers aiming to enhance code organization, scalability, and reusability. In this tutorial, we will explore the implementation of unit of work with the generic repository pattern in C#. By mastering these patterns, we’ll be equipped to build robust and maintainable ASP.NET Core applications with ease.
Unit of Work: Grouping Transactions for Consistency and Atomicity
- The unit of work is a design pattern that allows us to group multiple database operations into a single transactional scope.
- It offers a cohesive approach to handling complex business logic that spans across multiple repositories, ensuring consistency and atomicity.
- By employing the unit of work pattern, we gain better control over our database interactions, enabling us to commit or roll back changes in a unified manner.
- This pattern enhances reliability, data consistency, and error handling within the application, resulting in more robust and scalable solutions.
Repository Pattern: Encapsulating Data Access Logic
- The repository pattern is an architectural pattern that promotes the separation of concerns by encapsulating data access logic within dedicated repository classes.
- Repositories act as mediators between the data access layer and the rest of the application, shielding the latter from the complexities of data storage mechanisms.
- The use of generic repositories aligns perfectly with the DRY (Don’t Repeat Yourself) principle, as it eliminates the need for separate repositories for each entity.
- Generic repositories reduce code duplication and maintenance efforts by providing a standardized approach to interact with various entity types.
- This pattern ensures a consistent and unified interface for common CRUD (Create, Read, Update, Delete) operations on different entity types, streamlining development and improving code readability.
By combining the unit of work and generic repository pattern in our ASP.NET Core projects, we can achieve transactional integrity, separation of concerns, and reduced code repetition. This powerful combination empowers us as developers to build maintainable, scalable, and highly efficient applications. In our upcoming tutorial, we will explore an implementation of the unit of work and generic repository pattern in C#. Next, we will dive into our step-by-step guidance on integrating these patterns into our ASP.NET Core projects and unlocking the full potential of our application development.
Generic Repository
Firstly, we’re going to create our generic repository interface and class:
public interface IGenericRepository<T> where T : class { Task<IEnumerable<T>> GetAsync(Expression<Func<T, bool>>? filter = null, Func<IQueryable<T>, IOrderedQueryable<T>>? orderBy = null, string includeProperties = ""); Task<T> GetByIdAsync(object id); Task AddAsync(T obj); void Update(T obj); Task RemoveAsync(object id); } public class GenericRepository<T> : IGenericRepository<T> where T : class { private ApplicationDbContext _context; private DbSet<T> _dbSet; public GenericRepository(ApplicationDbContext context) { _context = context; _dbSet = _context.Set<T>(); } public virtual async Task<IEnumerable<T>> GetAsync( Expression<Func<T, bool>>? filter = null, Func<IQueryable<T>, IOrderedQueryable<T>>? orderBy = null, string includeProperties = "") { IQueryable<T> 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 await orderBy(query).ToListAsync(); else return await query.ToListAsync(); } public async Task<T> GetByIdAsync(object id) { var element = await _dbSet.FindAsync(id); return element!; } public async Task AddAsync(T obj) => await _dbSet.AddAsync(obj); public void Update(T obj) => _dbSet.Update(obj); public async Task RemoveAsync(object id) { T existing = await GetByIdAsync(id); _dbSet.Remove(existing!); } }
- Our generic repository represents a standard way of performing CRUD operations on a given entity as defined by the T parameter.
- Our GetAsync method allows us to retrieve entities based on optional filtering, ordering, and inclusion of related properties.
- Note that the unit of work will be responsible for committing changes to the underlying database context, which is why we won’t see SaveChanges in our repositories.
Implementing Generic Repository with Specific Methods
Next, we will implement our generic repository, showcasing its flexibility and extensibility and also demonstrate how additional specific methods can be easily added to the generic repository.
Suppose we are building a doctor’s appointment system and we have an entity for Patient, we could create the following PatientRepository:
public interface IPatientRepository : IGenericRepository<Patient> { } public class PatientRepository : GenericRepository<Patient>, IPatientRepository { private readonly ApplicationDbContext _context; public PatientRepository(ApplicationDbContext context) : base(context) { _context = context; } }
We could create an AppointmentRepository in the same way:
public interface IAppointmentRepository : IAppointmentRepository<Appointment> { } public class AppointmentRepository : GenericRepository<Appointment>, IAppointmentRepository { private readonly ApplicationDbContext _context; public AppointmentRepository(ApplicationDbContext context) : base(context) { _context = context; } }
We can also add methods to specific repositories where we need to:
public interface IPatientRepository : IGenericRepository<Patient> { IEnumerable<Patient> GetAllForPatientViewModel(); } public class PatientRepository : GenericRepository<Patient>, IPatientRepository { private readonly ApplicationDbContext _context; public PatientRepository(ApplicationDbContext context) : base(context) { _context = context; } public IEnumerable<Patient> GetAllForPatientViewModel() { return _context .Patients .Select(gp => new Patient { Id = p.Id, FirstName = p.FirstName!, LastName = p.LastName! Phone = p.Phone! }); } }
Adding Repositories to a Unit of Work
Now that we have our generic repository and repository implementations, we can now bring our repositories together into a UnitOfWork class:
public interface IUnitOfWork { IPatientRepository Patients { get; } IAppointmentRepository Appointments { get; } Task<int> SaveAsync(); } public class UnitOfWork : IUnitOfWork { private readonly ApplicationDbContext _context; public IPatientRepository Patients { get; } public IAppointmentRepository Appointments { get; } public UnitOfWork(ApplicationDbContext context, IPatientRepository patientRepository, IAppointmentRepository appointmentRepository) { _context = context; Patients = patientRepository; Appointments = appointmentRepository; } public Task<int> SaveAsync() => _context.SaveChangesAsync(); }
How to use Unit of Work
Imagine a scenario where we can create an appointment and update patient details simultaneously on a single screen. This scenario requires two separate database transactions—one for creating the appointment and another for updating the patient. By leveraging the unit of work pattern, we can seamlessly combine these operations into a single transaction. This ensures that both actions either succeed or fail together, maintaining data consistency and integrity:
public interface IAppointmentService { async Task CreateAppointment(CreateAppointmentDTO createAppointmentDTO); } public class AppointmentService : IAppointmentService { IUnitOfWork _work; public AppointmentService(IUnitOfWork work) { _work = work; } private async Task CreateAppointment(CreateAppointmentDTO createAppointmentDTO) { if (!string.IsNullOrWhiteSpace(createAppointmentDTO.UpdatedPatientPhone)) { var patient = await _work.Patients.GetById(createAppointmentDTO.PatientId); patient.Phone = createAppointmentDTO.UpdatedPatientPhone; _work.Patients.Update(patient); } Appointment newAppointment = new Appointment { Start = createAppointmentDTO.Start, End = createAppointmentDTO.End, PatientId = createAppointmentDTO.PatientId, DoctorId = createAppointmentDTO.DoctorId }; await _work.Appointments.AddAsync(newAppointment); await _work.SaveAsync(); } }
Conclusion
In conclusion, implementing the unit of work with the generic repository pattern in C# ASP.NET Core projects brings several benefits. By integrating repositories into a unit of work, we as developers can achieve better transaction control, reduce code duplication, and maintain data consistency. The combination of these patterns empowers us to create scalable and maintainable ASP.NET Core applications with efficient data access capabilities. Embracing the unit of work with the generic repository pattern enables us to streamline data management and enhance application performance. It is a valuable approach to elevate the overall quality and efficiency of C# ASP.NET Core projects.
Thank you for reading this blog post and stay tuned for the next one as it will be a more practical implementation of the unit of work as we will look into CRUD (Create, Read, Update, Delete) operations using the unit of work pattern.