Skip to main content

Quick Start

This document shows a complete, working example from scratch: defining a model, creating a validator, registering it in DI and using it in an ASP.NET Core endpoint. The goal is to have validation working in less than 10 minutes.

1. Install the Packages

dotnet add package Vali-Validation
dotnet add package Vali-Validation.AspNetCore

2. Define the Model

// Models/CreateProductRequest.cs
public class CreateProductRequest
{
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public decimal Price { get; set; }
public int Stock { get; set; }
public string Category { get; set; } = string.Empty;
public string? ImageUrl { get; set; }
}

3. Create the Validator

Validators inherit from AbstractValidator<T>. All rules are defined in the constructor.

// Validators/CreateProductRequestValidator.cs
using Vali_Validation.Core.Validators;

public class CreateProductRequestValidator : AbstractValidator<CreateProductRequest>
{
private static readonly string[] ValidCategories =
new[] { "Electronics", "Clothing", "Food", "Books", "Other" };

public CreateProductRequestValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.WithMessage("The product name is required.")
.MinimumLength(3)
.WithMessage("The name must have at least 3 characters.")
.MaximumLength(200)
.WithMessage("The name cannot exceed 200 characters.");

RuleFor(x => x.Description)
.MaximumLength(1000)
.WithMessage("The description cannot exceed 1000 characters.");

RuleFor(x => x.Price)
.GreaterThan(0)
.WithMessage("The price must be greater than 0.")
.LessThan(100000)
.WithMessage("The price cannot exceed 100,000.");

RuleFor(x => x.Stock)
.GreaterThanOrEqualTo(0)
.WithMessage("The stock cannot be negative.");

RuleFor(x => x.Category)
.NotEmpty()
.WithMessage("The category is required.")
.In(ValidCategories)
.WithMessage("The category must be one of: Electronics, Clothing, Food, Books, Other.");

RuleFor(x => x.ImageUrl)
.Url()
.WithMessage("The image URL is not valid.")
.When(x => x.ImageUrl != null);
}
}

4. Register in DI

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

var builder = WebApplication.CreateBuilder(args);

// Registers all IValidator<T> from the current assembly
builder.Services.AddValidationsFromAssembly(typeof(Program).Assembly);

// If using AspNetCore:
builder.Services.AddValiValidationProblemDetails();

var app = builder.Build();

// Middleware that converts ValidationException to HTTP 400
app.UseValiValidationExceptionHandler();

5. Use in an Endpoint

Option A: Minimal API with endpoint filter

// Program.cs (continued)
app.MapPost("/products", async (
CreateProductRequest request,
IValidator<CreateProductRequest> validator,
IProductRepository repository) =>
{
// Validate manually
var result = await validator.ValidateAsync(request);
if (!result.IsValid)
{
return Results.ValidationProblem(result.Errors.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value.ToArray()));
}

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

Or using the endpoint filter (cleaner):

app.MapPost("/products", async (
CreateProductRequest request,
IProductRepository repository) =>
{
var product = await repository.CreateAsync(request);
return Results.Created($"/products/{product.Id}", product);
})
.WithValiValidation<CreateProductRequest>(); // Automatic validation before the handler

Option B: MVC Controller

[ApiController]
[Route("api/[controller]")]
[ValiValidate] // Automatically validates all arguments with a registered IValidator<T>
public class ProductsController : ControllerBase
{
private readonly IProductRepository _repository;

public ProductsController(IProductRepository repository)
{
_repository = repository;
}

[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateProductRequest request)
{
// If we get here, validation has already passed
var product = await _repository.CreateAsync(request);
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
}

6. Run and Test

Run the application:

dotnet run

Test with invalid data:

curl -X POST http://localhost:5000/products \
-H "Content-Type: application/json" \
-d '{"name": "A", "price": -5, "stock": -1, "category": "Unknown"}'

HTTP 400 response:

{
"type": "https://tools.ietf.org/html/rfc7807",
"title": "Validation Failed",
"status": 400,
"errors": {
"Name": ["The name must have at least 3 characters."],
"Price": ["The price must be greater than 0."],
"Stock": ["The stock cannot be negative."],
"Category": ["The category must be one of: Electronics, Clothing, Food, Books, Other."]
}
}

Test with valid data:

curl -X POST http://localhost:5000/products \
-H "Content-Type: application/json" \
-d '{
"name": "Laptop Pro 15",
"description": "High-performance laptop",
"price": 1299.99,
"stock": 50,
"category": "Electronics",
"imageUrl": "https://example.com/laptop.jpg"
}'

HTTP 201 response:

{
"id": 1,
"name": "Laptop Pro 15",
"price": 1299.99,
"category": "Electronics"
}

Summary of What You Learned

In this example you saw:

  1. Defining a validator by inheriting from AbstractValidator<T> and using RuleFor in the constructor
  2. Chained rules with custom messages via WithMessage
  3. Conditionals with When for optional rules
  4. DI registration with AddValidationsFromAssembly
  5. Usage in endpoints manually or automatically with WithValiValidation<T>
  6. Standard error format application/problem+json

Next Steps

  • Validators in depth — AbstractValidator, Include, global CascadeMode, ValidateParallelAsync
  • Basic Rules — Complete catalog of all available rules
  • Modifiers — WithMessage, When/Unless, StopOnFirstFailure and more