Table of Contents

Introduction

How to validate parameters of entity methods:

using System;
using System.Collections.Generic;
using System.Text;
using Admonish;

namespace Domain
{
    public class Entity
    {
        public Entity(int age, string? name)
        {
            Validator
                .Create()
                .Min(nameof(age), age, 0)
                .NonNullOrWhiteSpace(nameof(name), name)
                .Check(
                    !(name == "Dracula" && age < 100),
                    "Cannot create such a young vampire.")
                .ThrowIfInvalid();

            Age = age;
            Name = name;
        }

        public string Name { get; }
        public int Age { get; }
    }
}

If you need additional validation in an app service, for example, you need to check if such an entity already exists in the database, you do it like this:

public class AppService
{
    private static Dictionary<string, Entity> _db =
        new Dictionary<string, Entity>();

    internal void AddEntity(CreateEntityDto dto)
    {
        Validator
            .Create()
            .Check(
                nameof(dto.Name),
                !_db.ContainsKey(dto.Name ?? ""),
                "An entity with this name already exists.")
            .ThrowIfInvalid();

        var e = new Entity(dto.Age, dto.Name);
        _db.Add(e.Name, e);
    }

    internal int GetCount(int minAge)
    {
        return _db.Values.Where(x => x.Age >= minAge).Count();
    }
}

Imagine you have a custom validation exception (defined e.g. in your WebApiUtils library) which you handle with a special middleware. You can configure Admonish to throw the needed excepton type, so that validation errors from domain and application modules are still handled by your middleware without making any changes in it:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
        services.AddTransient<AppService>();

        // Throw a custom exception on validation
        // errors that is handled by ErrorHandlerMiddleware.
        Admonish.Validator.UnsafeConfigureException(
            r => new CustomValidationException(r.ToDictionary()));
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseHsts();
        }

        // Convert the custom validation exception to a 400 response.
        app.UseMiddleware<ErrorHandlerMiddleware>();

        app.UseHttpsRedirection();
        app.UseRouting();

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

        app.UseEndpoints(endpoints => {
            endpoints.MapControllers();
        });
    }
}

The middleware code could look like this.

public class ErrorHandlerMiddleware
{
    private readonly RequestDelegate _next;
    private static readonly RouteData EmptyRouteData = new RouteData();
    private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor();

    public ErrorHandlerMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (CustomValidationException ve)
        {
            var details = new ValidationProblemDetails(ve.Errors)
            {
                Type = "urn:acme-corp:validation-error"
            };
            await WriteError(context, details);
        }
    }

    private Task WriteError(HttpContext context, object error)
    {
        RouteData routeData = context.GetRouteData() ?? EmptyRouteData;
        var actionContext = new ActionContext(context, routeData, EmptyActionDescriptor);
        var result = new ObjectResult(error)
        {
            StatusCode = StatusCodes.Status400BadRequest
        };
        return result.ExecuteResultAsync(actionContext);
    }
}