In this article, we’ll look at how to use source generators to validate IOptions and ensure they meet the required configuration expectations.
Let’s dive in!
Why We Need to Validate IOptions
In ASP.NET Core we have ample choice when it comes to configuration providers. This gives us freedom when choosing a way to set our configuration data. But what happens if the configuration data isn’t properly set and doesn’t meet our requirements? This can lead to security problems, runtime failures, or unexpected behavior.
When we validate the configuration data in our applications, we prevent misconfigurations. Moreover, by adding validation, we enforce constraints and ensure our applications operate as expected.
In this article, we’ll opt for the options pattern:
public class NotificationOptions { [Required] public string Sender { get; init; } [Required] public bool EnableSms { get; init; } [Required] public bool EnableEmail { get; init; } [Required] [Range(1, 10)] public int MaxNumberOfRetries { get; init; } }
We create the NotificationOptions
class and add several properties that we need for the proper handling of notifications. We also utilize attributes to decorate the properties, specifying different conditions they must adhere to.
Next, we add our settings:
"NotificationOptions": { "Sender": "Code-Maze", "EnableSms": false, "EnableEmail": true, "MaxNumberOfRetries": 3 }
Inside our appsettings.json
file, we add the NotificationOptions
section and its corresponding properties.
Let’s see how we can enforce validation by utilizing source generators!
How to Use Source Generators to Validate IOptions
With .NET 8, we can utilize source generators to create validators:
[OptionsValidator] public partial class ValidateNotificationOptions : IValidateOptions<NotificationOptions> { }
We create the ValidateNotificationOptions
class and then implement the IValidateOptions<TOptions>
interface. The next step is to make the class partial
and decorate it with the OptionsValidator
attribute. By using the attribute with an empty partial class that implements the IValidateOptions<TOptions>
interface, we instruct the compiler to use source generators and create an implementation of that interface for us.
Next, we register our options:
builder.Services.AddOptions<NotificationOptions>() .BindConfiguration(nameof(NotificationOptions));
In our Program
class, we use the AddOptions()
and BindConfiguration()
methods. with the former, we register our NotificationOptions
class, and with the latter we bind them to the corresponding section of the appsettings.json
file.
We have one more step:
builder.Services.AddSingleton<IValidateOptions<NotificationOptions>, ValidateNotificationOptions>()
Here, we register our IValidateOptions<TOptions>
interface implementation with a Singleton lifetime. Now, every time we request an IOptions<NotificationOptions>
instance, the compiler will try to construct it and validate its properties based on the attributes we’ve used. If any values don’t adhere to the defined constraints an exception will be thrown at runtime.
IOptions Validation During Startup When Using Source Generators
Runtime exceptions can be very problematic and cause unwanted problems. Let’s see how to remedy them:
builder.Services.AddSingleton<IValidateOptions<NotificationOptions>, ValidateNotificationOptions>() .AddOptionsWithValidateOnStart<NotificationOptions>();
Again, in the Program
class, we add the AddOptionsWithValidateOnStart<TOptions>()
method to the call. This will validate our NotificationOptions
class during application startup and prevent unwanted runtime errors.
This will still cause our program to crash if there are problems with our configuration data. However unpleasant this might be, it will ensure that we cannot ship our application until it is properly configured.
Conclusion
In this article we explored how using source generators to validate IOptions is essential for maintaining application integrity and security. By embracing validation during application startup, we proactively address configuration issues, ensuring we only deploy properly configured applications. While this may introduce initial runtime exceptions, it’s a necessary step to guarantee proper configuration before deployment. Overall, robust validation practices are essential for maintaining the integrity and effectiveness of our applications.