Design Pattern
.NET .NET Core C# Patterns Visual Studio

Using Design Patterns in .NET Core – Part 11 – Mediator

Welcome to today’s post.

In today’s post I will be discussing another software design pattern which will prove to be extremely useful in your .NET Core applications, and that is the mediator pattern.

The mediator pattern is categorized as a behavioral design pattern. A behavioral design pattern deals with algorithms and responsibilities. The mediator pattern has been discussed quite often in the past decade with the growing adoption of cloud-based environments. These include systems that use the SaaS based architecture, and those that utilize Microservice based architectures. Most recently the use of the third party Mediatr library within a Microservice architecture.

In the first section I will explain how the Mediator design pattern is used in Microservices.

Usage of the Mediator Pattern in Microservices

With the Microservice-based architectures, the need to synchronize data across different isolated application components that use their own databases is required so that the common data that the applications share achieves a state of eventual consistency. Examples of data sources that are shared across components would include lookup data and commonly referenced business data such as product catalogues or inventory records.

With systems those clients require additional scalability to handle high volume transactions and maintain responsiveness are prime candidates to use the mediator pattern. The mediator pattern is well suited to single command requests or even batched command requests that can be submitted to an API or service. Each command request can then be parsed, validated, and then routed to a method, which can be within the API or to a remote API service through a messaging broker, such as a Service Bus.

The most common problem the mediator pattern solves is its ability to decouple components within an application domain. In the case of a .NET Core Web API, we can decouple the controller from a service class by using a mediator that parses, validates, and routes the request to the best matching method within a service class.

The UML class model below shows how we decouple our service classes from the controller through the mediator class.

In the next section I will show how we implement a command processor with the Mediator pattern.

Command Processing with Mediator Pattern

We construct a command handler class that stores the command and an identifier that represents the command id. In the example below I use an identifier that represents the type of command that is dispatched for processing.

I have a set of command constants I define that are used for each command:

public enum CommandConstants
{
    LOAN_COMMAND_ID = 1,
    RETURN_COMMAND_ID = 2
}

The command handler class is shown below:

public class CommandHandler<T>
{
    public T Command { get; }
    public int Id { get; }
    public CommandHandler(T command, int id)
    {
  	    Command = command;
        Id = id;
    }
}

The interface for the mediator with a Send() command that takes a command handler of type class is shown below:

public interface IMediator
{
    public Task Send(CommandHandler<LoanCommand> command);
}

I then define the abstract base class for the commands I will dispatch through the mediator. Properties that are not shared between the classes have been declared as virtual so that they can be overridden and exposed within the derived classes.

The LoanCommand abstract base class is defined below: 

public abstract class LoanCommand
{        
    public string UserId { get; private set; }

    public string UserName { get; private set; }

    public bool OnShelf { get; private set; }

    public DateTime DateDue { get; private set; }

    public int BookID { get; private set; }

    public virtual DateTime DateLoaned { get; set; }

    public virtual DateTime DateReturn { get; set; }

    public virtual string ReturnMethod { get; set; }
        
    public virtual int LoanID { get; set; }

    protected LoanCommand(string userId, string userName,
        Boolean onShelf, DateTime dateDue, int bookID)
    {
       	this.UserId = userId;
       	this.UserName = userName;
        this.OnShelf = onShelf;
        this.DateDue = dateDue;
        this.BookID = bookID;
    }
}

I then define the concrete classes for two of the commands I will dispatch through the mediator.

Each class is derived from the LoanCommand class, with their non-common properties derived from the base class.

The LoanBorrowClass is defined below:

public class LoanBorrowCommand: LoanCommand
{
    public override DateTime DateLoaned { get; set; }

    public LoanBorrowCommand(string userId, string userName, DateTime dateLoaned, 
        DateTime dateDue, bool onShelf, int bookID): 
        base(userId, userName, onShelf, dateDue, bookID)
    {
        this.DateLoaned = dateLoaned;
    }
}

The LoanReturnClass is defined below:

public class LoanReturnCommand: LoanCommand
{
    public override int LoanID { get; set; }

    public override DateTime DateReturn { get; set; }

    public override string ReturnMethod { get; set; }

    public LoanReturnCommand(string userId, string userName,
        DateTime dateDue, DateTime dateReturn, bool onShelf,
        string returnMethod, int bookID, int loanID) :
        base(userId, userName, onShelf, dateDue, bookID)
    {
       	this.DateReturn = dateReturn;
        this.ReturnMethod = returnMethod;
        this.LoanID = loanID;
    }
}

In the next section I will show how to make use of the Command Handler to dispatch commands from within the Mediator class.

Implementation of Command Dispatching with the Mediator Class

Once we have defined the loan commands, we implement the Send() method of the mediator. This involves checking the values of the command within the command handler, then act on the command type, which is stored within the identifier property. An instance of the command handler class is a parameter within the Send() method.

The Mediator class implementation is shown below:

public class Mediator: IMediator
{
    private readonly ILogger<Mediator> _logger;
    private readonly ILoanService _loanService; 

    public Mediator(
        ILoanService loanService, 
        ILogger<Mediator> logger)
    {
       	_loanService = loanService;
        _logger = logger;
    }

    public async Task Send(CommandHandler<LoanCommand> command)
    {
       	CommandConstants commandID = (CommandConstants)command.Id;
        var commandName = command.Command.GetType().ToString();

        var loanCmd = new Models.LoanViewModel()
        {
            ReturnMethod = command.Command.ReturnMethod,
            DateDue = command.Command.DateDue,
            OnShelf = command.Command.OnShelf,
            BookID = command.Command.BookID,
            LoanedBy = command.Command.UserName,
            DateLoaned = command.Command.DateLoaned
        };

        switch (commandID)
        {
           	case CommandConstants.LOAN_COMMAND_ID:
                loanCmd.ReturnMethod = command.Command.ReturnMethod;
                await _loanService.SaveLoan(loanCmd);
            break;

            case CommandConstants.RETURN_COMMAND_ID:
         	    loanCmd.ID = command.Command.LoanID;
                await _loanService.ReturnLoan(loanCmd);
            break;
        }

        _logger.LogInformation($"Mediator() - Sending Command {commandName}");
    }
}

As we can see, we have retrieved the command constants from the command identifier using the following line:

CommandConstants commandID = (CommandConstants)command.Id;

Then we used a switch to determine which service to call based on the value of the command identifier matching one of the command constants:

LOAN_COMMAND_ID

RETURN_COMMAND_ID

In the next section I will show how to make use of the Mediator class within a Web API controller method.

Usage of the Mediator Command within a Web API Method

For our Web API application, we can make use of the Mediator class within controller methods. For the LoanBook() API method I construct the Web API that despatches the loan command through the mediator class as shown:

[HttpPost("api/[controller]/LoanBook")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task<ActionResult> LoanBook([FromBody] LoanViewModel loanViewModel)
{
    LoanCommand loanCommand = new LoanBorrowCommand(
        loanViewModel.LoanedBy,
        loanViewModel.LoanedBy,
        loanViewModel.DateLoaned,
        loanViewModel.DateDue,
        loanViewModel.OnShelf,
        loanViewModel.BookID);

    CommandHandler<LoanCommand> commandHandler = 
        new CommandHandler<LoanCommand>(
            loanCommand, 
            Convert.ToInt32(CommandConstants.LOAN_COMMAND_ID)
        );
            
    if (ModelState.IsValid)
    {
       	try
        {
            await _mediator.Send(commandHandler);
            return Ok(loanViewModel);
        }
        catch (Exception ex)
        {
            return BadRequest(new { ex.Message });
        }
    }
    return BadRequest();
}

Before calling the mediator, I created and populated instances for the

LoanCommand and CommandHandler<LoanCommand> classes, with the CommandHandler<T> instance passed through the mediator Send() method.

Below is a similar implementation for the ReturnBook() API method:

[HttpPost("api/[controller]/ReturnBook")]
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public async Task<ActionResult> ReturnBook([FromBody] LoanViewModel loanViewModel)
{
    LoanCommand loanCommand = new LoanReturnCommand(
        loanViewModel.LoanedBy,
        loanViewModel.LoanedBy,
        loanViewModel.DateDue,
        loanViewModel.DateReturn,
        loanViewModel.OnShelf,
        loanViewModel.ReturnMethod,
        loanViewModel.BookID,
        loanViewModel.ID);

    CommandHandler<LoanCommand> commandHandler =
        new CommandHandler<LoanCommand>(
            loanCommand,
            Convert.ToInt32(CommandConstants.RETURN_COMMAND_ID)
        );

    if (ModelState.IsValid)
    {
       	try
        {
           	await _mediator.Send(commandHandler);
            return Ok(loanViewModel);
        }
        catch (Exception ex)
        {
            return BadRequest(new { ex.Message });
        }
    }
    return BadRequest();
}

In the Swagger interface, both our API methods in this example as HTTP POST methods as shown:

When running the Web API, as we can see for the ReturnBook() API method, we pass in the body that has a JSON payload that consists of the book loan parameters:

A response status of 200 is returned when the call succeeds. In the event of failure, a bad request of 400 is returned. Below is an example of a successful response:

As you can see from the implementation above, the mediator design pattern, as I mentioned earlier, despite having a reputation as only being useful in architectures that are of a high complexity, their use has shown that even though we have added some overhead (in terms of the Command handler classes) when we decouple the controller from the service classes, the major benefits we have gained here are:

  1. Making our API service methods testable though the Mediator class.
  2. Delegating cross cutting concerns into the Mediator class, promoting a cleaner, more testable implementation.
  3. Centralising the potential points of failure so that application maintenance is improved.
  4. Increase the scalability of API calls where the mediator is integrated with a messaging broker (such as Service Bus).

That is all for today’s post.

I hope you found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial