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:
- Performance cost — attribute discovery and validation are reflection-heavy, which matters in hot paths or high-throughput startup scenarios.
- AOT incompatibility — Native AOT and aggressive trimming (
<PublishTrimmed>true</PublishTrimmed>) can eliminate the metadata that reflection depends on, causingIL2026/IL3050warnings 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:
- An options class decorated with Data Annotation attributes.
- A partial class carrying
[OptionsValidator]that implementsIValidateOptions<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
partialclass 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>.