Dealing with Validation – Domain vs Contextual
I’ve always found validation to be one of the most difficult and tedious aspects of writing enterprise software. No matter how you organize your rules, you are going to usually end up with duplication. To make matters worse, the rules aren’t written by developers, they are created by the business. This causes a disconnect between knowledge and domain experts, and the people who are implementing the validation in the code. As the rules change over time, and as the developers who originally worked on the system move on, the validation becomes increasingly difficult to manage. As the system matures, it ultimately ends up becoming a significant source of pain for all those involved. That’s assuming, of course, that you even have validation in the first place.
UI Validation
I want to start off by briefly discussing UI validation. I’m not going to go too in depth into this because UI validation concerns are really driven by the user experience you require. Running complex business rules as the user fills out the form is not something that should be included unless you have a specific user experience need that has to be met. For the most part, it should be restricted to basic data validation checks for integrity such as length, allowed characters, etc. Also, include validation on user interactions with the UI such as required fields being filled out based on a drop down choice. All UI validation should be assumed to be unsafe and is only there to improve the user experience and reduce calls to the server.
Domain Validation
When we speak of the Domain in Domain Driven Design, we are speaking about the business itself. This includes the models, validation, services, events and anything else that composes the business and its problems. For the purposes of this post, I want to just focus on the validation; the business rules that govern the system. How we go about organizing all the different validation concerns across the system will impact how manageable it is long term.
We interact with the domain through the application layer. This could be a web application, an API, a mobile app, etc. Regardless of how we interact with it, the domain validation is consistent for all applications. This includes but is not limited to the following:
- Data validation such as length, allowed characters, duplication, etc.
- Complex business rules
- Interaction that is not application specific. Ex: Department heads must approve a business order in order to go to the next step in the process.
Contextual Validation
Contextual validation is validation that typically resides in the application layer and is tied to how we interact with the domain. It is validation that is not consistent across the system and is wholly reliant on the context of the interaction. Some examples of this include the following:
- User permission validation at the application level.
- Existence checks. Depending on the application, the existence of a record may or may not be required.
- Application access.
By separating how we interact with the domain from the domain itself, we end up with validation that is more organized and less likely to be duplicated. It also reduces the possibility that we will be forced to conditionally run validation based on which context is being used to interact with the domain.
Simple Implementation Example
We’ll start off with a couple of small classes for purposes of example. In this case I am using FluentValidation for our validation implementation. Our scenario is that we have a cloud platform that provides multiple applications, and our users can be members of one or more of them. One of the applications has a rule that a user must reside in the US. I am using RequestInjector to directly inject dependencies into my requests. You can read more about it in this post: Request Injection in ASP.NET Core. Below are the classes:
public class User { public List<CloudApp> CloudApps { get; set; } public DateTime DateOfBirth { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string UserName { get; set; } public string State { get; set; } public string CountryCode { get; set; } } public class CloudApp { public int Id { get; set; } public string Name { get; set; } }
Next we have the validator for the User domain model. Personally, I keep these validators in the same file as the domain model, but it is up to you if you want to split them off into their own files.
public class UserValidator : AbstractValidator<User> { public UserValidator() { RuleFor(m => m.FirstName).NotEmpty().Must(m => m.Length <= 25); RuleFor(m => m.LastName).NotEmpty().Must(m => m.Length <= 50); } }
As you can see in our domain validator we are only doing simple validation on length for the example. These rules will apply to all requests in the system regardless of where they come from. Next, we will have two requests: One for a financial application that requires everyone who registers through it to reside in the US as seen below.
public class AddUserForFinancialAppRequest : IRequest { public User User { get; set; } IUserRepository userRepository; public AddUserForFinancialAppRequest(IUserRepository userRepository) { this.userRepository = userRepository; } public async Task<IActionResult> Handle() { //Do work to process request return new OkObjectResult(await userRepository.AddAsync(User)); } } public class AddUserForFinancialAppRequestValidator : AbstractValidator<AddUserForFinancialAppRequest> { public AddUserForFinancialAppRequestValidator() { RuleFor(m => m.User).SetValidator(new UserValidator()); RuleFor(m => m.User.CountryCode.ToUpper()).Equal("US"); } }
Once again, I keep the validator in the same file to cut down on the file structure of the solution. In this validator, we first call the domain validator followed by whatever custom rules we have for this request. In the AddUserForEveryoneAppRequest we are only going to call the domain validator since we have no contextual validation that we need to run:
public class AddUserForEveryoneAppRequest : IRequest { public User User { get; set; } IUserRepository userRepository; public AddUserForEveryoneAppRequest(IUserRepository userRepository) { this.userRepository = userRepository; } public async Task<IActionResult> Handle() { //Do work to process request return new OkObjectResult(await userRepository.AddAsync(User)); } } public class AddUserForEveryoneAppRequestValidator : AbstractValidator<AddUserForEveryoneAppRequest> { public AddUserForEveryoneAppRequestValidator() { RuleFor(m => m.User).SetValidator(new UserValidator()); } }
And there you have it. A clear separation between domain and contextual validation. FluentValidation is hooked up to ASP.NET Core so the final piece you will need to make it all work is a ValidationFilter:
public class ValidationFilter : ActionFilterAttribute { public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { var modelState = context.ModelState; if (!modelState.IsValid) { var errors = modelState.Values.SelectMany(v => v.Errors).Select(m => m.ErrorMessage).ToList(); context.Result = new BadRequestObjectResult(errors); } return base.OnActionExecutionAsync(context, next); } }