Generic MediatR CRUD Feature
The notion of having a generic repository is that you get to write less code for a bunch of similar functionalities for each entity. For example,
No matter how many types you have, if they extend the Entity
base class then you have a common read and write functionality,
The entity we have in our hand is the WeatherForecast
which as stated earlier entends the base Entity
class,
public abstract class Entity
{
public int Id { get; set; }
}
public class WeatherForecast : Entity
{
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
}
In the realm of CQRS, we should consider seperating the read and write functionalities into their respective command and query featues. Using MediatR, you would want to create two features to tackle this,
- Features
|
- Weather // Feature for a specific domain entity
|
- Create // Generalized command featue
| |
| - Command
| - CommandHandler
| - IRepository // Feature level repository contract
|
- Read // Generalized query featue
|
- Query
- QueryHandler
- IRepository
Here comes the time you should get your thinking cap on! What happens if you a are adding another domain entity? Let's just say the new entity is FooBar
. You end up with a new enity specific feature folder with same old read and write features,
- Features
|
- Weather // Feature for a specific domain entity
| |
| - Create // Generalized command featue
| | |
| | - Command
| | - CommandHandler
| | - IRepository // Feature level repository contract
| |
| - Read // Generalized query featue
| |
| - Query
| - QueryHandler
| - IRepository
|
- Foobar // Feature for a specific domain entity
|
- Create // Generalized command featue
| |
| - Command
| - CommandHandler
| - IRepository // Feature level repository contract
|
- Read // Generalized query featue
|
- Query
- QueryHandler
- IRepository
It completely depends on you whether to go with the pain of creating individual features or make a single generic one to rule them all.
For the lack of a better name, let's call the generic feature Crud
,
- Features
|
- Crud // Generic feature for all domain entity
|
- Create // Generalized command featue
| |
| - Command
| - CommandHandler
| - IRepository // Feature level repository contract
|
- Read // Generalized query featue
|
- Query
- QueryHandler
- IRepository
The generic Query
should look like this,
The generic QueryHandler
,
Feature level generic repository contract.
Generally you would register all the handlers with the DI container just by calling,
But in the case of our generic handlers this won't work as it needs to know the specific type for which you are writing the handler. To overcome this problem, register these hanglers explicity,
To call this handler from a specific controller action, inject the IMediator
interface in the constructor and call the feature as shown below,
Bonus:
You might not want to expose an entity directly from a controller. In that scenario, a view-model/dto could come in handy. The mapping of the entity to a view-model could be done inside the handlers. You would want to modify your Query
as follows,
The handler should be modified as follows,
AutoMapper for mapping an entity to a view-model/dto
The registration of the handler is also modified,
The WeatherForecastModel
view-model is nothing but a `POCO `exposing only the properties that are intended. To make the mapping work, Automapper
needs a mapping configuration as follows,
Here, TemperatureF
is a computed property which is only available in the view-model
One last thing you would want to do is to register Automapper
services in the DI container,
Just like before, call the feature from a controller's action method,
And here you have it; a generic feature to handle all crud related operations for every entiy. Browse the repository source code to find the implementation of the generic Write
command which is pretty similar to the Read
feature that I've demonstrated.
Repository:
Links: