In my previous blog, I explained the process of creating strongly typed access to a group of related settings in the .NET Core using the Options pattern. I also talked about the IOptions, IOptionsSnapshot, and IOptionsMonitor interface in .Net Core. In this blog, I have moved a step ahead by diving deep into the additional features offered by IOptions on .NET Core.
Here, I am going to build the same example as discussed in my previous blog on IOptions. Before I start the process, kindly go through the blog to understand the basics of the Options pattern in .NET Core.
Quick Recap Of The Application: We have a .NET 5 Web API project with an API to generate the report. Next, send that report to some recipients with a subject that we can customize. So, we have a ReportController with a post API for the same. We have a couple of services, namely ReportService and EmailService to generate the report and send the report as an email, respectively. The EmailService is registered as Singleton scoped dependency, and it reads the configuration parameter for Subject and Recipients via IOptionsMonitor.
We will go back to having EmailOptions as the private field in EmailService rather than IOptionsMonitor<EmailOptions>. Since we are reading CurrentValue in the constructor and EmailService is a singleton service, this will give rise to the problem where the service won’t read the latest value from the configuration while the application is still running. Now, let’s fix that using the OnChange listener provided by IOptionsMonitor. In the constructor, in addition to setting the EmailOptions, we will also register an OnChange listener for IOptionsMonitor. In that listener, we will reset our field to the latest value provided by IOptionsMonitor.
EmailOptions in EmailService
Register OnChange in EmailService
Now, let us start the application and see what happens when we change configuration values. As soon as I change the configuration values and save the file, the breakpoint which I had set on the OnChange listener got hit.
Email Options modified in appsettings.json
OnChange in EmailService
Application run with Email Options modified
This is precisely what we wanted. Now, whenever EmailService’s Send method is called, we can read the latest values from the configuration. Whenever EmailOptions configuration changes, we capture the newest value in the OnChange listener. Note that the OnChange listener is only available for IOptionsMonitor and not IOptionsSnapshot, and the reason being IOptionsSnapshot never had this problem in the first place. With IOptionsSnapshot, we were already able to read the latest configuration values in our service.
Now, let us consider another scenario. If some exceptions occur in our code, we should handle them gracefully and email the administrators to let them know about the exception. And, we want to use the same EmailService to send the admin email. Let us first write some code to handle the exceptions in our code. We will add UseExceptionHandler middleware in Configure method of the Startup class and take the exception thrown from subsequent middleware in the pipeline. We can also inject IEmailService in the Configure method, which has already been registered as a dependency in the ConfigureServices process. It is this code where we will retrieve the exception details and send emails to the administrators.
Please Note: this is just an example, and it is not the standard way notifications are handled for an application.
UseExceptionHandler in Configure
Let us now implement the SendAdmin method that we have used in Exception Handler in our EmailService. Also, let us add another section called AdminEmail to our appsettings.json file. In the SendAdmin process, we want to read from the AdminEmail section rather than the Email section of the application. Let us see how we can implement this with our existing setup?
We will create another class for AdminEmailOptions similar to EmailOptions class and then inject it the same way as EmailOptions into the EmailService.
appsettings.json with AdminEmail section
ConfigureServices in Startup.cs
Now, we are done!
At this point, I am not very excited about this implementation as there is excessive code duplication. Surely there is a better way but first let us check that the performance works. To do this, we need to have our application throw some errors so that the Exception Handler comes into action. Let us throw an exception from ReportService and run the application.
Throwing an exception from ReportService.cs
Application running with exception thrown
As we can see, the AdminEmailOptions has correctly read the AdminEmail section of the configuration. As I mentioned earlier, there is too much code duplication with this approach. Essentially we are using the same structure of Email Options with different values at different places. We should be able to use the same class for both, which we are going to do next. So, first of all, let us get rid of the additional AdminEmailOptions class and add the section name for AdminEmail in the existing EmailOptions class.
Since we do not have AdminEmailOptions class anymore, we will remove its configuration in the ConfigureServices method and configure EmailOptions twice. One for the Email section and the other for AdminSection. The difference from the earlier version is that now we are passing section name as the first parameter. And this is the crux of the solution. What we have implemented here is called Named Option. We have registered two instances of EmailOptions with different names, and whenever we want to fetch the Email Options from configuration, we have to specify the name of the configuration.
Register Named Options in Startup for EmailOptions
Now, let us fix our EmailService. We don’t need AdminEmailOptions anymore. Instead, we will change the type of adminEmailOptionsVal field to EmailOptions. In the constructor, we need just one instance of IOptionsMonitor, and while setting the two areas, we need to call its Get method and pass the section’s name.
That’s it. If we run the application, we can see that both the Send method and SendAdmin method are reading the Email parameters from their respective section. Instead of throwing from ReportService, we will throw an exception from EmailService’s Send method, just after we have written to console to call both Send and SendAdmin methods in the same run.
Exception thrown from Send method of EmailService
And if we run the application now, we can see that both the Send and SendAdmin methods have picked values from their respective configuration section.
So with that, we conclude the demonstration of the Options pattern in .NET Core. Options pattern is a handy feature provided in .NET Core applications, and some of the features that we have covered are:
- Strongly-typed configurations
- Reading configuration changes after the application starts
- Named Options
These features are provided via IOptions, IOptionsSnapshot, and the IOptionsMonitor interface, and we should use the implementation as per the need of our application.
I hope you enjoyed reading this. Do try this method and give your reviews. Till then happy coding!
Note: The source code for this application can be found at mmoiyadi/IOptions.NetCore.