How the Mediator design pattern simplifies an application

Published: Tuesday 30 July 2019

As your computer application gets more complex, more classes may be added. So, the more classes you add, the more difficult it can be to communicate between these classes. This can be a problem if these classes are located in different assemblies.

This is where the mediator design pattern comes in. With the mediator design pattern, the objects communicate within the mediator object, rather than directly with each other. As a result of this, it reduces dependencies on objects that communicate with each other, eliminating coupling.

MediatR

There are many third party mediator design patterns out there. The one we will concentrate on is MediatR. MediatR is descibed as a "Simple mediator implementation in .NET" on their GitHub repository, and we are going to use it in this article.

We are going to use MediatR in a simple ASP.NET Core application, but first we need to know how it works.

How does MediatR work?

MediatR works in three stages:

  • We create a request. The request is a custom class that you can set up
  • The request is sent to a request handler, which does the work you want it to do
  • As a result of the request handler finishing, it will return a response class, which is a custom class that you can set up

Here is a chart of how it exactly works.

Now for our example

So we are going to create a MediatR example a ASP.NET Core application. But first things first, you will need to add the following dependencies to your application, which are written by Jimmy Bogard:

  • MediatR
  • MediatR.Extensions.Autofac.DependencyInjection

We are now going to set up the request and response class. The point in this is that the request class is going to have two numbers. The response class will have the total.

public class AddRequest : IRequest<AddResponse>
{
	public AddRequest(int numberOne, int numberTwo)
	{
		NumberOne = numberOne;
		NumberTwo = numberTwo;
	}

	public virtual int NumberOne { get; }

	public virtual int NumberTwo { get; }

}
// AddResponse.cs
public class AddResponse
{
	public AddResponse(int total)
	{
		Total = total;
	}

	public virtual int Total { get; }
}

Next, it's time to set up the request handler. It will take the two numbers in the request handler and add them together. It will then return the total of this addition into the "response" class.

// AddRequestHandler.cs
public class AddRequestHandler : IRequestHandler<AddRequest, AddResponse>
{
	public Task<AddResponse> Handle(AddRequest request, CancellationToken cancellationToken)
	{
		var additionResult = request.NumberOne + request.NumberTwo;
		var response = new AddResponse(additionResult);

		return Task.FromResult(response);
	}
}

You may have noticed in the request and request handler classes that we are inheriting some interfaces. These interfaces are from the MediatR library.

The IRequest interface is a generic method that expects the name of the class that is going to contain the response. This is always inherited in the request class.

The same story is with the IRequestHandler interface, only that it inherits the request handler class and expects the request and response classes as it's generic dependencies.

How to add MediatR to Dependency Injection

Earlier, we mentioned that we had to include the MediatR.Extensions.Autofac.DependencyInjection package. As you may have guessed from the package name, this allows us to use MediatR as part of Dependency Injection. If we include services.AddMediatR(typeof(Startup).Assembly) inside the ConfigurationServices method in Startup.cs, this makes it as part of dependency injection.

Run MediatR from a Controller

Now that we have MediatR as part of dependency injection, we can use it as part of our MVC application. This MediatR object is stored within the IMediator class and we can pass that in as a parameter into our controller constructor.

Then, it's a case of setting up an action, creating our request. With this request, we send it through mediator and get our response.

// HomeController.cs
public class HomeController : Controller
{
	protected readonly IMediator _mediator;

	public HomeController(IMediator mediator)
	{
		_mediator = mediator;
	}

	public async Task<IActionResult> Index()
	{
		var request = new AddRequest(3, 9); // This is the request class
		var response = await _mediator.Send(request); // We send the request class through mediator which returns the response class

		return View(response);
	}
}
<!-- Index.cshtml -->
@model AddResponse
@{
    ViewData["Title"] = "Home Page";
}
The result is @Model.Total

In the example above, we have outputted the result into the view.

Adding to Dependency Injection

We haven't told our web application that if we send a particular request class through mediator, we expect to return a particular response class. This is where we can add it in dependency injection.

It's a case of adding a new transient method in dependency injection and including the interface and class name of the request handler within it.

// Startup.cs
public class Startup
{
	public Startup(IConfiguration configuration)
	{
		Configuration = configuration;
	}

	public IConfiguration Configuration { get; }

	// This method gets called by the runtime. Use this method to add services to the container.
	public void ConfigureServices(IServiceCollection services)
	{
		services.AddTransient<IRequestHandler<AddRequest, AddResponse>, AddRequestHandler>(); // MediatR dependency injection example
	}
}

Simplifying the application

Obviously, my example is very simple to make it easier for you to understand. As explained earlier, the whole point of the mediator design pattern is to use this pattern to connect classes that may be in different projects.

If you connect these classes directly, you may have problems with circular dependency. This is where one assembly relies on another and vice-versa. This is not possible to do in a .NET application.

If you wish to download the above example, it is available in my GitHub respository, or you can view the full example below:

// AddRequest.cs
using MediatR;

namespace MediatR_Example.Models
{
	public class AddRequest : IRequest<AddResponse>
	{
		public AddRequest(int numberOne, int numberTwo)
		{
			NumberOne = numberOne;
			NumberTwo = numberTwo;
		}

		public virtual int NumberOne { get; }

		public virtual int NumberTwo { get; }

	}
}
// AddRequestHandler.cs
using MediatR;
using System.Threading;
using System.Threading.Tasks;

namespace MediatR_Example.Models
{
	public class AddRequestHandler : IRequestHandler<AddRequest, AddResponse>
	{
		public Task<AddResponse> Handle(AddRequest request, CancellationToken cancellationToken)
		{
			var additionResult = request.NumberOne + request.NumberTwo;
			var response = new AddResponse(additionResult);

			return Task.FromResult(response);
		}
	}
}
// AddResponse.cs
namespace MediatR_Example.Models
{
	public class AddResponse
	{
		public AddResponse(int total)
		{
			Total = total;
		}

		public virtual int Total { get; }
	}
}
// HomeController.cs
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using MediatR_Example.Models;
using MediatR;

namespace MediatR_Example.Controllers
{
	public class HomeController : Controller
	{
		protected readonly IMediator _mediator;

		public HomeController(IMediator mediator)
		{
			_mediator = mediator;
		}

		public async Task<IActionResult> Index()
		{
			var request = new AddRequest(3, 9);
			var response = await _mediator.Send(request);

			return View(response);
		}
	}
}
<!-- Index.cshtml -->
@model AddResponse
@{
    ViewData["Title"] = "Home Page";
}
The result is @Model.Total
// Startup.cs
using MediatR;
using MediatR_Example.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace MediatR_Example
{
	public class Startup
	{
		public Startup(IConfiguration configuration)
		{
			Configuration = configuration;
		}

		public IConfiguration Configuration { get; }

		// This method gets called by the runtime. Use this method to add services to the container.
		public void ConfigureServices(IServiceCollection services)
		{
			services.Configure<CookiePolicyOptions>(options =>
			{
				// This lambda determines whether user consent for non-essential cookies is needed for a given request.
				options.CheckConsentNeeded = context => true;
				options.MinimumSameSitePolicy = SameSiteMode.None;
			});


			services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
			services.AddMediatR(typeof(Startup).Assembly);

			services.AddTransient<IRequestHandler<AddRequest, AddResponse>, AddRequestHandler>(); // Mediator dependency injection request
		}

		// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
		public void Configure(IApplicationBuilder app, IHostingEnvironment env)
		{
			if (env.IsDevelopment())
			{
				app.UseDeveloperExceptionPage();
			}
			else
			{
				app.UseExceptionHandler("/Home/Error");
				// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
				app.UseHsts();
			}

			app.UseHttpsRedirection();
			app.UseStaticFiles();
			app.UseCookiePolicy();

			app.UseMvc(routes =>
			{
				routes.MapRoute(
					name: "default",
					template: "{controller=Home}/{action=Index}/{id?}");
			});
		}
	}
}