CQRS Pattern via MediatR

What is CQRS?

CQRS stands for Command and Query Responsibility Segregation. This pattern is used to separate read operations vs. update operations. Whether CQRS helps with better performance and scalability is a topic for discussion, but one thing is for sure: it helps with the maintainability and readability of the code.

With CQRS, the code looks neat and clean. CQRS allows the read and write workloads to scale independently, and may result in fewer lock contentions.

Situation

Let’s say we want to implement an Assets Service with simple APIs like Get All Assets, Get Asset By Id, and Create Asset (typical simple CRUD operations). Once the asset is created, you want to send an email notification to the back office.

Typically, we implement it using a straightforward pattern as shown in the diagram below.

Implementing Assets service using traditional pattern

Typically, we have an Asset Business Service, and Email Notifications Services and inject these services into the Assets Controller. Assets Repository is injected into Assets Business Service for performing database operations.

This code works, that’s not a problem. But it has a few issues:-

  • Controller and Business Services – although communicating with each other through DI – have a direct dependency on each other. The controller knows about business services.
  • There is a lot going on in the Assets Business Service. That class has a tendency to grow as soon as more APIs are added to the controller.

Mediator Patten

The mediator pattern helps in terms of encapsulating the interactions between various objects. Instead of having two or more objects take a direct dependency on each other, they instead interact with an intermediate object called a mediator.

What if we introduce a mediator between a controller and a business service class as shown in the diagram below? This way a direct dependency between the controller and business service can be resolved.

Implementing Assets Service using the mediator

This solves one problem only though. The problem of the Assets Business Service class growing and becoming a code smell remains. That problem can be solved by using the aforementioned CQRS pattern.

CQRS advocates to differentiate between read operations vs. write operations. With CRQS, read operations are called Queries, and write operations are referred to as Commands. So, if I apply this to the Assets Service – Get All Assets and Get Assets are Queries and Create Asset is a command.

How do I implement CQRS via Mediator pattern in .NET? In walks the topic of this blog – MediatR Nuget package.

MediatR Library

MediatR is an open-source library trying to solve a simple problem of decoupling the in-process sending of messages from handling messages. It is a cross-platform library and supports NetStandard2.0. MediatR supports request/response, commands, queries, notifications, and events, synchronous and async with intelligent dispatching.

So the same Assets Service using MediatoR and CQRS looks like something like this:-

Assets Service using MediatR

As can be seen in the diagram above:-

  • Controller and Business Service do not have direct dependencies. Both of them talk to each other through a mediator (loose coupling)
  • Read operations like Get All Assets and Get Asset By Id have different query classes and respective handler classes
  • Write operation like Create Asset has its own command class and the respective handler
  • Email Notification has it’s own object under Notifications
  • Events, notifications, commands, and queries are good ole’ POCO classes. Handlers are simple and so unit-testable!

Step 1 – Add the MediatR Nuget package to the project

Install-Package MediatR

Step 2 – Add Queries

namespace MediatorDemo.Queries
{
	public record GetAssetsByIdQuery(string assetId) : IRequest<Asset>
	{
	}
}

namespace MediatorDemo.Queries
{
	public class GetAssetsQuery : IRequest<IEnumerable<Asset>>
	{
	}
}

Step 3 – Add Query Handlers

namespace MediatorDemo.Handlers
{
	public class GetAssetsByIdQueryHandler : IRequestHandler<GetAssetsByIdQuery, Asset>
	{
		public async Task<Asset> Handle(GetAssetsByIdQuery request, CancellationToken cancellationToken)
		{
            // Returns the asset by Id by querying the databse repo
			var asset = new Asset() { AssetIdentifier = request.assetId, AssetName = "New Asset", AssetQuantity = 34};
			return asset;
		}
	}
}

namespace MediatorDemo.Handlers
{
	public class GetAssetsQueryHandler : IRequestHandler<GetAssetsQuery, IEnumerable<Asset>>
	{
		public async Task<IEnumerable<Asset>> Handle(GetAssetsQuery request, CancellationToken cancellationToken)
		{
            // dummy code showing return of multiple assets  
			var dummyAssets = new List<Asset>()
			{
				new Asset() { AssetIdentifier = Guid.NewGuid().ToString(), AssetName = "Dental Scanner 1", AssetQuantity = 12 },
				new Asset() { AssetIdentifier = Guid.NewGuid().ToString(), AssetName = "Dental Scanner 2", AssetQuantity = 22 }
			};
			return dummyAssets;
		}
	}
}

Step 4 – Add Commands

namespace MediatorDemo.Commands
{
	public class AddAssetCommand : IRequest<Asset>
	{
	}
}

Step 5 – Add Command Handlers

namespace MediatorDemo.Handlers
{
	public class AddAssetCommandHandler : IRequestHandler<AddAssetCommand, Asset>
	{
		public Task<Asset> Handle(AddAssetCommand request, CancellationToken cancellationToken)
		{
			//Add asset to the database 
			throw new NotImplementedException();
		}
	}
}

Step 6 – Add Notifications

namespace MediatorDemo.Notifications
{
	public record NewAssetAddedNotification(Asset asset) : INotification
	{
	}
}

Step 7 – Add Notification Handlers

namespace MediatorDemo.Handlers
{
	public class NewAssetAddedNotificationHandler : INotificationHandler<NewAssetAddedNotification>
	{
		public Task Handle(NewAssetAddedNotification notification, CancellationToken cancellationToken)
		{
			// send email to the back office  
			return Task.CompletedTask;
		}
	}
}

Step 8 – Add Controllers

namespace MediatorDemo.Controllers
{
	[ApiController]
	public class AssetsController : ControllerBase
	{
		IMediator _mediator;
		public AssetsController(IMediator mediator)
		{
			this._mediator = mediator;
		}

		[Route("api/assets")]
		[HttpGet]
		public async Task<ActionResult> GetAssets()
		{
			var assets = await _mediator.Send(new GetAssetsQuery());
			return Ok(assets);
		}

		[Route("api/assets/{assetId}")]
		[HttpGet]
		public async Task<ActionResult> GetAssetsById([FromRoute]string assetId)
		{
			var asset = await _mediator.Send(new GetAssetsByIdQuery(assetId));
			return Ok(asset);
		}

		[HttpPost]
		public async Task<ActionResult> AddAsset([FromRoute] Asset asset)
		{
			await _mediator.Send(new AddAssetCommand());
			await _mediator.Publish(new NewAssetAddedNotification(asset));
			return Ok();
		}
	}
}

Step 9 – Add MediatR using the pipeline

builder.Services.AddMediatR(cfg => {
	cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
});
  • Note that Commands and Queries have implemented the IRequet interface, while Notifications need to implement the INotification interface.
  • Here, I am demonstrating only one notification, but the important thing to note is there can be multiple event handlers available for one event. (Kind of like having multiple Subscriptions for one Topic.). When an event fires – all event handlers are invoked one after the other.
  • Note that notifications have the Publish() method and commands/queries have the Send() method.

Conclusion

CQRS provides a good separation of concerns at the code level. Mediator pattern helps implement loose coupling between various objects. MediatR is the best bate to implement simple queries, commands, and notifications in. NET. Let’s go rock some clean code!

Leave a comment