You Know Generic Collections, But Have You Explored the Generic Repository Pattern in C#?
Reusability, Flexibility, and Scalability with Generics
Hello, C# enthusiast! 👋 If you’ve worked with collections and data access in .NET, you’ve likely encountered generics. But have you ever wondered how they can power not just collections but also robust design patterns like the Generic Repository Pattern?
In this blog, we’ll explore generic collections, understand their power, and transition to implementing a Generic Repository Pattern to simplify and standardize data access in your applications. Let’s get started! 💡
Part 1: Generic Collections
What Are Generic Collections?
A generic collection is a type-safe, reusable data structure that works with any data type. They’re defined in the System.Collections.Generic
namespace and eliminate the need for type casting, ensuring compile-time safety and better performance.
Why Use Generic Collections?
Type Safety: Avoid runtime errors with compile-time checks.
Performance: Reduce boxing/unboxing for value types.
Flexibility: Use the same collection for any data type.
Reusability: Write less code by avoiding type-specific collections.
Commonly Used Generic Collections
List<T>: A dynamic array that allows resizing.
var numbers = new List<int> { 1, 2, 3 }; numbers.Add(4); Console.WriteLine(string.Join(", ", numbers)); // Output: 1, 2, 3, 4
Dictionary<TKey, TValue>: A key-value pair collection.
var dictionary = new Dictionary<int, string> { { 1, "One" }, { 2, "Two" } }; Console.WriteLine(dictionary[1]); // Output: One
HashSet<T>: A collection of unique elements.
var hashSet = new HashSet<int> { 1, 2, 2, 3 }; Console.WriteLine(string.Join(", ", hashSet)); // Output: 1, 2, 3
Queue<T>: A first-in-first-out (FIFO) collection.
var queue = new Queue<string>(); queue.Enqueue("First"); queue.Enqueue("Second"); Console.WriteLine(queue.Dequeue()); // Output: First
Stack<T>: A last-in-first-out (LIFO) collection.
var stack = new Stack<int>(); stack.Push(10); stack.Push(20); Console.WriteLine(stack.Pop()); // Output: 20
Part 2: Generic Repository Pattern
What is the Repository Pattern?
The Repository Pattern is a design pattern that provides an abstraction over data access logic. It acts as a middle layer between the database and your application, ensuring:
Separation of Concerns: Keeps business logic and data access logic separate.
Testability: Simplifies unit testing by mocking repository behavior.
Maintainability: Centralizes data access logic in one place.
Introducing the Generic Repository Pattern
A Generic Repository extends the Repository Pattern by enabling reusability for multiple entities. Instead of creating separate repositories for each entity (e.g., CustomerRepository
, OrderRepository
), you define a single generic repository.
Building a Generic Repository Pattern
1. Define an Interface
The interface outlines the CRUD operations for any entity.
public interface IGenericRepository<T> where T : class
{
Task<IEnumerable<T>> GetAllAsync();
Task<T> GetByIdAsync(int id);
Task AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(int id);
}
2. Implement the Generic Repository
The implementation uses DbContext
from Entity Framework Core.
public class GenericRepository<T> : IGenericRepository<T> where T : class
{
private readonly DbContext _context;
private readonly DbSet<T> _dbSet;
public GenericRepository(DbContext context)
{
_context = context;
_dbSet = context.Set<T>();
}
public async Task<IEnumerable<T>> GetAllAsync()
{
return await _dbSet.ToListAsync();
}
public async Task<T> GetByIdAsync(int id)
{
return await _dbSet.FindAsync(id);
}
public async Task AddAsync(T entity)
{
await _dbSet.AddAsync(entity);
await _context.SaveChangesAsync();
}
public async Task UpdateAsync(T entity)
{
_dbSet.Update(entity);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await GetByIdAsync(id);
if (entity != null)
{
_dbSet.Remove(entity);
await _context.SaveChangesAsync();
}
}
}
3. Register the Repository in Dependency Injection
Configure the repository in Startup.cs
or Program.cs
.
builder.Services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>));
4. Use the Generic Repository
Inject and use the repository in your service or controller.
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IGenericRepository<Product> _productRepository;
public ProductsController(IGenericRepository<Product> productRepository)
{
_productRepository = productRepository;
}
[HttpGet]
public async Task<IActionResult> GetAll()
{
var products = await _productRepository.GetAllAsync();
return Ok(products);
}
[HttpPost]
public async Task<IActionResult> Add(Product product)
{
await _productRepository.AddAsync(product);
return CreatedAtAction(nameof(GetAll), new { id = product.Id }, product);
}
}
Combining Generic Collections with the Repository Pattern
Generic collections and the Generic Repository Pattern complement each other beautifully. For example:
Use a
List<T>
orIEnumerable<T>
to manage entities retrieved from the repository.Use a
Dictionary<TKey, TValue>
to store key-value mappings of frequently accessed entities (e.g., caching).
Example: Caching Repository Results
public class ProductCacheService
{
private readonly IGenericRepository<Product> _repository;
private readonly Dictionary<int, Product> _cache = new();
public ProductCacheService(IGenericRepository<Product> repository)
{
_repository = repository;
}
public async Task<Product> GetCachedProductAsync(int id)
{
if (_cache.TryGetValue(id, out var product))
{
return product;
}
product = await _repository.GetByIdAsync(id);
if (product != null)
{
_cache[id] = product;
}
return product;
}
}
Benefits of Using the Generic Repository Pattern
Reusability: Use the same repository logic for multiple entities.
Consistency: Centralize data access logic and ensure uniform practices.
Scalability: Easily extend functionality without rewriting code.
Testability: Mock repositories for unit testing.
Conclusion: A Match Made in C# Heaven
Generic collections and the Generic Repository Pattern embody the principles of reusability and flexibility in .NET. While generic collections provide type-safe, efficient ways to handle data, the Generic Repository Pattern ensures clean, consistent, and testable data access across your application.
By combining these tools, you can create scalable, maintainable, and performant applications with ease. Ready to take your .NET skills to the next level? Start implementing generic repositories today!
Happy coding! 😊