From 64773a09c5f33694e9f4764874277d60ac24a516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muhammed=20Ali=20=C3=96ZKAYA?= Date: Sun, 2 Nov 2025 10:29:26 +0300 Subject: [PATCH] Add blog post `Repository Pattern in the ASP.NET Core` --- .../post.md | 277 ++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 docs/en/Blog-Posts/2025-11-02-Repository-Pattern-in-the-Aspnetcore/post.md diff --git a/docs/en/Blog-Posts/2025-11-02-Repository-Pattern-in-the-Aspnetcore/post.md b/docs/en/Blog-Posts/2025-11-02-Repository-Pattern-in-the-Aspnetcore/post.md new file mode 100644 index 0000000000..d1692471aa --- /dev/null +++ b/docs/en/Blog-Posts/2025-11-02-Repository-Pattern-in-the-Aspnetcore/post.md @@ -0,0 +1,277 @@ +# Repository Pattern in the ASP.NET Core + +If you’ve built a .NET app with a database, you’ve likely used Entity Framework, Dapper, or ADO.NET. They’re useful tools; still, when they live inside your business logic or controllers, the code can become harder to keep tidy and to test. + +That’s where the **Repository Pattern** comes in. + +At its core, the Repository Pattern acts as a **middle layer between your domain and data access logic**. It abstracts the way you store and retrieve data, giving your application a clean separation of concerns: + +* **Separation of Concerns:** Business logic doesn’t depend on the database. +* **Easier Testing:** You can replace the repository with a fake or mock during unit tests. +* **Flexibility:** You can switch data sources (e.g., from SQL to MongoDB) without touching business logic. + +Let’s see how this works with a simple example. + +## A Simple Example with Product Repository + +Imagine we’re building a small e-commerce app. We’ll start by defining a repository interface for managing products. + +You can find the complete sample code in this GitHub repository: + +https://github.com/m-aliozkaya/RepositoryPattern + +### Domain model and context + +We start with a single entity and a matching `DbContext`. + +`Product.cs` + +```csharp +using System.ComponentModel.DataAnnotations; + +namespace RepositoryPattern.Web.Models; + +public class Product +{ + public int Id { get; set; } + + [Required, StringLength(64)] + public string Name { get; set; } = string.Empty; + + [Range(0, double.MaxValue)] + public decimal Price { get; set; } + + [StringLength(256)] + public string? Description { get; set; } + + public int Stock { get; set; } +} +``` + +`"AppDbContext.cs` + +```csharp +using Microsoft.EntityFrameworkCore; +using RepositoryPattern.Web.Models; + +namespace RepositoryPattern.Web.Data; + +public class AppDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Products => Set(); +} +``` + +### Generic repository contract and base class + +All entities share the same CRUD needs, so we define a generic interface and an EF Core implementation. + +`Repositories/IRepository.cs` + +```csharp +using System.Linq.Expressions; + +namespace RepositoryPattern.Web.Repositories; + +public interface IRepository where TEntity : class +{ + Task GetByIdAsync(int id, CancellationToken cancellationToken = default); + Task> GetAllAsync(CancellationToken cancellationToken = default); + Task> GetListAsync(Expression> predicate, CancellationToken cancellationToken = default); + Task AddAsync(TEntity entity, CancellationToken cancellationToken = default); + Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default); + Task DeleteAsync(int id, CancellationToken cancellationToken = default); +} +``` + +`Repositories/EfRepository.cs` + +```csharp +using Microsoft.EntityFrameworkCore; +using RepositoryPattern.Web.Data; + +namespace RepositoryPattern.Web.Repositories; + +public class EfRepository(AppDbContext context) : IRepository + where TEntity : class +{ + protected readonly AppDbContext Context = context; + + public virtual async Task GetByIdAsync(int id, CancellationToken cancellationToken = default) + => await Context.Set().FindAsync([id], cancellationToken); + + public virtual async Task> GetAllAsync(CancellationToken cancellationToken = default) + => await Context.Set().AsNoTracking().ToListAsync(cancellationToken); + + public virtual async Task> GetListAsync( + System.Linq.Expressions.Expression> predicate, + CancellationToken cancellationToken = default) + => await Context.Set() + .AsNoTracking() + .Where(predicate) + .ToListAsync(cancellationToken); + + public virtual async Task AddAsync(TEntity entity, CancellationToken cancellationToken = default) + { + await Context.Set().AddAsync(entity, cancellationToken); + await Context.SaveChangesAsync(cancellationToken); + } + + public virtual async Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default) + { + Context.Set().Update(entity); + await Context.SaveChangesAsync(cancellationToken); + } + + public virtual async Task DeleteAsync(int id, CancellationToken cancellationToken = default) + { + var entity = await GetByIdAsync(id, cancellationToken); + if (entity is null) + { + return; + } + + Context.Set().Remove(entity); + await Context.SaveChangesAsync(cancellationToken); + } +} +``` + +Reads use `AsNoTracking()` to avoid tracking overhead, while write methods call `SaveChangesAsync` to keep the sample straightforward. + +### Product-specific repository + +Products need one extra query: list the items that are almost out of stock. We extend the generic repository with a dedicated interface and implementation. + +`Repositories/IProductRepository.cs` + +```csharp +using RepositoryPattern.Web.Models; + +namespace RepositoryPattern.Web.Repositories; + +public interface IProductRepository : IRepository +{ + Task> GetLowStockProductsAsync(int threshold, CancellationToken cancellationToken = default); +} +``` + +`Repositories/ProductRepository.cs` + +```csharp +using Microsoft.EntityFrameworkCore; +using RepositoryPattern.Web.Data; +using RepositoryPattern.Web.Models; + +namespace RepositoryPattern.Web.Repositories; + +public class ProductRepository(AppDbContext context) : EfRepository(context), IProductRepository +{ + public Task> GetLowStockProductsAsync(int threshold, CancellationToken cancellationToken = default) => + Context.Products + .AsNoTracking() + .Where(product => product.Stock <= threshold) + .OrderBy(product => product.Stock) + .ToListAsync(cancellationToken); +} +``` + +### 🧩 A Note on Unit of Work + +The Repository Pattern is often used together with the **Unit of Work** pattern to manage transactions efficiently. + +> 💡 *If you want to dive deeper into the Unit of Work pattern, check out our separate blog post dedicated to that topic. https://abp.io/community/articles/lv4v2tyf + +### Service layer and controller + +Controllers depend on a service, and the service depends on the repository. That keeps HTTP logic and data logic separate. + +`Services/ProductService.cs` + +```csharp +using RepositoryPattern.Web.Models; +using RepositoryPattern.Web.Repositories; + +namespace RepositoryPattern.Web.Services; + +public class ProductService(IProductRepository productRepository) +{ + private readonly IProductRepository _productRepository = productRepository; + + public Task> GetProductsAsync(CancellationToken cancellationToken = default) => + _productRepository.GetAllAsync(cancellationToken); + + public Task> GetLowStockAsync(int threshold, CancellationToken cancellationToken = default) => + _productRepository.GetLowStockProductsAsync(threshold, cancellationToken); + + public Task GetByIdAsync(int id, CancellationToken cancellationToken = default) => + _productRepository.GetByIdAsync(id, cancellationToken); + + public Task CreateAsync(Product product, CancellationToken cancellationToken = default) => + _productRepository.AddAsync(product, cancellationToken); + + public Task UpdateAsync(Product product, CancellationToken cancellationToken = default) => + _productRepository.UpdateAsync(product, cancellationToken); + + public Task DeleteAsync(int id, CancellationToken cancellationToken = default) => + _productRepository.DeleteAsync(id, cancellationToken); +} +``` + +`Controllers/ProductsController.cs` + +```csharp +using Microsoft.AspNetCore.Mvc; +using RepositoryPattern.Web.Models; +using RepositoryPattern.Web.Services; + +namespace RepositoryPattern.Web.Controllers; + +public class ProductsController(ProductService productService) : Controller +{ + private readonly ProductService _productService = productService; + + public async Task Index(CancellationToken cancellationToken) + { + const int lowStockThreshold = 5; + var products = await _productService.GetProductsAsync(cancellationToken); + var lowStock = await _productService.GetLowStockAsync(lowStockThreshold, cancellationToken); + + return View(new ProductListViewModel(products, lowStock, lowStockThreshold)); + } + + // remaining CRUD actions call through ProductService in the same way +} +``` + +The controller never reaches for `AppDbContext`. Every operation travels through the service, which keeps tests simple and makes future refactors easier. + +### Dependency registration and seeding + +The last step is wiring everything up in `Program.cs`. + +```csharp +builder.Services.AddDbContext(options => + options.UseInMemoryDatabase("ProductsDb")); +builder.Services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +``` + +The sample also seeds three products so the list page shows data on first run. + +Run the site with: + +```powershell +dotnet run --project RepositoryPattern.Web +``` + +## How ABP approaches the same idea + +ABP includes generic repositories by default (`IRepository`), so you often skip writing the implementation layer shown above. You inject the interface into an application service, call methods like `InsertAsync` or `CountAsync`, and ABP’s Unit of Work handles the transaction. When you need custom queries, you can still derive from `EfCoreRepository` and add them. + +For more details, check out the official ABP documentation on repositories: https://abp.io/docs/latest/framework/architecture/domain-driven-design/repositories + +### Closing note + +This setup keeps data access tidy without being heavy. Start with the generic repository, add small extensions per entity, pass everything through services, and register the dependencies once. Whether you hand-code it or let ABP supply the repository, the structure stays the same and your controllers remain clean.