Value Objects and Error Messages
I haven’t done one of these for a while. This post is a review of a code submitted by a reader. You can request such a review by using the submission form in the right sidebar or on this page.
Value Objects and Error Messages
Below are the code and questions. Some parts of the code were in German, so I translated them into English. Hopefully, I didn’t lose any semantics. And if I did, it’s Google Translate’s fault :)
Start of citation.
public class AllowanceAmount : ValueObject
{
public const int MaxLength = 50;
public string Value { get; }
private AllowanceAmount(string value)
{
Value = value;
}
protected override IEnumerable<object> GetEqualityComponents()
{
yield return Value;
}
public static Result<AllowanceAmount> Create(string allowance)
{
allowance = allowance?.Trim();
if (string.IsNullOrEmpty(allowance))
return Result.Ok(new AllowanceAmount(null));
if (allowance.Length > MaxLength)
return Result.Fail<AllowanceAmount>($"'Allowance' cannot be longer than {MaxLength} characters.");
return Result.Ok(new AllowanceAmount(allowance));
}
}
For all Value Objects, we used your base class and the following best practices: private constructor, static Factory method with invariant enforcement. We made a Value Object for each property with validation rules. The bad thing is that we have the name of the property hard-coded in our error message (Allowance
in the code sample above).
For example, if we have properties
public AllowanceAmount Allowance1 { get; private set; }
and
public AllowanceAmount Allowance2 { get; private set; }
in the same entity, we don’t know exactly which Allowance
is failed based on the error message. We create our Value Objects in the Application Layer (=our command handlers) and return the error messages from the domain layer to the client (Angular):
var bezeichnungResult = KatalogeintragBezeichnung.Create(updateItem.Bezeichnung);
var bezeichnungWeiblichResult = kademischerGradBezeichnungWeiblich.Create(
updateItem.BezeichnungWeiblich);
var typResult = AkademischerGradTyp.Create(updateItem.Type);
var rangResult = AkademischerGradRang.Create(updateItem.Rank);
What do you think about Value Objects like Optional70LimitedString
? There’s the same problem with it, the error message doesn’t include the property name. For example:
public Optional70LimitedString LastName { get; private set; }
Can you advice how to handle error messages from the domain layer? Should we display domain layer error messages (which Result
object contains) in the UI or should we translate it?
End of citation.
I haven’t translated the second piece of code into English, but I think you get the idea: multiple Value Objects get created and validated, and you need to show the result of that validation to the client.
Here’s, by the way, the link to the Value Object base class I currently use on my projects: Value Object: a better implementation.
There are 3 questions in here:
-
How to map a specific validation error to the source of it, i.e. the name of the field the user made a mistake in?
-
Should you display the errors from the domain model as is or should you translate/transform them when displaying on the UI?
-
What do I think about Optional70LimitedString Value Object?
I’ve answered the last one in the previous blog post. In short: I don’t recommend using a Value Object with such a name because this name doesn’t belong to the ubiquitous language of the problem domain. Use names that can be traced to specific domain concepts instead, such as Password
, Email
, or AllowanceAmount
.
The answer to the second question depends on your project. In most typical applications with JavaScript-based UI, it’s fine to use error messages from the API as is. There’s a quite tight coupling between such a UI and the API already anyway, and so there’s no need to decouple the validation errors from their descriptions.
It’s a different situation when you’ve got an API-to-API communication, though, such as that of microservices. In this case, you want to use an error code. The reason why is because the client API most likely won’t display that error to anyone. Instead, it will try to react to it, and for that, it needs to distinguish one type of error from another. A unique numeric identifier works better than a string here.
And even if the client API does need to show an error message to its users, it still needs to present that message in its own terms, using its own ubiquitous language. Numeric codes allow the client API to easily associate a specific error message with every type of error the server API may return. For example, if an Identity Server returns you a "ClientId is Invalid" error, you would probably want to represent it as something like "Invalid username or password. Please, try again" instead.
But again, in typical client-server applications, like with AngularJS as the client and ASP.NET as the server, the tight coupling between the two tiers in terms of the error messages is fine.
Mapping a validation error to its source
Alright, that brings us to the last (or, rather, first) question:
How to map a specific validation error to the source of it, i.e. the name of the field the user made a mistake in?
This question nails it. The single drawback of the approach with using Value Objects is that it complicates validation error handling in CRUD scenarios.
It’s easy to generate validation errors with an input model like this:
public class CustomerModel
{
[MaxLength(50, ErrorMessage = "Allowance 1 cannot be longer than 50 characters")]
public string Allowance1 { get; set; }
[MaxLength(50, ErrorMessage = "Allowance 2 cannot be longer than 50 characters")]
public string Allowance2 { get; set; }
}
The attributes and the built-in ASP.NET validation mechanism will do all the work for you. This code:
[HttpPost]
public IActionResult Create([FromBody] CustomerModel item)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
// ...
}
magically results in proper validation errors on the UI.
How the hell are you going to do the same when all your validation logic is baked into the Value Objects and you are not allowed to use the validation attributes?
This is what a typical error handling code looks like when you are using Value Objects:
public class CustomerModel
{
public string Allowance1 { get; set; }
public string Allowance2 { get; set; }
}
[HttpPost]
public IActionResult Create([FromBody] CustomerModel item)
{
Result<AllowanceAmount> allowance1OrError = AllowanceAmount.Create(item.Allowance1);
Result<AllowanceAmount> allowance2OrError = AllowanceAmount.Create(item.Allowance2);
Result result = Result.Combine(allowance1OrError, allowance2OrError);
if (result.IsFailure)
return BadRequest(result.Error);
// ...
}
Note the absence of attributes on the customer model. They are discouraged because they would introduce duplication of the domain knowledge regarding what constitutes a valid allowance amount.
The drawback here is that there’s no way to track the error back to the specific allowance. The other inconvenience is that the API displays only one error at a time. If there are several errors, only the first one will be shown.
So, how to overcome this problem?
The original validation code that relies on ASP.NET produces the following JSON:
{
"Message": "The request is invalid.",
"ModelState": {
"model.Allowance1": [ "Allowance 1 cannot be longer than 50 characters." ],
"model.Allowance2": [ "Allowance 2 cannot be longer than 50 characters." ]
}
}
You can emulate the same behavior with a little bit of coding. This is what the solution could look like:
public class CustomerModel
{
public string Allowance1 { get; set; }
public string Allowance2 { get; set; }
}
[HttpPost]
public IActionResult Create([FromBody] CustomerModel item)
{
Result<AllowanceAmount> allowance1OrError = AllowanceAmount.Create(item.Allowance1);
Result<AllowanceAmount> allowance2OrError = AllowanceAmount.Create(item.Allowance2);
if (Result.Combine(allowance1OrError, allowance2OrError).IsFailure)
{
var errors = new[]
{
new ErrorEntry(() => item.Allowance1, allowance1OrError),
new ErrorEntry(() => item.Allowance2, allowance2OrError)
};
return BadRequest(errors);
}
// ...
}
public class ErrorEntry
{
public readonly Expression<Func<object>> Expression;
public readonly Result Result;
public ErrorEntry(Expression<Func<object>> expression, Result result)
{
Expression = expression;
Result = result;
}
}
private BadRequestObjectResult BadRequest(ErrorEntry[] entries)
{
foreach (ErrorEntry entry in entries)
{
if (entry.Result.IsSuccess)
continue;
ModelState.AddModelError(ConvertExpression(entry.Expression), entry.Result.Error);
}
return BadRequest(ModelState);
}
private string ConvertExpression(Expression<Func<object>> propertyExpression)
{
var expression = propertyExpression.Body as MemberExpression;
if (expression == null)
throw new ArgumentException(propertyExpression.Body.ToString());
return expression.Member.Name;
}
Here, we are combining the errors with their sources using LINQ expressions. Expressions help you avoid hard-coding field names so that when a property in the input model is renamed for some reason, the resulting JSON will reflect this change automatically.
Here’s what the new Create
returns:
{
"Allowance1": [ "'Allowance' cannot be longer than 50 characters." ],
"Allowance2": [ "'Allowance' cannot be longer than 50 characters." ]
}
The result now contains the names of the properties that cause the validation error. And it also lists all those errors, not just the first one. Which is pretty neat.
Note that you don’t have to mimic the output of the ASP.NET validation mechanism. You are most likely parsing that output on the UI manually anyway, so it’s a good idea to come up a JSON structure that satisfies your specific needs. Create a custom envelope class that would contain either a list of errors or a result and use it across all your API endpoints. You can employ the above approach to populate that envelope with the detailed information about validation errors.
Alright, that’s it for today. If you want me to review your own code, send it over using the submit form on this page. Feel free to ask any questions too.
Reference list
Subscribe
Comments
comments powered by Disqus