In this article, I will briefly discuss what CQRS and Mediator patterns are. I’ll talk about their advantages and disadvantages, and I will pratcially implement the later in a demo project using the MediatR package.
The source code created in this article is available at my Github repository:
https://github.com/Gwergilius/Mediator.Api.Template/tree/v1.0.0
About CQRS pattern
CQRS is often mentioned together with Mediator pattern. CQRS is an abbreviation for Command/Query Responsibility Segregation. It’s a design pattern allowing developers to separate the responsibility of commands (object manipulation) and queries (reads) into different models.
Traditionally we are using CRUD pattern (Create-Read-Update-Delete) for data manipilation, where the CRUD-like service or controller is responsible four all four operations. With CQRS we would instead split these operations into two models: one for the queries (aka “R”), and one for the commands (aka “CUD”)

As we can see, the Application simply separtates the query and command models. The CQRS pattern itself makes no formal requirements of how this separation occures. This can be as simple as creating separate classes in the same application (as we will do with MediatR) to physically separate services that run on different servers. Such a decision is based on factors such as scaling and infrastructure requirements, so we won’t go into that right now.
What is Mediator Pattern
The Mediator pattern simply defines an object that wraps the interactions between objects. Instead of two or more objects directly depending on each other, they instead interact with a “broker” who is responsible for sending interactions to the other party.

The Mediator pattern can be understood as a kind of “in-process messaging”. That is, the components do not communicate with each other directly, but send requests through the Mediator. Each handler is responsible for serving a specific type of requests .
MediatR
is a NuGet package that implements the infrastructure necessary to build applications based on the Mediator pattern. By adding the MediatR component to our application, we can reduce the dependencies between our components; i.e. our application will be less coupled, i.e. easier to manage. Moreover, with this model we can scale the separate handler components differently. For example, each of the CRUD operations runs to a separate handler, which allows us to choose different pricing models for reading and data manipulation components.

Boosts an ASP.NET Core Web API by the MediatR package
Create a new ASP.NET Core Web API
First off, let’s create an empty solution and a new ASP.NET Core Web Application project in it. I’ll do it by PowerShell commands:
> dotnet new sln -n MediatR.Example
> dotnet new webapi -o MediatR.Example.API
> dotnet sln add MediatR.Example.API
The second command will create a new ASP.NET Web API project for target framework of Net7.0, with no authentication, and with HTTPS and OpenAPI (Swagger) support turned on. (By adding other parameters to the command you can fine tune your new project) .
You can also create the project by the
Create a new project
wizard at the Visual Studio’s startup page. You should select theASP.NET Core Web API
template here, and leave all the settings on their default values.
You can open the solution in your Visual Studio IDE. Let’s make some changes on it.
Recover our good old Startup class
Let’s create a Startup.cs
class in the project’s root directory, and move the initialization code into it. Honestly, I prefer the good old Startup class to the new approch of mixing every startup thing in the Program class. The Program class and the (initial version of) the Startup class shall be formed as follows:
Program.cs
:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var startup = new MediatR.Example.API.Startup(builder.Configuration);
startup.ConfigureServices(builder.Services);
// Configure the HTTP request pipeline.
var app = builder.Build();
startup.Configure(app, app.Environment);
app.Run();
Startup.cs
(initial version):
namespace MediatR.Example.API;
public class Startup
{
private IConfiguration _configuration;
public Startup(IConfiguration configuration)
{
this._configuration = configuration;
}
/// <summary>
/// Add services to the container.
/// </summary>
/// <param name="services"></param>
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
services.AddEndpointsApiExplorer();
services.AddSwaggerGen();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
{
if (environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints => endpoints.MapControllers());
}
}
Let me point out some changes done when the code was moved from the Program class to the Startup class:
1. IConfiguration dependency
A dependency of type IConfiguration
was inserted through the constructor. The sample API itself doesn’t actually use this, but it will come in handy in many other applications. That is why it is included in this template.
2. Prefer UseEndpoints to MapControllers
Honestly, I hate the app.MapControllers()
method. It encapsulates the UseRouting
and the UserEndpoint
middlewares.
UseRouting
and UseEndpoints
middleware provide more flexibility and control over the routing and handling of requests in your API. The UseRouting
middleware is responsible for determining which endpoint should handle a given request based on the route template, while the UseEndpoints
middleware is responsible for executing the endpoint and generating a response.
On the other hand, MapControllers
middleware is a shorthand for configuring routing and endpoints for controllers. This middleware is intended for simple scenarios where you want to map all your controllers to a specific route prefix and use conventional routing for your controllers.
If you have more complex routing requirements or need to handle requests in a specific way, you should use UseRouting
and UseEndpoints
instead of MapControllers
.
Carve out the data source into a separate service
Let’s introduce a low level service providing WeatherForecast data. Let’s create a folder called Services
, and a new class in it called WeatherForecastService
.
namespace MediatR.Example.API.Services;
public class WeatherForecastService
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm",
"Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly WeatherForecast[] _forecasts;
public WeatherForecastService()
{
_forecasts = Enumerable.Range(1, 30).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
public Task<IEnumerable<WeatherForecast>> GetWeatherForecast()
=> Task.FromResult(_forecasts.AsEnumerable());
}
The code in this service is almost the same as the one initially provided by the WeatherForecast controller. The main difference is that the forecast collection is not generated during the responding the forecast request, but in the service’s constructor. Therefore, the response to the GetWeatherForecast request will always be the same. Not to menthin that we can easily add another method later that provides a single forecasts.
After the service is created, select the class name and press Ctrl+. for the quick actions on the class. Extract the class’ interface in a new file, and an IWeatherForecastService.cs
file is generated in the Services
folder. Add it to the DI container in the Startup.cs class.
Startup.cs
(fragment)
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
services.AddEndpointsApiExplorer();
services.AddSwaggerGen();
services.AddScoped<IWeatherForecastService, WeatherForecastService>();
}
As you could see above, the code lines have actually taken from the controller. So, remove them from the controller code. To allow the application run properly, add a dependency on our new service to the controller injected through the constructor.
Controllers/WeatherForecastController.cs (updated):
using MediatR.Example.API.Services;
using Microsoft.AspNetCore.Mvc;
namespace MediatR.Example.API.Controllers;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private readonly IWeatherForecastService _weaterForecastService;
private readonly ILogger _logger;
public WeatherForecastController(
IWeatherForecastService weaterForecastService,
ILogger<WeatherForecastController> logger)
{
_weaterForecastService = weaterForecastService;
_logger = logger;
}
[HttpGet]
public async Task<IEnumerable<WeatherForecast>> Get()
{
return await _weaterForecastService.GetWeatherForecast();
}
}
At this point, our sample application is correct again. You could run and test it from your Visual Studio IDE without any error.
Implement your first MediatR reauest
Add the classes required by the MediatR pattern
Let’s introduce the MediatR pattern. First, install the MediatR package via the Package Manager Console. Then install the extensions wiring up MediatR with the ASP.NET DI container.
PM> install-package MediatR
PM> install-package MediatR.Extensions.Microsoft.DependencyInjection
Create two folders in your solution, one called Requests
, the other is RequestHandlers
.
Create a new class in the Requests
folder representing the request for getting the WeatherForecast list:
Requests/GetAllForecastsRequest.cs:
namespace MediatR.Example.API.Requests;
public record GetAllForecastsRequest : IRequest<IEnumerable<WeatherForecast>>
{
}
It’s a very simple request, since it has no parameter at all. It’s marked by the IRequest<IEnumerable<WeatherForecast>>
interface declaring that it will provide an IEnumerable<WeatherForecast>
instance as a return value.
Note: It’s a good practice to use records for Requests instead of classes, as they are usually immutable types. However you must use classes, when
- You pass return values from your handlers through your request instances
- You use the request instances on multiple threads (see Notifications later)
Then create a class under the RequestHandlers
folder. We shall implement the IRequestHandler
interface for the GetAllForecast
request what shall provides an IEnumerable<WeatherForecast>
instance (Line 6). The handler will actually serve the request
by utilizing an IWeatherForecastService
instance injected through the handler’s constructor (Lines 10-11).
RequestHandlers/GetAllForecastHandler.cs:
namespace MediatR.Example.API.RequestHandlers;
using Requests;
using Services;
public class GetAllForecastsHandler :
IRequestHandler<GetAllForecastsRequest, IEnumerable<WeatherForecast>>
{
private readonly IWeatherForecastService _forecastService;
public GetAllForecastsHandler(IWeatherForecastService forecastService)
=> _forecastService = forecastService;
public async Task<IEnumerable<WeatherForecast>> Handle(GetAllForecastRequest request, CancellationToken cancellationToken)
=> await _forecastService.GetWeatherForecast();
}
Eliminate the controller’s dependency from the service
Let’s leave the service to the handler and use the central mediator in the controller instead. Remove the service instance from the controller, inject an IMediator instance instead, and send a GetAllForecast request from the controllers Get method:
Controllers/WeatherForecastController.cs (final):
using Microsoft.AspNetCore.Mvc;
namespace MediatR.Example.API.Controllers;
using Requests;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private readonly IMediator _mediator;
private readonly ILogger _logger;
public WeatherForecastController(
IMediator mediator,
ILogger<WeatherForecastController> logger)
{
_mediator = mediator;
_logger = logger;
}
[HttpGet]
public async Task<IEnumerable<WeatherForecast>> Get()
{
return await _mediator.Send(new GetAllForecastRequest());
}
}
If we run the code now, we will receive an ugly System.InvalidOperationException
saying
Unable to resolve service for type 'MediatR.IMediator' while attempting to activate 'MediatR.Example.API.Controllers.WeatherForecastController'.
Yes, you are right. We need to initialize the MediatR component in our Startup
class by adding an IMediator
instance and all our requests and handlers to the DI container. Forunately, the MediatR package provides a simple method to do this. Let’s append an AddMediatR
call to our ConfigureServices
method in the Startup
class:
Startup.cs (fragment, final):
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
services.AddEndpointsApiExplorer();
services.AddSwaggerGen();
services.AddScoped<IWeatherForecastService, WeatherForecastService>();
services.AddMediatR(typeof(Startup));
}
The type parameter we pass to the AddMediatR invocation designates the Assembly to be searched for requests and request handlers in it. The AddMediatR method will scan the Assembly containing the specified type and registers the requests and handlers found in it into the DI container. You can use any type from your API assembly, We could have used any type of the API assembly, it is arbitrary that we chose the Startup class. If your requests and/or handlers are implemented by multiple assemblies, their reference types must be listed separated by commas in the parameter list of the AddMetiatR method.
Test your request by Postman
Now let’s make sure everything is working as expected.
First, let’s hit CTRL+F5 to build and run our app. You will see log messages in the terminal window such as

You shall remember the URL in the first message — e.g.: https://localhost:7269 (your port number may differs) — we will need it for creating HTTP request testing our web API.
You can test the API trough the Swagger UI generate by the compiler, or send HTTP requests by Postman. I strongly recommend to use Postman, as it has much more powerful features than Swagger can provide by its raw HTML panel. So, fire up Postman and create a new request (incorporating the URL the API listening on), save the request and Send it:

Congratulation! The response we recieved when the request is sent proves that MediatR is working correctly. The values we see are the ones initialized by our WeatherForecastService. We’ve just implemented our first MediatR request.
Passing parameters to the request
The actual task of a MediatR request is to receive parameters from the HTTP request, validate them, and convert them into a form interpretable by the handler. In the previous chapter, we had a very simple task: we asked for all forecasts and did not need parameters to refine the task. However, the situation changes when we only want to get a forecast for a specified day.
Let’s add a new request to our solution for asking the API to provide a single forecast. The expected result will be a single WeatherForecast
instance now, and the request will receive a date parameter through it constructor. Let’s say we get the incoming date as a string. Then the request must validate that the string represents a valid date, convert it to an internal DateOnly
format, and save it for later use by the request handler. This is what our new request does.
Requests/GetForecastRequest.cs
:
namespace MediatR.Example.API.Requests;
public record GetForecastRequest : IRequest<WeatherForecast>
{
public GetForecastRequest(string date)
{
if(DateOnly.TryParse(date, out var dateValue))
{
Date = dateValue;
}
else
{
throw new FormatException($"'{date}': Not a valid date");
}
}
public DateOnly Date { get; }
}
Our controller will have a second method getting a single forecast. It will simply create a new GetForecastRequest
instance by passing its input parameter to it, and sending it to the good old mediator instance.
Controllers/WeatherForecast.cs:
using Microsoft.AspNetCore.Mvc;
namespace MediatR.Example.API.Controllers;
using Requests;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private readonly IMediator _mediator;
private readonly ILogger _logger;
public WeatherForecastController(
IMediator mediator,
ILogger<WeatherForecastController> logger)
{
_mediator = mediator;
_logger = logger;
}
[HttpGet]
public async Task<IEnumerable<WeatherForecast>> Get()
{
return await _mediator.Send(new GetAllForecastRequest());
}
[HttpGet("{date}")]
public async Task<WeatherForecast> Get(string date)
{
return await _mediator.Send(new GetForecastRequest(date));
}
}
Then let’s extend our WeatherForecastService
with a method that returns a single forecast, or null if no forecast is found that matches the input date.:
Services/WeatherForecastService.cs
:
namespace MediatR.Example.API.Services;
public class WeatherForecastService : IWeatherForecastService
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm",
"Balmy", "Hot", "Sweltering", "Scorching"
};
private WeatherForecast[] _forecasts;
public WeatherForecastService()
{
_forecasts = Enumerable.Range(1, 30).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
public Task<IEnumerable<WeatherForecast>> GetWeatherForecast()
=> Task.FromResult(_forecasts.AsEnumerable());
public Task<WeatherForecast?> GetWeatherForecast(DateOnly date)
=> Task.FromResult(_forecasts.FirstOrDefault(x => x.Date == date));
}
Remember that the new method shall be published on the IWeatherForecast
interface as well:
Services/IWeatherForecastService.cs
:
namespace MediatR.Example.API.Services;
public interface IWeatherForecastService
{
Task<IEnumerable<WeatherForecast>> GetWeatherForecast();
Task<WeatherForecast?> GetWeatherForecast(DateOnly date);
}
Finally, let’s implement our new Request Handler using the new service method to retrieve a particular forecast:
RequestHandlers/GetForecastHandler.cs:
namespace MediatR.Example.API.RequestHandlers;
using Services;
using Requests;
public class GetForecastHandler : IRequestHandler<GetForecastRequest, WeatherForecast>
{
private readonly IWeatherForecastService _forecastService;
public GetForecastHandler(IWeatherForecastService forecastService)
{
_forecastService = forecastService;
}
public async Task<WeatherForecast> Handle(
GetForecastRequest request,
CancellationToken cancellationToken)
{
var forecast = await _forecastService.GetWeatherForecast(request.Date);
if(forecast != null)
{
return forecast;
}
throw new FileNotFoundException($"{request.Date}: there is no forecast for this date");
}
}
Note: the handler itself handles the case when we request a forecast for a date that is not included in our “database”. This relieves the controller of all tasks, so it remains as thin as possible.
Error handling can be solved with a central exception handler, which translates exceptions into HTTP error codes and standard error messages. If we prefer the Result pattern to the exceptions, then we would have to indicate the error in the returned Result structure. However, the error handling still remains the handler’s responsibility. Since error handling is beyond the scope of this article, we will not cover it here
Conclusion
The above example shows a separation of request, response, logic, and communication using MediatR. And if you want to move to CQRS (Command and Query Responsibility Segregation) pattern, the mediator can make the transition seamless.