AutoValidate.Generator discovers every AbstractValidator<T>
in your assembly and generates AddValidators() —
no assembly scanning, no reflection, no runtime overhead.
dotnet add package AutoValidate.Generator
dotnet add package FluentValidation
public class OrderValidator
: AbstractValidator<Order>
{
public OrderValidator()
{
RuleFor(x => x.Name).NotEmpty();
RuleFor(x => x.Total).GreaterThan(0);
}
}
public class CustomerValidator
: AbstractValidator<Customer> { }
// ✨ AutoValidate.g.cs
public static IServiceCollection AddValidators(
this IServiceCollection services)
{
services.AddScoped<
IValidator<Order>,
OrderValidator>();
services.AddScoped<
IValidator<Customer>,
CustomerValidator>();
return services;
}
Convention-based, zero-config FluentValidation registration — with attributes for the cases that need more control.
Any non-abstract class that inherits AbstractValidator<T>
is automatically registered — no attributes, no manual wiring. Works with indirect inheritance chains too.
No assembly scanning at startup. No reflection. The AddValidators()
method is plain generated C# — as fast as hand-written registration. Fully AOT compatible.
Opt a validator out of auto-registration — useful for test validators, base classes, or validators you want to register manually with custom configuration.
Override the DI lifetime per validator.
Scoped (default),
Singleton for stateless validators,
Transient for validators with per-request deps.
Registers a hosted service that validates the model instance at app startup. If validation fails, the app won't start — catch config errors before they reach production.
.WithValidation<T>() attaches a generated
ValidationFilter<T> endpoint filter.
Returns RFC 7807 ValidationProblem automatically.
AV001 warns when multiple validators target the same model — only the first is registered. Catches registration conflicts at build time, not at runtime.
Validators that inherit from an intermediate base class are discovered correctly. Abstract base validators are automatically excluded from DI registration.
Fully qualified type names throughout the generated code — no namespace collisions, no ambiguous type errors, works in multi-project solutions.
Everything you need to know — in code.
// 1. Define validators as you normally would
public class Order
{
public string CustomerName { get; set; } = "";
public decimal Total { get; set; }
}
public class OrderValidator : AbstractValidator<Order>
{
public OrderValidator()
{
RuleFor(x => x.CustomerName).NotEmpty().MaximumLength(200);
RuleFor(x => x.Total).GreaterThan(0);
}
}
// 2. Register everything in one line — no manual wiring
builder.Services.AddValidators();
// Generated code (AddValidators):
// services.AddScoped<IValidator<Order>, OrderValidator>();
// 3. Use as normal
public class OrderService(IValidator<Order> validator)
{
public async Task<bool> ValidateAsync(Order order)
=> (await validator.ValidateAsync(order)).IsValid;
}
using AutoValidate;
// Default is Scoped — no attribute needed
public class OrderValidator : AbstractValidator<Order> { }
// → services.AddScoped<IValidator<Order>, OrderValidator>()
// Singleton — validator is stateless, safe to share
[ValidatorLifetime(ValidatorLifetime.Singleton)]
public class ConfigValidator : AbstractValidator<AppConfig> { }
// → services.AddSingleton<IValidator<AppConfig>, ConfigValidator>()
// Transient — validator has per-request dependencies
[ValidatorLifetime(ValidatorLifetime.Transient)]
public class RequestValidator : AbstractValidator<CreateOrderRequest> { }
// → services.AddTransient<IValidator<CreateOrderRequest>, RequestValidator>()
// Skip a validator entirely (test doubles, manual registration, etc.)
[SkipValidator]
public class TestOrderValidator : AbstractValidator<Order> { }
// [ValidateOnStartup] — fail fast on bad configuration
using AutoValidate;
public class AppSettings
{
public string ConnectionString { get; set; } = "";
public string ApiKey { get; set; } = "";
public int MaxRetries { get; set; }
}
[ValidateOnStartup]
public class AppSettingsValidator : AbstractValidator<AppSettings>
{
public AppSettingsValidator()
{
RuleFor(x => x.ConnectionString).NotEmpty();
RuleFor(x => x.ApiKey).MinimumLength(32);
RuleFor(x => x.MaxRetries).InclusiveBetween(1, 10);
}
}
// Register AppSettings in DI so it can be resolved at startup:
builder.Services.AddSingleton(
builder.Configuration.GetSection("AppSettings").Get<AppSettings>()!);
builder.Services.AddValidators();
// Generated also adds: services.AddHostedService<ValidatorStartupService<AppSettings>>()
// If AppSettings is invalid, the app throws InvalidOperationException on startup:
// "Startup validation failed for AppSettings: Connection string must not be empty."
// WithValidation<T>() — automatic validation endpoint filter
// Requires .NET 7+
var app = builder.Build();
// Attach validation to any endpoint:
app.MapPost("/orders", async (Order order, IOrderService svc) =>
{
var result = await svc.CreateAsync(order);
return Results.Created($"/orders/{result.Id}", result);
})
.WithValidation<Order>();
// Generated ValidationFilter<T>:
// - Finds the first argument of type T in the request
// - Calls IValidator<T>.ValidateAsync(model)
// - Returns Results.ValidationProblem(errors) if invalid (RFC 7807)
// - Calls next(context) if valid
// Response on failure (400 Bad Request):
// {
// "type": "https://tools.ietf.org/html/rfc7807",
// "title": "One or more validation errors occurred.",
// "errors": {
// "CustomerName": ["'Customer Name' must not be empty."],
// "Total": ["'Total' must be greater than '0'."]
// }
// }
// ── Indirect inheritance — discovered correctly ──────────────────────────────
public abstract class BaseValidator<T> : AbstractValidator<T>
{
protected BaseValidator() { RuleFor(x => x).NotNull(); }
}
// ConcreteValidator is registered; BaseValidator<T> (abstract) is not
public class OrderValidator : BaseValidator<Order>
{
public OrderValidator() { RuleFor(x => x.Name).NotEmpty(); }
}
// ── Multiple validators — AV001 warning if duplicate ────────────────────────
public class OrderValidatorV1 : AbstractValidator<Order> { }
public class OrderValidatorV2 : AbstractValidator<Order> { }
// ⚠ AV001: Multiple validators found for 'Order': OrderValidatorV1, OrderValidatorV2.
// Only the first will be registered.
// ── Combine with FluentValidation.AspNetCore ─────────────────────────────────
// AutoValidate.Generator replaces AddValidatorsFromAssembly().
// Keep using .Validate(), .ValidateAndThrowAsync(), IValidator<T> etc. as normal.
builder.Services.AddValidators(); // replaces: AddValidatorsFromAssembly(assembly)
builder.Services.AddFluentValidationAutoValidation(); // optional: MVC auto-validation
Problems surfaced as compiler warnings — never at runtime.
| ID | Severity | Description |
|---|---|---|
| AV001 | ⚠ Warning | Multiple validators found for the same model type — only the first is registered |
| AV002 | ⚠ Warning | AutoValidate attribute applied to a class that does not inherit AbstractValidator<T> |
A direct replacement for FluentValidation's built-in assembly scanning.
| Feature | AddValidatorsFromAssembly() |
AutoValidate.Generator |
|---|---|---|
| Discovery method | Runtime reflection | Compile-time |
| Startup overhead | Assembly scan on every start | Zero |
| AOT / NativeAOT | ❌ Incompatible | ✅ Fully supported |
| Duplicate detection | Runtime exception | Build-time warning (AV001) |
| Startup validation | Manual | [ValidateOnStartup] |
| Minimal API filter | Manual | .WithValidation<T>() |