Contact Us

Contact Us



Pleas confirm by checkbox


Technical

Create Strongly Typed Configurations in .NET Core

Author_img
By Mohammed Moiyadi August 13, 2021

Create Strongly Typed Configurations in .NET Core

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

.Net Typed Configuration

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

.Net Typed Configuration

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

.Net Typed Configuration

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.

.Net Typed Configuration

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

.Net Typed Configuration

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

.Net Typed Configuration

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

.Net Typed Configuration

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

.Net Typed Configuration

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.

.Net Typed Configuration

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!

Related posts
3 Crucial Ways of Inserting Code into a Running Application without Creating Bottlenecks
Technical

3 Crucial Ways of Inserting Code into a Running Application without Creating Bottlenecks

By kulwinder.singh January 28, 2022
Apache Spark Standalone Setup On Linux/macOS
Technical

Apache Spark Standalone Setup On Linux/macOS

By kulwinder.singh October 20, 2021
Apache Flink Standalone Setup on Linux/macOS
Technical

Apache Flink Standalone Setup on Linux/macOS

By kulwinder.singh October 13, 2021
Identity, Authentication, And Access Management Using Azure Active Directory  
Technical

Identity, Authentication, And Access Management Using Azure Active Directory  

By kulwinder.singh September 22, 2021
Things to Know Before You Select A Crypto Wallet
Blockchain

Things to Know Before You Select A Crypto Wallet

By kulwinder.singh September 20, 2021
Solve 3 Most Irritating Outlook Email Rendering Issues.
Technical

Solve 3 Most Irritating Outlook Email Rendering Issues.

By kulwinder.singh September 15, 2021
Intuit Wasabi – A Scalable A/B Testing Solution
Technical

Intuit Wasabi – A Scalable A/B Testing Solution

By kulwinder.singh September 01, 2021
How To Pick The Right Data Analytics Strategy For Serverless Systems?
Big Data

How To Pick The Right Data Analytics Strategy For Serverless Systems?

By kulwinder.singh August 25, 2021
Change Notifications and Named Options using Options pattern in .NET Core
Technical

Change Notifications and Named Options using Options pattern in .NET Core

By kulwinder.singh August 18, 2021
Partitioning Database - A Divide and Rule Strategy
Technical

Partitioning Database - A Divide and Rule Strategy

By kulwinder.singh June 02, 2021

Stay updated

Get the latest creative news from Fubiz about art, design and pop-culture.