Create Strongly Typed Configurations in .NET Core
using IOptions, IOptionsSnapshot and IOptionsMonitor
In this article, I am going to demonstrate the use of the Options pattern to create strongly typed access to a group of related settings in .NET Core. Options pattern mainly provides Interface Segregation Principle (‘I’ from SOLID design principle) and Separation of Concern which essentially means that a set of related configuration parameters are grouped together into a separate class which provides for the type safety and then those classes are injected into the different parts of the application via an interface. So we don’t need to inject the entire configuration but only the configuration information which a specific part of the application needs.
This is achieved via IOptions, IOptionsSnapshot and IOptionsMonitor interface in .NET Core. Let us create an application to demonstrate the use of each one of them to understand better. I have created a .NET Core Web API project from the template and created a controller named ReportController for a resource called report.
The responsibility of this resource is to generate some reports based on some user input parameters and additionally send the generated report to a set of users whose email addresses are configured in our appsettings.json file. So we have a service to generate the report called ReportService and another to email the generated report EmailService. The EmailService reads the email parameters from the configuration. The ReportService, as well as EmailService, are injected via Scoped dependency. Of course, the scopes of these dependencies can vary based on different application needs. We will just go ahead with this and see what happens when the scope of the dependency changes in the latter part of the article. So this is how our ReportController looks like:
There is just one method that takes some input parameters and generates a report by calling the GenerateReport method of ReportService. The generated report is then sent using EmailService’s Send method. Let us have a look at services and how they are injected.
Let us also add the configuration parameters in the appsettings.json file
There are two parameters namely the subject of the report and the recipient’s email address we want the report to send to.
Let us first look at how we would implement this without using the Options pattern. In this case, the EmailService depends on IConfiguration to read the report and email-specific parameters.
Let us now run the application to see it in action. We are calling the post endpoint using the Swagger UI which is provided with the default template for Web API projects in .NET 5
We are just outputting the report sending to console for simplicity and as we can see the configuration parameters are correctly read from appsettings.json. Now, with the application still running, let us change the Subject of the email in appsettings.json to a different value and see what result we get
So our application is able to read the modified configuration parameters using the IConfiguration approach. All good so far.
This approach works well when we have a couple of configuration parameters (in this case Subject and Recipient) and to read them separately from the configuration object should not be a problem. But when the number of parameters increases then things will get trickier. For example, let us we want to have a ‘cc’ and a ‘bcc’ fields to our email parameters. For each of them, we will have to read separately and provide validations. This will clearly not work well for the single responsibility principle. Wouldn’t it be great if we can encapsulate all these related parameters in a single class called EmailOptions and use that class in our EmailService. And this is where the IOptions comes into the picture.
So let us create an Options folder and create a class to store these two email parameters. Also, there is a string constant to uniquely identify the specific section of the configuration.
In order to use this class in our EmailService we first need to configure it in ConfigureServices method in our Startup class.
Now that it is configured, let us use it in our EmailService class. We will first add IOptions<EmailOptions> in the constructor to get access to the EmailOptions instance. Now we can store it as a field and get its value from the Value property of IOptions and that is it. We are ready to use EmailOptions in our service. Let us change the Send method to use EmailOptions rather than Configuration to read the email parameters. Also let us remove the dependency of Configuration from our service. Our EmailService now looks like this
Let us now see it in action. Go back to the original value of configuration params and run the application.
As you can see we are getting the same result but now our service is not dependent on the entire Configuration but only a part of it and all related parameters are grouped into a single entity.
However, there is one issue with this approach. What if my application is still running and I change one of the configuration parameters.
As you can see, we are still using the old values of Subject and Recipient. Well, that was not the case with our previous approach. So how to fix that?
So instead of using IOptions in our service, let us use IOptionsSnapshot and see what happens.
The first line in the output is with the original parameters and the next line with modified parameters with the application still running.
So we got the result we wanted. So IOptionsSnapshot provides us exactly what it says, a snapshot of the configuration.
Ok, all seems good so far. Well not quite! I think our EmailService should be registered with singleton dependency rather than scoped dependency. Normally in applications using Email Services, the email service does not change very often, and hence it makes sense to just use a single instance of that service. So let us make the required change.
Now let us run the application
So what happened here? Well, the inner exception says:
”Some services are not able to be constructed (Error while validating the service descriptor ‘ServiceType: Options.NetCore.Services.Interfaces.IEmailService Lifetime: Singleton ImplementationType: Options.NetCore.Services.Implementations.EmailService’: Cannot consume scoped service ‘Microsoft.Extensions.Options.IOptionsSnapshot`1[Options.NetCore.Options.EmailOptions]’ from singleton ‘Options.NetCore.Services.Interfaces.IEmailService’.)”
…and here is the problem. As the error message states IOptionsSnapshot is a scoped dependency and hence can’t be used inside services registered with singleton scope which our EmailService is. So how to fix that? Well, IOptionsMonitor is the answer. Let us change from IOptionsSnapshot to IOptionsMonitor in our service and instead of reading from Value property read from CurrentValue property.
Ok, we are good to go. Let us run the application
And with that, we seem to have resolved our issue. Note that if we now change the config parameters for the email section with the app still running, we will still read the original value from config and the reason is that our EmailService is scoped to singleton so for subsequent requests the same instance is consumed with original values from configuration. In order to solve this issue, let us change our EmailService so that it has IOptionMonitor<EmailOptions> as its field instead of EmailOptions. We also need to change the places where we are reading config value to read from CurrentValue property rather than the field itself. So our EmailService looks like below:
Now if we run the application and change the configuration while it is still running, we see the modified values picked up by the application
Conclusion
As we saw there are multiple ways of using Options in .NET Core application and which one is best depends on the use case. There are other features provided by IOptions than what I have demonstrated in this article. I will cover them in some future article. Hope you liked this !!!
Happy Coding !!!
Source: docs.microsoft.com