Skip to Content
Course content

80: Configuration and Options Pattern

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I see this happen all the time when developers first start building production-ready APIs. They know that settings—like API keys, connection strings, or timeout values—belong in appsettings.json, but they treat the configuration system like a giant dictionary. It works for five minutes, and then it falls apart the moment the app scales or the config file gets a typo.

The "String-ly Typed" Configuration Trap

// A typical "beginner" implementation of a notification service
public class EmailService 
{
    private readonly IConfiguration _config;

    public EmailService(IConfiguration config)
    {
        _config = config;
    }

    public void SendEmail(string recipient, string message)
    {
        // This is where the danger lives
        var smtpServer = _config["SmtpSettings:Server"];
        var port = int.Parse(_config["SmtpSettings:Port"]); 

        Console.WriteLine($"Connecting to {smtpServer} on port {port}...");
        // Logic to send email...
    }
}

At first glance, this looks fine. But look closer. I've used "magic strings" to access my settings. If I accidentally type "SmtpSetting:Server" (missing the 's'), smtpServer becomes null, and the app might crash later in a way that's hard to debug. Even worse, int.Parse will throw a FormatException if someone puts "Five Hundred" instead of "500" in the JSON file. Your app crashes at runtime, not at startup, which is the worst time for a failure.

Plus, if you want to unit test EmailService, you now have to mock the entire IConfiguration tree, which is a tedious pain in the neck.

Moving to Strongly Typed Options

The professional way to handle this is the Options Pattern. Instead of asking the configuration system for a specific string, we define a class that represents our settings. This turns our configuration into a first-class object.

First, we create a simple POCO (Plain Old CLR Object) that matches the structure of our JSON:

public class SmtpOptions
{
    public const string SectionName = "SmtpSettings";
    public string Server { get; set; } = string.Empty;
    public int Port { get; set; }
}

Now, we tell the Dependency Injection (DI) container how to bind the JSON section to this class in Program.cs:

// In Program.cs
builder.Services.Configure<SmtpOptions>(
    builder.Configuration.GetSection(SmtpOptions.SectionName));

Finally, we refactor the service to use IOptions<T>. Notice how we no longer care about the JSON structure inside the service; we just care about the SmtpOptions object.

public class EmailService 
{
    private readonly SmtpOptions _options;

    public EmailService(IOptions<SmtpOptions> options)
    {
        // .Value is where the bound settings live
        _options = options.Value;
    }

    public void SendEmail(string recipient, string message)
    {
        // Now it's type-safe. No casting, no magic strings here.
        Console.WriteLine($"Connecting to {_options.Server} on port {_options.Port}...");
    }
}

Handling Runtime Configuration Changes

One thing I should mention: IOptions<T> is a singleton. Once the app starts, those values are locked in. If you change your appsettings.json while the app is running, IOptions won't see the changes until you restart the process.

If you're building something that needs to update settings on the fly (like a feature flag or a logging level), you have two other tools in your belt:

  • IOptionsSnapshot<T>: Useful for scoped services. It re-reads the config on every request, ensuring you have the latest values for that specific execution.
  • IOptionsMonitor<T>: A singleton service that provides a CurrentValue property and an OnChange event. This is the go-to for long-running background services.

In most cases, IOptions is enough, but knowing the difference prevents those "Why isn't my config updating?" debugging sessions that waste an entire afternoon.




📋 Practical Task

Implementing a Validated API Configuration for a Payment Gateway

You are integrating a third-party payment gateway. The gateway requires an API Key and a Timeout value. You need to ensure the application fails immediately on startup if these values are missing or invalid, rather than failing during a transaction.

Your Task:

  1. Create a class called PaymentGatewayOptions with two properties: ApiKey (string) and TimeoutSeconds (int).
  2. In your Program.cs, use the .Configure<T> method to bind this class to a section in appsettings.json named "PaymentGateway".
  3. Create a service called PaymentProcessor that injects IOptions<PaymentGatewayOptions>.
  4. Inside the PaymentProcessor, implement a method ProcessPayment(decimal amount) that prints a message saying: "Processing payment of [amount] using API Key [ApiKey] with a timeout of [TimeoutSeconds]s."
  5. Bonus Challenge: Research PostConfigure or ValidateDataAnnotations to ensure that the ApiKey cannot be null or empty upon application startup.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.