The Repository Pattern is one of the most valuable design patterns for creating maintainable and testable .NET applications. It provides a clean separation between your data access logic and business logic, making your code more modular and easier to maintain.
What is the Repository Pattern?
The Repository Pattern is an abstraction that isolates the data layer from the rest of the application. It mediates between the domain and data mapping layers, acting like an in-memory collection of domain objects.
Key benefits include:
- Separation of concerns: Business logic is separated from data access logic
- Improved testability: Easier to unit test business logic by mocking repositories
- Centralized data access logic: Reduces duplication of query logic
- Flexibility: Easier to switch between different data sources or ORMs
Basic Implementation
Let's start with a basic implementation of the Repository Pattern in a .NET application:
1. Define the Entity
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public DateTime CreatedAt { get; set; }
}2. Create the Repository Interface
public interface IRepository<T> where T : class
{
IEnumerable<T> GetAll();
T GetById(int id);
void Add(T entity);
void Update(T entity);
void Delete(T entity);
void SaveChanges();
}3. Implement the Generic Repository
public class Repository<T> : IRepository<T> where T : class
{
protected readonly DbContext _context;
protected readonly DbSet<T> _dbSet;
public Repository(DbContext context)
{
_context = context;
_dbSet = context.Set<T>();
}
public virtual IEnumerable<T> GetAll()
{
return _dbSet.ToList();
}
public virtual T GetById(int id)
{
return _dbSet.Find(id);
}
public virtual void Add(T entity)
{
_dbSet.Add(entity);
}
public virtual void Update(T entity)
{
_dbSet.Attach(entity);
_context.Entry(entity).State = EntityState.Modified;
}
public virtual void Delete(T entity)
{
if (_context.Entry(entity).State == EntityState.Detached)
{
_dbSet.Attach(entity);
}
_dbSet.Remove(entity);
}
public void SaveChanges()
{
_context.SaveChanges();
}
}4. Create Specific Repository (Optional)
For specific entities, you might want to extend the generic repository with custom methods:
public interface ICustomerRepository : IRepository<Customer>
{
IEnumerable<Customer> GetPremiumCustomers();
Customer GetByEmail(string email);
}
public class CustomerRepository : Repository<Customer>, ICustomerRepository
{
public CustomerRepository(DbContext context) : base(context)
{
}
public IEnumerable<Customer> GetPremiumCustomers()
{
return _dbSet.Where(c => c.IsPremium).ToList();
}
public Customer GetByEmail(string email)
{
return _dbSet.FirstOrDefault(c => c.Email == email);
}
}Using the Repository in Services
Now, let's see how to use the repository in a service class:
public class CustomerService
{
private readonly ICustomerRepository _customerRepository;
public CustomerService(ICustomerRepository customerRepository)
{
_customerRepository = customerRepository;
}
public void RegisterCustomer(string name, string email)
{
// Check if customer already exists
var existingCustomer = _customerRepository.GetByEmail(email);
if (existingCustomer != null)
{
throw new InvalidOperationException("Customer with this email already exists");
}
// Create new customer
var customer = new Customer
{
Name = name,
Email = email,
CreatedAt = DateTime.UtcNow
};
// Add to repository
_customerRepository.Add(customer);
_customerRepository.SaveChanges();
}
public IEnumerable<Customer> GetAllCustomers()
{
return _customerRepository.GetAll();
}
}Dependency Injection Setup
In ASP.NET Core, you can register your repositories in the Startup.cs file:
public void ConfigureServices(IServiceCollection services)
{
// Register DbContext
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
// Register repositories
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
services.AddScoped<ICustomerRepository, CustomerRepository>();
// Register services
services.AddScoped<CustomerService>();
// Other service registrations...
}Unit Testing with Mocked Repositories
One of the biggest advantages of the Repository Pattern is improved testability. Here's an example of how to unit test the CustomerService using a mocked repository:
[Fact]
public void RegisterCustomer_WithNewEmail_ShouldAddCustomer()
{
// Arrange
var mockRepo = new Mock<ICustomerRepository>();
mockRepo.Setup(repo => repo.GetByEmail(It.IsAny<string>()))
.Returns((Customer)null);
var service = new CustomerService(mockRepo.Object);
// Act
service.RegisterCustomer("John Doe", "john@example.com");
// Assert
mockRepo.Verify(repo => repo.Add(It.Is<Customer>(c =>
c.Name == "John Doe" &&
c.Email == "john@example.com")), Times.Once);
mockRepo.Verify(repo => repo.SaveChanges(), Times.Once);
}
[Fact]
public void RegisterCustomer_WithExistingEmail_ShouldThrowException()
{
// Arrange
var existingCustomer = new Customer
{
Id = 1,
Name = "Existing User",
Email = "existing@example.com"
};
var mockRepo = new Mock<ICustomerRepository>();
mockRepo.Setup(repo => repo.GetByEmail("existing@example.com"))
.Returns(existingCustomer);
var service = new CustomerService(mockRepo.Object);
// Act & Assert
Assert.Throws<InvalidOperationException>(() =>
service.RegisterCustomer("New User", "existing@example.com"));
mockRepo.Verify(repo => repo.Add(It.IsAny<Customer>()), Times.Never);
}Advanced Patterns
For more complex applications, consider these advanced patterns:
Unit of Work Pattern
The Unit of Work pattern can be combined with the Repository Pattern to manage transactions across multiple repositories:
public interface IUnitOfWork : IDisposable
{
ICustomerRepository Customers { get; }
IOrderRepository Orders { get; }
int Complete();
}
public class UnitOfWork : IUnitOfWork
{
private readonly ApplicationDbContext _context;
public ICustomerRepository Customers { get; private set; }
public IOrderRepository Orders { get; private set; }
public UnitOfWork(ApplicationDbContext context)
{
_context = context;
Customers = new CustomerRepository(_context);
Orders = new OrderRepository(_context);
}
public int Complete()
{
return _context.SaveChanges();
}
public void Dispose()
{
_context.Dispose();
}
}Conclusion
The Repository Pattern is a powerful tool for creating clean, maintainable, and testable .NET applications. By abstracting data access logic, it allows you to focus on business logic without worrying about the underlying data storage mechanisms.
While it does add some complexity to your codebase, the benefits in terms of separation of concerns, testability, and maintainability make it well worth considering for medium to large-scale applications.
Happy coding!
