Skip to main content

Compile-Time options validation with the OptionsValidator source generator

In the previous post, we looked at how to implement IValidateOptions<T> by hand — writing a dedicated validator class, injecting services, and expressing cross-property constraints that Data Annotations can't handle. That approach gives you full control and is the right tool when validation logic is genuinely complex.

While researching that post I discovered another feature that's worth knowing about: when your validation can be expressed with Data Annotation attributes, the options validation source generator (available since .NET 8) will write the IValidateOptions<T> implementation for you at compile time. You get the safety of startup validation without the boilerplate, and as a bonus the generated code is reflection-free and AOT-compatible.

The problem with runtime data annotations

Before the source generator existed, the standard way to add annotation-based validation was ValidateDataAnnotations():

This works, but it uses reflection at runtime to walk your options class, discover attributes, and invoke validators. That has two consequences:

  1. Performance cost — attribute discovery and validation are reflection-heavy, which matters in hot paths or high-throughput startup scenarios.
  2. AOT incompatibility — Native AOT and aggressive trimming (<PublishTrimmed>true</PublishTrimmed>) can eliminate the metadata that reflection depends on, causing IL2026 / IL3050 warnings or outright runtime failures.

The source generator eliminates both issues by emitting strongly typed validation code at build time.


How it works

The generator is part of Microsoft.Extensions.Options and is enabled automatically when you reference version 8 or later (which includes any modern ASP.NET Core project). No additional NuGet package or project property is required — unless you're also using the configuration binding generator for AOT, in which case you should also add:

<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>

to your .csproj.

To activate the generator for a specific options class, you need exactly two things:

  1. An options class decorated with Data Annotation attributes.
  2. A partial class carrying [OptionsValidator] that implements IValidateOptions<T> — with an empty body.

The generator fills in the body.

A minimal example

Start with an options class. Nothing special here — standard annotations, the same ones you'd use with ValidateDataAnnotations():

Now add the partial validator class — this is all the code you write:

Register it in Program.cs:

That's it. The source generator emits the full Validate implementation into a file called Validators.g.cs in your project's intermediate output directory.

Note: You do not call .ValidateDataAnnotations() here. The generated validator replaces it entirely.

What the generator actually emits

It's worth examining the generated code to understand what you're getting. For the SmtpOptions class above, the generator produces something close to:

A few things to observe:

Attribute instances are static singletons. The generator creates a file-scoped static class (__Attributes) that holds one pre-allocated instance of each attribute. Instead of instantiating new RequiredAttribute() on every validation call, the same instance is reused. This is a meaningful allocation improvement for options that are validated frequently.

RangeAttribute is replaced. Because the BCL's RangeAttribute calls Convert.ChangeType via reflection internally, the generator substitutes it with its own __SourceGen__RangeAttribute, which performs the same bounds check without reflection. The same substitution applies to MaxLengthAttribute, MinLengthAttribute, and LengthAttribute. From your code's perspective, nothing changes — the attributes on your class are untouched; the replacement only happens in the generated validator.

No reflection at validation time. Each property is validated by calling Validator.TryValidateValue with an explicit list of attribute instances, rather than using Validator.TryValidateObject (which discovers attributes via reflection). The generator knows your properties at compile time and bakes that knowledge directly into the emitted code.

Nested options validation

The generator supports transitive validation through the [ValidateObjectMembers] attribute. If your options class contains a nested object, you can instruct the generator to recurse into it:

The generator will inline the validation logic for SmtpOptions and DatabaseOptions directly into ValidateAppOptions.Validate, without requiring separate registrations for the nested types. This is particularly useful if you have a single root options class that aggregates several sub-sections.

If the nested type itself has a separately registered validator, you can alternatively use [ValidateEnumeratedItems] for collection properties to validate each element.

AOT and trimming compatibility

If you're publishing with Native AOT (<PublishAot>true</PublishAot>) or aggressive trimming, the reflection-based ValidateDataAnnotations() path will generate trim warnings and may silently skip validation at runtime. The source generator removes this risk entirely.

To get full AOT coverage for the configuration binding side as well (i.e., IConfiguration.Bind()), also enable the configuration binding source generator:

<PropertyGroup>
    <PublishAot>true</PublishAot>
    <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
</PropertyGroup>

With both generators active, the entire options pipeline — binding and validation — is reflection-free.

Summary

The [OptionsValidator] source generator is a low-effort way to eliminate validation boilerplate when Data Annotation attributes are sufficient. You write an empty partial class, add one attribute, and the generator emits a fully optimized, AOT-compatible IValidateOptions<T> implementation at build time. The generated code pre-allocates attribute instances, avoids reflection at validation time, and replaces reflection-heavy BCL attributes with custom equivalents where necessary.

The key points to take away:

  • Available automatically with Microsoft.Extensions.Options ≥ 8; no extra package needed.
  • Decorate a partial class with [OptionsValidator] — the body is generated for you.
  • Do not call .ValidateDataAnnotations(); the generated validator replaces it.
  • Use [ValidateObjectMembers] to recurse into nested options types.
  • For AOT builds, pair it with <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>.
  • For complex cross-property or service-dependent validation, combine it with a hand-written IValidateOptions<T>.

Popular posts from this blog

Kubernetes–Limit your environmental impact

Reducing the carbon footprint and CO2 emission of our (cloud) workloads, is a responsibility of all of us. If you are running a Kubernetes cluster, have a look at Kube-Green . kube-green is a simple Kubernetes operator that automatically shuts down (some of) your pods when you don't need them. A single pod produces about 11 Kg CO2eq per year( here the calculation). Reason enough to give it a try! Installing kube-green in your cluster The easiest way to install the operator in your cluster is through kubectl. We first need to install a cert-manager: kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.5/cert-manager.yaml Remark: Wait a minute before you continue as it can take some time before the cert-manager is up & running inside your cluster. Now we can install the kube-green operator: kubectl apply -f https://github.com/kube-green/kube-green/releases/latest/download/kube-green.yaml Now in the namespace where we want t...

Azure DevOps/ GitHub emoji

I’m really bad at remembering emoji’s. So here is cheat sheet with all emoji’s that can be used in tools that support the github emoji markdown markup: All credits go to rcaviers who created this list.

Podman– Command execution failed with exit code 125

After updating WSL on one of the developer machines, Podman failed to work. When we took a look through Podman Desktop, we noticed that Podman had stopped running and returned the following error message: Error: Command execution failed with exit code 125 Here are the steps we tried to fix the issue: We started by running podman info to get some extra details on what could be wrong: >podman info OS: windows/amd64 provider: wsl version: 5.3.1 Cannot connect to Podman. Please verify your connection to the Linux system using `podman system connection list`, or try `podman machine init` and `podman machine start` to manage a new Linux VM Error: unable to connect to Podman socket: failed to connect: dial tcp 127.0.0.1:2655: connectex: No connection could be made because the target machine actively refused it. That makes sense as the podman VM was not running. Let’s check the VM: >podman machine list NAME         ...