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.
So, let’s begin our exploration!
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.
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.