When you build .NET applications with strongly typed configuration, IOptions<T> and its variants give you a clean way to bind appsettings.json sections to C# classes. But binding isn't the same as validating - a missing required value or an out-of-range number will happily bind to a default and silently break your app at runtime. IValidateOptions<T> is the hook .NET provides to fix that.
The problem: silent misconfiguration
Consider a typical options class:
If Host is missing from appsettings.json, your app starts fine. The failure surfaces only when the first email is sent — in production, at 2 AM. Data Annotations ([Required], [Range]) combined with ValidateDataAnnotations() help, but they fall short when you need:
- Cross-property validation (e.g.,
Portmust be 465 whenUseSslistrue) - Async or database-backed checks
- Conditional logic depending on environment
- Reusable validators shared across multiple options types
This is where IValidateOptions<T> comes in.
What is IValidateOptions<T>?
IValidateOptions<T> is an interface in Microsoft.Extensions.Options with a single method:
You implement this interface in a dedicated class, register it in the DI container, and the Options infrastructure calls it automatically — either lazily (on first access) or eagerly at startup when combined with ValidateOnStart().
Basic example
Here is a minimal validator for the SMTP options class above:
Register it alongside the options binding in Program.cs:
If validation fails, OptionsValidationException is thrown during app.Run(), so your service never starts with bad config — a much better failure mode than a runtime NullReferenceException deep in your domain logic.
Accessing other services inside a validator
Because the validator is a regular DI-registered class, you can inject dependencies. This is one of the key advantages over Data Annotations, which have no access to the container.
Cross-property validation
This is where IValidateOptions<T> really pulls ahead. Data Annotations cannot express constraints between properties.
Named options
IValidateOptions<T> supports named options via the name parameter. This matters when you register multiple instances of the same options type — for example, two different upstream API clients.
Registration with named options:
Combining with OptionsBuilder<T>
The OptionsBuilder<T> API (returned by AddOptions<T>()) has a .Validate() overload that accepts a delegate. This is fine for simple cases. For anything non-trivial, the dedicated IValidateOptions<T> class is preferable — it keeps validation logic testable and out of Program.cs.
You can stack both on the same options type — the framework runs all registered validators and aggregates the failures.
Summary
IValidateOptions<T> is the right tool when your configuration validation requirements outgrow what Data Annotations can express. The main takeaways:
- Implement
IValidateOptions<T>in a dedicated class and register it as a singleton. - Use
ValidateOnStart()to catch misconfiguration at startup rather than at first use. - Inject services freely — environment, logging, or even a config service — because the validator is a first-class DI citizen.
- Express cross-property constraints and conditional logic without fighting the attribute model.
- Write focused unit tests against the validator class in total isolation from the host.
For most production .NET applications, reaching for IValidateOptions<T> over inline .Validate() delegates pays dividends the first time a broken config would have slipped into a staging or production deploy.