Custom Rules
Custom rules let validation use application services while still producing the same localized validation error response as built-in rules. Use them for checks that need database access, tenant state, an external service, or application logic that does not belong inside an attribute constructor.
CustomRule
Adds a custom validation rule resolved from dependency injection. Use the attribute form when the rule can sit directly on a DTO property, and the fluent form when it belongs in a Validator<TRequest> chain with other expression-based rules. A rule type that does not implement ICustomValidationRule<TRequest, TProperty> for the property it is applied to is refused at startup by AddAspNetValidation, not on the request that reaches it.
[CustomRule<UniqueEmailRule>]
public string Email { get; init; } = string.Empty;RuleFor(x => x.Email)
.CustomRule<UniqueEmailRule>();Create a custom rule type
Use .CustomRule<TRule>() from a Validator<TRequest> when the rule only belongs to that request or when fluent validation reads better than attributes.
using AlmightyShogun.AspNet.RequestValidation;
public sealed class CreateAccountRequest
{
public string Email { get; init; } = string.Empty;
}
public sealed class CreateAccountRequestValidator
: Validator<CreateAccountRequest>
{
protected override void Rules()
{
RuleFor(request => request.Email)
.Required()
.Email()
.CustomRule<UniqueEmailRule>();
}
}using System;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
public sealed class UserRepository
{
private static readonly HashSet<string> Emails
= new(StringComparer.OrdinalIgnoreCase)
{
"admin@example.com"
};
public Task<bool> EmailExistsAsync(
string email,
CancellationToken cancellationToken = default
) => Task.FromResult(Emails.Contains(email));
}using System.Threading;
using System.Threading.Tasks;
using AlmightyShogun.AspNet.RequestValidation;
public sealed class UniqueEmailRule(UserRepository userRepository)
: ICustomValidationRule<CreateAccountRequest, string>
{
public async Task<ValidationRuleResult> ValidateAsync(
CreateAccountRequest request,
string? value,
CancellationToken cancellationToken = default
)
{
if (string.IsNullOrWhiteSpace(value))
return ValidationRuleResult.Success();
bool exists = await userRepository
.EmailExistsAsync(value, cancellationToken);
return exists
? ValidationRuleResult.Failure("validation.unique")
: ValidationRuleResult.Success();
}
}The failure key is resolved through the message files described on the Localization page. When the key cannot be resolved, the response contains the key itself, which keeps the API response stable during missing translation work.
Create a domain-specific attribute
Use CustomRuleAttribute<TRule> when the same DI-backed rule should be attached directly to a request property. This keeps the request DTO compact when the validation rule is reusable and does not need fluent expression setup.
using AlmightyShogun.AspNet.RequestValidation;
public sealed class CreateAccountRequest
{
[Required]
[Email]
[CustomRule<UniqueEmailRule>]
public string Email { get; init; } = string.Empty;
}Derive from CustomRuleAttribute when an application wants a domain-specific attribute name instead of writing [CustomRule<TRule>] everywhere.
using System;
using AlmightyShogun.AspNet.RequestValidation;
public sealed class UniqueEmailAttribute : CustomRuleAttribute
{
protected override Type CreateCustomRule()
=> CustomRule<UniqueEmailRule>();
}using AlmightyShogun.AspNet.RequestValidation;
public sealed class RegisterAccountRequest
{
[Required]
[Email]
[UniqueEmail]
public string Email { get; init; } = string.Empty;
}Type signature
public interface ICustomValidationRule<in TRequest, in TProperty>
where TRequest : class
{
Task<ValidationRuleResult> ValidateAsync(
TRequest request,
TProperty? value,
CancellationToken cancellationToken = default
);
}
public abstract class CustomRuleAttribute : ValidationRuleAttribute
{
protected CustomRuleAttribute();
protected abstract Type CreateCustomRule();
protected static Type CustomRule<TRule>()
where TRule : class;
}
public sealed class CustomRuleAttribute<TRule> : CustomRuleAttribute
where TRule : class;