In general, we put our business logic in service classes. Sometimes they are larger, sometimes smaller. There is also a dilemma of whether they should know about each other. Let us consider what our code might look like if we created a separate class for each method of the service. We will use the MediatR library to do this.
If you are wondering why, the answer is simple: to increase consistency and reduce coupling. We will not deal with concepts like CQRS or CQS for now.
Some theory
Normally, the in-system messages and the code that processes them are tightly coupled. In large ASP.NET projects, it usually looks like this:
This can lead to 2 things:
- Refactoring business logic can bring about changes at multiple places in the project.
- The increasing number of endpoints correlates with the DI constructor explosion in your MVC controllers
Using a mediator pattern, we create an object that encapsulates how objects interact. This, in turn, reduces dependencies between communicating objects and allows the interaction between them (in the example: controller and service) to be changed independently.
After incorporating MediatR into our projects, the project will look more or less like this:
Which service (or repository) is used after the controller action is called is determined at the beginning of the project. In the following project, all interactions are determined by scanning the assembly, but there is also the possibility to define connections explicitly.
Using the MediatR library
To take advantage of the MediatR library, you must add two packages to the project.
The first package named MediatR contains the entire core. The second package contains MediatR.Extensions.Microsoft.DependencyInjection. This helps in proper registration of the library in the . NET Core MVC application DI container. Also, this package automatically registers all the Handlers in our project.
The easiest way to add them is to use the "AddMediatR" method, which extends the IServiceCollection interface.
If our handlers are in a different assembly, we can overload this method and simply tell it which assembly to look in.
First Notification
Notifications here are simple POCO objects that implement the INotification interface. Notification Handlers execute code, when such objects are passed to MediatR and one such notification can trigger many handlers. Notification Handlers do not return anything.
This is just a pointer interface. It does not force us to implement any special method.
Next, we move on to introduce the class that will handle the notification described above. The class must implement INotificationHandler, while we need to specify in the T parameter which notification we want to handle in this class.
The interface will force us to implement the method "public Task Handle(T notification, CancellationToken cancellationToken)". It is in this method that our entire implementation of the Notification handler will be. Let's create an example class "MyFirstNotificationHandler".
All we need to do is add the retrieval of the mediator instance from the DI container to the controller. Then, in some controller action, we can send our event using the Publish method.
Done! We have a complete event implementation in C# and .NET Core. We can invoke it by using swagger. After typing anything and clicking execute, we get 200 Success response from the web API. Here is how it looks:
You have probably noticed that I’ve used Console WriteLine 3 times in the code. As you can see, the output is there after executing our endpoint.
This might seem unimportant right now, but we will need it to debug MediatR’s decorator implementation, called Pipeline Behaviours, later in this tutorial.
Benefits
- Changes can be easily made later
- Such code is easy to test
- All your code has a pattern that can be easily reused by another programmer
- It is easy to find suitable functions in the code
- Here, the Single Responsibility Principle is taken very seriously. There is no better way to fulfill it.
Getting a response after sending a request
We are making it too easy for ourselves. So let us consider a problem: what happens when we need to get a response from an event? For example, to find out if a particular operation was successful, or as part of an error message to the user. This behavior is also provided for in the MediatoR library.
In the MediatoR world we call such a use case a "Request". It is something like an HTTP Request :), or at least you can see the similarities in operation. We send a request and receive a response.
Coming back to the point - our object POCO must inherit from the interface IRequest. Where T stands for the return type to be returned by the handler.
Now, just as with the notification, we need to implement a class, which will be responsible for handling the MyFirstRequest request. This time, the class has to implement the IRequestHandler interface, where we must specify in T the class of the request to handle. Tout is the data type returned by the Handle method. The interface will force us to implement the method "public Task Handle(T notification, CancellationToken cancellationToken)". Our class will look as follows.
Now we just need to call the Send method on the MediatR instance and pass the newly created MyFirstRequest object. The Send method will return the result of calling the Handle method from our MyFirstRequestHandler.
Only one handler can be registered per request type. This is a limitation of this library that we must adhere to. If more than one class is to handle a request, then we need to use…
Pipeline Behaviours
Sometimes it is necessary to process certain types of requests through multiple classes. For example:
- the first class might be responsible for logging that the request was sent to the system
- the class might be caching: for repeated requests for the same data, we might store responses in memory or Redis and return them from there instead of processing the database request
- another one might be validation that checks if the provided data is correct, etc.
In this part of the article you are reading, we will create example Pipeline Behaviours for caching and logging.
Let us start with the tracing behaviour. As you can see, it's a simple class that implements the IPipelineBehavior interface. For simplicity, I'll output some text as in the previous examples:
await next() calls either the next behaviour or finally, the handler for that request. Here is some code for the second behaviour:
The last thing to be done is to actually register those classes in Startup.cs:
Keep in mind, that the order of pipeline registration is important. The behaviours will be executed in the LIFO style: the code of the first class, in this case:
- TracingBehaviour will be executed up to its await next(); line,
- CachingBehaviour will write line to console and await next();
- Handler will do it’s thing
- CachingBehaviour will finalize
- TracingBehaviour will execute it’s so-called “postlogic”
If this reminds you of the middlewares in ASP.NET Core, it's just that, only without references to libraries that deal with web projects, so it can be used in non-web applications without all the "fat".
Summary
By using MediatR, we have reduced the coupling between controllers and the services they invoke. Instead of referencing multiple services, controllers now only reference the MediatR class. This makes it easier to maintain code and apply different configurations to your project. Note, however, that it might be more difficult for new developers who are not familiar with the concept. So make sure you refer them to this article ;)
Thanks for taking the time to read the article. I hope that some knowledge remains, and you will apply everything in practice. The code used in this article is available at https://github.com/tomaszcekalo/MyFirstMediatorWebApi