Implementing Generic Repository Pattern in ASP.NET Core Web API

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.

Leave a Reply

Your email address will not be published. Required fields are marked *