Repository Pattern Explained with ASP.NET Core and Entity Framework

Gulab MauryaGulab Maurya
6 min read

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 as CompanyBasicInformation etc.

  • Centralized data access: Abstracts EF Core logic into one place.

  • Basic CRUD support:

    • GetAllAsync() – retrieves all records

    • GetByIdAsync(id) – retrieves one by primary key

    • AddAsync(entity) – adds a new record

    • Update(entity) – updates an existing record

    • Delete(entity) – removes a record

    • SaveChangesAsync() – commits all changes

  • How it works:

  • Internally uses DbSet<T> from the injected ApplicationDbContext

  • 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:

  1. ICompanyService – the interface for the service

  2. CompanyService – 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!

0
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.