If your API were a club, model validation would be the bouncer. It checks IDs, keeps out the troublemakers, and sometimes holds up the line. With .NET 10, that bouncer swapped the clipboard for a barcode scanner. Same rules, far less delay.

In this post, we will unpack what changed in ASP.NET Core model validation, why it matters to p95 latency, and how to verify the gains with small, focused benchmarks. Spoiler: you get the speed-up by upgrading, not by rewriting your controllers.

What actually got faster

Here is the high-level story behind the performance win in .NET 10:

  • Validation metadata is precomputed at build time, replacing reflection-based discovery during requests.
  • Execution uses direct, typed calls rather than virtual, object-based invocation.
  • Hot paths dropped LINQ and reduce allocations with spans and pooled buffers where possible.
  • Boxing of value types during validation is avoided.
  • Error materialization is lazier, which means fewer strings and less GC churn when many errors occur.

The practical result is better average latency, tighter standard deviation, and a noticeable lift at p95 and p99 when inputs are invalid or mixed.

A tiny API that benefits immediately

Let’s build a small controller that validates telemetry coming from a droid on Tatooine. Nothing clever here. The point is that the attributes are the same, but the runtime gets faster in .NET 10.

First, a DTO with classic DataAnnotations:

using System.ComponentModel.DataAnnotations;
public sealed class CreateDroidRequest
{
[Required]
public string DroidId { get; set; } = string.Empty;
[Range(-40, 85)]
public double TemperatureC { get; set; }
[Range(0, 100)]
public int BatteryLevel { get; set; }
}

Now a minimal controller that relies on automatic validation through the ApiController attribute:

using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("droids")]
public class DroidsController : ControllerBase
{
[HttpPost]
public IActionResult Create(CreateDroidRequest request) => Ok(new { ok = true });
}

And the smallest possible Program.cs to wire it up:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
app.Run();

If the request body violates the attributes, ASP.NET Core returns a 400 with a ProblemDetails payload. That behavior is unchanged. The difference in .NET 10 is how quickly the framework decides that the request is invalid.

Show me the numbers with a micro-benchmark

Benchmarks should be honest and repeatable. The easiest way to see the delta is to run two identical APIs, one on .NET 8 and one on .NET 10, then send the same invalid payload to both.

Below is a small BenchmarkDotNet harness that posts an invalid JSON payload to two different base addresses. Keep the samples small so that the framework work shows up clearly.

using System.Net.Http;
using System.Text;
using BenchmarkDotNet.Attributes;
[MemoryDiagnoser]
public class DroidValidationBench
{
private readonly HttpClient _v8 = new() { BaseAddress = new("http://localhost:5100") };
private readonly HttpClient _v10 = new() { BaseAddress = new("http://localhost:5200") };
private readonly StringContent _bad = new("{\"droidId\":\"\",\"temperatureC\":120,\"batteryLevel\":150}", Encoding.UTF8, "application/json");
[Benchmark]
public Task Net8_Invalid() => _v8.PostAsync("/droids", _bad);
[Benchmark]
public Task Net10_Invalid() => _v10.PostAsync("/droids", _bad);
}

And the tiny Program for the benchmark project:

using BenchmarkDotNet.Running;
BenchmarkRunner.Run<DroidValidationBench>();

Run your two APIs on different ports, then execute the benchmark in Release. Expect .NET 10 to tighten the error bars and reduce average time for invalid inputs. On busy services where invalid requests are common, this shows up as a smoother p95.

Write custom validation attributes that play nice with the hot path

You still can write custom attributes, and they now execute inside a faster pipeline. Keep them lightweight and allocation conscious. Here is a playful example that allows only certain lightsaber colors.

using System.ComponentModel.DataAnnotations;
public sealed class LightsaberColorAttribute : ValidationAttribute
{
private static readonly HashSet<string> Allowed = new(StringComparer.OrdinalIgnoreCase)
{ "blue", "green", "purple" };
protected override ValidationResult? IsValid(object? value, ValidationContext ctx)
=> value is string s && Allowed.Contains(s)
? ValidationResult.Success
: new ValidationResult($"{ctx.MemberName} is not an approved color");
}

Use it like any other attribute on your model:

public sealed class JediSignup
{
[Required]
[LightsaberColor]
public string SaberColor { get; set; } = "";
}

Tips for custom validators that behave well under load:

  • Avoid allocations inside IsValid. Cache lookups in static fields where possible.
  • Prefer ordinal comparisons for strings.
  • Keep error messages simple, then let your API layer format ProblemDetails.

Make parsing fast too

Validation is only part of the hot path. If your payloads are large, System.Text.Json source generation can shave off reflection during deserialization.

Define a context:

using System.Text.Json.Serialization;
[JsonSerializable(typeof(CreateDroidRequest))]
public partial class DroidJsonContext : JsonSerializerContext {}

Then use it when you need manual serialization or deserialization:

using System.Text.Json;
var dto = JsonSerializer.Deserialize(json, DroidJsonContext.Default.CreateDroidRequest);

Combine faster parsing with faster validation to remove headwind from every request.

Practical guardrails for teams

  • Stick to attributes first. The engine is tuned for them in .NET 10.
  • Keep DTOs small and sealed. Simple models are easier for the runtime to validate quickly.
  • Normalize error shapes with ProblemDetails so clients can depend on consistent responses.
  • Observe p95 and p99 in production. Faster validation cuts tail latency where invalid traffic is common.

FAQ for the curious

  • Do I need to change my attributes to get the speed-up?
    • No. Upgrade your app to target .NET 10 and the framework does the heavy lifting.
  • Does this help minimal APIs?
    • Yes, where you opt into DataAnnotations validation. Controllers with ApiController get it automatically.
  • What about FluentValidation or other libraries?
    • They can still be used. The gains described here are specific to the built-in pipeline.

Wrap up

Validation did not become optional. It just became cheaper. By moving work out of the request path and into build time, .NET 10 gives you lower latency and fewer allocations without changing your controller code. Pair it with fast JSON parsing and thoughtful custom validators, and your bouncer will keep the line moving even on Friday night.