Back to: ASP.NET Core Web API Tutorials
Generic Repository Pattern in ASP.NET Core Web API
In this article, we will discuss implementing the Generic Repository Pattern in an ASP.NET Core Web API Application. In our previous article, we discussed the Non‑Generic Repository Pattern, creating a separate repository class per entity to encapsulate data access. While that approach affords more control, it often leads to repetitive code across repositories. In this chapter, we will adopt the Generic Repository Pattern to centralize and DRY up CRUD operations, using C# generics.
Why Use a Generic Repository in ASP.NET Core Web API?
In software development, repetitive code is a maintenance headache and leads to bugs. When working with data access layers, many entities require the same basic CRUD (Create, Read, Update, Delete) operations. Instead of writing individual repository classes for each entity, which can be tedious and error-prone, a Generic Repository Pattern allows us to write a reusable, type-safe repository class that works with any entity type.
A generic repository encapsulates all common data access operations into a single class that can operate on any entity type T. This approach significantly reduces code duplication and centralizes data access logic, allowing for easier maintenance and consistency.
The Motivation for a Generic Repository
Many entities (e.g., Category, Product, Customer) share common CRUD operations:
- Get all
- Get by ID
- Add
- Update
- Delete
The Generic Repository Pattern aims to centralize these operations in a reusable, type-safe way, reducing code duplication and enforcing the DRY (Don’t Repeat Yourself) principle. So, with a generic repository, we will get the following benefits:
- Reusability: Implement common CRUD once and reuse for all entities.
- Code Centralization: A Single codebase for data access logic reduces maintenance overhead.
- DRY Principle: Eliminates boilerplate Add, Update, Delete, GetById methods across repository classes.
- Consistency: Enforces uniform patterns for data access, error handling, and async usage across the application.
Designing the Generic Repository Interface
The generic repository interface defines a contract for data operations common to all entities and provides a strongly typed, asynchronous API to manage entities of type T. So, create an interface named IRepository within the Repositories folder and then copy and paste the following code.
namespace ECommerceAPI.Repositories { // Generic repository interface defining data access contract for entity type T // The generic constraint 'where T : class' ensures T is a reference type (entity class) public interface IRepository<T> where T : class { // Asynchronously retrieves all entities of type T as an enumerable collection Task<IEnumerable<T>> GetAllAsync(); // Asynchronously retrieves a single entity by its primary key (int id) // Returns null if entity is not found Task<T?> GetByIdAsync(int id); // Asynchronously adds a new entity of type T to the data source Task AddAsync(T entity); // Updates an existing entity of type T in the data source void Update(T entity); // Deletes the specified entity of type T from the data source void Delete(T entity); // Asynchronously checks if an entity with the given id exists in the data source // Returns true if found, otherwise false Task<bool> ExistsAsync(int id); // Asynchronously saves all pending changes (add, update, delete) to the database Task SaveAsync(); } }
Core CRUD Operations in Generic Interface
Asynchronous programming improves scalability and responsiveness, especially for I/O-bound operations like database access. All repository methods that interact with the database are implemented asynchronously.
- GetAllAsync(): Retrieve all entities asynchronously.
- GetByIdAsync(int id): Retrieve a single entity by its primary key.
- AddAsync(…): Add a new entity.
- Update(…): Update an existing entity.
- Delete(…): Remove an entity.
- ExistsAsync(…): Check existence by id.
- SaveAsync(): Persist changes to the database.
Implementing the Generic Repository Class
Now, we will see how to implement the generic repository interface with Entity Framework Core, making use of DbContext and DbSet<T>. We need to create a concrete class GenericRepository<T> implementing IRepository<T>. So, create a class file named GenericRepository.cs within the Repositories/Implementations folder and then copy and paste the following code.
using ECommerceAPI.Data; using Microsoft.EntityFrameworkCore; namespace ECommerceAPI.Repositories.Implementations { // GenericRepository class implements IRepository<T> for any class type T public class GenericRepository<T> : IRepository<T> where T : class { // EF Core DbContext instance for database operations private readonly ECommerceDbContext _context; // Private field to hold the DbSet for the specific entity type T private readonly DbSet<T> _dbSet; // Constructor receives DbContext instance via dependency injection public GenericRepository(ECommerceDbContext context) { // Assign injected DbContext to private field _context = context; // Set the DbSet for the entity type T using EF Core's Set<T>() _dbSet = _context.Set<T>(); } // Retrieves all entities of type T asynchronously without tracking public async Task<IEnumerable<T>> GetAllAsync() { // AsNoTracking improves performance for read-only queries by disabling change tracking // ToListAsync executes the query and returns the results as a list asynchronously return await _dbSet.AsNoTracking().ToListAsync(); } // Finds an entity by its primary key asynchronously public async Task<T?> GetByIdAsync(int id) { // FindAsync searches the database for an entity with the given primary key // Returns null if no entity is found return await _dbSet.FindAsync(id); } // Adds a new entity to the context asynchronously public async Task AddAsync(T entity) { // AddAsync adds the entity to the DbSet and marks it as Added in the change tracker // Actual insertion happens when SaveChangesAsync is called await _dbSet.AddAsync(entity); } // Updates an existing entity in the context public void Update(T entity) { // Update marks the entity state as Modified in the change tracker // Changes are persisted to the database upon SaveChangesAsync call _dbSet.Update(entity); } // Deletes an entity from the context public void Delete(T entity) { // Remove marks the entity state as Deleted in the change tracker // Entity is removed from the database after SaveChangesAsync is called _dbSet.Remove(entity); } // Checks asynchronously if an entity with the given id exists in the database public async Task<bool> ExistsAsync(int id) { // Reuse GetByIdAsync method to try fetching the entity var entity = await GetByIdAsync(id); // Return true if entity is found, otherwise false return entity != null; } // Saves all changes made in the context to the database asynchronously public async Task SaveAsync() { // SaveChangesAsync commits all Insert, Update, Delete operations tracked by DbContext to the database await _context.SaveChangesAsync(); } } }
Register the generic repository in the Program.cs:
builder.Services.AddScoped(typeof(IRepository<>), typeof(GenericRepository<>));
Modifying Controllers
Now, we will modify the Controllers to use the generic repository instead of specific repositories.
Modifying Categories Controller
Please modify the Categories Controller as follows.
using ECommerceAPI.DTOs; using ECommerceAPI.Models; using ECommerceAPI.Repositories; using Microsoft.AspNetCore.Mvc; namespace ECommerceAPI.Controllers { [ApiController] [Route("api/[controller]")] public class CategoriesController : ControllerBase { // Repository for Category entity operations private readonly IRepository<Category> _categoryRepository; // Constructor: dependency injection of category repository interface public CategoriesController(IRepository<Category> categoryRepository) { // Assign injected repository instance to private field _categoryRepository = categoryRepository; } // HTTP GET api/categories - Retrieves all categories asynchronously [HttpGet] public async Task<IActionResult> GetCategories() { // Fetch all Category entities from repository var categories = await _categoryRepository.GetAllAsync(); // Map each Category entity to a CategoryDTO for data transfer to clients var dtos = categories.Select(c => new CategoryDTO { CategoryId = c.CategoryId, // Map primary key Name = c.Name, // Map category name Description = c.Description // Map category description }); // Return HTTP 200 OK with list of category DTOs as JSON response return Ok(dtos); } // HTTP GET api/categories/{id} - Retrieves a category by ID [HttpGet("{id}")] public async Task<IActionResult> GetCategory(int id) { // Fetch Category entity by primary key asynchronously var c = await _categoryRepository.GetByIdAsync(id); // If category not found, return HTTP 404 Not Found if (c == null) return NotFound(); // Map the entity to CategoryDTO for response var dto = new CategoryDTO { CategoryId = c.CategoryId, Name = c.Name, Description = c.Description }; // Return HTTP 200 OK with the single category DTO return Ok(dto); } // HTTP POST api/categories - Creates a new category [HttpPost] public async Task<IActionResult> CreateCategory([FromBody] CategoryDTO dto) { // Check if incoming DTO data satisfies validation attributes if (!ModelState.IsValid) return BadRequest(ModelState); // Map DTO to Category entity for persistence var category = new Category { Name = dto.Name, Description = dto.Description }; // Add new category entity asynchronously to repository await _categoryRepository.AddAsync(category); // Persist changes to the database asynchronously await _categoryRepository.SaveAsync(); dto.CategoryId = category.CategoryId; // Update DTO with generated CategoryId from database // Return HTTP 200 OK with the created category DTO including generated ID return Ok(dto); } // HTTP PUT api/categories/{id} - Updates an existing category by ID [HttpPut("{id}")] public async Task<IActionResult> UpdateCategory(int id, [FromBody] CategoryDTO dto) { // Check that the ID in URL matches the DTO's CategoryId if (id != dto.CategoryId) return BadRequest("Id mismatch"); // Return 400 Bad Request if mismatch // Validate the incoming DTO against model validation attributes if (!ModelState.IsValid) return BadRequest(ModelState); // Return 400 Bad Request if validation fails // Fetch the existing category entity by ID var existing = await _categoryRepository.GetByIdAsync(id); // Return 404 Not Found if category does not exist if (existing == null) return NotFound(); // Update properties of the existing entity with DTO values existing.Name = dto.Name; existing.Description = dto.Description; // Mark the entity as modified in the repository _categoryRepository.Update(existing); // Persist the update to the database asynchronously await _categoryRepository.SaveAsync(); // Return 204 No Content indicating successful update without body return NoContent(); } // HTTP DELETE api/categories/{id} - Deletes a category by ID [HttpDelete("{id}")] public async Task<IActionResult> DeleteCategory(int id) { // Fetch the category entity to delete by ID var existing = await _categoryRepository.GetByIdAsync(id); // Return 404 Not Found if the category does not exist if (existing == null) return NotFound(); // Mark the entity for deletion _categoryRepository.Delete(existing); // Persist the deletion asynchronously await _categoryRepository.SaveAsync(); // Return 204 No Content indicating successful deletion return NoContent(); } } }
Modifying Products Controller
Please modify the ProductsController as follows.
using ECommerceAPI.DTOs; using ECommerceAPI.Models; using ECommerceAPI.Repositories; using Microsoft.AspNetCore.Mvc; namespace ECommerceAPI.Controllers { [ApiController] [Route("api/[controller]")] public class ProductsController : ControllerBase { // Repository for Product entity CRUD operations private readonly IRepository<Product> _productRepository; // Repository for Category entity to validate categories private readonly IRepository<Category> _categoryRepository; // Constructor with dependency injection for repositories public ProductsController(IRepository<Product> productRepository, IRepository<Category> categoryRepository) { // Assign injected Product repository to private field _productRepository = productRepository; // Assign injected Category repository to private field _categoryRepository = categoryRepository; } // HTTP GET api/products - Retrieves all products [HttpGet] public async Task<IActionResult> GetProducts() { // Retrieve all Product entities asynchronously var products = await _productRepository.GetAllAsync(); // Map each Product entity to a ProductDTO object var dtos = products.Select(p => new ProductDTO { ProductId = p.ProductId, Name = p.Name, Description = p.Description, Price = p.Price, CategoryId = p.CategoryId, CategoryName = p.Category?.Name }); // Return HTTP 200 OK response with the list of ProductDTOs as JSON return Ok(dtos); } // HTTP GET api/products/{id} - Retrieves a product by ID [HttpGet("{id}")] public async Task<IActionResult> GetProduct(int id) { // Fetch a single Product entity by ID asynchronously var p = await _productRepository.GetByIdAsync(id); // If no product found with given ID, return HTTP 404 Not Found if (p == null) return NotFound(); // Map the Product entity to ProductDTO var dto = new ProductDTO { ProductId = p.ProductId, Name = p.Name, Description = p.Description, Price = p.Price, CategoryId = p.CategoryId, CategoryName = p.Category?.Name }; // Return HTTP 200 OK with the ProductDTO in response body return Ok(dto); } // HTTP POST api/products - Creates a new product [HttpPost] public async Task<IActionResult> CreateProduct([FromBody] ProductDTO dto) { // Validate incoming model state (DTO validation via data annotations) if (!ModelState.IsValid) return BadRequest(ModelState); // Return 400 Bad Request with validation errors // Check if the CategoryId provided in DTO exists in Category repository bool categoryExists = await _categoryRepository.ExistsAsync(dto.CategoryId); if (!categoryExists) return BadRequest("Invalid CategoryId"); // Return 400 if category is invalid // Map ProductDTO to Product entity for insertion var product = new Product { Name = dto.Name, Description = dto.Description, Price = dto.Price, CategoryId = dto.CategoryId }; // Add new Product entity to repository asynchronously await _productRepository.AddAsync(product); // Persist changes to database asynchronously await _productRepository.SaveAsync(); // Update DTO with generated ProductId from database dto.ProductId = product.ProductId; // Return HTTP 200 OK with the created product DTO return Ok(dto); } // HTTP PUT api/products/{id} - Updates an existing product [HttpPut("{id}")] public async Task<IActionResult> UpdateProduct(int id, [FromBody] ProductDTO dto) { // Verify that the ID in URL matches the ID in request body DTO if (id != dto.ProductId) return BadRequest("Id mismatch"); // Return 400 Bad Request if mismatch // Validate DTO using data annotations if (!ModelState.IsValid) return BadRequest(ModelState); // Return 400 Bad Request with validation errors // Retrieve existing Product entity by ID var existing = await _productRepository.GetByIdAsync(id); // If not found, return 404 Not Found if (existing == null) return NotFound(); // Validate CategoryId in DTO exists in the repository bool categoryExists = await _categoryRepository.ExistsAsync(dto.CategoryId); if (!categoryExists) return BadRequest("Invalid CategoryId"); // Update the fields of the existing Product entity with DTO values existing.Name = dto.Name; existing.Description = dto.Description; existing.Price = dto.Price; existing.CategoryId = dto.CategoryId; // Mark the entity as updated in repository _productRepository.Update(existing); // Persist changes asynchronously await _productRepository.SaveAsync(); // Return 204 No Content indicating successful update with no response body return NoContent(); } // HTTP DELETE api/products/{id} - Deletes a product by ID [HttpDelete("{id}")] public async Task<IActionResult> DeleteProduct(int id) { // Retrieve the Product entity to delete by ID var existing = await _productRepository.GetByIdAsync(id); // If not found, return 404 Not Found if (existing == null) return NotFound(); // Mark the entity for deletion in the repository _productRepository.Delete(existing); // Persist changes asynchronously to database await _productRepository.SaveAsync(); // Return 204 No Content indicating successful deletion return NoContent(); } } }
Modifying Customers Controller
Please modify the CustomersController as follows.
using ECommerceAPI.DTOs; using ECommerceAPI.Models; using ECommerceAPI.Repositories; using Microsoft.AspNetCore.Mvc; namespace ECommerceAPI.Controllers { [ApiController] [Route("api/[controller]")] public class CustomersController : ControllerBase { private readonly IRepository<Customer> _customerRepository; // Repository for Customer entity operations // Constructor: injects the customer repository dependency public CustomersController(IRepository<Customer> customerRepository) { _customerRepository = customerRepository; // Assign injected repository instance to private field } // HTTP GET api/customers - Retrieves all customers asynchronously [HttpGet] public async Task<IActionResult> GetCustomers() { // Fetch all Customer entities from repository asynchronously var customers = await _customerRepository.GetAllAsync(); // Map each Customer entity to a CustomerDTO for safe transfer to clients var dtos = customers.Select(c => new CustomerDTO { CustomerId = c.CustomerId, // Map customer ID FullName = c.FullName, // Map full name Email = c.Email // Map email address }); // Return HTTP 200 OK with the list of CustomerDTOs as JSON response return Ok(dtos); } // HTTP GET api/customers/{id} - Retrieves a single customer by ID [HttpGet("{id}")] public async Task<IActionResult> GetCustomer(int id) { // Fetch Customer entity by primary key asynchronously var c = await _customerRepository.GetByIdAsync(id); // If no customer found, return HTTP 404 Not Found if (c == null) return NotFound(); // Map the entity to CustomerDTO var dto = new CustomerDTO { CustomerId = c.CustomerId, FullName = c.FullName, Email = c.Email }; // Return HTTP 200 OK with the single customer DTO return Ok(dto); } // HTTP POST api/customers - Creates a new customer [HttpPost] public async Task<IActionResult> CreateCustomer([FromBody] CustomerDTO dto) { // Validate the incoming DTO against model validation attributes if (!ModelState.IsValid) return BadRequest(ModelState); // Return 400 Bad Request if validation fails // Map DTO to Customer entity for insertion var customer = new Customer { FullName = dto.FullName, Email = dto.Email }; // Add the new Customer entity asynchronously to the repository await _customerRepository.AddAsync(customer); // Persist changes to the database asynchronously await _customerRepository.SaveAsync(); dto.CustomerId = customer.CustomerId; // Update DTO with generated CustomerId from database // Return HTTP 200 OK with the created customer DTO including ID return Ok(dto); } // HTTP PUT api/customers/{id} - Updates an existing customer [HttpPut("{id}")] public async Task<IActionResult> UpdateCustomer(int id, [FromBody] CustomerDTO dto) { // Check that the ID in the URL matches the ID in the request DTO if (id != dto.CustomerId) return BadRequest("Id mismatch"); // Return 400 Bad Request if mismatch // Validate the DTO using data annotations if (!ModelState.IsValid) return BadRequest(ModelState); // Return 400 Bad Request if validation fails // Retrieve the existing Customer entity by ID var existing = await _customerRepository.GetByIdAsync(id); // Return 404 Not Found if customer does not exist if (existing == null) return NotFound(); // Update the entity's properties with values from DTO existing.FullName = dto.FullName; existing.Email = dto.Email; // Mark the entity as updated in the repository _customerRepository.Update(existing); // Persist changes asynchronously to the database await _customerRepository.SaveAsync(); // Return 204 No Content indicating successful update without response body return NoContent(); } // HTTP DELETE api/customers/{id} - Deletes a customer by ID [HttpDelete("{id}")] public async Task<IActionResult> DeleteCustomer(int id) { // Retrieve the Customer entity to delete by ID var existing = await _customerRepository.GetByIdAsync(id); // Return 404 Not Found if the customer does not exist if (existing == null) return NotFound(); // Mark the entity for deletion _customerRepository.Delete(existing); // Persist the deletion asynchronously to the database await _customerRepository.SaveAsync(); // Return 204 No Content indicating successful deletion return NoContent(); } } }
Modifying Orders Controller
Please modify the Orders Controller as follows.
using ECommerceAPI.DTOs; using ECommerceAPI.Models; using ECommerceAPI.Repositories; using Microsoft.AspNetCore.Mvc; namespace ECommerceAPI.Controllers { [ApiController] [Route("api/[controller]")] public class OrdersController : ControllerBase { // Repository for accessing Order entities private readonly IRepository<Order> _orderRepository; // Repository for accessing Customer entities (used for validation) private readonly IRepository<Customer> _customerRepository; // Repository for accessing Product entities (used for validation) private readonly IRepository<Product> _productRepository; // Constructor injects the repositories via dependency injection public OrdersController( IRepository<Order> orderRepository, IRepository<Customer> customerRepository, IRepository<Product> productRepository) { _orderRepository = orderRepository; _customerRepository = customerRepository; _productRepository = productRepository; } // HTTP GET api/orders - Retrieves all orders asynchronously [HttpGet] public async Task<IActionResult> GetOrders() { // Fetch all Order entities asynchronously var orders = await _orderRepository.GetAllAsync(); // Map each Order entity to OrderDTO, including nested OrderItems mapping var dtos = orders.Select(o => new OrderDTO { OrderId = o.OrderId, // Map Order ID OrderDate = o.OrderDate, // Map Order date CustomerId = o.CustomerId, // Map Customer ID CustomerName = o.Customer?.FullName, // Map Customer name safely (null-check) OrderAmount = o.OrderAmount, // Map total order amount OrderItems = o.OrderItems?.Select(oi => new OrderItemDTO // Map each nested OrderItem { OrderItemId = oi.OrderItemId, ProductId = oi.ProductId, ProductName = oi.Product?.Name, // Map Product name with null check Quantity = oi.Quantity, UnitPrice = oi.UnitPrice }).ToList() ?? new List<OrderItemDTO>() // Provide empty list if no order items }); // Return HTTP 200 OK with the list of OrderDTOs as JSON return Ok(dtos); } // HTTP GET api/orders/{id} - Retrieves a single order by ID [HttpGet("{id}")] public async Task<IActionResult> GetOrder(int id) { // Fetch Order entity by ID asynchronously var o = await _orderRepository.GetByIdAsync(id); // Return 404 Not Found if order does not exist if (o == null) return NotFound(); // Map Order entity to OrderDTO including nested order items var dto = new OrderDTO { OrderId = o.OrderId, OrderDate = o.OrderDate, CustomerId = o.CustomerId, CustomerName = o.Customer?.FullName, OrderAmount = o.OrderAmount, OrderItems = o.OrderItems?.Select(oi => new OrderItemDTO { OrderItemId = oi.OrderItemId, ProductId = oi.ProductId, ProductName = oi.Product?.Name, Quantity = oi.Quantity, UnitPrice = oi.UnitPrice }).ToList() ?? new List<OrderItemDTO>() }; // Return HTTP 200 OK with the OrderDTO return Ok(dto); } // HTTP POST api/orders - Creates a new order [HttpPost] public async Task<IActionResult> CreateOrder([FromBody] OrderDTO dto) { // Validate the incoming OrderDTO model if (!ModelState.IsValid) return BadRequest(ModelState); // Validate that the referenced Customer exists bool customerExists = await _customerRepository.ExistsAsync(dto.CustomerId); if (!customerExists) return BadRequest("Invalid CustomerId"); // Validate that each referenced Product exists foreach (var item in dto.OrderItems) { bool productExists = await _productRepository.ExistsAsync(item.ProductId); if (!productExists) return BadRequest($"Invalid ProductId: {item.ProductId}"); } // Map OrderDTO to Order entity, calculate order amount, map OrderItems var order = new Order { CustomerId = dto.CustomerId, OrderDate = DateTime.UtcNow, // Set current UTC time as order date OrderAmount = dto.OrderItems.Sum(i => i.Quantity * i.UnitPrice),// Calculate total order amount OrderItems = dto.OrderItems.Select(i => new OrderItem // Map nested OrderItems { ProductId = i.ProductId, Quantity = i.Quantity, UnitPrice = i.UnitPrice }).ToList() }; // Add the new Order entity asynchronously await _orderRepository.AddAsync(order); // Persist changes to the database asynchronously await _orderRepository.SaveAsync(); // Update DTO with generated values from database (OrderId and OrderDate) dto.OrderId = order.OrderId; dto.OrderDate = order.OrderDate; // Return HTTP 200 OK with created OrderDTO return Ok(dto); } // HTTP PUT api/orders/{id} - Updates an existing order by ID [HttpPut("{id}")] public async Task<IActionResult> UpdateOrder(int id, [FromBody] OrderDTO dto) { // Check that URL ID matches DTO OrderId if (id != dto.OrderId) return BadRequest("Id mismatch"); // Validate incoming DTO if (!ModelState.IsValid) return BadRequest(ModelState); // Fetch existing Order entity by ID var existing = await _orderRepository.GetByIdAsync(id); if (existing == null) return NotFound(); // Validate referenced Customer exists bool customerExists = await _customerRepository.ExistsAsync(dto.CustomerId); if (!customerExists) return BadRequest("Invalid CustomerId"); // Validate each Product in OrderItems exists foreach (var item in dto.OrderItems) { bool productExists = await _productRepository.ExistsAsync(item.ProductId); if (!productExists) return BadRequest($"Invalid ProductId: {item.ProductId}"); } // Update scalar properties on existing order existing.CustomerId = dto.CustomerId; existing.OrderDate = dto.OrderDate; existing.OrderAmount = dto.OrderItems.Sum(i => i.Quantity * i.UnitPrice); // Simplified approach: clear existing order items and add new ones existing.OrderItems.Clear(); existing.OrderItems = dto.OrderItems.Select(i => new OrderItem { ProductId = i.ProductId, Quantity = i.Quantity, UnitPrice = i.UnitPrice }).ToList(); // Mark entity as updated in repository _orderRepository.Update(existing); // Persist changes asynchronously await _orderRepository.SaveAsync(); // Return 204 No Content indicating successful update with no response body return NoContent(); } // HTTP DELETE api/orders/{id} - Deletes an order by ID [HttpDelete("{id}")] public async Task<IActionResult> DeleteOrder(int id) { // Retrieve Order entity by ID var existing = await _orderRepository.GetByIdAsync(id); // Return 404 Not Found if order does not exist if (existing == null) return NotFound(); // Mark order entity for deletion _orderRepository.Delete(existing); // Persist deletion asynchronously await _orderRepository.SaveAsync(); // Return 204 No Content indicating successful deletion return NoContent(); } } }
Benefits of the Generic Repository Pattern in ASP.NET Core Web API
- Code Reusability and DRY Principle: One repository implementation handles CRUD for multiple entities, avoiding code duplication. Reduces code duplication since we don’t write the same CRUD logic repeatedly.
- Consistency Across Data Access Layer: This layer provides a uniform API for data operations across different entity types, making it easier to maintain and update common behavior in one place.
- Simplified Dependency Injection and Testing: One generic interface can be injected for any entity. It is easier to mock and swap implementations during unit testing.
- Faster Development: Developers can focus on business logic rather than repetitive data access code, which accelerates building CRUD endpoints for many entities.
Drawbacks of the Generic Repository Pattern in ASP.NET Core Web API
- Lack of Flexibility for Complex Queries: Generic repositories mainly support basic CRUD. Complex business logic queries or joins don’t fit well; they require additional customization.
- Difficult to Optimize for Performance: Generic methods may not efficiently handle specific performance optimizations like eager loading, projections, or filtering. If not carefully extended, they can result in inefficient queries.
- Not Ideal for Domain-Specific Operations: Domain logic often requires custom repository methods, and pure generic repositories don’t provide a place to implement entity-specific operations.
When to Use Generic vs Non-Generic Repositories
Generic repositories work well for common CRUD scenarios without complex business logic. However, they have limitations when queries or operations are specific to a domain entity, such as inventory checks or pricing rules. In such cases, combining generic repositories with specific (non-generic) repositories that extend the generic one is advisable. So, to get the best of both worlds, many real-world applications combine both:
- Generic Repository handles common CRUD operations for all entities.
- Non-Generic (Specific) Repositories extend the generic repository to implement domain-specific methods and complex queries.
The Generic Repository Pattern is ideal for simplifying and standardizing CRUD operations. It complements Non-Generic Repositories well when specialized logic is required.Â
In the next article, I will discuss Combining Generic and Non-Generic Repositories in ASP.NET Core Web API Applications. In this article, I explain Implementing Generic Repository Pattern in ASP.NET Core Web API Application with Examples. I hope you enjoy this article on Implementing Generic Repository Pattern in ASP.NET Core Web API.