🚀 v1.0.0 — Convention discovery, [SkipValidator], [ValidatorLifetime], [ValidateOnStartup], Minimal API helper

FluentValidation wiring at
compile time.

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
your validators (unchanged)
public class OrderValidator
    : AbstractValidator<Order>
{
    public OrderValidator()
    {
        RuleFor(x => x.Name).NotEmpty();
        RuleFor(x => x.Total).GreaterThan(0);
    }
}

public class CustomerValidator
    : AbstractValidator<Customer> { }
generated at build time
// ✨ AutoValidate.g.cs
public static IServiceCollection AddValidators(
    this IServiceCollection services)
{
    services.AddScoped<
        IValidator<Order>,
        OrderValidator>();
    services.AddScoped<
        IValidator<Customer>,
        CustomerValidator>();
    return services;
}

Everything you need

Convention-based, zero-config FluentValidation registration — with attributes for the cases that need more control.

🔍

Convention-based discovery

Any non-abstract class that inherits AbstractValidator<T> is automatically registered — no attributes, no manual wiring. Works with indirect inheritance chains too.

Zero runtime cost

No assembly scanning at startup. No reflection. The AddValidators() method is plain generated C# — as fast as hand-written registration. Fully AOT compatible.

🚫

[SkipValidator]

Opt a validator out of auto-registration — useful for test validators, base classes, or validators you want to register manually with custom configuration.

⏱️

[ValidatorLifetime]

Override the DI lifetime per validator. Scoped (default), Singleton for stateless validators, Transient for validators with per-request deps.

🚦

[ValidateOnStartup]

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.

🔌

Minimal API integration

.WithValidation<T>() attaches a generated ValidationFilter<T> endpoint filter. Returns RFC 7807 ValidationProblem automatically.

🛡️

Build-time diagnostics

AV001 warns when multiple validators target the same model — only the first is registered. Catches registration conflicts at build time, not at runtime.

🧬

Inheritance chain support

Validators that inherit from an intermediate base class are discovered correctly. Abstract base validators are automatically excluded from DI registration.

📦

Namespaced types

Fully qualified type names throughout the generated code — no namespace collisions, no ambiguous type errors, works in multi-project solutions.

Examples

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

Build-time diagnostics

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>

vs. AddValidatorsFromAssembly()

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>()