25 Aug 2026
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.
// 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.
// 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.
NoTracking means EF Core does not waste memory tracking entities you will never save..UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
b.MigrationsHistoryTable(nameof(Blog) + "_EFMigrationHistory");
IQueryable<BlogPost> queryable = _dbContext.BlogPosts;
IBlogDbContext does not expose a DbSet, the handler cannot touch that table. Natural encapsulation without wrapper classes.public interface IBlogDbContext : ISaveChangesAsync
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.
I continuously build, learn and experiment with innovative technology. Allow me to share what I learn, with you.