RO EN

Persistence through EF Core in the Infrastructure Layer

Persistence through EF Core in the Infrastructure Layer
Doru Bulubasa
06 August 2025

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.