Validation made simple!
Pascal Vorwerk

Pascal Vorwerk @pascal_vorwerk

About: Junior .NET Developer. Eager to learn everything the computer world has to offer!

Joined:
May 15, 2025

Validation made simple!

Publish Date: May 15
0 0

Validation in ASP.NET Core for Minimal API's (and a look at .NET 10)


Validation is probably the most important step when retrieving input values in your API! In the past, I have often used libraries like FluentValidation to perform validation in my API's. However, now that this library is going to become commercial, I went to investigate how to perform the same operations this library enables through the standard ways.


None of this is rocket science or not described by the original documentation of Asp.Net Core. This blog is simply to provide a quick start to setup validation!


Who would have guessed.. weatherforecasts!

First, let's start by defining our model we wan't to create an API around! Oh look at that, what a suprise.. the good old Weatherforecast..

public record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
Enter fullscreen mode Exit fullscreen mode

Great.. Let's try to simulate some kind of database setting for demo purposes.

public static class WeatherData
{
    private static List<WeatherForecast> _weatherForecasts = new();
    private static readonly string[] Summaries =
    [
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    ];

    public static void Initialize()
    {

        _weatherForecasts = Enumerable.Range(1, 5).Select(index =>
                new WeatherForecast
                (
                    DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
                    Random.Shared.Next(-20, 55),
                    Summaries[Random.Shared.Next(Summaries.Length)]
                ))
            .ToList();
    }

    public static List<WeatherForecast> GetWeatherForecasts()
    {
        return _weatherForecasts;
    }

    public static void AddWeatherForecast(WeatherForecast weatherForecast)
    {
        _weatherForecasts.Add(weatherForecast);
    }

    public static void UpdateWeatherForecast(WeatherForecast weatherForecast)
    {
        var index = _weatherForecasts.FindIndex(w => w.Date == weatherForecast.Date);
        if (index != -1)
        {
            _weatherForecasts[index] = weatherForecast;
        }
    }

    public static void DeleteWeatherForecast(DateOnly date)
    {
        var index = _weatherForecasts.FindIndex(w => w.Date == date);
        if (index != -1)
        {
            _weatherForecasts.RemoveAt(index);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Endpoints

Great! Now that that is place, we will define a few endpoints following the REST design principles:

var group = endpoints.MapGroup("/api/weatherforecasts")
            .WithTags("WeatherForecast");

group.MapGet("/", GetAllWeatherForeCasts); // Get all weatherforecasts
group.MapPost("/", CreateWeatherForecast); // Create a new weatherforecast
group.MapPut("/date:datetime", UpdateWeatherForecast); // Update a weatherforecast
group.MapDelete("/date:datetime", DeleteWeatherForecast); // Delete a weatherforecast
Enter fullscreen mode Exit fullscreen mode

Now.. The validation

Usually, most of the time, in a REST design only a few endpoints need validation from user input. The POST and PUT endpoints both receive some kind of user input to create or replace the weather endpoints.

Let's define the models the users can input for each request.

public class CreateWeatherForecastRequest
{
    public DateTime Date { get; set; }

    public int TemperatureC { get; set; }

    public string Summary { get; set; } = string.Empty;
}
Enter fullscreen mode Exit fullscreen mode
public class UpdateWeatherForecastRequest
{
    public int TemperatureC { get; set; }

    public string Summary { get; set; } = string.Empty;
}
Enter fullscreen mode Exit fullscreen mode

Data annotations

The first thing we can do to start validating these models is adding some data annotations. These annotations can be used for basic and simple requirements on the data you receive.

Let's apply a few notations to our models:

public class CreateWeatherForecastRequest
{
    [Required]
    public DateTime Date { get; set; }

    [Required]
    [Range(-20, 55)]
    public int TemperatureC { get; set; }

    [Required]
    [StringLength(50)]
    public string Summary { get; set; } = string.Empty;
}
Enter fullscreen mode Exit fullscreen mode
public class UpdateWeatherForecastRequest
{
    [Required]
    [Range(-20, 55)]
    public int TemperatureC { get; set; }

    [Required]
    [StringLength(50)]
    public string Summary { get; set; } = string.Empty;
}
Enter fullscreen mode Exit fullscreen mode

IValidatableObject

Great! However, sometimes we need to handle some more complex rules. For example, can we really classify a -20 degree temperature as a 'Hot' summary? How do validate these types of business rules? For this, we can implement the IValidateableObject interface!

public class CreateWeatherForecastRequest : IValidatableObject
{
    [Required]
    public DateTime Date { get; set; }

    [Required]
    [Range(-20, 55)]
    public int TemperatureC { get; set; }

    [Required]
    [StringLength(50)]
    public string Summary { get; set; } = string.Empty;

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        // Here you can add custom validation logic if needed

        // Example: A -20 temperature cannot have a "Hot" summary..
        if (TemperatureC == -20 && Summary.Equals("Hot", StringComparison.OrdinalIgnoreCase))
        {
            yield return new ValidationResult(
                "A temperature of -20 cannot have a 'Hot' summary.",
                [nameof(Summary)]);
        }

        // Or.. the date must be in the future
        if (Date <= DateTime.Now)
        {
            yield return new ValidationResult(
                "The date must be in the future.",
                [nameof(Date)]);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Here you can see some additional business rules we came up with for creating a new weather forecast!

Now.. how do we start using these attributes and this new method that has been implemented?


ValidationFilter

For controller API's, you don't need to do anything else then register the model your endpoint. The pipeline will automatically perform the validation on both the data annotations and call the Validate method. However, in .NET 9, minimal API's need to manually setup these steps. In .NET 10, this gets simplified (see).

Because of this, I have made a filter one can add to the endpoints you want to validate.

public class ValidationFilter<T> : IEndpointFilter where T : class
{
    public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next)
    {
        var argument = context.Arguments.OfType<T>().FirstOrDefault();

        if (argument == null)
        {
            return Results.BadRequest("Invalid request payload");
        }

        var validationContext = new ValidationContext(argument);
        var validationResults = new List<ValidationResult>();

        // This call validates all properties of the object, or only the required ones if 'ValidateAllProperties' is set to false
        Validator.TryValidateObject(argument, validationContext, validationResults, true);

        // If there are no validation results, we can continue in the pipeline
        if (validationResults.Count == 0) return await next(context);

        var errorDict = new Dictionary<string, List<string>>();

        foreach (var result in validationResults)
        {
            var memberNames = result.MemberNames.Any() ? result.MemberNames : [string.Empty];

            foreach (var memberName in memberNames)
            {
                if (!errorDict.ContainsKey(memberName)) errorDict[memberName] = [];

                errorDict[memberName].Add(result.ErrorMessage ?? "Validation error");
            }
        }

        var finalErrors = errorDict.ToDictionary(
            kvp => kvp.Key,
            kvp => kvp.Value.ToArray()
        );

        return Results.ValidationProblem(finalErrors);
    }
}
Enter fullscreen mode Exit fullscreen mode

Keep in mind that this is only needed for now, this is not needed anymore once we can add validation to our minimal API's in .NET 10!

If you don't want to use a filter, you can simply validate the object at the start of your endpoint method:

       // Update an existing weather forecast
        group.MapPut("/{date:datetime}", ([FromRoute] DateTime date, [FromBody] UpdateWeatherForecastRequest request) =>
        {
            // Because no validation filter was added to the route, it is not validated. We can however still manually validate the object..
            var validationContext = new ValidationContext(request);
            var validationResults = new List<ValidationResult>();
            var isValid = Validator.TryValidateObject(request, validationContext, validationResults, true);

            if (!isValid)
            {
                return Results.ValidationProblem(
                    validationResults.ToDictionary<ValidationResult, string, string[]>(v => v.MemberNames.First(), v => [v.ErrorMessage ?? "Not provided"]));
            }


            var weatherForecast = new WeatherForecast(new DateOnly(date.Year, date.Month, date.Day), request.TemperatureC, request.Summary);
            WeatherData.UpdateWeatherForecast(weatherForecast);
            return Results.NoContent();
        });
Enter fullscreen mode Exit fullscreen mode

Result

Let's test our custom rule about having a -20 temperature with a 'Hot' summary!

Image description

As we want, this returns a validation details response, telling the consumer about our custom business rule.

Image description


Summary

In this blog, we took a quick look at validation for minimal API's in .NET 9, whilst keeping in mind that some things become easier once .NET 10 comes out!

Some other topics I am thinking to elaborate on:

  • Using this validation in Blazor forms to perform client side validation.
  • Using custom attributes in validation.
  • Perform database checking as a validation step in Blazor. For example for unique email checks!

Let me know if you would like me to write a blog for these subjects! Hopefully this blog was helpful for anyone not yet known with validation in ASP.NET Core

All of this code is made available here.

Comments 0 total

    Add comment