Introduction
Today I own the Service Fabric infrastructure piece in my team, to run applications and services written by otherw. But before I wrote a Service Fabric application to deploy other Service Fabric applications, it’s called accordingly – Deployment Service. It’s up and running in public and two national cloud environments, works perfectly fine (except huge backlog that that was put on hold and then effectively cut, of course). Initially it consisted of three “classic” layers:
- Controller (ASP.NET Core)
- Service (business logic)
- Repository (CosmosDB)
I’m using Simple Injector as the Dependency Injection container of my choice with active usage of the Decorator pattern.
Having an interface like this for one of the “services” that plays the central role in the application:
public interface IDeploymentService
{
Task<DeploymentPayload> GetOrCreateDeployment(StartDeploymentRequest request);
Task<DeploymentPayload> GetDeployment(Guid deploymentId);
Task CancelDeployment(Guid deploymentId);
}
I ended up with the following hierarchy of classes that implement and/or “decorate” it:
- IDeploymentServices
- DeploymentService
- MultiTenantDeploymentServiceDecorator
- LoggingDeploymentServiceDecorator
DeploymentService was implemented in a way that its methods called each other and it was working fine. Sooner or later almost every application receives a feature request which often leads to a lot of rethinking and restructuring, it’s called multi-tenancy. I implemented it relatively easily by introducing just yet another decorator, conventionally called MultiTenantDeploymentServiceDecorator.
Now, when a call reaches the bottom of the stack, i.e.:
- IDeploymentService.GetOrCreateDeployment(request);
- LoggingDeploymentServiceDecorator.GetOrCreateDeployment(request);
- MultiTenantDeploymentServiceDecorator.GetOrCreateDeployment(request);
- DeploymentService.GetOrCreateDeployment(request);
at DeploymentService then calls to other methods will be to DeploymentService itself as well. What bypasses the multi-tenancy decorator and brakes the functionality:
public sealed class DecoratorService : IDeploymentService
{
public async Task<DeploymentPayload> GetOrCreateDeployment(StartDeploymentRequest request)
{
// this call
var deployment = await GetDeployment(request.DeploymentId);
}
public async Task<DeploymentPayload> GetDeployment(Guid deploymentId)
{
// will jump here
}
}
public sealed class MultiTenantDeploymentServiceDecorator : IDeploymentService
{
public async Task<DeploymentPayload> GetDeployment(Guid deploymentId)
{
// and not here
}
}
This issue forced me to drop current architecture and redo it in the right way: Command Query Responsibility Segregation (CQRS).
Handlers, commands and queries
After refactoring each service’s method into its own class accepting (handling) its own type of command or query, I ended up with the following classes:
public sealed class StartDeploymentHandler : IRequestHandler<StartDeploymentCommand, DeploymentPayload>
{
}
public sealed class GetDeploymentHandler : IRequestHandler<GetDeploymentQuery, DeploymentPayload>
{
}
public sealed class CancelDeploymentHandler : IRequestHandler<CancelDeploymentCommand>
{
}
With single logging decorator (neat!):
public sealed class LoggingHandler<T, TResult> : IRequestHandler<T, TResult>
{
}
And multi-tenancy decorator wherever it’s applicable:
public sealed class MultiTenantGetDeploymentHandler : IRequestHandler<GetDeploymentQuery, DeploymentPayload>
{
}
MediatR
Instead of reinventing the wheel, I’m using the interfaces from small, slick and well-designed, open source library called MediatR which implements the Mediator pattern.
So now my ASP.NET Core controller looks like this:
[ApiController]
public sealed class DeploymentController : ControllerBase
{
private readonly IMediator _mediator;
public DeploymentController(IMediator mediator) =>
_mediator = mediator;
[HttpPut]
public async Task<IActionResult> StartDeployment(StartDeploymentRequest request, CancellationToken cancellationToken)
{
var command = new StartDeploymentCommand(request);
var deployment = _mediator.Send(command, cancellationToken);
}
}
Here’s how its unit test looks like now:
[TestClass]
public class DeploymentControllerTest
{
[TestMethod]
public async Task StartDeployment_Should_Return_StatusCode_Accepted_And_State_Queued_When_Deployment_Is_Accepted_And_Queued()
{
// Arrange
var deploymentId = Guid.NewGuid();
var request = new StartDeploymentRequest
{
DeploymentId = deploymentId
};
var deploymentPayload = new DeploymentPayload
{
DeploymentId = deploymentId
};
var mediator = new Mock<IMediator>();
mediator.Setup(m => m.Send(It.Is<GetDeploymentQuery>(q => q.DeploymentId == request.DeploymentId)))
.ReturnsAsync((DeploymentPayload)null);
mediator.Setup(m => m.Send(It.Is<CreateDeploymentCommand>(c => c.Request == request)))
.ReturnsAsync(deploymentPayload);
var controller = new DeploymentController(mediator.Object);
// Act
var result = await controller.StartDeployment(request, CancellationToken.None);
// Assert
}
}
And it illustrates perfectly the issue with Mediator in general and MediatR in particular: the code that uses it violates, or stops following, even if with good intentions, the SOLID principles. Namely, it’s not possible anymore to tell what are the dependencies of DeploymentController as it stops telling that explicitly. At least not in the design time. I personally find it now an anti-pattern due to the very same reasons to why Service Locator/Inversion of Control is.
Back to injecting handlers
So instead DeploymentController’s constructor now looks like this:
[ApiController]
public sealed class DeploymentController : ControllerBase
{
public DeploymentController(
IRequestHandler<StartDeploymentRequest, DeploymentPayload> startDeploymentHandler,
IRequestHandler<GetDeploymentQuery, DeploymentPayload> getDeploymentHandler,
IRequestHandler<CancelDeploymentCommand> cancelDeploymentHandler)
{
}
}