Skip to content

Validation

Foundatio.Repositories provides a validation system for ensuring document integrity before persistence. This guide covers implementing validation, handling exceptions, and validation patterns.

Document Validation

ValidateAndThrowAsync

Override ValidateAndThrowAsync in your repository to implement custom validation:

csharp
public class EmployeeRepository : ElasticRepositoryBase<Employee>
{
    public EmployeeRepository(EmployeeIndex index) : base(index) { }

    protected override Task ValidateAndThrowAsync(Employee document)
    {
        if (string.IsNullOrEmpty(document.Name))
            throw new DocumentValidationException("Name is required");
        
        if (string.IsNullOrEmpty(document.Email))
            throw new DocumentValidationException("Email is required");
        
        if (!IsValidEmail(document.Email))
            throw new DocumentValidationException("Email format is invalid");
        
        if (document.Age < 0 || document.Age > 150)
            throw new DocumentValidationException("Age must be between 0 and 150");
        
        return Task.CompletedTask;
    }

    private bool IsValidEmail(string email)
    {
        return email.Contains('@') && email.Contains('.');
    }
}

When Validation Runs

Validation is called automatically during:

  • AddAsync - Before adding new documents
  • SaveAsync - Before saving/updating documents

Validation is not called during:

  • PatchAsync / PatchAllAsync - Partial updates bypass validation
  • RemoveAsync / RemoveAllAsync - Deletions don't require validation

Async Validation

For validation that requires async operations (e.g., checking uniqueness):

csharp
protected override async Task ValidateAndThrowAsync(Employee document)
{
    // Basic validation
    if (string.IsNullOrEmpty(document.Email))
        throw new DocumentValidationException("Email is required");
    
    // Check for duplicate email
    var existing = await FindOneAsync(q => q
        .FieldEquals(e => e.Email, document.Email)
        .ExcludedId(document.Id));
    
    if (existing != null)
        throw new DuplicateDocumentException($"Email {document.Email} is already in use");
}

Exception Hierarchy

Foundatio.Repositories provides a hierarchy of exceptions for different error scenarios:

DocumentValidationException

Thrown when document validation fails:

csharp
public class DocumentValidationException : DocumentException
{
    public DocumentValidationException() { }
    public DocumentValidationException(string message) : base(message) { }
}

Usage:

csharp
throw new DocumentValidationException("Name is required");
throw new DocumentValidationException($"Age {age} is out of valid range");

DocumentNotFoundException

Thrown when a document is not found:

csharp
public class DocumentNotFoundException : DocumentException
{
    public string Id { get; }
    
    public DocumentNotFoundException(string id) : base($"Document \"{id}\" could not be found")
    {
        Id = id;
    }
}

When thrown:

  • PatchAsync when document doesn't exist
  • SaveAsync when updating a non-existent document (in some cases)
  • RemoveAsync when deleting a non-existent document

DuplicateDocumentException

Thrown when attempting to create a duplicate document:

csharp
public class DuplicateDocumentException : DocumentException
{
    public DuplicateDocumentException(string message) : base(message) { }
}

Usage:

csharp
throw new DuplicateDocumentException($"Document with email {email} already exists");

VersionConflictDocumentException

Thrown when optimistic concurrency check fails:

csharp
public class VersionConflictDocumentException : DocumentException
{
    public VersionConflictDocumentException() { }
    public VersionConflictDocumentException(string message) : base(message) { }
    public VersionConflictDocumentException(string message, Exception inner) : base(message, inner) { }
}

When thrown:

  • SaveAsync when the document version doesn't match (if IVersioned)

Skipping Validation

Per-Operation

Skip validation for specific operations:

csharp
// Skip validation for trusted data
await repository.AddAsync(trustedEntity, o => o.SkipValidation());

// Explicitly control validation
await repository.SaveAsync(entity, o => o.Validation(false));

Use Cases for Skipping Validation

  • Bulk imports - Trusted data from verified sources
  • System operations - Internal updates that bypass business rules
  • Migrations - Data transformations during schema changes

WARNING

Only skip validation when you're certain the data is valid. Invalid data can cause issues downstream.

Validation Patterns

Fluent Validation Integration

Integrate with FluentValidation for complex validation rules:

csharp
public class EmployeeValidator : AbstractValidator<Employee>
{
    public EmployeeValidator()
    {
        RuleFor(e => e.Name)
            .NotEmpty().WithMessage("Name is required")
            .MaximumLength(100).WithMessage("Name cannot exceed 100 characters");
        
        RuleFor(e => e.Email)
            .NotEmpty().WithMessage("Email is required")
            .EmailAddress().WithMessage("Invalid email format");
        
        RuleFor(e => e.Age)
            .InclusiveBetween(18, 100).WithMessage("Age must be between 18 and 100");
    }
}

public class EmployeeRepository : ElasticRepositoryBase<Employee>
{
    private readonly IValidator<Employee> _validator;

    public EmployeeRepository(EmployeeIndex index, IValidator<Employee> validator) 
        : base(index)
    {
        _validator = validator;
    }

    protected override async Task ValidateAndThrowAsync(Employee document)
    {
        var result = await _validator.ValidateAsync(document);
        if (!result.IsValid)
        {
            var errors = string.Join("; ", result.Errors.Select(e => e.ErrorMessage));
            throw new DocumentValidationException(errors);
        }
    }
}

Validation in Event Handlers

Validate in event handlers for cross-cutting concerns:

csharp
public class EmployeeRepository : ElasticRepositoryBase<Employee>
{
    public EmployeeRepository(EmployeeIndex index) : base(index)
    {
        DocumentsAdding.AddHandler(ValidateNewEmployee);
        DocumentsSaving.AddHandler(ValidateEmployeeUpdate);
    }

    private Task ValidateNewEmployee(object sender, DocumentsEventArgs<Employee> args)
    {
        foreach (var employee in args.Documents)
        {
            // Validate new employee specific rules
            if (employee.StartDate < DateTime.UtcNow.Date)
                throw new DocumentValidationException("Start date cannot be in the past");
        }
        return Task.CompletedTask;
    }

    private Task ValidateEmployeeUpdate(object sender, ModifiedDocumentsEventArgs<Employee> args)
    {
        foreach (var modified in args.Documents)
        {
            var original = modified.Original;
            var current = modified.Value;
            
            // Prevent certain changes
            if (original != null && original.EmployeeId != current.EmployeeId)
                throw new DocumentValidationException("Employee ID cannot be changed");
        }
        return Task.CompletedTask;
    }
}

Conditional Validation

Apply different validation rules based on context:

csharp
protected override Task ValidateAndThrowAsync(Employee document)
{
    // Always validate required fields
    if (string.IsNullOrEmpty(document.Name))
        throw new DocumentValidationException("Name is required");
    
    // Additional validation for active employees
    if (document.Status == EmployeeStatus.Active)
    {
        if (string.IsNullOrEmpty(document.Department))
            throw new DocumentValidationException("Active employees must have a department");
        
        if (document.ManagerId == null)
            throw new DocumentValidationException("Active employees must have a manager");
    }
    
    return Task.CompletedTask;
}

Error Handling

Catching Validation Errors

csharp
try
{
    await repository.AddAsync(employee);
}
catch (DocumentValidationException ex)
{
    _logger.LogWarning("Validation failed: {Message}", ex.Message);
    return BadRequest(ex.Message);
}
catch (DuplicateDocumentException ex)
{
    _logger.LogWarning("Duplicate document: {Message}", ex.Message);
    return Conflict(ex.Message);
}

Comprehensive Error Handling

csharp
try
{
    await repository.SaveAsync(employee);
}
catch (DocumentValidationException ex)
{
    // Validation failed
    return BadRequest(new { error = "Validation failed", message = ex.Message });
}
catch (DocumentNotFoundException ex)
{
    // Document doesn't exist
    return NotFound(new { error = "Not found", id = ex.Id });
}
catch (VersionConflictDocumentException ex)
{
    // Concurrent modification
    return Conflict(new { error = "Version conflict", message = ex.Message });
}
catch (DuplicateDocumentException ex)
{
    // Duplicate document
    return Conflict(new { error = "Duplicate", message = ex.Message });
}
catch (RepositoryException ex)
{
    // Other repository errors
    _logger.LogError(ex, "Repository error");
    return StatusCode(500, new { error = "Internal error" });
}

Validation Result Pattern

For APIs that need to return validation errors without exceptions:

csharp
public class ValidationResult
{
    public bool IsValid { get; set; }
    public List<string> Errors { get; set; } = new();
}

public class EmployeeService
{
    private readonly IEmployeeRepository _repository;

    public async Task<(Employee Employee, ValidationResult Validation)> CreateEmployeeAsync(
        Employee employee)
    {
        var validation = ValidateEmployee(employee);
        if (!validation.IsValid)
            return (null, validation);
        
        try
        {
            var result = await _repository.AddAsync(employee);
            return (result, validation);
        }
        catch (DocumentValidationException ex)
        {
            validation.IsValid = false;
            validation.Errors.Add(ex.Message);
            return (null, validation);
        }
    }

    private ValidationResult ValidateEmployee(Employee employee)
    {
        var result = new ValidationResult { IsValid = true };
        
        if (string.IsNullOrEmpty(employee.Name))
            result.Errors.Add("Name is required");
        
        if (string.IsNullOrEmpty(employee.Email))
            result.Errors.Add("Email is required");
        
        result.IsValid = result.Errors.Count == 0;
        return result;
    }
}

Next Steps

Released under the Apache 2.0 License.