100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
.NET Framework

Repository Pattern in MVC

How the repository pattern abstracts data access behind interfaces, decoupling MVC controllers from Entity Framework and improving testability.

Models and DataAdvanced9 min readJul 10, 2026
Analogies

Why Abstract Data Access?

The repository pattern introduces an interface, such as IProductRepository with methods like GetById(int id), GetAll(), Add(Product product), and Delete(int id), that sits between your MVC controllers and the underlying data-access technology. Instead of a controller instantiating a DbContext and writing LINQ queries directly, it depends on IProductRepository, and a concrete EfProductRepository class implements that interface using Entity Framework internally. This indirection means a controller's data-access calls are expressed in terms of domain operations rather than persistence details, and swapping the underlying storage — or substituting a fake implementation for unit tests — doesn't require touching controller code at all.

🏏

Cricket analogy: It is like a captain calling for 'a defensive field' rather than personally positioning all eleven fielders by name; the coach's tactical instruction stays the same regardless of whether the actual execution is done by a experienced fielding coach or a stand-in, similar to how MS Dhoni delegated field placements through general calls.

Defining and Implementing the Repository Interface

A well-scoped repository interface exposes only the operations a domain actually needs, avoiding a generic leaky abstraction like IQueryable<T> GetAll() that lets EF-specific query composition leak all the way up into the controller. A typical interface pairs domain-focused methods — IEnumerable<Product> GetByCategory(int categoryId), Product GetById(int id) — with a concrete class that injects an AppDbContext and translates each method into the corresponding LINQ query, keeping all Entity Framework-specific code, including .Include() calls and change tracking, fully contained inside the implementation.

🏏

Cricket analogy: It is like a bowling coach giving a specific, purposeful drill list — 'yorker practice', 'bouncer control' — instead of just handing a bowler a bucket of balls and saying 'do whatever'; the specific interface keeps training focused the way Bharat Arun structured India's pace attack sessions.

csharp
public interface IProductRepository
{
    Product GetById(int id);
    IEnumerable<Product> GetByCategory(int categoryId);
    void Add(Product product);
    void Update(Product product);
    void Delete(int id);
    void Save();
}

public class EfProductRepository : IProductRepository
{
    private readonly AppDbContext db;

    public EfProductRepository(AppDbContext context) => db = context;

    public Product GetById(int id) =>
        db.Products.Include(p => p.Reviews).FirstOrDefault(p => p.Id == id);

    public IEnumerable<Product> GetByCategory(int categoryId) =>
        db.Products.Where(p => p.CategoryId == categoryId).ToList();

    public void Add(Product product) => db.Products.Add(product);

    public void Update(Product product) =>
        db.Entry(product).State = EntityState.Modified;

    public void Delete(int id)
    {
        var product = db.Products.Find(id);
        if (product != null) db.Products.Remove(product);
    }

    public void Save() => db.SaveChanges();
}

// Controller depends on the abstraction, injected via constructor
public class ProductsController : Controller
{
    private readonly IProductRepository repo;
    public ProductsController(IProductRepository repository) => repo = repository;

    public ActionResult Index(int categoryId) =>
        View(repo.GetByCategory(categoryId));
}

Testability and the Unit of Work

Because the controller depends on IProductRepository rather than a concrete EfProductRepository, a unit test can construct the controller with an in-memory fake or a mocked repository (using a library like Moq) that returns predetermined data instantly, with no database, no connection string, and no test-database cleanup required. In larger applications with several repositories sharing one DbContext, a Unit of Work class wraps multiple repositories and exposes a single Save() method, ensuring that a controller action touching both IOrderRepository and IInventoryRepository commits both changes together in one SaveChanges() call rather than two separate, potentially inconsistent transactions.

🏏

Cricket analogy: It is like a coach running a tactical simulation on a whiteboard instead of an actual match to test a batting order, getting instant results without needing a real stadium, real players, or a real toss, similar to how analysts model scenarios before a World Cup final.

Register the interface-to-implementation mapping in your dependency injection container's composition root (for example, container.Bind<IProductRepository>().To<EfProductRepository>() in Ninject or the equivalent in Unity/Autofac) so MVC's controller factory can resolve the concrete implementation automatically when a controller is instantiated.

Avoid the 'generic repository over IQueryable' anti-pattern where GetAll() returns IQueryable<T> and controllers freely chain .Where() and .Include() on it — this defeats the entire purpose of the abstraction because EF-specific query composition leaks straight into the controller, and swapping implementations later becomes impossible without rewriting every controller.

  • The repository pattern introduces an interface between controllers and data access, hiding Entity Framework specifics behind domain-focused methods.
  • Controllers depend on the interface (e.g., IProductRepository), not the concrete EF implementation, via constructor injection.
  • Well-scoped repository methods express intent (GetByCategory) rather than leaking IQueryable composition to callers.
  • This abstraction makes controllers unit-testable with mocked or in-memory fake repositories, with no real database required.
  • A Unit of Work class coordinates multiple repositories sharing one DbContext so related changes commit together atomically.
  • Dependency injection containers wire the interface-to-implementation mapping at the composition root.
  • Returning raw IQueryable<T> from a generic repository is an anti-pattern that leaks persistence details into controllers.

Practice what you learned

Was this page helpful?

Topics covered

#NETFramework#ASPNETMVCStudyNotes#MicrosoftTechnologies#RepositoryPatternInMVC#Repository#Pattern#MVC#Abstract#StudyNotes#SkillVeris