Stop Using IOptions Wrong in .NET!

Published: (December 26, 2025 at 01:41 PM EST)
4 min read
Source: Dev.to

Source: Dev.to

📌 IOptionsThe Simple One

Think of it as: a sticky note you write once and never change.

What it does: loads your settings when the app starts, and that’s it.

Example

public class NotificationSettings
{
    public string SmtpServer { get; set; }
    public int Port { get; set; }
    public string DefaultSender { get; set; }
}

public class NotificationService
{
    private readonly NotificationSettings _settings;

    public NotificationService(IOptions<NotificationSettings> options)
    {
        _settings = options.Value;
    }
}

Use it for: settings that never change – e.g., SMTP server, database URL, default sender email.

The catch: if you change the config file you must restart the app to see the changes.


📌 IOptionsSnapshotThe Request‑Friendly One

Think of it as: checking your messages every time someone asks you.

What it does: gets fresh settings for every HTTP request that comes in.

Example

public class NotificationController : ControllerBase
{
    private readonly IOptionsSnapshot<NotificationSettings> _prefs;

    public NotificationController(IOptionsSnapshot<NotificationSettings> prefs)
    {
        _prefs = prefs;
    }

    [HttpPost("send")]
    public IActionResult SendNotification()
    {
        // Gets fresh preferences for each request
        var enableEmail = _prefs.Value.EnableEmailNotifications;
        var enableSms   = _prefs.Value.EnableSmsNotifications;

        // Send based on current preferences
        // …
        return Ok();
    }
}

Use it for: user preferences, notification toggles, A/B testing—situations where different requests might need different settings.

The catch: works only in web requests, not in background jobs, and reads the config on every request.


📌 IOptionsMonitorThe Live‑Updates One

Think of it as: having live notifications turned ON – you know immediately when something changes.

What it does: watches your config file and updates automatically when you change it (no restart needed).

Example

public class NotificationBackgroundService : BackgroundService
{
    private readonly IOptionsMonitor<NotificationLimits> _monitor;

    public NotificationBackgroundService(IOptionsMonitor<NotificationLimits> monitor)
    {
        _monitor = monitor;

        // Runs automatically when config changes!
        _monitor.OnChange(thresholds =>
        {
            Console.WriteLine("Notification thresholds updated!");
            UpdateNotificationRules(thresholds);
        });
    }

    protected override async Task ExecuteAsync(CancellationToken token)
    {
        while (!token.IsCancellationRequested)
        {
            // Use current threshold values
            var maxPerHour = _monitor.CurrentValue.MaxNotificationsPerHour;
            await CheckAndSendNotifications(maxPerHour);
            await Task.Delay(TimeSpan.FromMinutes(5), token);
        }
    }
}

Use it for: background jobs, scheduled tasks, or any scenario where you need live updates without restarting.

The catch: a bit more complex to set up and uses slightly more memory.


🎯 How Do I Choose?

Ask yourself one simple question: “Will my config change while the app is running?”

AnswerRecommended option
NoIOptions – simple and fast
YesIs it for web requests or background work?
Web requestsIOptionsSnapshot
Background workIOptionsMonitor

⚠️ Mistakes I’ve Made (so you don’t have to)

MistakeWhy it’s a problem
Using IOptionsSnapshot in a background serviceIt only works for scoped web requests
Storing IOptionsSnapshot.Value in a variableYou lose the “fresh‑every‑request” benefit
Forgetting to listen to OnChange with IOptionsMonitorYou won’t know when the config changes
Not validating settingsThe app can crash at random times when the config is wrong

💡 Cool Tricks I Wish I Knew Earlier

Trick #1 – Multiple configs of the same type

services.Configure<EmailSettings>("Email", config.GetSection("Email"));
services.Configure<SmsSettings>("SMS",   config.GetSection("SMS"));
services.Configure<PushSettings>("Push", config.GetSection("Push"));

// Later in your code:
var emailSettings = _options.Get<EmailSettings>("Email");
var smsSettings   = _options.Get<SmsSettings>("SMS");

Trick #2 – Validate your config on startup

services.AddOptions<NotificationSettings>()
    .Validate(settings => !string.IsNullOrEmpty(settings.SmtpServer),
              "Hey! You forgot the SMTP Server!")
    .Validate(settings => settings.Port > 0 && settings.Port (Configuration.GetSection("NotificationLimits"));

Consume (e.g., in a background service)

public class NotificationWorker : BackgroundService
{
    private readonly IOptionsMonitor<NotificationLimits> _limits;

    public NotificationWorker(IOptionsMonitor<NotificationLimits> limits)
    {
        _limits = limits;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var limits = _limits.CurrentValue;
            // Use limits.MaxPerUser and limits.TimeWindowHours …
            await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
        }
    }
}

{
  "NotificationLimits": {
    "MaxPerUser": 5,
    "EnableEmailNotifications": true,
    "EnableSmsNotifications": false
  }
}
// When starting your app
builder.Services.Configure<NotificationLimits>(
    builder.Configuration.GetSection("NotificationLimits"));

Background service – needs live updates!

public class NotificationQueueService : BackgroundService
{
    private readonly IOptionsMonitor<NotificationLimits> _monitor;

    public NotificationQueueService(IOptionsMonitor<NotificationLimits> monitor)
    {
        _monitor = monitor;
    }

    protected override async Task ExecuteAsync(CancellationToken token)
    {
        // Update limits immediately when config changes
        _monitor.OnChange(limits =>
        {
            Console.WriteLine($"Notification limits updated to {limits.MaxPerUser} per user");
            UpdateQueueProcessor(limits);
        });

        while (!token.IsCancellationRequested)
        {
            await ProcessNotificationQueue(_monitor.CurrentValue);
            await Task.Delay(TimeSpan.FromSeconds(10), token);
        }
    }
}

API endpoint – needs fresh data per request

public class NotificationController : ControllerBase
{
    private readonly IOptionsSnapshot<NotificationLimits> _limits;

    public NotificationController(IOptionsSnapshot<NotificationLimits> limits)
    {
        _limits = limits;
    }

    [HttpPost("send")]
    public async Task<IActionResult> SendNotification([FromBody] NotificationRequest request)
    {
        var maxPerUser = _limits.Value.MaxPerUser;

        if (await CheckUserNotificationCount(request.UserId) >= maxPerUser)
            return BadRequest("Notification limit reached");

        // Each request gets current limits
        await SendNotificationAsync(request);
        return Ok();
    }
}

🎓 Remember This

OptionWhen to UseAnalogy
IOptionsWrite‑once, read‑foreverA tattoo – permanent after you set it
IOptionsSnapshotFresh data on every requestChecking the weather each morning
IOptionsMonitorLive updates when configuration changesYour social‑media feed updating in real time

Have you used these before?
Which one confused you the most? Drop a comment below! 👇

If this helped you, save it for later – you’ll thank yourself when you’re debugging at 2 AM!

Back to Blog

Related posts

Read more »