3

I have a .Net 5 Web API project and use MediatR to encapsulate my business logic into commands and queries since I don't like to have a single CRUD service handling everything related to a specific resource because my usecases are too complex.

For the data access part I'm using EF Core.

Given the example query to retrieve a list of users via GET /users the API controller fires a query

public sealed class GetUsersQueryHandler : IRequestHandler<GetUsersQuery, IEnumerable<User>>
{
    public async Task<IEnumerable<User>> Handle(GetUsersQuery getUsersQuery, CancellationToken cancellationToken)
    {
        // logic goes here => access data via repository
    }
}

This query would make use of the IUsersRepository to access the data

public interface IUsersRepository
{
    Task<IEnumerable<User>> GetUsers();

    /*
        get one, create one, update one, delete one, ... 
    */
}

public sealed class UsersRepository : IUsersRepository
{
    private readonly DatabaseContext _databaseContext;

    public UsersRepository(DatabaseContext databaseContext)
    {
        _databaseContext = databaseContext;
    }

    public async Task<IEnumerable<User>> GetUsers() => await _databaseContext.Users.ToListAsync();
}

This repository implementation feels wrong to me since it forces me to follow CRUD. My data source queries are never that simple

  • Query handlers might want to query the database in different ways
    • Fetch all users above 30
    • Fetch all users with no tasks
    • Fetch all users with a red car and 3 jobs
  • Command handlers might want to run transactional logic and even query from multiple repositories
    • Create a new user task and write to the audit log table on success
    • Create a new user and store some details in another table on success

so things might get messy or mixed up. On the other hand I could

  • write the data access logic directly in the query/command handlers

    This violates the single responsibility and I wouldn't be able to reuse data access logic.

  • use MediatR to create reusable isolated data access handlers

    Basically this means splitting up a repository into multiple classes, one class per action. This sounds good to me but on the other hand this might be an overkill.

How did you solve such problems?

5
  • 1
    I see two parts in your question. One is where to draw boundaries in a CQRS system and the other is how to get this MediatR library to conform to the boundaries set. The former has been answered quite often here and the latter is off-topic (explaining or help writing code). If i understood your question incorrectly clarify please.
    – marstato
    May 7, 2021 at 9:11
  • @marstato: come on, this question does not really ask for help to write code - even if an answer may require a code scetch, this is clearly a conceptual question.
    – Doc Brown
    May 7, 2021 at 9:22
  • @DocBrown okay, not exactly writing code. But at least how to use the libraries in the CQRS context.
    – marstato
    May 7, 2021 at 10:07
  • @marstato: so what? Are we now going to close every question which is not 100% technology agnostic?
    – Doc Brown
    May 7, 2021 at 16:37
  • Repositories are about de-coupling the application away from SQL, however EF already succeeds at this for many common database operations; so I'd suggest a "YAGNI" sanity check of whether repository achieves anything in your specific project that you couldn't do equally well with DbContext/DbSet directly. This is to ensure you're solving a real problem (For example, wrapping database logic which EF by itself can't handle) and not just falling into traps such as "this is how we've always done it" or "this is what examples on the internet are doing". May 8, 2021 at 0:08

1 Answer 1

3

This repository implementation feels wrong to me since it forces me to follow CRUD.

Well, you chose a simplistic example where the entire business logic (i.e. your handles) effectively mirrors an available method in your repository.

Had you picked a different example, you would have had business logic that is more than just a CRUD-passthrough. Just inventing something for the sake of example:

public sealed class GetLivingUsersQueryHandler : IRequestHandler<GetLivingUsersQuery, IEnumerable<User>>
{
    private readonly IUserRepository userRepository;
    private readonly IObituaryService obituaryService;

    // basic DI constructor omitted

    public async Task<IEnumerable<User>> Handle(GetLivingUsersQuery getLivingUsersQuery, CancellationToken cancellationToken)
    {
        var users = await userRepository.GetUsersAsync();

        var livingUsers = users.Where(user => !obituaryService.HasDied(user));

        return livingUsers;
    }
}

And there you have it: complex business logic that is not solely CRUD-driven.

Query handlers might want to query the database in different ways

Just to be clear, a "query" (as in "query handler") does not refer to a database query, it refers to a CQRS query. Those are two very different concepts.

Since the actual database query handling is represented by your repository, it is therefore your task to ensure that the repository allows the querying of data in the database in the way you need it to.

Whether that is one large Get method with optional filtering, or specific methods such as GetUsersOlderThan(int age), is completely contextual. Either can become overkill in certain scenarios. For simple column-equality filters, I tend to just make one method with optional filtering, but the choice here is yours.

Command handlers might want to run transactional logic

Again, the responsibility of the database is left up to your repositories. If you need transactions that span across repositories, then a unit of work is going to help you control that.

That being said, there is an argument to be made for not wrapping EF's DbContext/DbSet<> into a UOW/repository, because DbContext/DbSet<> already is a unit of work (DbContext allows transactions) over a bunch of repositories (DbSet<>).
However, this means that EF crosses the boundary of the data layer. Purists will argue that a third-party-library may never bleed into your domain. I, on the other hand, argue that this advice applies only to libraries and not frameworks, and it's not called Entity Library after all. But that is a very long and hotly debated discussion that I'm not going to rehash here.

and even query from multiple repositories

(assuming no UOW) So inject the repositories you need into your command handler. Nowhere is it said that a handler is only allowed to access one repository.

(assuming a UOW) Commonly, the UOW provides access to all its repositories. So by injecting the UOW into your handler, you have access to all repositories that are covered by the UOW.

so things might get messy or mixed up

I'm not sure where the mix up here is. I get the feeling that because you're conflating "database query" and "CQRS query", that you're not seeing that your handler and your repository are two different layers (handler = business, repository = datalayer), with each their own focus.

The datalayer (and thus repository) handles the basic database querying. You can make this interface as complex as you need it to be. It doesn't need to be barebones CRUD, but barebones CRUD is a good initial start, and you can develop extra methods along the way.

The business layer (and thus handler) performs your complex business logic, which might entail speaking to multiple services (be they repositories, external adapters, internal services, ...) in order to achieve the needed outcome.

Yes, some business logic may sometimes be a pure passthrough onto a repository method. If you are fetching data which comes from one database table and requires no post-processing, you end up with a very simple business method.
But when there is some actual business logic to be done, the handler can be adjusted to perform more than just this call onto the repository.

use MediatR to create reusable isolated data access handlers

It is important to note here that the mediator pattern (which Mediatr is a popular implementation of) is just a pattern, and it doesn't inherently tell you that it handles business logic.

You could, in principle, use a mediator pattern in the datalayer (instead of your repositories), and then have a regular service in your business logic.

It is fairly common to see the mediator pattern used to handle the business layer, and it's fairly common to see the repository pattern used to handle the data layer. But those are not set in stone. You can actually implement the mediator pattern on any layer where you want to decouple individual operations and not try to mash them into pre-categorized boxes.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.