In an architecture based on the principles of Clean Architecture or DDD (Domain-Driven Design), separation of responsibilities is essential. An important aspect is separating data access logic into an Infrastructure Layer, where we use Entity Framework Core to interact with the database.
🧩 Why do we separate persistence into the Infrastructure Layer?
-
We keep the business code clean and testable.
-
We allow easy testing of the logic using mocks or fakes (through interfaces).
-
We can change the persistence engine (e.g., EF Core → Dapper or MongoDB) without touching the business logic.
🔧 Step 1: Defining a common interface – IApplicationDbContext
This interface will be used in all areas that need to access the database, but without knowing the concrete implementation (i.e., ApplicationDbContext).
public interface IApplicationDbContext
{
DbSet<Post> Posts { get; }
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
Note that we expose only what is necessary (e.g., DbSets and the save method).
🧱 Step 2: Concrete implementation – ApplicationDbContext
This class inherits from DbContext from EF Core and implements the IApplicationDbContext interface.
public class ApplicationDbContext : DbContext, IApplicationDbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options) { }
public DbSet<Post> Posts => Set<Post>();
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
=> base.SaveChangesAsync(cancellationToken);
}
🧪 Step 3: Registration in DI (Dependency Injection)
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")));
services.AddScoped<IApplicationDbContext>(provider => provider.GetRequiredService<ApplicationDbContext>());
Thus, the code that receives IApplicationDbContext (e.g., a CommandHandler) does not know it is using EF Core – which makes the code testable and decoupled.
✍️ Usage example in CreatePostCommandHandler
public class CreatePostCommandHandler : IRequestHandler<CreatePostCommand, Guid>
{
private readonly IApplicationDbContext _context;
public CreatePostCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<Guid> Handle(CreatePostCommand request, CancellationToken cancellationToken)
{
var post = new Post
{
Id = Guid.NewGuid(),
Title = request.Title,
Content = request.Content
};
_context.Posts.Add(post);
await _context.SaveChangesAsync(cancellationToken);
return post.Id;
}
}
✅ Conclusion
By defining an interface (IApplicationDbContext) and using it in the business code, we maintain a clear separation between:
-
business logic (which knows nothing about EF Core)
-
data infrastructure (which implements EF Core)
This separation is fundamental for scalability, testability, and long-term maintenance.