Combining Generic and Non-Generic Repositories in ASP.NET Core Web API

Combining Generic and Non-Generic Repositories

In this article, we will discuss Combining Generic and Non-Generic Repositories in an ASP.NET Core Web API Application. In the previous chapters, we understood both Generic and Non-Generic Repository Patterns and implemented them independently. However, real-world enterprise applications often benefit from combining both approaches, using generic repositories for standard CRUD operations and extending them with specific methods in non-generic repositories for complex business logic.

In this chapter, we will demonstrate how to combine these patterns in a clean, scalable way using Entity Framework Core, allowing us to:

  • Reuse common data access logic via generics
  • Extend specific repositories with entity-specific methods
  • Achieve maximum flexibility and maintainability
When to Use Generic vs Non-Generic Repositories in Enterprise Applications:

In modern enterprise applications, both Generic and Non-Generic Repositories have their own advantages and limitations. Understanding when to use each is crucial to building a maintainable, scalable, and clean data access layer.

  • The Generic Repository is ideal for standard CRUD operations that are common across all entities. It reduces code duplication, enforces consistency, and accelerates development by providing reusable methods for Get, Add, Update, Delete, etc.
  • Non-Generic Repository is best suited for complex scenarios where specific entities require custom data access methods that cannot be generalized. This includes specialized queries, business logic embedded in data access, or operations involving multiple related entities.

Generic repositories are designed for simplicity and reuse, but cannot handle all data access needs. When you require operations such as:

  • Joins across multiple tables with filters
  • Aggregations, groupings, or projections
  • Stored procedures or raw SQL execution
  • Domain-specific queries like GetOrdersByCustomer, GetProductsByCategoryWithStock

Use a Non-Generic repository or extend the generic one with custom methods.

Extending the Generic Repository with Specific Repositories

To combine both patterns, create interfaces for specific repositories that inherit from the generic repository interface, and then add custom methods as needed. In our application, we use the Repository Pattern to abstract and encapsulate data access logic. To avoid repeating common CRUD operations (Create, Read, Update, Delete) for each entity, we created a generic repository interface:

namespace ECommerceAPI.Repositories
{
    public interface IRepository<T> where T : class
    {
        Task<IEnumerable<T>> GetAllAsync();
        Task<T?> GetByIdAsync(int id);
        Task AddAsync(T entity);
        void Update(T entity);
        void Delete(T entity);
        Task<bool> ExistsAsync(int id);
        Task SaveAsync();
    }
}

This generic interface handles the basic database operations for any entity type T.

IProductRepository Extends IRepository<Product>

We have already created the IProductRepository interface. So, please modify the IProductRepository interface as follows. Now, the IProductRepository interface is designed specifically for the Product entity and extends this generic interface:

using ECommerceAPI.Models;
namespace ECommerceAPI.Repositories
{
    // Define an interface named IProductRepository that inherits from the generic IRepository interface
    // It specifically works with the Product entity type
    public interface IProductRepository : IRepository<Product>
    {
        // Declare an asynchronous method to get all products belonging to a specific category
        Task<IEnumerable<Product>> GetProductsByCategoryAsync(int categoryId);

        // Declare an asynchronous method to get top selling products limited by count
        
        Task<IEnumerable<Product>> GetTopSellingProductsAsync(int count);
    }
}

By extending IRepository<Product>, the IProductRepository automatically inherits all the generic CRUD operations specialized for Product, like:

  • Retrieving all products,
  • Fetching a product by its ID,
  • Adding, updating, and deleting products,
  • Checking product existence,
  • And saving changes.
Why Extend the Generic Interface?

Extending the generic repository interface allows us to:

  • Reuse common data operations without rewriting them.
  • Maintain a clean, DRY (Don’t Repeat Yourself) codebase.
  • Add Product-specific data access methods that don’t fit into the generic CRUD operations.

For example, the two custom methods declared in IProductRepository are:

  • GetProductsByCategoryAsync(int categoryId): Retrieves all products filtered by a specific category. This query requires a specific filter condition, so it belongs to the product repository interface rather than the generic one.
  • GetTopSellingProductsAsync(int count): Fetches the top count selling products based on sales data. This method represents a business-specific query that cannot be generalized for all entities.

For a better understanding, please have a look at the following image.

Combining Generic and Non-Generic Repositories in ASP.NET Core Web API

Product Repository Implementation

The concrete Product repository class will inherit from the generic repository and implement additional methods defined in the IProductRepository interface. In our data access layer, the ProductRepository class acts as the concrete implementation of the product-specific repository. It serves two key purposes:

  • Inheriting generic CRUD functionality from the base GenericRepository<Product> class, so it automatically has implementations for standard operations like Add, Update, Delete, GetById, and GetAll.
  • Implementing additional product-specific data access methods declared in the IProductRepository interface, which encapsulate custom queries and business logic specific to the Product entity.

We have already created the ProductRepository class. Please modify it as follows. The following class inherits from the generic repository specialized for Product entities. It also implements IProductRepository to provide custom product-related data retrieval beyond the basic CRUD.

using ECommerceAPI.Data;
using ECommerceAPI.Models;
using Microsoft.EntityFrameworkCore;

namespace ECommerceAPI.Repositories.Implementations
{
    // Define the ProductRepository class that inherits from the generic repository for Product entity
    // Implements the IProductRepository interface to provide additional product-specific data access methods
    public class ProductRepository : GenericRepository<Product>, IProductRepository
    {
        // Private readonly field to hold the injected EF Core DbContext instance
        private readonly ECommerceDbContext _context;

        // Constructor receives the DbContext instance via dependency injection
        // Passes the context to the base GenericRepository constructor for generic CRUD operations
        public ProductRepository(ECommerceDbContext context) : base(context)
        {
            _context = context;  // Store the context for use in custom methods
        }

        // Asynchronous method to get products filtered by a specific category ID
        public async Task<IEnumerable<Product>> GetProductsByCategoryAsync(int categoryId)
        {
            // Query the Products DbSet to filter products by the categoryId
            return await _context.Products
                .Where(p => p.CategoryId == categoryId)   // Filter products where CategoryId matches parameter
                .Include(p => p.Category)                 // Eagerly load the related Category entity to avoid lazy loading issues
                .AsNoTracking()                           // Improve performance by disabling change tracking for read-only queries
                .ToListAsync();                           // Execute the query asynchronously and return the result as a list
        }

        // Asynchronous method to get top-selling products limited to a specific count
        public async Task<IEnumerable<Product>> GetTopSellingProductsAsync(int count)
        {
            // Query the Products DbSet and order products by total quantity sold in descending order
            return await _context.Products
                // Calculate sum of quantities from related OrderItems for each product, sort descending by this value
                .OrderByDescending(p => p.OrderItems.Sum(oi => oi.Quantity))
                .Include(p => p.Category)     // Include related Category data
                .Take(count)                  // Take only the top 'count' products from the sorted list
                .AsNoTracking()               // Disable change tracking for performance optimization
                .ToListAsync();               // Execute asynchronously and return list of products
        }
    }
}
IOrderRepository Extends IRepository<Order>

We have already created the IOrderRepository interface. So, please modify the IOrderRepository interface as follows. Now, the IOrderRepository interface is specifically designed for the Order entity by extending the generic interface:

using ECommerceAPI.Models;
namespace ECommerceAPI.Repositories
{
    // Define an interface named IOrderRepository which extends the generic IRepository interface
    // This interface is specialized for working with the Order entity
    public interface IOrderRepository : IRepository<Order>
    {
        // Declare an asynchronous method that retrieves all orders
        // Includes related Customer and OrderItems entities for comprehensive order details
        Task<IEnumerable<Order>> GetAllOrdersWithDetailsAsync();

        // Declare an asynchronous method to get all orders placed by a specific customer
        Task<IEnumerable<Order>> GetOrdersByCustomerAsync(int customerId);

        // Declare an asynchronous method to retrieve orders placed within a specified date range
        // Takes two DateTime parameters: startDate and endDate to filter orders
        Task<IEnumerable<Order>> GetOrdersByDateRangeAsync(DateTime startDate, DateTime endDate);
    }
}

By inheriting from IRepository<Order>, the IOrderRepository interface automatically gains all the generic CRUD operations for Order, such as:

  • Retrieving all orders,
  • Fetching an order by ID,
  • Adding, updating, and deleting orders,
  • Checking if an order exists,
  • Saving changes to the database.
Why Extend the Generic Repository?

Extending the generic repository interface for the Order entity enables us to:

  • Reuse the generic CRUD operations so we don’t duplicate code.
  • Add Order-specific data operations that are unique to business requirements and cannot be generalized.

The additional methods declared in IOrderRepository provide advanced querying capabilities, such as:

  • GetAllOrdersWithDetailsAsync(): This method efficiently fetches all orders with related data, including customers and order items, in one query. It is useful for scenarios like displaying complete order information.
  • GetOrdersByCustomerAsync(int customerId): This function retrieves orders filtered by a specific customer ID, supporting customer-specific order history or reporting.
  • GetOrdersByDateRangeAsync(DateTime startDate, DateTime endDate): This function fetches orders placed within a given date range, enabling date-based filtering for analytics or reporting.

For a better understanding, please have a look at the following image.

When to Use Generic vs Non-Generic Repositories in Enterprise Applications

Order Repository Implementation

The concrete Order repository class will inherit from the generic repository and implement additional methods defined in the IOrderRepository interface. The OrderRepository class serves as the concrete implementation for order-related data access in the application. It plays a crucial role by:

  • Inheriting common CRUD operations from the generic base class GenericRepository<Order>. This means it automatically supports standard database operations such as adding, updating, deleting, and fetching orders without needing to implement these repeatedly.
  • Implementing additional order-specific methods declared in the IOrderRepository interface. These methods address complex business requirements unique to orders, such as fetching orders with detailed related entities, filtering by customer, or filtering by date range.

We have already created the OrderRepository class. Please modify it as follows. This class inherits generic CRUD capabilities designed for Order entities and implements IOrderRepository to provide specialized order-related query methods.

using ECommerceAPI.Data;
using ECommerceAPI.Models;
using Microsoft.EntityFrameworkCore;

namespace ECommerceAPI.Repositories.Implementations
{
    // Define OrderRepository class extending GenericRepository for Order entity
    // Implements IOrderRepository interface to add order-specific data operations
    public class OrderRepository : GenericRepository<Order>, IOrderRepository
    {
        // Private field to hold the injected EF Core DbContext instance
        private readonly ECommerceDbContext _context;

        // Constructor receives DbContext via dependency injection
        // Passes the context to base generic repository class constructor
        public OrderRepository(ECommerceDbContext context) : base(context)
        {
            _context = context; // Save context for use in custom methods
        }

        // Asynchronously retrieves all orders including related customers and order items with product details
        public async Task<IEnumerable<Order>> GetAllOrdersWithDetailsAsync()
        {
            return await _context.Orders
                .Include(o => o.Customer)               // Eagerly load Customer entity related to Order
                .Include(o => o.OrderItems)             // Eagerly load OrderItems collection
                    .ThenInclude(oi => oi.Product)      // For each OrderItem, eagerly load the associated Product
                .AsNoTracking()                         // Disable change tracking for read-only query to improve performance
                .ToListAsync();                         // Execute query asynchronously and return the result as a list
        }

        // Asynchronously retrieves orders placed by a specific customer
        public async Task<IEnumerable<Order>> GetOrdersByCustomerAsync(int customerId)
        {
            return await _context.Orders
                .Where(o => o.CustomerId == customerId)   // Filter orders where CustomerId matches parameter
                .Include(o => o.OrderItems)               // Eagerly load related OrderItems
                    .ThenInclude(oi => oi.Product)        // Eagerly load Products for each OrderItem
                .AsNoTracking()                           // Disable tracking for read-only operation
                .ToListAsync();                           // Execute and get list asynchronously
        }

        // Asynchronously retrieves orders within a specified date range
        public async Task<IEnumerable<Order>> GetOrdersByDateRangeAsync(DateTime startDate, DateTime endDate)
        {
            return await _context.Orders
                .Where(o => o.OrderDate >= startDate && o.OrderDate <= endDate)  // Filter orders by OrderDate between start and end dates
                .Include(o => o.Customer)               // Include related Customer entity
                .Include(o => o.OrderItems)             // Include related OrderItems
                    .ThenInclude(oi => oi.Product)      // Include Products for each OrderItem
                .AsNoTracking()                         // Disable change tracking to improve performance
                .ToListAsync();                         // Run asynchronously and return list of orders
        }
    }
}
Using Dependency Injection Patterns for Multiple Repositories

Register your repositories in Program.cs to enable constructor injection across controllers or services. This allows us to inject either the generic repository interface for basic CRUD or the specific repository interface when we need custom methods.

builder.Services.AddScoped(typeof(IRepository<>), typeof(GenericRepository<>));
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddScoped<ICategoryRepository, CategoryRepository>();
builder.Services.AddScoped<ICustomerRepository, CustomerRepository>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
Modify Controllers to Use These Repositories

Now, we will modify the Products and Orders Controller to use these Repositories.

Modify Products Controller

Please modify the ProductsController as follows. The ProductsController is the Web API endpoint layer that handles HTTP requests related to products. By injecting and using the IProductRepository interface, the controller receives the repository via Dependency Injection. This promotes loose coupling and adheres to the Dependency Inversion Principle. The repository instance is stored in a private read-only field for use in action methods.

using ECommerceAPI.DTOs;
using ECommerceAPI.Models;
using ECommerceAPI.Repositories;
using Microsoft.AspNetCore.Mvc;

namespace ECommerceAPI.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class ProductsController : ControllerBase
    {
        private readonly IProductRepository _productRepository;

        public ProductsController(IProductRepository productRepository)
        {
            _productRepository = productRepository;
        }

        // GET: api/products
        [HttpGet]
        public async Task<IActionResult> GetProducts()
        {
            var products = await _productRepository.GetAllAsync();

            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 Ok(dtos);
        }

        // GET: api/products/{id}
        [HttpGet("{id}")]
        public async Task<IActionResult> GetProduct(int id)
        {
            var product = await _productRepository.GetByIdAsync(id);

            if (product == null)
                return NotFound();

            var dto = new ProductDTO
            {
                ProductId = product.ProductId,
                Name = product.Name,
                Description = product.Description,
                Price = product.Price,
                CategoryId = product.CategoryId,
                CategoryName = product.Category?.Name
            };

            return Ok(dto);
        }

        // GET: api/products/bycategory/{categoryId}
        [HttpGet("bycategory/{categoryId}")]
        public async Task<IActionResult> GetProductsByCategory(int categoryId)
        {
            var products = await _productRepository.GetProductsByCategoryAsync(categoryId);

            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 Ok(dtos);
        }

        [HttpGet("top/{count}")]
        public async Task<IActionResult> GetTopSellingProducts(int count)
        {
            var products = await _productRepository.GetTopSellingProductsAsync(count);

            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 Ok(dtos);
        }

        // POST: api/products
        [HttpPost]
        public async Task<IActionResult> CreateProduct([FromBody] ProductDTO dto)
        {
            if (!ModelState.IsValid)
                return BadRequest(ModelState);

            var product = new Product
            {
                Name = dto.Name,
                Description = dto.Description,
                Price = dto.Price,
                CategoryId = dto.CategoryId
            };

            await _productRepository.AddAsync(product);
            await _productRepository.SaveAsync();

            dto.ProductId = product.ProductId;

            return Ok(dto);
        }

        // PUT: api/products/{id}
        [HttpPut("{id}")]
        public async Task<IActionResult> UpdateProduct(int id, [FromBody] ProductDTO dto)
        {
            if (id != dto.ProductId)
                return BadRequest("Product ID mismatch.");

            if (!ModelState.IsValid)
                return BadRequest(ModelState);

            var existing = await _productRepository.GetByIdAsync(id);
            if (existing == null)
                return NotFound();

            existing.Name = dto.Name;
            existing.Description = dto.Description;
            existing.Price = dto.Price;
            existing.CategoryId = dto.CategoryId;

            _productRepository.Update(existing);
            await _productRepository.SaveAsync();

            return NoContent();
        }

        // DELETE: api/products/{id}
        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteProduct(int id)
        {
            var existing = await _productRepository.GetByIdAsync(id);
            if (existing == null)
                return NotFound();

            _productRepository.Delete(existing);
            await _productRepository.SaveAsync();

            return NoContent();
        }
    }
}
Modify Orders Controller

Please modify the OrdersController as follows. The OrdersController is the API layer handling HTTP requests related to orders. It receives an instance of IOrderRepository through constructor injection, promoting loose coupling and testability. The controller delegates all data access operations to this repository.

using ECommerceAPI.DTOs;
using ECommerceAPI.Models;
using ECommerceAPI.Repositories;
using Microsoft.AspNetCore.Mvc;

namespace ECommerceAPI.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class OrdersController : ControllerBase
    {
        private readonly IOrderRepository _orderRepository;

        public OrdersController(IOrderRepository orderRepository)
        {
            _orderRepository = orderRepository;
        }

        // GET: api/orders
        [HttpGet]
        public async Task<IActionResult> GetAllOrders()
        {
            var orders = await _orderRepository.GetAllOrdersWithDetailsAsync();

            var dtos = orders.Select(o => 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()
            });

            return Ok(dtos);
        }

        // GET: api/orders/{id}
        [HttpGet("{id}")]
        public async Task<IActionResult> GetOrder(int id)
        {
            var order = await _orderRepository.GetByIdAsync(id);

            if (order == null)
                return NotFound();

            var dto = new OrderDTO
            {
                OrderId = order.OrderId,
                OrderDate = order.OrderDate,
                CustomerId = order.CustomerId,
                CustomerName = order.Customer?.FullName,
                OrderAmount = order.OrderAmount,
                OrderItems = order.OrderItems.Select(oi => new OrderItemDTO
                {
                    OrderItemId = oi.OrderItemId,
                    ProductId = oi.ProductId,
                    ProductName = oi.Product?.Name,
                    Quantity = oi.Quantity,
                    UnitPrice = oi.UnitPrice
                }).ToList()
            };

            return Ok(dto);
        }

        // GET: api/orders/bycustomer/{customerId}
        [HttpGet("bycustomer/{customerId}")]
        public async Task<IActionResult> GetOrdersByCustomer(int customerId)
        {
            var orders = await _orderRepository.GetOrdersByCustomerAsync(customerId);

            var dtos = orders.Select(o => 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()
            });

            return Ok(dtos);
        }

        // GET: api/orders/bydaterange?startDate=yyyy-MM-dd&endDate=yyyy-MM-dd
        [HttpGet("bydaterange")]
        public async Task<IActionResult> GetOrdersByDateRange([FromQuery] DateTime startDate, [FromQuery] DateTime endDate)
        {
            var orders = await _orderRepository.GetOrdersByDateRangeAsync(startDate, endDate);

            var dtos = orders.Select(o => 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()
            });

            return Ok(dtos);
        }

        // POST: api/orders
        [HttpPost]
        public async Task<IActionResult> CreateOrder([FromBody] OrderDTO dto)
        {
            if (!ModelState.IsValid)
                return BadRequest(ModelState);

            var order = new Order
            {
                OrderDate = dto.OrderDate,
                CustomerId = dto.CustomerId,
                OrderAmount = dto.OrderAmount,
                OrderItems = dto.OrderItems.Select(oi => new OrderItem
                {
                    ProductId = oi.ProductId,
                    Quantity = oi.Quantity,
                    UnitPrice = oi.UnitPrice
                }).ToList()
            };

            await _orderRepository.AddAsync(order);
            await _orderRepository.SaveAsync();

            dto.OrderId = order.OrderId;

            return Ok(dto);
        }

        // PUT: api/orders/{id}
        [HttpPut("{id}")]
        public async Task<IActionResult> UpdateOrder(int id, [FromBody] OrderDTO dto)
        {
            if (id != dto.OrderId)
                return BadRequest("Order ID mismatch.");

            if (!ModelState.IsValid)
                return BadRequest(ModelState);

            var existing = await _orderRepository.GetByIdAsync(id);
            if (existing == null)
                return NotFound();

            existing.OrderDate = dto.OrderDate;
            existing.CustomerId = dto.CustomerId;
            existing.OrderAmount = dto.OrderAmount;

            // Update order items - simplistic replace approach (advanced logic can be implemented)
            existing.OrderItems.Clear();
            foreach (var oiDto in dto.OrderItems)
            {
                existing.OrderItems.Add(new OrderItem
                {
                    ProductId = oiDto.ProductId,
                    Quantity = oiDto.Quantity,
                    UnitPrice = oiDto.UnitPrice
                });
            }

            _orderRepository.Update(existing);
            await _orderRepository.SaveAsync();

            return NoContent();
        }

        // DELETE: api/orders/{id}
        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteOrder(int id)
        {
            var existing = await _orderRepository.GetByIdAsync(id);
            if (existing == null)
                return NotFound();

            _orderRepository.Delete(existing);
            await _orderRepository.SaveAsync();

            return NoContent();
        }
    }
}
Benefits of this Hybrid Approach

The Hybrid Approach, combining Generic Repositories with Non-Generic (Specific) Repositories, offers several key benefits in building maintainable, scalable, and clean data access layers in ASP.NET Core Web API applications.

  • Code Reusability and DRY Principle: Generic repositories provide reusable, standard CRUD operations for all entities, so you write the code for common operations like Get, Add, Update, and Delete only once. This eliminates repetitive code and adheres to the DRY (Don’t Repeat Yourself) principle.
  • Flexibility for Complex Queries: Some business scenarios require complex or entity-specific queries (e.g., fetching orders with customer details, filtering products by category). Non-generic specific repositories extend the generic ones to add these specialized methods without cluttering the generic repositories. This keeps complex logic cleanly separated and maintainable.
  • Separation of Concerns: The generic repository handles basic, common data access. The specific repositories focus on domain/business-specific queries and operations. This clear separation improves code readability, organization, and testing.
  • Better Maintainability: When requirements change or new entities are added, the generic repository doesn’t need modification. Changes related to specific business logic only affect the corresponding specific repository, minimizing impact and risk.
  • Performance Optimization: Specific repositories can implement optimized queries (e.g., eager loading with .Include(), filtering, paging) designed for their entities. Generic repositories remain simple and lightweight, focusing on basic operations.
  • Scalability and Extensibility: The hybrid approach scales well as your application grows. You can add new entities simply by implementing generic repository interfaces, or you can add new complex operations only in specific repositories without bloating your generic base.

This is why the hybrid pattern is the recommended real-world approach for modern ASP.NET Core projects, especially as they grow beyond basic CRUD!

What Next?

Even with repositories, each repository by default manages its own set of operations. In real business scenarios, we often need to coordinate changes across multiple repositories in a single transaction (for example: placing an order, updating inventory, and modifying customer points, all together). The Unit of Work pattern solves this by:

  • Ensuring that all operations across multiple repositories are treated as a single transaction.
  • Committing all changes at once, or rolling back everything if something fails.
  • Managing the lifetime and scope of the database context in a clean, centralized way.

In the next article, I will discuss implementing a Unit of Work with a Repository Pattern in the ASP.NET Core Web API Application with Examples. In this article, I explain how to Combine Generic and Non-Generic Repositories in ASP.NET Core Web API with Examples. I hope you enjoy this article on Combining Generic and Non-Generic Repositories in ASP.NET Core Web API.

Leave a Reply

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