Now we are at the point where we can transfer a Value and an ErrorMessage to the operation consumers; what about transferring multiple errors, such as validation errors? To achieve this, we can convert our ErrorMessage property from a string to an IEnumerable<string> or another type of collection that fits your needs better. Here I chose the IReadOnlyCollection<string> interface and the ImmutableList<string> class so we know that external actors can’t mutate the results:
namespace OperationResult.MultipleErrorsWithValue;
public record class OperationResult
{
public OperationResult()
{
Errors = ImmutableList<string>.Empty;
}
public OperationResult(params string[] errors)
{
Errors = errors.ToImmutableList();
}
public bool Succeeded => !HasErrors();
public int?
Value { get; init; }
public IReadOnlyCollection<string> Errors { get; init; }
public bool HasErrors()
{
return Errors?.Count > 0;
}
}
Let’s look at the new pieces in the preceding code before continuing:
- The errors are now stored in ImmutableList<string> object and returned as an IReadOnlyCollection<string>.
- The Succeeded property accounts for a collection instead of a single message and follows the same logic.
- The HasErrors method improves readability.
- The default constructor represents the successful state.
- The constructor that takes error messages as parameters represents a failed state and populates the Errors property.
Now that the operation result is updated, the operation itself can stay the same. The consumer stays almost the same as well (see the highlight in the code below), but we need to tell ASP.NET how to serialize the result:
app.MapGet(
“/multiple-errors-with-value”,
object (OperationResult.MultipleErrorsWithValue.Executor executor)
=> {
var result = executor.Operation();
if (result.Succeeded)
{
// Handle the success
return $”Operation succeeded with a value of ‘{result.Value}’.”;
}
else
{
// Handle the failure
return result.Errors;
}
}
);
We must specify the method returns an object (the highlighted code) so ASP.NET understands that the return value of our delegate can be anything. Without this, the return type could not be inferred, and the code would not compile. That makes sense since the function is returning a string in one path and an IReadOnlyCollection<string> in another.During the executing, ASP.NET serializes the IReadOnlyCollection<string> Errors property to JSON before outputting it to the client to help visualize the collection.
Returning a plain/text string when the operation succeeds and an application/json array when it fails is not a good practice. I suggest avoiding this in real applications. Either return JSON or plain text. Do not mix content types in a single endpoint unless necessary per specifications. Mixing content types only creates avoidable complexity and confusion. Moreover, it is way easier for the consumers of the API to always expect the same content type.
When designing system contracts, consistency and uniformity are usually better than incoherency, ambiguity, and variance.
Our Operation Result pattern implementation is getting better and better but still lacks a few features. One of those features is the possibility to propagate messages that are not errors, such as information messages and warnings, which we implement next.