Table of Contents

Validation errors

Input validation is a failure the caller can fix, so it is not an exception. Arc4u.Results defines ValidationError for it, Arc4u.FluentValidation turns FluentValidation failures into ValidationErrors, and Arc4u.AspNetCore.Results answers 422 Unprocessable Entity with a ValidationProblemDetails. This page is part of the results guide; the error types are introduced in Fluent result chains.

Create a ValidationError

ValidationError is created with Create and completed with With... methods.

Member Type Default Description
Create(errorMessage) string none (required) The message returned to the client.
WithSeverity / Severity Severity Error Error, Warning or Info.
WithCode / Code string Empty string A code for your logs. It is not part of the HTTP response.
WithMetadata(key, value) string, object none Adds metadata. An existing key is kept: the call is then ignored.

ValidationError converts implicitly to a failed Result. The WithValidationError extensions add one to an existing Result or Result<T>, with an optional code and severity:

using Arc4u.Results.Validation;
using FluentResults;

public static class Checks
{
    public static Result<int> Quantity(int quantity)
    {
        var result = Result.Ok(quantity);

        if (quantity <= 0)
        {
            result = result.WithValidationError("The quantity must be positive.", "QTY_POSITIVE");
        }

        if (quantity > 100)
        {
            result = result.WithValidationError("Large order, please confirm.", Severity.Warning);
        }

        return result;
    }
}

Adding an error fails the result, whatever its severity: a result that only holds a warning is failed.

The HTTP response

Endpoints converted with the methods of Results to HTTP responses answer 422 when the result holds at least one ValidationError. The errors member groups the messages by the name of their severity:

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json; charset=utf-8

{"type":"https://github.com/Arc4u-org/Arc4u/wiki/StatusCodes#validation-error","title":"Error from validation.","status":422,"errors":{"Error":["Id 3 is not valid."],"Warning":["Something to check."]}}

The keys are Error, Warning and Info, not the names of the properties that failed. The code and the metadata of a ValidationError are not returned. If your clients need the property name, build the ProblemDetails yourself with SetFromErrorFactory.

Validate with FluentValidation

Arc4u.FluentValidation (assembly Arc4u.Validation) adds extension methods to IValidator<T> in the Arc4u.Validation namespace. They return a Result<T>: successful and holding the validated value, or failed with one ValidationError per FluentValidation failure.

Method Returns
ValidateWithResultAsync(value) ValueTask<Result<T>>
ValidateWithResultAsync(value, cancellationToken) ValueTask<Result<T>>
ValidateWithResult(value) Result<T>

The conversion of each ValidationFailure:

FluentValidation ValidationError
ErrorMessage Message
ErrorCode Code. Unless you call WithErrorCode, FluentValidation sets it to the name of the validator, for example GreaterThanValidator.
Severity.Error Severity.Error
Severity.Warning Severity.Warning
Severity.Info Severity.Info

ToValidationError and ToResultErrors do the same conversion on a ValidationFailure or a list of them, when you run the validator yourself.

using Arc4u.Validation;
using FluentResults;
using FluentValidation;

public record NewOrder(string Name, int Quantity);

public class NewOrderValidator : AbstractValidator<NewOrder>
{
    public NewOrderValidator()
    {
        RuleFor(x => x.Name).NotEmpty().WithErrorCode("NAME_REQUIRED");
        RuleFor(x => x.Quantity).GreaterThan(0);
    }
}

public class OrderService(IValidator<NewOrder> validator)
{
    public async Task<Result<NewOrder>> CreateAsync(NewOrder order, CancellationToken cancellationToken)
    {
        var validation = await validator.ValidateWithResultAsync(order, cancellationToken);

        return validation.IsFailed ? validation : Result.Ok(order);
    }
}

Register validators with FluentValidation's own mechanisms, for example builder.Services.AddScoped<IValidator<NewOrder>, NewOrderValidator>(). The controller returns the result with ToActionCreatedResultAsync or any other method of the mapping page; an invalid NewOrder answers 422.

Note

Arc4u.Results.Validation.Severity and FluentValidation.Severity have the same name. A file that imports both namespaces must qualify the one it uses.

Rules for DateTime properties

Arc4u.FluentValidation also contains rules for DateTime properties.

Rule Passes when the property is
IsUtcDateTime() A DateTime whose Kind is Utc
IsDateOnly() A DateTime whose time of day is zero
using Arc4u.Validation;
using FluentValidation;

public class CustomerValidator : AbstractValidator<Customer>
{
    public CustomerValidator()
    {
        RuleFor(x => x.CreatedOn).IsUtcDateTime();
    }
}

public class Customer
{
    public DateTime CreatedOn { get; set; }
}

Troubleshooting

The errors object has Error and Warning keys, not property names

That is the current format, see The HTTP response.

A warning makes the request fail

Every ValidationError fails the result, including warnings and Info. If a rule is only advisory, do not report it through the result.

See also