How to use the Unit of Work pattern in ASP.NET Core
By FoxLearn 12/31/2024 7:32:09 AM 107
In business applications, CRUD operations (Create, Read, Update, Delete) are fundamental for interacting with a data store. However, managing complex data transactions requires more than just storing and retrieving data. To create flexible, maintainable, and reusable code, developers can rely on design patterns such as Unit of Work and Repository.
What is the Unit of Work Design Pattern?
The Unit of Work design pattern ensures data integrity and consistency by guaranteeing that all changes made to multiple objects are either committed together or rolled back if an error occurs.
Implementing the Unit of Work Design Pattern in C#
To implement the Unit of Work pattern, we define an interface that specifies operations like committing and rolling back changes, along with methods to interact with repositories for CRUD operations.
Create a DbContext Class
In Entity Framework, DbContext
serves as the gateway to the database.
public class CustomDbContext : DbContext { public CustomDbContext(DbContextOptions<CustomDbContext> options) : base(options) { } public DbSet<Customer> Customers { get; set; } }
Define the IUnitOfWork Interface
This interface defines operations such as Commit()
, Rollback()
, and Repository<T>()
, which provides access to repositories for specific entity types.
public interface IUnitOfWork : IDisposable { void Commit(); void Rollback(); IRepository<T> Repository<T>() where T : class; }
Implement the UnitOfWork Class
The concrete implementation of the IUnitOfWork
interface handles the transaction management, including committing or rolling back changes.
public class UnitOfWork : IUnitOfWork, IDisposable { private readonly DbContext _dbContext; public UnitOfWork(DbContext dbContext) { _dbContext = dbContext; } public void Commit() { _dbContext.SaveChanges(); } public void Rollback() { foreach (var entry in _dbContext.ChangeTracker.Entries()) { if (entry.State == EntityState.Added) entry.State = EntityState.Detached; } } public IRepository<T> Repository<T>() where T : class { return new Repository<T>(_dbContext); } public void Dispose() { _dbContext.Dispose(); GC.SuppressFinalize(this); } }
Define the Repository Interface
The repository abstracts the data access logic, making it easier to interact with the data source without needing to directly manage SQL queries.
public interface IRepository<T> where T : class { T GetById(object id); IList<T> GetAll(); void Add(T entity); void Update(T entity); }
Implement the Repository Class
The concrete implementation of the repository provides the actual logic for interacting with the database.
public class Repository<T> : IRepository<T> where T : class { private readonly CustomDbContext _dbContext; private readonly DbSet<T> _dbSet; public Repository(DbContext dbContext) { _dbContext = dbContext; _dbSet = dbContext.Set<T>(); } public T GetById(object id) { return _dbSet.Find(id); } public IList<T> GetAll() { return _dbSet.ToList(); } public void Add(T entity) { _dbSet.Add(entity); } public void Update(T entity) { _dbSet.Update(entity); } }
Register the DbContext
To enable dependency injection (DI) in the application, register the DbContext
in the Program.cs
file:
builder.Services.AddDbContextPool<CustomDbContext>(options => options.UseSqlServer("YourConnectionStringHere"));
In this example, a Customer entity is managed, and several operations such as adding, updating, and retrieving customers will be performed using the Unit of Work pattern.
- Commit: When all the operations related to a customer are successful (e.g., adding a new customer, updating their information, etc.), changes are committed to the database.
- Rollback: If any part of the process fails (e.g., an invalid input or a database constraint violation), all changes are rolled back to maintain consistency.
public class CustomerService { private readonly IUnitOfWork _unitOfWork; public CustomerService(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; } public void AddCustomer(Customer customer) { var customerRepo = _unitOfWork.Repository<Customer>(); customerRepo.Add(customer); _unitOfWork.Commit(); } public void UpdateCustomer(Customer customer) { var customerRepo = _unitOfWork.Repository<Customer>(); customerRepo.Update(customer); _unitOfWork.Commit(); } public void DeleteCustomer(int customerId) { var customerRepo = _unitOfWork.Repository<Customer>(); var customer = customerRepo.GetById(customerId); if (customer != null) { customerRepo.Remove(customer); _unitOfWork.Commit(); } } }
In this scenario, if adding or updating a customer fails (perhaps due to a database constraint), the Rollback()
method would be called to revert all changes to the previous state, ensuring the database remains consistent.
- Content Negotiation in Web API
- How to fix 'InvalidOperationException: Scheme already exists: Bearer'
- How to fix System.InvalidOperationException: Scheme already exists: Identity.Application
- Add Thread ID to the Log File using Serilog
- Handling Exceptions in .NET Core API with Middleware
- InProcess Hosting in ASP.NET Core
- Limits on ThreadPool.SetMinThreads and SetMaxThreads
- Controlling DateTime Format in JSON Output with JsonSerializerOptions