In this article, we’ll look at resolving the IOptions instance in the Program class, ensuring we avoid common pitfalls and learn some best practices.

As software developers, we frequently encounter the challenge of managing application configurations. However, to make it easy for us .NET supports the Options pattern. This provides a robust way to access configurations in a strongly typed manner, enhancing code readability and maintainability. However, developers often fall into the trap of trying to access these configured options within the Program class directly. While this practice may appear straightforward, it is fraught with potential issues.

To download the source code for this article, you can visit our GitHub repository.

So, let’s begin our exploration!

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

The Common (But Problematic) Approach to Reading Settings

Let’s take a look at a common approach for reading settings in the Program class:

builder.Services.Configure<MySettings>(builder.Configuration.GetSection("MySettings"));

var sp = builder.Services.BuildServiceProvider();
var mySettingsWrapped = sp.GetRequiredService<IOptions<MySettings>>();

Though this approach seems to get the job done, it introduces several issues.

First, we misuse the service provider. Here, we invocate BuildServiceProvider() early, resulting in an autonomous service provider instance, risking the creation of duplicate singleton services and complicating lifecycle and resource management.

Moreover, we access services directly in the Program class, bypassing .NET’s structured dependency injection, and complicating maintenance and troubleshooting.

To learn more about service resolution, check out our article How to Resolve Instances With ASP.NET Core DI.

How to Properly Resolve an IOptions Instance

Now that we have seen why the common approach is undesirable, let’s explore the other possibilities that offer a cleaner and more efficient way to handle application settings.

Read a Single Setting Value

Sometimes, we only need a single setting from the configuration:

var configurationValue = builder.Configuration.GetValue<string>("MySettings:ImportantSetting");

Here, we use the GetValue() method, which facilitates retrieving a single value. With this, we avoid the overhead of creating complete settings objects and simplifying our code.

Bind the Settings Model

For scenarios requiring multiple settings from a configuration section, directly binding to a manually created settings model proves to be adequately sufficient:

var myOptions = new MySettings();
builder.Configuration.Bind("MySettings", myOptions);

With the Bind() method, we map the "MySettings" section’s values onto the MySettings model.

Resolve the IOptions Instance With Options.Create()

Although having IOptions<T> instances within Program class typically indicates poor design, there’s a clean solution. So, let’s take a look:

var myOptions = new MySettings();

builder.Configuration.Bind("MySettings", myOptions);

IOptions<MySettings> myOptionsWrapped = Options.Create(myOptions);

Building on our previous example, we use the Options.Create() method, from the Microsoft.Extensions.Options namespace, to encapsulate our model in an IOptions instance, which is the correct way to resolve an IOptions instance.

Resolve IOptions Using Service Implementation Factory

Registering configurations and accessing them through the service implementation factory is a good design:

builder.Services.AddHttpContextAccessor();

builder.Services.Configure<MySettings>(builder.Configuration.GetSection("MySettings"));

builder.Services.AddScoped<IBusinessService, BusinessService>(x =>
{
    var accessor = x.GetRequiredService<IHttpContextAccessor>();
    var tenant = accessor.HttpContext.Request.Headers["tenant"];
    var options = x.GetRequiredService<IOptions<MySettings>>();

    return new BusinessService(options, tenant);
});

Here, we start by registering the IHttpContextAccessor interface with the AddHttpContextAccessor() method, allowing us to access and extract the tenant header from HTTP requests.

Next, we use the Configure() method to register our configuration values.

Finally, we create a scoped IBusinessService for a multi-tenant application, reliant on IOptions<MySettings> and a “tenant” request header. Furthermore, a delegate enables dynamic service instantiation based on the tenant identified in the request header.

Conclusion

Through exploring various configuration management strategies, we emphasized the importance of avoiding directly resolving configuration options instances in the Program class. Embracing the framework’s dependency injection capabilities is crucial, utilizing IConfiguration for straightforward configuration needs and reserving IOptions instances for scenarios that truly require configuration settings.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!