> logger) =>
289
CHAPTER 6 | Tackle Business Complexity in a Microservice with DDD and CQRS Patterns
_logger = logger;
public
async Task
Handle
(TRequest request,
RequestHandlerDelegate next)
{
_logger.
LogInformation
($
"Handling {typeof(TRequest).Name}"
);
var
response = await
next
();
_logger.
LogInformation
($
"Handled {typeof(TResponse).Name}"
);
return
response;
}
}
Just by implementing this behavior class and by registering it in the pipeline (in the MediatorModule
above), all the commands processed through MediatR will be logging information about the
execution.
The eShopOnContainers ordering microservice also applies a second behavior for basic validations,
the
ValidatorBehavior
class that relies on the
FluentValidation
library, as shown in the following code:
public
class
ValidatorBehavior
: IPipelineBehavior
{
private
readonly
IValidator[] _validators;
public
ValidatorBehavior
(IValidator[] validators) =>
_validators = validators;
public
async Task
Handle
(TRequest request,
RequestHandlerDelegate next)
{
var
failures = _validators
.
Select
(v => v.
Validate
(request))
.
SelectMany
(result => result.
Errors
)
.
Where
(error => error !=
null
)
.
ToList
();
if
(failures.
Any
())
{
throw
new
OrderingDomainException
(
$
"Command Validation Errors for type {typeof(TRequest).Name}"
,
new
ValidationException
(
"Validation exception"
, failures));
}
var
response = await
next
();
return
response;
}
}
Here the behavior is raising an exception if validation fails, but you could also return a result object,
containing the command result if it succeeded or the validation messages in case it didn’t. This would
probably make it easier to display validation results to the user.
Then, based on the
FluentValidation
library, you would create validation for the data passed with
CreateOrderCommand, as in the following code:
public
class
CreateOrderCommandValidator : AbstractValidator
{
public
CreateOrderCommandValidator
()
{
290
CHAPTER 6 | Tackle Business Complexity in a Microservice with DDD and CQRS Patterns
RuleFor
(command => command.
City
).
NotEmpty
();
RuleFor
(command => command.
Street
).
NotEmpty
();
RuleFor
(command => command.
State
).
NotEmpty
();
RuleFor
(command => command.
Country
).
NotEmpty
();
RuleFor
(command => command.
ZipCode
).
NotEmpty
();
RuleFor
(command => command.
CardNumber
).
NotEmpty
().
Length
(
12
,
19
);
RuleFor
(command => command.
CardHolderName
).
NotEmpty
();
RuleFor
(command =>
command.
CardExpiration
).
NotEmpty
().
Must
(BeValidExpirationDate).
WithMessage
(
"Please specify
a valid card expiration date"
);
RuleFor
(command => command.
CardSecurityNumber
).
NotEmpty
().
Length
(
3
);
RuleFor
(command => command.
CardTypeId
).
NotEmpty
();
RuleFor
(command => command.
OrderItems
).
Must
(ContainOrderItems).
WithMessage
(
"No
order items found"
);
}
private
bool
BeValidExpirationDate
(DateTime dateTime)
{
return
dateTime >= DateTime.
UtcNow
;
}
private
bool
ContainOrderItems
(IEnumerable orderItems)
{
return
orderItems.
Any
();
}
}
You could create additional validations. This is a very clean and elegant way to implement your
command validations.
In a similar way, you could implement other behaviors for additional aspects or cross-cutting concerns
that you want to apply to commands when handling them.
Additional resources
The mediator pattern
•
Mediator pattern
https://en.wikipedia.org/wiki/Mediator_pattern
The decorator pattern
•
Decorator pattern
https://en.wikipedia.org/wiki/Decorator_pattern
MediatR (Jimmy Bogard)
•
MediatR.
GitHub repo.
https://github.com/jbogard/MediatR
•
CQRS with MediatR and AutoMapper
https://lostechies.com/jimmybogard/2015/05/05/cqrs-with-mediatr-and-automapper/
•
Put your controllers on a diet: POSTs and commands.
https://lostechies.com/jimmybogard/2013/12/19/put-your-controllers-on-a-diet-posts-and-
commands/
291
CHAPTER 6 | Tackle Business Complexity in a Microservice with DDD and CQRS Patterns
•
Tackling cross-cutting concerns with a mediator pipeline
https://lostechies.com/jimmybogard/2014/09/09/tackling-cross-cutting-concerns-with-a-
mediator-pipeline/
•
CQRS and REST: the perfect match
https://lostechies.com/jimmybogard/2016/06/01/cqrs-and-rest-the-perfect-match/
•
MediatR Pipeline Examples
https://lostechies.com/jimmybogard/2016/10/13/mediatr-pipeline-examples/
•
Vertical Slice Test Fixtures for MediatR and ASP.NET Core
https://lostechies.com/jimmybogard/2016/10/24/vertical-slice-test-fixtures-for-mediatr-and-
asp-net-core/
•
MediatR Extensions for Microsoft Dependency Injection Released
https://lostechies.com/jimmybogard/2016/07/19/mediatr-extensions-for-microsoft-
dependency-injection-released/
Fluent validation
•
Jeremy Skinner. FluentValidation.
GitHub repo.
https://github.com/JeremySkinner/FluentValidation
292
CHAPTER 7 | Implement resilient applications
CHAPTER
7
Implement resilient
applications
Your microservice and cloud-based applications must embrace the partial failures that will certainly
occur eventually. You must design your application to be resilient to those partial failures.
Resiliency is the ability to recover from failures and continue to function. It isn’t about avoiding
failures but accepting the fact that failures will happen and responding to them in a way that avoids
downtime or data loss. The goal of resiliency is to return the application to a fully functioning state
after a failure.
It’s challenging enough to design and
deploy a microservices
-based application. But you also need to
keep your application running in an environment where some sort of failure is certain. Therefore, your
application should be resilient. It should be designed to cope with partial failures, like network
outages or nodes or VMs crashing in the cloud. Even microservices (containers) being moved to a
different node within a cluster can cause intermittent short failures within the application.
The many individual components of your application should also incorporate health monitoring
features. By following the guidelines in this chapter, you can create an application that can work
smoothly in spite of transient downtime or the normal hiccups that occur in complex and cloud-based
deployments.
Important
eShopOnContainer had been using the
Polly library
to implement resiliency using
Typed Clients
up
until the release 3.0.0.
Starting with release 3.0.0, the HTTP calls resiliency
is implemented using a
Linkerd mesh
, that handles
retries in a transparent and configurable fashion, within a Kubernetes cluster, without having to
handle those concerns in the code.
The Polly library is still used to add resilience to database connections, specially while starting up the
services.
Warning
All code samples and images in this section were valid before using Linkerd and are not updated to
reflect the current actual code. So they make sense in the context of this section.
293
CHAPTER 7 | Implement resilient applications
Handle partial failure
In distributed systems like microservices-
based applications, there’s an ever
-present
risk of partial
failure. For instance, a single microservice/container can fail or might not be available to respond for a
short time, or a single VM or server can crash. Since clients and services are separate processes, a
service might not be able to respond in a timely way to a client’s request. The service might be
overloaded and responding very slowly to requests or might simply not be accessible for a short time
because of network issues.
For example, consider the Order details page from the eShopOnContainers sample application. If the
ordering microservice is unresponsive when the user tries to submit an order, a bad implementation
of the client process (the MVC web application)
—
for example, if the client
code were to use
synchronous RPCs with no timeout
—
would block threads indefinitely waiting for a response. Besides
creating a bad user experience, every unresponsive wait consumes or blocks a thread, and threads are
extremely valuable in highly scalable applications. If there are many blocked threads, eventually the
application’s runtime can run out of threads.
In that case, the application can become globally
unresponsive instead of just partially unresponsive, as shown in Figure 8-1.
Figure 8-1. Partial failures because of dependencies that impact service thread availability
In a large microservices-based application, any partial failure can be amplified, especially if most of
the internal microservices interaction is based on synchronous HTTP calls (which is considered an anti-
pattern). Think about a system that receives millions of incoming calls per day. If your system has a
bad design that’s based on long chains of synch
ronous HTTP calls, these incoming calls
might result in
many more millions of outgoing calls (let’s suppose a ratio of 1:4) to dozens of internal microservices
as synchronous dependencies. This situation is shown in Figure 8-2, especially dependency #3, that
starts a chain, calling dependency #4, which then calls #5.
294
CHAPTER 7 | Implement resilient applications
Figure 8-2. The impact of having an incorrect design featuring long chains of HTTP requests
Intermittent failure is guaranteed in a distributed and cloud-based system, even if every dependency
itself has excellent availability. It’s a fact you need to consider.
If you do not design and implement techniques to ensure fault tolerance, even small downtimes can
be amplified. As an example, 50 dependencies each with 99.99% of availability would result in several
hours of downtime each month because of this ripple effect. When a microservice dependency fails
while handling a
high volume of requests, that failure can quickly saturate all available request threads
in each service and crash the whole application.
Figure 8-3. Partial failure amplified by microservices with long chains of synchronous HTTP calls
295
CHAPTER 7 | Implement resilient applications
To minimize this problem, in the section
Asynchronous microservice integrat
ion enforce microservice’s
autonomy
, this guide encourages you to use asynchronous communication across the internal
microservices.
In addition, it’s essential that you design your microservices and client
applications to handle partial
failures
—
that is, to build resilient microservices and client applications.
Strategies to handle partial failure
To deal with partial failures, use one of the strategies described here.
Dostları ilə paylaş: