The story is a part of the series about software architecture powered by command-query separation principle and aspect-oriented programming.
Data validation
With the request handler mediator explained and implemented in the previous post, all the prerequisites are in place to start building reusable generic components. Such components, that model specific cross-cutting concerns, fit nicely into the architecture this series is talking about.
Even though modeling cross-cutting concerns in OO languages is challenging and may lead to tightly-coupling, the request handler abstraction provides necessary tooling to make it simple and right.
Implementing cross-cutting concerns using a Decorator design pattern around the generic interface IRequestHandler<TRequest, TResponse>
along with generic type constraints, enables non-intrusive, conditional, and dynamical addition of certain behavior to an individual handler instance. The complete solution is, reasonably, powered by a dependency injection container, which covers responsibility for the creation and management of such instances.
Validator — definition and model
It’s natural to start with the data validation, as the first concern to implement around the mentioned interface. Dealing with corrupt data and ensuring data input correctness is a known topic and can be achieved in many ways; starting from in-lined conditional statements to dedicated methods to specialized objects with, sometimes, complex hierarchies.
In this case, the validation logic will be enclosed into an object with a single responsibility to validate data. Objects responsible for validation are going to implement a simple generic interface with a single method that takes some data as input and returns a result of the validation:
public interface IValidator<in T>
{
ValueTask<Result<bool>> ValidateAsync(T data, CancellationToken ct);
}
This simple abstraction covers a large variety of cases and validation needs. It has certain characteristics and design decisions worth explaining:
No generic type constraints — gives room to validate any type of data in C# (except pointer types).
A contravariant generic type parameter
in T
— enables reusability ifT
is a derived type, and there are already validators for its base (less derived) type. This is especially helpful for chaining validators in collections, such asIEnumerable<IValidator<T>>
.Asynchronous method returns
ValueTask<T>
, instead ofTask<T>
— removes allocation overhead when an asynchronous method returns synchronously, which is often a case for data validation logic. Additionally, it removes the need to have two distinct validation methods (Validate
andValidateAsync
) to achieve the same possibilities.Async ceremony on the side, the validation method returns
Result<T>
data structure — provides more predictable method behavior and easier error data propagation. The method doesn’t throw an exception when validation fails, and error data is always stored in theError
instance, retrievable byMatch<T>
method.
Validation decorator — model
With the basic validator abstraction defined, it’s straightforward to implement the generic validation decorator around request handlers. It will internally use and orchestrate validators to process them in the sequence. Therefore, the order of instances in a validator collection does matter.
Use the dependency injection container which guarantees the order of supplied registered types in a collection of types.
It’s clear that the given validation interface doesn’t suit all the needs in the field, the same way the proposed decorator won’t cover all the validation scenarios some business logic or existing code could dictate. Still, the great thing about this architecture is the possibility to create different validation decorators with their own rules and behavior (e.g. concurrent processing), and easily use them side by side or even exclusively.
Implementation could look something like this:
It’s important to notice how ValidationDecorator
has only one generic type constraint (TRequest: IRequest<TResponse>
) dictated by the interface it implements. When added to the composition root, this kind of decorator will always be applied to any IRequest<TResponse>
object. Since that’s the base interface, each TRequest
instance sent to its handler is going to be validated by this logic.
When determining how certain behavior is going to be applied, there are usually three approaches:
Required (always-on): behavior is applied to every request handled by a corresponding handler. There is no possibility to exclude a decorator dynamically. Excluding decorators from the DI composition is the only way to avoid them. No additional coding, easy to implement.
Required (opt-out): behavior is applied to every request handled by a corresponding handler. There is a possibility to exclude decorator statically or dynamically. Requires custom coding, complex to implement if seeking for an all-round good solution.
Optional (opt-in): behavior is applied only to requests which implement a specific flag interface. The interface is used as a decorator’s additional type constraint imposing a requirement for request types to fulfill in order to be processed.
Personally, I recommend the opt-in approach as a rule of thumb, even if it leads to a higher number of interfaces to deal with. It’s simply easier to work with a set of behaviors and pick only the suitable ones to use for a given request. Also, the opt-out approach isn’t easily implemented and may lead to the coupling or hard-coding (e.g. request type checks inside decorator to skip part of the logic). The only always-on behavior which is going to be covered in the whole series is validation due to its nature. Working with a valid request
instance is a must, thus validation decorator is mandatory. That’s the only reason why the proposed solution doesn’t use a custom-tailored flag interface as a constraint.
If that’s not preferable, a simple IValidatedRequest
empty flag interface can be introduced and added as a decorator’s type constraint. That enables the opt-in approach for the request validation as well. Simply, a custom TRequest
type can optionally implement a specific interface to activate a decorator that handles IValidatedRequest
types.
Last thing worth pointing out is an implicit conversion which makes it possible to directly return Error
instance instead of Result<TResult>
. Once the result is calculated by ValidateAsync
method, error
can be extracted using Match<T>
method and, finally, returned.
Essentially, on top of two additional types, ValidationDecorator<TRequest, TResponse>
and IValidator<T>
, it is possible to build custom validators for any TRequest
. The decorator will make sure to invoke it before execution reaches the handler for specific TRequest
— it’s that simple!
DataAnnotations Validator — model
There is still a need to implement validators for each request. That can be a tedious process, especially implementing validation logic which is repeatable like null or empty checks or max length checks. Basically, any input validation logic is monotonous to deal with.
For that matter, DataAnnotations library comes as a solution quite naturally. It’s just needed to implement a generic validator that will use DataAnnotations to validate objects. Afterward, simply decorate the objects’ properties with validation attributes, and thing happens:
This simple example serves the purpose and is capable to validate any object (e.g. CreateUserCommand
) passed into it. It will either return Error with appropriate data about validation failure or true if the validation is successful. Note that nested properties validation is missing, but it can be added by extending base attributes from DataAnnotations library.
FluentValidation Validator — model
Often simple input validation isn’t enough to ensure the data correctness on a domain level. Sometimes it’s necessary to (asynchronously) fetch additional data from a remote source like a database, or validation logic is composed of smaller, more specialized components (dependencies). In such scenarios, validation via attributes is simply not powerful enough, System.Attribute
type specifics make it inconvenient to use in more complex cases.
FluentValidation library is a good choice when it comes to more complex, domain validation logic. It supports asynchronous processing, has rich feature-set and intuitive API to use.
To fit it, it’s necessary to create an adapter which derives from the base AbstractValidator<T>
class and implements the new IValidator<T>
interface:
AbstractFluentValidator<TRequest>
is deliberately an abstract class to retain the same API interaction as the original library requires. It’s still needed to inherit the class to define validation rules inside the constructor, but the new class also makes derived validator compatible with the ValidationDecorator
:
While FluentValidation library offers the possibility to write a fully custom code via dedicated methods, there might be cases in which there is no need to introduce a new library (or not an option) due to the existing validators and other validation components.
To reuse them along with other validators, each legacy validator needs to have its own adapter that implements IValidator<T>
interface. Since only one method needs to be added, implementation is usually straightforward.
With all three validation approaches in place, the data validation responsibility can be completely moved away from the central handler logic. That makes the handler clean and more readable as the validation clutter is relocated elsewhere.
A higher level of encapsulation and clearer responsibility separation naturally raise performance concerns which shouldn’t be neglected. They can be tackled in many different ways; from multi-level and hybrid caching to contextual data propagation and exchange. To give an example, optimizing interaction with remote sources (database, web API, …) leveraging caching, reduces the number of expensive (slow) and volatile network roundtrips. Even if the approach can give a significant/positive impact on a general performance, it comes with the cost and must be carefully weighed.
Pipeline composition example with SimpleInjector
The last thing to cover is the object graph composition with the help of SimpleInjector DI container. The setup shown below gives a sort of pipeline-like composition in which every request goes through the validation component first, continuing as a validated request to the corresponding handler:
Due to SimpleInjector’s powerful support for open generic types registration, it is possible to define the order of instances in a collection of types resolved by the container. In other words, IEnumerable<IValidator<T>>
is always resolved into collection of ordered validators which are able to handle some type T.
In the case of CreateUserCommand
, validation will be done in the following order:
DataValidationValidator
— validates input data; ensures that both property values are set,Name
not longer than 25 characters andEmail
valid.CreateUserEmailValidator
— validates domain requirements; ensures thatEmail
is not used.CreateUserNameValidator
— validates domain requirements using “legacy” logic; ensures thatName
is allowed to use.
In the case of another arbitrary UpdateUserCommand
, validation order would start with DataValidationValidator
, continuing with reusable and/or dedicated validators for that type in the guaranteed order as well.
To conclude, with data validation components in place and flexible composition, it’s matter of creating a request and handler to handle some domain logic, possibly creating additional classes to cover specific validation behavior and run the code. There is no need to change or adapt anything else (Open-closed principle), especially not composition root, which should be the ultimate goal when working with DI containers.
In the next chapter, new cross-cutting concern and decorator will be covered, stay tuned to find out which one.
All the gists, examples or solutions will be done in C# using the latest .Net Core/Standard versions and hosted on Github.
Top comments (0)