IPostRepository.AddAsync(), GetByIdAsync() (in Command)
In modern architectures based on CQRS (Command Query Responsibility Segregation), one of the basic principles is the clear separation between the write side and the read side of the application. In previous articles, we discussed using ApplicationDbContext and applying transactions in Command Handlers. Today we go further and analyze an optional but frequently encountered concept: Repository Pattern for writing.
Why "optional"? Because there is no strict rule in CQRS that enforces the use of the Repository Pattern when working with Entity Framework Core. In many cases, direct access to DbContext is sufficient and even simpler. However, using a Repository can bring benefits regarding code clarity, testability, and separation of concerns.
🎯 What is the Repository Pattern?
The Repository Pattern represents an intermediate layer between the application and the data source. Its role is to expose a clear and well-defined interface for read or write operations, hiding the internal implementation details.
Instead of working directly with DbContext, we will call the methods of an IPostRepository that manages Post entities. This approach allows:
-
Reducing dependency on EF Core. The code in Handlers becomes less "coupled" to a specific technology.
-
Better testability, because we can replace the real repository with a mock or a fake in unit tests.
-
Clarity, because the data access logic is centralized in a single place.
🛠 Interface example – IPostRepository
Let's assume we have the Post entity. A simple repository for write operations could look like this:
public interface IPostRepository
{
Task AddAsync(Post post, CancellationToken cancellationToken = default);
Task<Post?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
}
The interface defines the essential methods that a Command Handler might need: adding a new post and finding a post by ID. We don't need many methods here – just what is strictly necessary for the application logic.
🏗 Concrete implementation
An implementation of this repository using EF Core could look like this:
public class PostRepository : IPostRepository
{
private readonly ApplicationDbContext _context;
public PostRepository(ApplicationDbContext context)
{
_context = context;
}
public async Task AddAsync(Post post, CancellationToken cancellationToken = default)
{
await _context.Posts.AddAsync(post, cancellationToken);
}
public async Task<Post?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
{
return await _context.Posts
.FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
}
}
Note that the implementation is quite simple: the repository does nothing more than "wrap" EF Core operations but provides a clearer and isolated interface.
⚙ Usage in Command Handler
Let's assume we have a CreatePostCommandHandler. Instead of using ApplicationDbContext directly, we can inject IPostRepository:
public class CreatePostCommandHandler : IRequestHandler<CreatePostCommand, Guid>
{
private readonly IPostRepository _repository;
private readonly IUnitOfWork _unitOfWork;
public CreatePostCommandHandler(IPostRepository repository, IUnitOfWork unitOfWork)
{
_repository = repository;
_unitOfWork = unitOfWork;
}
public async Task<Guid> Handle(CreatePostCommand request, CancellationToken cancellationToken)
{
var post = new Post(request.Title, request.Content);
await _repository.AddAsync(post, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return post.Id;
}
}
Here appears another interesting concept: Unit of Work, which can be used to centralize the call to SaveChangesAsync(). But even without Unit of Work, a repository already provides an additional level of clarity.
📌 Advantages and disadvantages
Advantages:
-
More organized and easier to read code.
-
Simpler unit tests (we mock the repository).
-
Respecting the "Dependency Inversion" principle (we depend on interfaces, not concrete implementations).
Disadvantages:
-
Additional code: the repository may seem redundant if it only "wraps" existing EF Core methods.
-
An extra layer of abstraction, which is not always necessary in small or simple applications.
🔎 Conclusion
Using a Repository for writing in CQRS is an architectural choice, not an absolute rule. In some projects, direct access to DbContext may be more efficient and simpler. In others, introducing an IPostRepository brings clarity, isolation, and facilitates testing.
The important thing is to evaluate your project's needs and choose the option that best fits your context. The Repository Pattern remains a valuable tool when you want to keep the code clean and respect DDD principles.