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:
Thank you for your help!
I test your code according to the two links you provided, then I find that without explicit registration the first example does not work:
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:
So, you should register each of them explicitly to make them work.