ASP.NET Core 8 Web API -

Validation

Validation is a critical part of building robust web APIs. It ensures that the data sent to the server meets the expected format and constraints before processing it. ASP.NET Core provides multiple ways to implement validation, including data annotations, custom validation attributes, and fluent validation. This guide covers all these methods with detailed examples and best practices.


1. Introduction to Validation

Validation in ASP.NET Core Web API ensures that the data received from the client adheres to the rules defined by the developer. It helps in preventing invalid data from being processed and stored, thereby maintaining data integrity.


2. Data Annotations

Data Annotations are attributes that you can apply to model properties to enforce validation rules. They are the simplest way to add validation in ASP.NET Core.

Example: Basic Data Annotations Model with Data Annotations:
        
            
public class Product
{
    public int Id { get; set; }

    [Required(ErrorMessage = "Name is required")]
    [StringLength(100, ErrorMessage = "Name can't be longer than 100 characters")]
    public string Name { get; set; }

    [Range(0.01, 1000.00, ErrorMessage = "Price must be between 0.01 and 1000.00")]
    public decimal Price { get; set; }
}

        
    


Controller with Model Validation:
        
            
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpPost]
    public IActionResult CreateProduct(Product product)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        // Save the product to the database (not shown)

        return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
    }

    [HttpGet("{id}")]
    public IActionResult GetProduct(int id)
    {
        // Retrieve the product from the database (not shown)

        var product = new Product { Id = id, Name = "Sample Product", Price = 9.99m };
        return Ok(product);
    }
}

        
    


3. Custom Validation Attributes

Custom validation attributes allow you to implement complex validation logic that cannot be achieved using standard data annotations.

Example: Custom Validation Attribute Custom Validation Attribute:
        
            
public class ValidPriceAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (value is decimal price && price >= 0.01m && price <= 1000.00m)
        {
            return ValidationResult.Success;
        }
        return new ValidationResult("Price must be between 0.01 and 1000.00");
    }
}

        
    


Model with Custom Validation Attribute:
        
            
public class Product
{
    public int Id { get; set; }

    [Required(ErrorMessage = "Name is required")]
    [StringLength(100, ErrorMessage = "Name can't be longer than 100 characters")]
    public string Name { get; set; }

    [ValidPrice]
    public decimal Price { get; set; }
}

        
    


4. Fluent Validation

Fluent Validation is an alternative to data annotations, offering a more expressive and flexible way to define validation rules.

Example: Fluent Validation Install FluentValidation:
        
            
dotnet add package FluentValidation.AspNetCore

        
    


Create a Validator:
        
            
public class ProductValidator : AbstractValidator<Product>
{
    public ProductValidator()
    {
        RuleFor(p => p.Name)
            .NotEmpty().WithMessage("Name is required")
            .MaximumLength(100).WithMessage("Name can't be longer than 100 characters");

        RuleFor(p => p.Price)
            .InclusiveBetween(0.01m, 1000.00m).WithMessage("Price must be between 0.01 and 1000.00");
    }
}

        
    


Register FluentValidation:
        
            
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers()
    .AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<ProductValidator>());

var app = builder.Build();

app.MapControllers();
app.Run();

        
    


5. Handling Validation Errors

It's important to handle validation errors gracefully and return meaningful responses to the client.

Example: Global Validation Error Handling Configure API Behavior:
        
            
builder.Services.Configure<ApiBehaviorOptions>(options =>
{
    options.InvalidModelStateResponseFactory = context =>
    {
        var errors = context.ModelState
            .Where(e => e.Value.Errors.Count > 0)
            .Select(e => new
            {
                Name = e.Key,
                Message = e.Value.Errors.First().ErrorMessage
            }).ToArray();

        return new BadRequestObjectResult(errors);
    };
});

        
    


6. Best Practices


7. Comprehensive Example

Here is a comprehensive example combining data annotations, custom validation, and FluentValidation.

Model with Data Annotations and Custom Validation:
        
            
public class Product
{
    public int Id { get; set; }

    [Required(ErrorMessage = "Name is required")]
    [StringLength(100, ErrorMessage = "Name can't be longer than 100 characters")]
    public string Name { get; set; }

    [ValidPrice]
    public decimal Price { get; set; }
}

        
    


Custom Validation Attribute:
        
            
public class ValidPriceAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (value is decimal price && price >= 0.01m && price <= 1000.00m)
        {
            return ValidationResult.Success;
        }
        return new ValidationResult("Price must be between 0.01 and 1000.00");
    }
}

        
    


FluentValidation Validator:
        
            
public class ProductValidator : AbstractValidator<Product>
{
    public ProductValidator()
    {
        RuleFor(p => p.Name)
            .NotEmpty().WithMessage("Name is required")
            .MaximumLength(100).WithMessage("Name can't be longer than 100 characters");

        RuleFor(p => p.Price)
            .InclusiveBetween(0.01m, 1000.00m).WithMessage("Price must be between 0.01 and 1000.00");
    }
}

        
    


Configure Services in Program.cs:
        
            
var builder = WebApplication.CreateBuilder(args);

// Add services to the container
builder.Services.AddControllers()
    .AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<ProductValidator>());

builder.Services.Configure<ApiBehaviorOptions>(options =>
{
    options.InvalidModelStateResponseFactory = context =>
    {
        var errors = context.ModelState
            .Where(e => e.Value.Errors.Count > 0)
            .Select(e => new
            {
                Name = e.Key,
                Message = e.Value.Errors.First().ErrorMessage
            }).ToArray();

        return new BadRequestObjectResult(errors);
    };
});

var app = builder.Build();

app.MapControllers();
app.Run();

        
    


Controller:
        
            
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpPost]
    public IActionResult CreateProduct(Product product)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        // Save the product to the database (not shown)

        return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
    }

    [HttpGet("{id}")]
    public IActionResult GetProduct(int id)
    {
        // Retrieve the product from the database (not shown)

        var product = new Product { Id = id, Name = "Sample Product", Price = 9.99m };
        return Ok(product);
    }
}

        
    

8. Conclusion

Validation is a crucial aspect of building secure and reliable web APIs. By leveraging data annotations, custom validation attributes, and FluentValidation, you can ensure that your ASP.NET Core 8 Web API receives and processes valid data. Following best practices such as centralizing validation logic, providing meaningful error messages, and handling validation errors gracefully will lead to a robust and maintainable application. This comprehensive guide equips you with the knowledge to implement effective validation in your ASP.NET Core 8 Web API projects.