asp.net-coredependency-injectionconfiguration

Why does IOptions<T> work with implicit configuration binding in one ASP.NET Core example but not in another?


I'm working on an ASP.NET Core application and encountered some unexpected behavior with the Options pattern and dependency injection. I have two different code examples below and I’m trying to understand why one behaves differently from the other.

First code sample:

// Program.cs

using StockApp;
using StockApp.Services;

var builder = WebApplication.CreateBuilder(args);

// Services
builder.Services.AddControllersWithViews();
builder.Services.AddHttpClient();
builder.Services.AddScoped<FinnhubService>();

var app = builder.Build();

// Middlewares
app.UseStaticFiles();
app.UseRouting();
app.MapControllers();

app.Run();

// HomeController.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using StockApp.Modells;
using StockApp.Services;

namespace StockApp.Controllers
{
    public class HomeController : Controller
    {
        private readonly FinnhubService _finnhubService;
        private readonly IOptions<TradingOptions> _tradingOptions;

        public HomeController(FinnhubService finnhubService, IOptions<TradingOptions> tradingOptions)
        {
            _finnhubService = finnhubService;
            _tradingOptions = tradingOptions;
        }

        [Route("/")]
        public async Task<IActionResult> Index()
        {
            if (_tradingOptions.Value.DefaultStockSymbol == null)
            {
                _tradingOptions.Value.DefaultStockSymbol = "MSFT";
            }

            Dictionary<string, object>? responseDictionary = await _finnhubService.GetStockPriceQuote(_tradingOptions.Value.DefaultStockSymbol);

            Stock stock = new Stock()
            {
                StockSymbol = _tradingOptions.Value.DefaultStockSymbol,
                CurrentPrice = Convert.ToDouble(responseDictionary?["c"].ToString()),
                LowestPrice = Convert.ToDouble(responseDictionary?["l"].ToString()),
                HighestPrice = Convert.ToDouble(responseDictionary?["h"].ToString()),
                OpenPrice = Convert.ToDouble(responseDictionary?["o"].ToString()),
            };

            return View(stock);
        }
    }
}

Second code sample:

// Program.cs

using ConfigurationExample;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

// Supply an object of WeatherApiOptions (with 'weatherapi' section) as a service
builder.Services.Configure<WeatherApiOptions>(builder.Configuration.GetSection("weatherapi"));
builder.Services.AddScoped<WeatherApiOptions>();

var app = builder.Build();

app.UseStaticFiles();
app.UseRouting();
app.MapControllers();

app.Run();

// HomeController.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;

namespace ConfigurationExample.Controllers
{
    public class HomeController : Controller
    {
        private readonly WeatherApiOptions? _options;

        public HomeController(IOptions<WeatherApiOptions> weatherApiOptions)
        {
            _options = weatherApiOptions.Value;
        }

        [Route("/")]
        public IActionResult Index()
        {
            ViewBag.ClientID = _options?.ClientID;
            ViewBag.ClientSecret = _options?.ClientSecret;

            return View();
        }
    }
}

Question

In the first code sample, the IOptions<TradingOptions> seems to work and provide the correct configuration values even though TradingOptions is not explicitly registered as a service.

In contrast, in the second code sample, if I remove

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

the WeatherApiOptions injection fails, and the application does not function correctly.

What is the difference in behavior between these two code samples? Why does IOptions<T> work without explicit registration in the first example but not in the second example? How should I properly register my configuration classes to ensure consistent behavior?

Additional Information

You can find the full code for both examples on my GitHub repository:

First Code

Second Code

Thank you for your help!


Solution

  • I test your code according to the two links you provided, then I find that without explicit registration the first example does not work:

    test image

    As my test result shows, the value of _tradingOptions.Value.DefaultStockSymbol is null. Then the value is set as "MSFT", which is the same value in your appsettings.json. That's why you think it's working.

    And when I add the registration builder.Services.Configure<TradingOptions>(builder.Configuration.GetSection(nameof(TradingOptions))); The program does not enter the if statement:

    test image 2

    So, you should register each of them explicitly to make them work.