Repository Pattern Explained with ASP.NET Core and Entity Framework

The Repository Pattern is used to create a clean, maintainable, and testable separation between your business logic and data access logic — especially in applications that use databases (like EF Core with ASP.NET Core).
Here’s a clear breakdown of why we need it:
1.Separation of Concerns (Removes Tight Coupling)
Without repository pattern: We mix business logic and database logic. Controllers or services access the
DbContext
directly, tightly coupling business logic with database access.[HttpGet("GetCompanyById")] public async Task<IActionResult> GetCompanyById(string CompanyId) { var companyBasics = await _context.CompanyBasicInformations.SingleOrDefaultAsync(x => x.CompanyId == CompanyId); if (companyBasics == null) { return NotFound($"Company with ID {CompanyId} not found."); } return Ok(companyBasics); }
For example, the controller communicates with the database directly, which violates separation of concerns. This is problematic because any changes to the database schema or access logic may require changes in the controller, making the code harder to maintain and test.
Why it matters: Your code becomes more modular and easier to understand.
2. Easier Unit Testing
It is difficult to write a test for the controller without side effect as we are directly dealing with dbcontext.
Repositories are interfaces, so you can easily mock them in unit tests.
Testing without hitting the actual database is simpler.
Why it matters: You can test your logic without setting up a real DB.
3. Avoid Duplication
Common queries (e.g., GetAll()
, FindById()
, etc.) are being duplicated in methods everytime we need it. This can be place at a central location and can be re used.
Why it matters: Reduces bugs and makes updates easier.
4. Centralized Validation & Filtering, Caching :
You can centralize rules like:
“Don't return soft-deleted records.”
“Apply default sorting.”
Why it matters: Keeps consistency across the app.
5. Abstraction over ORM (like EF Core)
Hides EF Core-specific methods (
DbSet
,AsNoTracking
, etc.).If you switch from EF Core to Dapper or MongoDB, only the repository changes — not the controller or service.
🔸 Why it matters: Future-proofs your architecture.
6. Cleaner, Shorter Controllers
Controllers shouldn’t know how to talk to databases.
With repositories, controllers only call meaningful methods.
Why it matters: Follows Single Responsibility Principle (SRP).
var user = await _userRepository.GetByIdAsync(id);
Implementing the Repository Pattern - A Real world example
Suppose you got a table CompanyBasicInformation in database and the corresponding model in Models.
CREATE TABLE CompanyBasicInformation (
CompanyId VARCHAR(10) PRIMARY KEY,
CompanyName VARCHAR(100),
CompanyCountry VARCHAR(100)
)
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Collections.Generic;
namespace RepositoryPatternDemo.Models
{
[Table("CompanyBasicInformation")]
public class CompanyBasicInformation
{
[Key]
[StringLength(10)]
public string CompanyId { get; set; }
[StringLength(100)]
public string CompanyName { get; set; }
[StringLength(100)]
public string CompanyCountry { get; set; }
}
}
Step 1 : Setup a dbcontext class.
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Reflection.Emit;
namespace RepositoryPatternDemo.Models
{
public class ApplicationDbContext : DbContext
{
public DbSet<CompanyBasicInformation> CompanyBasicInformations { get; set; }
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<CompanyBasicInformation>()
.HasKey(c => c.CompanyId);
base.OnModelCreating(modelBuilder);
}
}
}
Step 2: Define the Generic Repository Interface
namespace RepositoryPatternDemo.Repository
{
public interface IRepository<T> where T : class
{
Task<IEnumerable<T>> GetAllAsync();
Task<T?> GetByIdAsync(object id);
Task AddAsync(T entity);
void Update(T entity);
void Delete(T entity);
Task SaveChangesAsync();
}
}
Step 3: Create the Generic Repository Implementation
The Generic Repository
is a reusable, type-safe class that provides a consistent way to perform common CRUD operations (Create, Read, Update, Delete) on any entity in the application. By leveraging generics (<T>
), it eliminates the need to repeat data access logic for each entity class.
using Microsoft.EntityFrameworkCore;
using RepositoryPatternDemo.Models;
namespace RepositoryPatternDemo.Repository
{
public class Repository<T> : IRepository<T> where T : class
{
protected readonly ApplicationDbContext _context;
private readonly DbSet<T> _dbSet;
public Repository(ApplicationDbContext context)
{
_context = context;
_dbSet = _context.Set<T>();
}
public async Task<IEnumerable<T>> GetAllAsync() => await _dbSet.ToListAsync();
public async Task<T?> GetByIdAsync(object id) => await _dbSet.FindAsync(id);
public async Task AddAsync(T entity) => await _dbSet.AddAsync(entity);
public void Update(T entity) => _dbSet.Update(entity);
public void Delete(T entity) => _dbSet.Remove(entity);
public async Task SaveChangesAsync() => await _context.SaveChangesAsync();
}
}
Key Features:
Type-agnostic: Works with any class (
T
) such asCompanyBasicInformation
etc.Centralized data access: Abstracts EF Core logic into one place.
Basic CRUD support:
GetAllAsync()
– retrieves all recordsGetByIdAsync(id)
– retrieves one by primary keyAddAsync(entity)
– adds a new recordUpdate(entity)
– updates an existing recordDelete(entity)
– removes a recordSaveChangesAsync()
– commits all changes
How it works:
Internally uses
DbSet<T>
from the injectedApplicationDbContext
Uses asynchronous methods for performance
Keeps your services and controllers clean and focused on business logic
Step 4: Add a Specific Repository for Company
Here we have CompanyRepository which does two things
1. Inherits from IRepository and gets all the common methods like GetAll(), GetById(int Id), Add() So there's no need to redefine those methods again — they're already available.
2. Adds it’s own custom method for business needs.
public interface ICompanyRepository : IRepository<CompanyBasicInformation>
{
Task<CompanyBasicInformation?> GetByCompanyIdAsync(string companyId);
}
public class CompanyRepository : Repository<CompanyBasicInformation>, ICompanyRepository
{
public CompanyRepository(ApplicationDbContext context) : base(context) { }
public async Task<CompanyBasicInformation?> GetByCompanyIdAsync(string companyId)
{
return await _context.CompanyBasicInformations
.FirstOrDefaultAsync(c => c.CompanyId == companyId);
}
}
Step 5: Create the Company Service Layer
Why a Service Layer?
Even with a repository in place, your controller shouldn't contain business logic, validations, or any direct orchestration of data access. That’s where the Service Layer comes in.
We’ll create two components:
ICompanyService
– the interface for the serviceCompanyService
– the actual implementation
using RepositoryPatternDemo.Models;
namespace RepositoryPatternDemo.Services
{
public interface ICompanyService
{
Task<IEnumerable<CompanyBasicInformation>> GetAllCompaniesAsync();
Task<CompanyBasicInformation?> GetCompanyByIdAsync(string companyId);
Task<CompanyBasicInformation> CreateCompanyAsync(CompanyBasicInformation company);
}
}
using Microsoft.EntityFrameworkCore;
using RepositoryPatternDemo.Models;
using RepositoryPatternDemo.Repository;
namespace RepositoryPatternDemo.Services
{
public class CompanyService : ICompanyService
{
private readonly ICompanyRepository _companyRepo;
public CompanyService(ICompanyRepository companyRepo)
{
_companyRepo = companyRepo;
}
public async Task<IEnumerable<CompanyBasicInformation>> GetAllCompaniesAsync()
{
return await _companyRepo.GetAllAsync();
}
public async Task<CompanyBasicInformation?> GetCompanyByIdAsync(string companyId)
{
return await _companyRepo.GetByCompanyIdAsync(companyId);
}
public async Task<CompanyBasicInformation> CreateCompanyAsync(CompanyBasicInformation company)
{
await _companyRepo.AddAsync(company);
await _companyRepo.SaveChangesAsync();
return company;
}
}
}
Step 6: Register Services and Repositories in Program.cs
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// Ensure Microsoft.EntityFrameworkCore.SqlServer package is installed
builder.Services.AddScoped<ICompanyRepository, CompanyRepository>();
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
builder.Services.AddScoped<ICompanyService, CompanyService>();
var app = builder.Build();
//appsettings.json
{
"ConnectionStrings": {
"DefaultConnection": "Server=ABCD\\SQLEXPRESS;Database=CompanyInfo;Trusted_Connection=True;TrustServerCertificate=True;"
},
}
make sure you have proper connection string as shown above.
Step 7: Use the Service in Controller
[ApiController]
[Route("api/[controller]")]
public class CompanyController : ControllerBase
{
private readonly ICompanyService _companyService;
public CompanyController(ICompanyService companyService)
{
_companyService = companyService;
}
[HttpGet("GetAll")]
public async Task<IActionResult> GetAll()
{
var result = await _companyService.GetAllCompaniesAsync();
return Ok(result);
}
[HttpGet("GetById/{id}")]
public async Task<IActionResult> GetById(string id)
{
var result = await _companyService.GetCompanyByIdAsync(id);
return result == null ? NotFound() : Ok(result);
}
[HttpPost("Create")]
public async Task<IActionResult> Create([FromBody] CompanyBasicInformation company)
{
var created = await _companyService.CreateCompanyAsync(company);
return CreatedAtAction(nameof(GetById), new { id = created.CompanyId }, created);
}
}
You are all set to use Repository pattern!
Subscribe to my newsletter
Read articles from Gulab Maurya directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Gulab Maurya
Gulab Maurya
Principal Software Engineer | 13+ yrs in .NET, C#, ASP.NET Core | Passionate about Clean Architecture, Design Patterns & Scalable Systems | Writing to simplify complex backend concepts.