For ASP.Net Core you can use the ValidationProblemDetails and ValidationResult classes. Note that you can use these even if you are not using MVC.
A full example would be too long to post, but here's a quick overview which should get you started:
This example that uses a CQRS approach, where ReadItemQuery is the query for reading an item and ReadItemResult is the result if successful.
It also uses the Result<T> pattern to encapsulate the results and assumes that there is a "handler" class that actually implements the reading and uses the Result pattern to return any errors.
The important part is that it returns a ValidationProblem if there's one or more errors. The ValidationProblem type can encapsulate multiple errors that occurred. So the controller method might look something like this:
[HttpGet(Name = "ReadItem")]
[ProducesResponseType(typeof(ReadItemResult), StatusCodes.Status200OK)] // For successful response
[ProducesResponseType(StatusCodes.Status404NotFound)] // For not found
[ProducesResponseType(typeof(ValidationProblem), StatusCodes.Status400BadRequest)] // For validation or other client errors
public async Task<Results<
Ok<ReadItemResult>,
ValidationProblem,
NotFound>>
ReadItem([FromQuery] ReadItemQuery query, CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(query);
Result<ReadItemResult> result = await _handler.ReadItem(query, ct);
if (result.IsSuccess)
return TypedResults.Ok(result.Value);
_logger.LogError("{Error}", result.Error.ToString());
if (result.Error.Code == ReadItemErrors.ErrorNotFound)
return TypedResults.NotFound();
return result.ToValidationProblem(); // Convert your result errors to a ValidationProblem.
}
Where result.ToValidationProblem() is a Result method that converts your Result errors to a ValidationProblem.
You could use TypedResults.ValidationProblem() to do this.
I haven't included any code for the Results pattern, but I think you would already know about that.
On the client side, you'd need to check for validation errors like so:
public async Task<Result<ReadItemResult>> GetItem(ReadItemQuery query, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(query);
Uri uri = new (WebSerializer.ToQueryString("api/Items/ReadItem", query), UriKind.Relative);
var response = await _httpClient.GetAsync(uri, cancellationToken);
if (response.IsSuccessStatusCode)
return await resultFromSuccessfulResponse<ReadItemResult>(response, cancellationToken);
return response.StatusCode == System.Net.HttpStatusCode.NotFound
? Result<ReadItemResult>.Failure(new Error(ReadItemErrors.ErrorNotFound, $"Item with ID {query.Id} not found."))
: await resultFromFailedResponse<ReadItemResult>(response, cancellationToken);
}
static async Task<Result<T>> resultFromFailedResponse<T>(HttpResponseMessage response, CancellationToken cancellationToken)
{
var problem = await response.Content.ReadFromJsonAsync<ValidationProblemDetails>(cancellationToken);
return problem is null
? Result<T>.Failure(new Error("Unknown", "The result was null."))
: Result<T>.Failure(problem);
}
static async Task<Result<T>> resultFromSuccessfulResponse<T>(HttpResponseMessage response, CancellationToken cancellationToken)
{
var result = await response.Content.ReadFromJsonAsync<T>(cancellationToken);
return result != null
? Result<T>.Success(result)
: Result<T>.Failure(new Error("Unknown", "The result was null."));
}
Note how if there is a failed response that is not NotFound, we assume that a ValidationProblemDetails object has been returned.
Then resultFromFailedResponse<T>() reads the ValidationProblemDetails from the response and encapsulates it in a Result<T> object.
In your Web UI you would have to handle the ValidationProblemDetails stored in the Result<T> appropriately.
You could of course just use ValidationResult and ValidationProblemDetails without using the Result<T> pattern to encapsulate them - but often the Result<T> pattern might be used to encapsulate other result types too. It depends on what you need.