0
if (!hasAnyPhoneNumber)
    throw new NotEmptyException("PhoneNumber", MessageHelper.GetMessage(ErrorMessageName.NOT_EMPTY));

I haven't tried any specific solution yet, but my goal is to build a structure where, instead of throwing exceptions in the service layer, all validation errors are collected and returned to the frontend — similar to how FluentValidation works for DTOs.

3
  • So, create an validation-error type and return a list of errors? What specific requirements do you have? If you do not know where to start you can always start with the simplest possible approach and try to figure out if it is good enough. Commented Jun 30 at 8:52
  • You could use the Result Pattern (which is very different to exceptions), but will require some work in C# (because C# lacks discriminated unions). Commented Jun 30 at 9:08
  • IMHO create a class to encapsulate all these parameters. Add validation attributes. Bind it and validate it as you would with any asp.net-core view model. Commented Jul 7 at 3:02

1 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.