Skip to main content

Dependency Injection

Automatic Registration with AddValidationsFromAssembly​

The AddValidationsFromAssembly extension method scans an assembly and automatically registers all classes that implement IValidator<T> in the DI container.

// Program.cs
using Vali_Validation.Core.Extensions;

builder.Services.AddValidationsFromAssembly(
assembly: typeof(Program).Assembly,
lifetime: ServiceLifetime.Transient); // Transient is the default value

You only need to pass the assembly. The method discovers all concrete (non-abstract, non-interface) validators that inherit from AbstractValidator<T> and registers them as IValidator<T>.

Registration from Multiple Assemblies​

If your validators are spread across several projects, call the method once per assembly:

builder.Services.AddValidationsFromAssembly(typeof(Program).Assembly);
builder.Services.AddValidationsFromAssembly(typeof(ApplicationLayerMarker).Assembly);
builder.Services.AddValidationsFromAssembly(typeof(InfrastructureLayerMarker).Assembly);

Marker Classes for Assemblies​

A common practice is to create an empty "marker" class in each project to identify its assembly:

// MyApp.Application/ApplicationAssembly.cs
namespace MyApp.Application;

public static class ApplicationAssembly
{
public static readonly Assembly Reference = typeof(ApplicationAssembly).Assembly;
}

// MyApp.Api/Program.cs
builder.Services.AddValidationsFromAssembly(ApplicationAssembly.Reference);
builder.Services.AddValidationsFromAssembly(typeof(Program).Assembly);

ServiceLifetime: Which One to Choose​

LifetimeDescriptionWhen to use
TransientNew instance on each resolutionValidators with no dependencies or with Transient dependencies
ScopedOne instance per HTTP requestIf the validator depends on a DbContext or Scoped service
SingletonOne instance for the entire appOnly if the validator has no mutable dependencies

Golden Rule​

The validator lifetime must be equal to or shorter than the lifetime of its longest-lived dependencies.

// Correct: DbContext is Scoped, the validator is also Scoped
builder.Services.AddValidationsFromAssembly(assembly, ServiceLifetime.Scoped);

// Incorrect: DbContext is Scoped, the validator is Singleton → captive dependency
builder.Services.AddValidationsFromAssembly(assembly, ServiceLifetime.Singleton); // AVOID

Example with DbContext​

// Validator that accesses the DB (depends on DbContext which is Scoped)
public class CreateUserValidator : AbstractValidator<CreateUserRequest>
{
private readonly AppDbContext _context;

public CreateUserValidator(AppDbContext context)
{
_context = context;

RuleFor(x => x.Email)
.MustAsync(async (email, ct) =>
{
return !await _context.Users.AnyAsync(u => u.Email == email, ct);
})
.WithMessage("The email is already registered.");
}
}

// Program.cs — register as Scoped because it depends on DbContext (Scoped)
builder.Services.AddValidationsFromAssembly(
typeof(Program).Assembly,
ServiceLifetime.Scoped);

Injecting IValidator<T> into Constructors​

Once registered, you can inject IValidator<T> into any DI-managed class:

In a Minimal API Endpoint​

app.MapPost("/products", async (
[FromBody] CreateProductRequest request,
[FromServices] IValidator<CreateProductRequest> validator,
[FromServices] IProductRepository repository,
CancellationToken ct) =>
{
var result = await validator.ValidateAsync(request, ct);
if (!result.IsValid)
return Results.ValidationProblem(result.Errors.ToDictionary(
k => k.Key, v => v.Value.ToArray()));

var product = await repository.CreateAsync(request, ct);
return Results.Created($"/products/{product.Id}", product);
});

In an MVC Controller​

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
private readonly IValidator<CreateUserRequest> _validator;
private readonly IUserService _userService;

public UsersController(
IValidator<CreateUserRequest> validator,
IUserService userService)
{
_validator = validator;
_userService = userService;
}

[HttpPost]
public async Task<IActionResult> Create(
[FromBody] CreateUserRequest request,
CancellationToken ct)
{
var result = await _validator.ValidateAsync(request, ct);
if (!result.IsValid)
return BadRequest(result.Errors);

var user = await _userService.CreateAsync(request, ct);
return CreatedAtAction(nameof(GetById), new { id = user.Id }, user);
}
}

In an Application Service​

public class OrderService : IOrderService
{
private readonly IValidator<CreateOrderRequest> _createValidator;
private readonly IValidator<UpdateOrderRequest> _updateValidator;
private readonly IOrderRepository _repository;

public OrderService(
IValidator<CreateOrderRequest> createValidator,
IValidator<UpdateOrderRequest> updateValidator,
IOrderRepository repository)
{
_createValidator = createValidator;
_updateValidator = updateValidator;
_repository = repository;
}

public async Task<Order> CreateAsync(CreateOrderRequest request, CancellationToken ct)
{
await _createValidator.ValidateAndThrowAsync(request, ct);
return await _repository.CreateAsync(request, ct);
}

public async Task<Order> UpdateAsync(int id, UpdateOrderRequest request, CancellationToken ct)
{
await _updateValidator.ValidateAndThrowAsync(request, ct);
return await _repository.UpdateAsync(id, request, ct);
}
}

Complete Program.cs Example​

using Vali_Validation.Core.Extensions;
using Vali_Validation.AspNetCore; // For UseValiValidationExceptionHandler

var builder = WebApplication.CreateBuilder(args);

// ── Infrastructure ────────────────────────────────────────────────────────────
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

// ── Validation ────────────────────────────────────────────────────────────────
// Registers all IValidator<T> from the Application assembly
// Scoped because some validators depend on AppDbContext
builder.Services.AddValidationsFromAssembly(
ApplicationAssembly.Reference,
ServiceLifetime.Scoped);

// If you also have validators in the API project (rare, but possible)
builder.Services.AddValidationsFromAssembly(
typeof(Program).Assembly,
ServiceLifetime.Transient);

// ── ASP.NET Core ──────────────────────────────────────────────────────────────
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Support for ValidationProblem in Minimal APIs
builder.Services.AddValiValidationProblemDetails();

// ── Application Services ─────────────────────────────────────────────────────
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<IProductService, ProductService>();

var app = builder.Build();

// ── HTTP Pipeline ─────────────────────────────────────────────────────────────
app.UseSwagger();
app.UseSwaggerUI();

// Catches ValidationException and returns HTTP 400 in RFC 7807 format
app.UseValiValidationExceptionHandler();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

Manual Registration (without AddValidationsFromAssembly)​

If you prefer to register validators manually (useful in small projects or for fine control):

// Individual registration
builder.Services.AddTransient<IValidator<CreateProductRequest>, CreateProductRequestValidator>();
builder.Services.AddScoped<IValidator<CreateOrderRequest>, CreateOrderRequestValidator>();
builder.Services.AddSingleton<IValidator<AppSettings>, AppSettingsValidator>();

// With factory (for validators with complex dependencies)
builder.Services.AddScoped<IValidator<CreateUserRequest>>(sp =>
{
var context = sp.GetRequiredService<AppDbContext>();
var emailService = sp.GetRequiredService<IEmailService>();
return new CreateUserRequestWithEmailVerificationValidator(context, emailService);
});

Dynamic Validator Resolution​

In advanced cases, you can resolve the correct validator at runtime using the container:

public class ValidationService
{
private readonly IServiceProvider _serviceProvider;

public ValidationService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}

public async Task<ValidationResult> ValidateAsync<T>(T instance, CancellationToken ct)
where T : class
{
var validator = _serviceProvider.GetService<IValidator<T>>();
if (validator == null)
return new ValidationResult(); // No validator = valid

return await validator.ValidateAsync(instance, ct);
}
}

Testing: Mocking IValidator<T>​

Since code consumes IValidator<T>, you can mock it in tests without spinning up the full DI container:

// With Moq
public class OrderServiceTests
{
[Fact]
public async Task CreateAsync_ValidRequest_CallsRepository()
{
// Arrange
var validatorMock = new Mock<IValidator<CreateOrderRequest>>();
validatorMock
.Setup(v => v.ValidateAsync(It.IsAny<CreateOrderRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(new ValidationResult()); // No errors

var repositoryMock = new Mock<IOrderRepository>();
var service = new OrderService(validatorMock.Object, repositoryMock.Object);
var request = new CreateOrderRequest { CustomerId = "123" };

// Act
await service.CreateAsync(request, CancellationToken.None);

// Assert
repositoryMock.Verify(r => r.CreateAsync(request, It.IsAny<CancellationToken>()), Times.Once);
}

[Fact]
public async Task CreateAsync_InvalidRequest_ThrowsValidationException()
{
// Arrange
var failResult = new ValidationResult();
failResult.AddError("CustomerId", "The customer does not exist.");

var validatorMock = new Mock<IValidator<CreateOrderRequest>>();
validatorMock
.Setup(v => v.ValidateAsync(It.IsAny<CreateOrderRequest>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(failResult);

validatorMock
.Setup(v => v.ValidateAndThrowAsync(It.IsAny<CreateOrderRequest>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new ValidationException(failResult));

var service = new OrderService(validatorMock.Object, Mock.Of<IOrderRepository>());

// Act & Assert
await Assert.ThrowsAsync<ValidationException>(
() => service.CreateAsync(new CreateOrderRequest(), CancellationToken.None));
}
}

Next Steps​

  • ASP.NET Core — Middleware, endpoint filters and action filters
  • MediatR — Registration alongside MediatR
  • Vali-Mediator — Registration alongside Vali-Mediator