DbContext-as-Repository: Skip the Abstraction You Do Not Need

DbContext-as-Repository: Skip the Abstraction You Do Not Need

25 Aug 2026

DbContext-as-Repository: Skip the Abstraction You Do Not Need

Repository pattern has been the default recommendation in .NET for over a decade. Create an interface, implement it, inject it, wrap every DbSet call in a method. But here is the uncomfortable truth: for most applications, EF Core's DbContext already IS the repository. Adding another abstraction on top of it creates boilerplate without meaningful benefit.

Brand Website V3 skips the explicit Repository classes entirely. Each module exposes a DbContext interface that handlers inject directly. This interface exposes DbSet<T> properties and inherits ISaveChangesAsync. That is the repository. Clean, minimal, and without unnecessary ceremony.

The DbContext Interface IS Your Repository

// Blog.Database/IBlogDbContext.cs
public interface IBlogDbContext : ISaveChangesAsync
{
    DbSet<BlogPost> BlogPosts { get; set; }
    DbSet<BlogPostCategory> BlogPostCategories { get; set; }
    DbSet<BlogPostTag> BlogPostTags { get; set; }
    DbSet<AttachmentEntity> BlogPostAttachments { get; set; }
}

This is it. This is the "repository" for the entire Blog module. It exposes the DbSet properties that handlers need, and it inherits ISaveChangesAsync so handlers can commit changes. There is no IRepository<BlogPost> with 15 methods, half of which nobody uses.

DI Registration: Clean and Explicit

// Blog.Database/Extensions.cs (DI registration)
services.AddDbContext<IBlogDbContext, BlogDbContext>(options =>
    options
        .UseSqlServer(configuration.GetConnectionString("DefaultConnection"), b =>
        {
            b.MigrationsAssembly(typeof(BlogDbContext).Assembly.FullName);
            b.MigrationsHistoryTable(nameof(Blog) + "_EFMigrationHistory");
        })
        .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));

The registration wires the interface to the concrete implementation, configures SQL Server with a per-module migration history table, and sets NoTracking as the default query behavior. Every handler that needs database access just injects IBlogDbContext.

Why It Works

  • No tracking by default for read-heavy workloads. Most handlers are queries. NoTracking means EF Core does not waste memory tracking entities you will never save.
.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
  • Separate migration history tables per module. Blog, Reviews, and Shop each have their own migration history. Modules can migrate independently.
b.MigrationsHistoryTable(nameof(Blog) + "_EFMigrationHistory");
  • Handlers use LINQ directly on DbSet. No repository wrapper between you and EF Core. Full query flexibility, full LINQ power.
 IQueryable<BlogPost> queryable = _dbContext.BlogPosts;
  • The interface enforces what handlers can access. If IBlogDbContext does not expose a DbSet, the handler cannot touch that table. Natural encapsulation without wrapper classes.
public interface IBlogDbContext : ISaveChangesAsync

When You DO Need More

This does not mean you can never create a service class. If a query involves multiple database calls, complex aggregations, or cross-entity logic that does not belong in a single handler, you can still create a dedicated service. The point is that you start with the DbContext interface and only add abstraction when you genuinely need it.


Brand Website V3 implements DbContext-as-Repository across all 11 domain modules. Each module has its own DbContext interface, its own migration history, and zero repository boilerplate. See the full pattern at kiss-code.com.


Join the community

I continuously build, learn and experiment with innovative technology. Allow me to share what I learn, with you.

Newsletter

Allow me to share what I learn, with you.

Share

Support

An error has occurred. 🗙