Immediate.Validations is a source generator validating
Immediate.Handlers
handler parameters.
You can install Immediate.Validations with NuGet:
Install-Package Immediate.Validations
Or via the .NET Core command line interface:
dotnet add package Immediate.Validations
Either command, from Package Manager Console or .NET Core CLI, will download and install Immediate.Validations.
Add Immediate.Validations to the Immediate.Handlers behaviors pipeline by including it in the list of default Behaviors for the assembly:
using Immediate.Validations.Shared;
[assembly: Behaviors(
typeof(ValidationBehavior<,>)
)]
Indicate that a class should be validated by adding the [Validate]
attribute and IValidationTarget<>
interface:
[Validate]
public partial record Query : IValidationTarget<Query>;
When Nullable Reference Types is enabled, any non-nullable reference types are automatically checked for null
. Other
validations are available like so:
[Validate]
public partial record Query : IValidationTarget<Query>
{
[GreaterThan(0)]
public required int Id { get; init; }
}
Since attributes cannot reference anything other than constant strings, the way to reference static and instance
properties, fields, and methods is to use the nameof()
to identify which property, field, or method should be used. Example:
[Validate]
public partial record Query : IValidationTarget<Query>
{
[GeneratedRegex(@"^\d+$")]
private static partial Regex AllDigitsRegex();
[Match(regex: nameof(AllDigitsRegex))]
public required string Id { get; init; }
}
Provide a custom message to any validation using the Message
property of the attribute. This message will be parsed
for template parameters, which will be applied to the message before rendering to the validation result. The target property
name is available as {PropertyName}
, and it's value via {PropertyValue}
.
Other parameter values will be added using their property name suffixed with Value
(for example, the
GreaterThanAttribute
uses a comparison
parameter, so the value is available via ComparisonValue
). If another
property on the target class is referenced via nameof(Property)
, the name of that property will be available using the
Name
suffix (for example, ComparisonName
for the comparison
property).
[Validate]
public partial record Query : IValidationTarget<Query>
{
[GreaterThan(0, Message = "'{PropertyName}' must be greater than '{ComparisonValue}'")]
public required int Id { get; init; }
}
If attributes are not enough to specify how to validate a class, an AdditionalValidations
method can be used to write
additional validations for the class.
[Validate]
public partial record Query : IValidationTarget<Query>
{
public required bool Enabled { get; init; }
public required int Id { get; init; }
private static void AdditionalValidations(
ValidationResult errors,
Query target
)
{
if (target.Enabled)
{
// Use a lambda to use the default message or override message;
// the message will be templated in the same way as attribute validations.
errors.Add(
() => GreaterThanAttribute.ValidateProperty(
target.Id,
0
)
);
}
if (false)
{
// Manually create a `ValidationError` and add it to the `ValidationResult`.
errors.Add(
new ValidationError()
{
PropertyName = "ExampleProperty",
ErrorMessage = "Example Message",
}
)
}
}
}
The result of doing the above is that when a parameter fails one or more validations, a ValidationException
is thrown,
which can be handled via ProblemDetails or any other infrastructure mechanism.
Example using ProblemDetails:
builder.Services.AddProblemDetails(ConfigureProblemDetails);
public static void ConfigureProblemDetails(ProblemDetailsOptions options) =>
options.CustomizeProblemDetails = c =>
{
if (c.Exception is null)
return;
c.ProblemDetails = c.Exception switch
{
ValidationException ex => new ValidationProblemDetails(
ex
.Errors
.GroupBy(x => x.PropertyName, StringComparer.OrdinalIgnoreCase)
.ToDictionary(
x => x.Key,
x => x.Select(x => x.ErrorMessage).ToArray(),
StringComparer.OrdinalIgnoreCase
)
)
{
Status = StatusCodes.Status400BadRequest,
},
// other exception handling as desired
var ex => new ProblemDetails
{
Detail = "An error has occurred.",
Status = StatusCodes.Status500InternalServerError,
},
};
c.HttpContext.Response.StatusCode =
c.ProblemDetails.Status
?? StatusCodes.Status500InternalServerError;
};