In this article, I’ll explain how to create strongly typed access to a group of related settings in the .NET Core using the Options pattern.
Options pattern mainly provides interface segregation principle (‘I’ from SOLID design principle). Separation of concern is a set of related configuration parameters grouped into a separate class providing the type safety. The classes are then 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 required by a specific part of the application.
We can achieve this 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 it better. I have created a .NET Core Web API project from the template and created a controller named ReportController for a resource called report.
Creating a new project
Create ASP.NET Core Web API project
The responsibility of this resource is to generate reports based on the user input parameters. Then, send the generated report to a set of users whose email addresses are configured in our appsettings.json file so that 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 go ahead with this and see what happens when the scope of the dependency changes in the latter part of the article.
Check out how our ReportController looks like:
public class ReportController : ControllerBase { private readonly IReportService reportService; private readonly IEmailService emailService; public ReportController(IReportService reportService, IEmailService emailService) { this.reportService = reportService; this.emailService = emailService; } [HttpPost] public IActionResult GenerateAndSendReport(ReportInputModel reportInputModel) { var report = reportService.GenerateReport(reportInputModel); if(report is null) { return NotFound(); } emailService.Send(report); return Ok(); } }
ReportController.cs
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.
public interface IReportService { string GenerateReport(ReportInputModel reportInputModel); }
IReportService.cs
public interface IEmailService { void Send(string report); }
IEmailService.cs
public class ReportService : IReportService { public string GenerateReport(ReportInputModel reportInputModel) { return $"Report generated for report id: {reportInputModel}"; } }
ReportService.cs
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddScoped<IEmailService, EmailService>(); services.AddScoped<IReportService, ReportService>(); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "Options.NetCore", Version = "v1" }); }); }
Configure EmailService and ReportService as Scoped dependencies in Startup.cs
Let us now add the configuration parameters in the appsettings.json file
"Email": { "Subject": "Options Report", "Recepient": "reportuser@someorg.com" }
appsettings.json
There are two parameters: the subject of the report and the recipient’s email address for the report to be sent 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.
public class EmailService : IEmailService { private readonly IConfiguration configuration; public EmailService(IConfiguration configuration) { this.configuration = configuration; } public void Send(string report) { Console.WriteLine($"Sending report titled {configuration["Email:Subject"]} " + $"to {configuration["Email:Recepient"]}"); } }
EmailService using IConfiguration
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
Application run using IConfiguration option
We are just outputting the report sending to console for simplicity. We can see that the configuration parameters are read correctly 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
"Email": { "Subject": "Options Report - Modified", "Recepient": "reportuser@someorg.com" }
Subject modified in appsetting.json
Result after modifying configuration with App still running
Now, our application can read the modified configuration parameters using the IConfiguration approach. All looks good so far.
This approach works well when we have a couple of configuration parameters (in this case, Subject and Recipient). To read them separately from the configuration object shouldn’t be a problem. But, when the number of parameters increases, then things will get trickier. For example, 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. It will not work well for the single responsibility principle. Wouldn’t it be great to 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 identify the specific section of the configuration uniquely.
public class EmailOptions { public const string Email = "Email"; public string Subject { get; set; } public string Recepient { get; set; } }
EmailOptions.cs
In order to use this class in our EmailService, we first need to configure it in ConfigureServices method in our Startup class.
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddScoped<IEmailService, EmailService>(); services.AddScoped<IReportService, ReportService>(); services.Configure<EmailOptions>(Configuration.GetSection( EmailOptions.Email)); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "Options.NetCore", Version = "v1" }); }); }
Configure EmailOptions in Startup.cs
After configuring it, 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, remove the dependency of Configuration from our service. Our EmailService now looks like this
public class EmailService : IEmailService { private readonly EmailOptions emailOptionsVal; public EmailService(IOptions<EmailOptions> emailOptions) { emailOptionsVal = emailOptions.Value; } public void Send(string report) { Console.WriteLine($"Sending report titled {emailOptionsVal.Subject} " + $"to {emailOptionsVal.Recepient}"); } }
EmailService.cs
Let us now see it in action. Go back to the original value of configuration params and run the application.
Run Application using IOptions
As you can see, we are getting the same result, but our service is not dependent on the entire configuration, only a part of it, though. 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.
"Email": { "Subject": "Options Report - Modified", "Recepient": "reportuser@someorg.com" }
appsettings.json modified
Result after modifying appsettings.json with App still running
As you can see, we are still using the old values for Subject and Recipient. Well, that was not the case with our previous approach. So how to fix this?
Instead of using IOptions in our service, let us use IOptionsSnapshot and see what happens.
public class EmailService : IEmailService { private readonly EmailOptions emailOptionsVal; public EmailService(IOptionsSnapshot<EmailOptions> emailOptions) { emailOptionsVal = emailOptions.Value; } public void Send(string report) { Console.WriteLine($"Sending report titled {emailOptionsVal.Subject} " + $"to {emailOptionsVal.Recepient}"); } }
EmailService.cs using IOptionsSnapshot
Run Application using IOptionsSnapshot
The first line in the output is with the original parameters—the following line with modified parameters and the application is still running.
So we achieved the result that we wanted. The 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. Hence, it makes sense to use a single instance of that service. Now, let us make the necessary change.
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddSingleton<IEmailService, EmailService>(); services.AddScoped<IReportService, ReportService>(); services.Configure<EmailOptions>(Configuration.GetSection( EmailOptions.Email)); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "Options.NetCore", Version = "v1" }); }); }
Startup.cs
Now, let us run the application
Error when using Singleton scope for EmailService
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.
public class EmailService : IEmailService { private readolny EmailOptions emailOptionsVal; public EmailService(IOptionsMonitor<EmailOptions> emailOptions) { emailOptionsVal = emailOptions.CurrentValue; } public void Send(string report) { Console.WriteLine($"Sending report titled {emailOptionsVal.Subject} " + $"to {emailOptionsVal.Recepient}"); } }
EmailService.cs using IOptionsMonitor
Ok, we are good to go. Let us run the application
Run Application with IOptionMonitor
And with that, we seem to have resolved the 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 the config. The reason being our EmailService scoped to singleton, so for subsequent requests, the same instance is consumed with actual values from configuration. To solve this issue, let us change our EmailService to have IOptionMonitor<EmailOptions> as its field instead of EmailOptions. We also need to change places where we are reading config value from the CurrentValue property rather than the field itself. So our EmailService looks like below:
public class EmailService : IEmailService { // private readonly EmailOptions emailOptionsVal; private readonly IOptionsMonitor<EmailOptions> emailOptionsVal; public EmailService(IOptionsMonitor<EmailOptions> emailOptions) { emailOptionsVal = emailOptions; } public void Send(string report) { Console.WriteLine($"Sending report titled {emailOptionsVal.CurrentValue.Subject} " + $"to {emailOptionsVal.CurrentValue.Recepient}"); } }
EmailService with IOptionsMonitor as field
If we run the application and change the configuration while it is still running, we see the modified values picked up by the application.
Run Application with IOptionsMonitor as field
Conclusion
As we saw, there are multiple ways of using Options in the .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 another blog in the future. Try this method, and then share your experience with us. I hope you enjoyed reading this. Till then, stay safe and happy coding!