D20Tek Mediator supports both synchronous and async commands. In many cases, synchronous commands are plenty. But when working with data files, external service calls, and databases, it is best for performance throughput to build async WebApi handlers and async commands that process that data.
To this point, we have mostly looked at synchronous ICommandHandlers. Here is a recap:
internal sealed record WeatherForecastCommand : ICommand<WeatherForecast[]>;
internal sealed record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
internal sealed class GetForecastCommandHandler : ICommandHandler<WeatherForecastCommand, WeatherForecast[]>
{
private static readonly string[] _summaries =
[
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
];
public WeatherForecast[] Handle(WeatherForecastCommand command) =>
Enumerable.Range(1, 5)
.Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
_summaries[Random.Shared.Next(_summaries.Length)]
)).ToArray();
}
internal static class WeatherForecastEndpoints
{
routes.MapGet("api/v1/weatherforecast", (IMediator mediator) => mediator.Send(new WeatherForecastCommand()))
.Produces<WeatherForecast[]>()
.WithName("GetWeatherForecast")
.WithTags("Weather Service");
}
...
// Map endpoints in Program.cs
app.MapForecastEndpoints();
Convert to Async Commands
We’re going to change several records and classes above to make them async and support the ICommandHandlerAsync interface (supported by our Mediator implementation).
1. Define an async command (so that it’s different from the base implementation – typically you wouldn’t have two in a project).
internal sealed record WeatherForecastAsyncCommand : ICommand<WeatherForecast[]>;
2. Now implement the GetForecastCommandHandlerAsync class, which derives from ICommandHandlerAsync
internal sealed class GetForecastCommandHandlerAsync : ICommandHandlerAsync<WeatherForecastAsyncCommand, WeatherForecast[]>
{
private static readonly string[] _summaries =
[
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
];
public Task<WeatherForecast[]> HandleAsync(WeatherForecastAsyncCommand command, CancellationToken cancellationToken) =>
Task.FromResult(Enumerable.Range(1, 5)
.Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
_summaries[Random.Shared.Next(_summaries.Length)]
)).ToArray());
}
Notice that ICommandHandlerAsync defines the HandleAsync method (rather than the corresponding ICommandHandler.Handle method). It will get the appropriate command as well as a cancellation token, which is useful for canceling long-running async methods. And it must return a Task<TResult> rather than just the result.
In this simple API, we just wrap the WeatherForecast array in a Task. But in commands that read data from a file or database, we would call other async methods whose result needs to be propagated up to the WebApi handler.
In all other aspects, the sync and async commands are nearly the same.
3. Finally, we need to add the async WebApi handler that uses this new command handler.
internal static class WeatherForecastEndpoints
{
public static void MapForecastEndpoints(this IEndpointRouteBuilder routes)
{
routes.MapGet("api/v1/async/weatherforecast",
async (IMediator mediator, CancellationToken cancellationToken) =>
await mediator.SendAsync(new WeatherForecastAsyncCommand(), cancellationToken))
.Produces<WeatherForecast[]>()
.WithName("GetWeatherForecastAsync")
.WithTags("Weather Service");
routes.MapGet("api/v1/weatherforecast", (IMediator mediator) => mediator.Send(new WeatherForecastCommand()))
.Produces<WeatherForecast[]>()
.WithName("GetWeatherForecast")
.WithTags("Weather Service");
}
}
With this added code, we now have two endpoints… one makes a synchronous call and the other is async. But both return the same data.
Notice that the async GET operation calls the IMediator.SendAsync method to go through the correct path, find the appropriate ICommandHandlerAsync, and call it with the command object. This path is completely parallel and complimentary with the synchronous version. And you application can have both sync and async commands based on the needs of each individual command/scenario.
Run the WebApi
Now when we run this project we can call either of the endpoints. Here’s the result of calling the async command handler version:

If you would like to see the full code for this project, please review the WeatherForecast endpoints found in the D20Tek Mediator sample.
Conclusion
D20Tek Mediator has full support for both synchronous and asynchronous command handlers. There are interfaces defined for building commands of either type (ICommandHandler or ICommandHandlerAsync). And the IMediator interface has both the Send and SendAsync methods, so that your calling code can use the appropriate path. Please try using both and see the results.
In future articles we will look at a full implementation of the MemberService WebApi. That service integrates with a database for full CRUD operations. And you will see the full implementation of async command handlers there.