Combining ASP.NET Core validation attributes with Value Objects
This is a continuation of the article series about some more advanced error handling techniques.
In the previous post, I advocated for the creation of an explicit Error
class, and enumeration of all possible errors in your application with it. This approach helps you to
-
Keep track of all known errors,
-
React to those errors differently.
The specific example I used in that article is returning 404
for an "entity not found" error as opposed to 400
for all other validation errors. By the way, if you want to learn more about 4xx
API response codes, check out this article: REST API response codes: 400 vs 500.
In this post, we’ll look at how to combine ASP.NET Core validation attributes with Value Objects. We’ll look at how the two deal with input validation on their own, outline the drawbacks of each and then proceed with the combined approach.
1. Input validation using ASP.NET Core validation attributes
Here’s StudentController
and the API endpoint for changing the student’s name and email from the previous article (I’ve removed validations that involve database calls for brevity):
[HttpPut("{id}")]
public IActionResult EditPersonalInfo(long id, [FromBody] StudentPersonalInfoDto dto)
{
Student student = _unitOfWork.Get<Student>(id);
student.Name = dto.Name;
student.Email = dto.Email;
_unitOfWork.Commit();
return Ok();
}
public sealed class StudentPersonalInfoDto
{
public string Name { get; set; }
public string Email { get; set; }
}
Let’s say that all incoming requests must have both Name
and Email
, and that the Email
field must contain a valid email address.
How would you implement such a validation?
ASP.NET offers a handy validation mechanism using attributes from the standard .NET’s System.ComponentModel.DataAnnotations
namespace. With it, all you need to do to enable input validation is put the following attributes on top of the DTO fields:
public sealed class StudentPersonalInfoDto
{
[Required]
public string Name { get; set; }
[Required, RegularExpression("^(.+)@(.+)$")]
public string Email { get; set; }
}
And also add this check to the controller:
[HttpPut("{id}")]
public IActionResult EditPersonalInfo(long id, [FromBody] StudentPersonalInfoDto dto)
{
if (!ModelState.IsValid) // Invokes the build-in
return BadRequest(ModelState); // validation mechanism
Student student = _unitOfWork.Get<Student>(id);
student.Name = dto.Name;
student.Email = dto.Email;
_unitOfWork.Commit();
return Ok();
}
Now if you try to update a student’s personal info with an incorrect email address, the API will respond with the following JSON:
{
"Email": [
"The field Email must match the regular expression '^(.+)@(.+)$'."
]
}
Problem solved! Or is it?
There are two issues with this approach:
-
Email address is a domain concept, and so are the rules describing what a valid email is. Moving those rules to the infrastructure layer (the attributes) leads to either domain knowledge duplication (violation of the DRY principle) or domain knowledge dilution. Domain knowledge duplication is when you keep the same business rule both in the domain layer and in other layers; dilution — when that knowledge partly resides in the domain layer and partly in other layers.
-
Non-standardized errors — ASP.NET generates error descriptions in its own format which may not be compatible with your company’s standards.
2. Input validation using Value Objects
A more DDD-friendly approach to input validation is to introduce Value Objects containing all the knowledge related to the corresponding domain primitives.
This is how an Email
value object may look like:
public class Email : ValueObject
{
public string Value { get; }
private Email(string value)
{
Value = value;
}
public static Result<Email> Create(string input)
{
if (string.IsNullOrWhiteSpace(input))
return Result.Failure<Email>(Errors.General.ValueIsRequired());
string email = input.Trim();
if (email.Length > 256)
return Result.Failure<Email>(Errors.General.ValueIsTooLong());
if (!Regex.IsMatch(email, @"^(.+)@(.+)$"))
return Result.Failure<Email>(Errors.General.ValueIsInvalid());
return Result.Ok(new Email(email));
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Value;
}
}
I’m using the ValueObject
base class from this article.
Now you can use Email
in the controller to do the validation (you can introduce a similar value object for the Name
property, but I’m omitting this for brevity):
[HttpPut("{id}")]
public IActionResult EditPersonalInfo(long id, [FromBody] StudentPersonalInfoDto dto)
{
Result<Email> emailOrError = Email.Create(dto.Email);
if (emailOrError.IsFailure)
return Error(emailOrError.Error);
Student student = _unitOfWork.Get<Student>(id);
student.Name = dto.Name;
student.Email = emailOrError.Value;
_unitOfWork.Commit();
return Ok();
}
A request with an incorrect email address will lead to the following error response:
{
"result": null,
"errorCode": "value.is.invalid",
"errorMessage": "Value is invalid",
"timeGenerated": "2019-10-12T14:31:47.0990694Z"
}
The benefit of the approach with value objects is concentration of all domain knowledge in the domain layer (where it should be). However, this approach has its own problems:
-
It’s not declarative. Probably the best selling point of the approach with attributes is its declarativeness: you can easily observe which input parameters have what validations by looking at the corresponding DTOs. You can’t do that with value objects — all those validations are now imperative in the controller’s code.
-
The loss of the connection to the input field name.
The second drawback is even more important than the first. Note that ASP.NET’s error response contains the name of the field that caused the validation to fail. Here it is again:
{
"Email": [
"The field Email must match the regular expression '^(.+)@(.+)$'."
]
}
This is important because the client often needs a way to map validation failures to particular input fields on the UI for better user experience.
And it’s not that easy to reintroduce this connection using the approach with value objects. The domain layer doesn’t and shouldn’t know where the raw values come from, and so you can’t just return the field name from Email.Create()
. The mapping of the errors from the domain model onto the DTO fields should be done in the controllers. Here’s one way to implement this mapping:
[HttpPut("{id}")]
public IActionResult EditPersonalInfo(long id, [FromBody] StudentPersonalInfoDto dto)
{
Result<Email> emailOrError = Email.Create(dto.Email);
if (emailOrError.IsFailure)
return Error(emailOrError.Error, nameof(dto.Email)); '1
/* ... */
}
Note the additional nameof(dto.Email)
in '1
. This expression translates into "Email"
, which we can put into the response as a separate field.
This is a decent approach for simple applications. In larger code bases, though, such a manual tracking of input fields gets messy really quickly, especially when you’ve got nested DTOs. For example, let’s say that along with the name and email address, students must indicate their city of origin:
public sealed class StudentPersonalInfoDto
{
public string Name { get; set; }
public string Email { get; set; }
public CityOfOriginDto CityOfOrigin { get; set; } // a nested DTO
}
public sealed class CityOfOriginDto
{
public string City { get; set; }
public string State { get; set; }
}
If the city information is missing or incorrect, you need to return an error with the full path to the invalid field. An incorrect state, for example, should result in the following response:
{
"result": null,
"errorCode": "value.is.invalid",
"errorMessage": "Value is invalid",
"invalidField": "cityOfOrigin.state", // fully qualified field name
"timeGenerated": "2019-10-12T14:31:47.0990694Z"
}
You might also have additional checks at the CityOfOriginDto
's level: for example, exclude some invalid city-state pairs. Such error responses should point to the parent "cityOfOrigin"
field.
As a good DDD citizen, you’ll have to create 3 value objects:
-
City
to encapsulate validation rules related to cities -
State
to encapsulate validation rules related to states -
CityOfOrigin
to encapsulate validation rules related to the combination ofCity
andState
.
This is the controller after the introduction of these value objects:
[HttpPut("{id}")]
public IActionResult EditPersonalInfo(long id, [FromBody] StudentPersonalInfoDto dto)
{
Result<City> cityOrError = City.Create(dto.CityOfOrigin.City);
if (cityOrError.IsFailure)
return Error(cityOrError.Error, "cityOfOrigin.city");
Result<State> stateOrError = State.Create(dto.CityOfOrigin.State);
if (stateOrError.IsFailure)
return Error(stateOrError.Error, "cityOfOrigin.state");
Result<CityOfOrigin> cityOfOriginOrError = CityOfOrigin.Create(
cityOrError.Value,
stateOrError.Value);
if (cityOfOriginOrError.IsFailure)
return Error(cityOfOriginOrError.Error, "cityOfOrigin");
/* ... */
}
It may look fine if you have to do this only once but quickly gets out of hand when the same CityOfOriginDto
starts appearing in multiple API endpoints. In which case you’ll have to repeat the same validation code in each of the respective controllers. The amount of validation code becomes overwhelming when there are multiple levels of indentation — when the nested DTO itself contains nested DTOs and so on several layers deep.
Is there a way to extract the validation out of the controllers but still keep all the corresponding domain knowledge in the value objects? Without having to track all those pesky field names? There is!
3. Combining ASP.NET Core validation attributes with Value Objects
You can combine the approaches with attributes and value objects. With this combined approach, you can:
-
Have ASP.NET track invalid field names automatically
-
Enjoy the declarativeness of validation attributes
-
Keep the domain knowledge DRY and concentrated in the domain layer
-
Simplify the controllers
In this section, I’ll show how to do that. The sample code uses ASP.NET Core 3.0.
3.1. Introducing custom attributes
The main idea of this combined approach is to create your own validation attributes which will delegate the actual validation to value objects.
All built-in validation attributes in DataAnnotations
inherit from the base ValidationAttribute
class, so our custom attributes will inherit from that class too. Here’s one for the Email
DTO property:
[AttributeUsage(AttributeTargets.Property)]
public sealed class EmailAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(
object value, ValidationContext validationContext)
{
if (value == null) '1
return ValidationResult.Success; '1
string email = value as string;
if (email == null)
return new ValidationResult(Errors.General.ValueIsInvalid().Serialize());
Result<Email> emailResult = Email.Create(email); '2
if (emailResult.IsFailure)
return new ValidationResult(emailResult.Error.Serialize()); '3
return ValidationResult.Success;
}
}
Note a couple things in this listing:
'1
— EmailAttribute
treats all nulls as a success. It’s the responsibility of the separate [Required]
attribute to check the input for nulls, EmailAttribute
should only be focused on email correctness.
'2
— Delegates the validation to the Email
value object.
'3
— Serializes the error into a string.
Let me elaborate on #3 here. The good part about validation attributes is that you don’t need to deal with field names — ASP.NET gathers them for you automatically (you’ll see how to extract that info shortly).
The not-so-good part is that all errors are represented with strings, and so you can’t just pass your own custom Error
instance into ValidationResult
. You can overcome this issue by serializing your errors into strings, though. The Serialize
method used in line '3
is part of the Error
class:
public sealed class Error : ValueObject
{
private const string Separator = "||";
public string Code { get; }
public string Message { get; }
internal Error(string code, string message)
{
Code = code;
Message = message;
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Code;
}
public string Serialize()
{
return $"{Code}{Separator}{Message}";
}
public static Error Deserialize(string serialized)
{
string[] data = serialized.Split(
new[] { Separator },
StringSplitOptions.RemoveEmptyEntries);
Guard.Require(data.Length >= 2, $"Invalid error serialization: '{serialized}'");
return new Error(data[0], data[1]);
}
}
3.2. Processing of custom attributes
It’s not enough to just create a custom attribute. If you put EmailAttribute
to StudentPersonalInfoDto
like this:
public sealed class StudentPersonalInfoDto
{
public string Name { get; set; }
[Email]
public string Email { get; set; }
public CityOfOriginDto CityOfOrigin { get; set; }
}
and test it out, the response from the API will be similar to the following:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "|1511030e-41966221ef070439.",
"errors": {
"Email": [
"value.is.invalid||Value is invalid"
]
}
}
And that’s clearly not how you’d like the error message to be formatted.
To process the new validation attribute correctly, you need to interject your own validator into the ASP.NET’s validation routine. Here’s how to do that in the Startup
class:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = // the interjection
ModelStateValidator.ValidateModelState;
});
/* ... */
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
}
}
ModelStateValidator
above is a custom model state validator that parses the serialized error and returns a proper response to the client:
public class ModelStateValidator
{
public static IActionResult ValidateModelState(ActionContext context)
{
(string fieldName, ModelStateEntry entry) = context.ModelState
.First(x => x.Value.Errors.Count > 0);
string errorSerialized = entry.Errors.First().ErrorMessage;
Error error = Error.Deserialize(errorSerialized);
Envelope envelope = Envelope.Error(error, fieldName);
var result = new BadRequestObjectResult(envelope);
return result;
}
}
The validator
-
Finds the first error (it should always be there, otherwise ASP.NET will not call the validator)
-
Gets the field name that caused the error, and the error’s string representation (which we created by calling
emailResult.Error.Serialize()
inEmailAttribute
). -
Converts the error into an
Envelope
instance, which is then serialized into the following response:
{
"result": null,
"errorCode": "value.is.invalid",
"errorMessage": "Value is invalid",
"invalidField": "email", // Nice-looking field name
"timeGenerated": "2019-10-14T20:41:06.6847647Z"
}
And that’s it! You don’t need to ever deal with the field names yourself: when ASP.NET calls validation attributes, it knows which DTO fields those attributes are attached to and allows you to extract this information in the model state validator. All the validation rules are contained in the domain layer, where they should be.
You aren’t restricted to the primitive types either. You can create an attribute for the CityOfOrigin
property, too. This attribute will check the rules related to the city of origin as a whole (those invalid city-state combinations I mentioned earlier).
Here’s how the DTO will look after all attributes are brought together:
public sealed class StudentPersonalInfoDto
{
[Required, Name]
public string Name { get; set; }
[Required, Email]
public string Email { get; set; }
[Required, CityOfOrigin]
public CityOfOriginDto CityOfOrigin { get; set; }
}
public sealed class CityOfOriginDto
{
[Required, City]
public string City { get; set; }
[Required, State]
public string State { get; set; }
}
Not only does this approach brings the declarativeness back, it also drastically simplifies the controller:
[HttpPut("{id}")]
public IActionResult EditPersonalInfo(long id, [FromBody] StudentPersonalInfoDto dto)
{
CityOfOrigin cityOfOrigin = CityOfOrigin.Create(
dto.CityOfOrigin.City,
dto.CityOfOrigin.State).Value; '1
/* ... */
}
Note the use of .Value
in '1
. CityOfOrigin.Create()
returns a Result
instance, and if that result is failed, Value
will throw an exception. Normally, this is not how you want to process Result
instances, but in this particular case, it’s actually beneficial — it helps your code fail fast and thus quickly reveal that some of your DTOs miss validation attributes. By the time the DTO comes to your controller, it should already be validated by the attributes.
One last thing. You’ll need to inherit all your controllers from a base ApplicationController
class:
[ApiController]
public class ApplicationController : ControllerBase
{
/* Ok(), FromResult(), Error() methods */
}
It inherits from ASP.NET’s ControllerBase
, which is a trimmed down version of ASP.NET’s Controller
and only contains methods needed for APIs. Also note the [ApiController]
attribute. By putting this attribute on the base class, you apply it to all subclasses too. The main benefit of this attribute is that you don’t need to invoke the following code manually in your controllers anymore:
if (!ModelState.IsValid) // Invokes the build-in
return BadRequest(ModelState); // validation mechanism
ASP.NET invokes it automatically for all controllers marked with the [ApiController]
attribute, which in turn triggers your custom model state validator.
4. Summary
-
The two approaches to input validations we’ve looked at are:
-
ASP.NET validation attributes
-
The use of Value Objects
-
-
The use of ASP.NET validation attributes leads to domain knowledge dilution or duplication.
-
Value Objects aren’t declarative. It’s also hard to relate errors to the input fields that contain those error.
-
You can combine the two approaches by
-
Introducing custom validation attributes that would delegate the actual validation to value objects
-
Interjecting a custom model state validator into ASP.NET’s validation routine.
-
-
The combined approach helps you both keep the declarativeness and contain the domain knowledge in the domain layer.
5. Other articles in the series
-
Combining ASP.NET Core validation attributes with Value Objects (this post)
Subscribe
Comments
comments powered by Disqus