In the following example, MyOtherService is registered in the IOC of a .NET application.
A Service1 must also be registered into the IOC to ensure MyOtherService can work with it.
|
|
MyOtherService and MySectionOptions are defined as follows:
|
|
|
|
There are caveats about this syntax. What are they?
Caveats
Scope Delta
If you were to register MyOtherService as a singleton and Service1 as scoped or transient, the latter gets captured and lives for the app’s lifetime, which defeats its intended shorter lifecycle. Ensure dependent services use the same lifetime to avoid issues.
ValidateOnBuild Bypass
Factory-based registrations aren’t checked at startup. So, if Service1 or IConfiguration is missing, you won’t know until the first resolution at runtime. You lose the safety net of ValidateScopes / ValidateOnBuild.
When you enable validation in Program.cs:
|
|
With the following constructor-based registration,
|
|
The app crashes immediately on startup with:
|
|
You find the bug before any request hits the server.
If you use factory-based registration,
|
|
Then, the app starts without errors. ValidateOnBuild sees the factory delegate as a black box — it can’t inspect what GetRequiredService calls are inside. The InvalidOperationException only fires when something first requests MyOtherService, potentially minutes, hours, or days later in production under a specific code path.
That’s the core tradeoff: factory delegates give you flexibility but move dependency errors from build-time to runtime, which is exactly the class of bugs ValidateOnBuild was designed to eliminate.
Manual Options Binding
Calling config.GetSection(...).Get<T>() inside the factory bypasses the Options pattern. You lose IOptionsMonitor<T> reload support and validation via data annotations.
Say we have:
|
|
With manual binding in the factory, three things can go wrong:
-
Validation attributes are ignored:
Get<T>()just maps values — it doesn’t run[Required]’ or[Range]. Ifappsettings.jsonhas“MaxRetries”: 999’ or omitsConnectionStringentirely, no error. You get a silent null or invalid value at runtime.With the Options pattern:
1 2 3 4services.AddOptions<MySectionOptions>() .BindConfiguration("MySection") .ValidateDataAnnotations() .ValidateOnStart(); // fails at startup, not at first useThe app won’t start if the config is invalid.
-
No hot reload: if
appsettings.jsonchanges at runtime (common with Azure App Configuration, Kubernetes ConfigMaps, etc.), your singleton captured a snapshot at construction time. It’s frozen as long as the application runs.If
MyOtherServicetakesIOptionsMonitor<MySectionOptions>instead:1 2 3 4 5 6 7 8 9 10 11 12 13 14public class MyOtherService { private readonly IOptionsMonitor<MySectionOptions> _options; public MyOtherService(IOptionsMonitor<MySectionOptions> options) { _options = options; } public void DoWork() { var current = _options.CurrentValue; // always fresh } }Every access reflects the latest config without restarting the app.
-
No named options:
Get<T>()gives you one flat binding. The Options pattern supports named instances out of the box:1 2services.Configure<MySectionOptions>("Primary", config.GetSection("Primary")); services.Configure<MySectionOptions>("Secondary", config.GetSection("Secondary"));Then resolved via
IOptionsSnapshot<T>.Get("Primary"). No equivalent exists with manual binding without reinventing the plumbing yourself.
Conclusion
So, if we were to rewrite the registration and update the class accordingly, we would obtain the following:
First, the MyOtherService constructor changes to use IOptionsMonitor:
|
|
Then, in the Programe.cs,
|
|
What this gives you compared to the original:
- Missing dependency is caught at startup (
ValidateOnBuild) - Invalid config is caught at startup (
ValidateOnStart+ValidateDataAnnotations) - Captive dependency is caught at startup (
ValidateScopes) - Config hot reload is handled (
IOptionsMonitor) - No factory means the entire DI graph is transparent and inspectable
Follow me
Thanks for reading this article. Make sure to follow me on X, subscribe to my Substack publication and bookmark my blog to read more in the future.
Credit: Photo by Ann H on Pexels—https://www.pexels.com/photo/pawns-connected-with-wooden-sticks-7422341/.