How to avoid (re)building the service provider?
Prerequisites
- .NET 6+
- Basic knowledge of dependency injection (DIP, IoC)
Context
Sometimes we need classes registered in DI while we are still configuring that same DI container.
In my case: when the app ran locally (dev), I needed to execute a file import directly. Otherwise, the operation had to be delegated to a queue system on a cloud provider (already implemented).
So a feature (👋 feature flags) had to be enabled depending on configuration available at startup.
Solution
Configuration setup
Let’s use the Options pattern.
If we define a FeatureFlags class with a boolean IsQueueActivated, we may want to resolve this class with its values automatically bound from configuration.
1
2
3
4
public class FeatureFlags
{
public bool IsQueueActivated { get; set; }
}
appsettings.json:
1
2
3
4
5
{
"FeatureFlags": {
"IsQueueActivated": true
}
}
Register in DI:
1
2
IServiceCollection services;
services.AddOptions<FeatureFlags>().BindConfiguration("FeatureFlags");
Define the two handlers
First, the local processing implementation:
1
2
3
4
5
6
7
public class HandleLocalImportRequest : IHandleImportRequest
{
public async Task ExecuteAsync()
{
// implementation
}
}
Second, the cloud/queue processing implementation:
1
2
3
4
5
6
7
public class HandleCloudCancelImportRequest : IHandleImportRequest
{
public async Task ExecuteAsync()
{
// implementation
}
}
Shared contract:
1
2
3
4
public interface IHandleImportRequest
{
Task ExecuteAsync();
}
Two separate classes, each with its own responsibility.
Using a factory to configure DI
We want to select the implementation based on IsQueueActivated.
A naive approach would be:
1
2
3
4
5
6
7
8
9
10
11
12
var serviceProvider = services.BuildServiceProvider();
var featureFlags = serviceProvider.GetService<IOptions<FeatureFlags>>();
if (featureFlags is not null && featureFlags.Value.IsQueueActivated)
{
services.TryAddTransient<IHandleImportRequest, HandleCloudCancelImportRequest>();
}
else
{
services.TryAddTransient<IHandleImportRequest, HandleLocalImportRequest>();
}
But this is problematic: we are rebuilding the service provider while it will already be built later at app startup.
If singletons exist, duplicates will be created each time the provider is built.
The ASP0000 analyzer warning should appear in your IDE.
How to avoid this while still accessing configuration through DI?
By using a factory.
Resolution is then deferred until IHandleImportRequest is actually requested. No need to rebuild the provider:
1
2
3
4
5
6
7
8
9
10
11
12
13
services.TryAddTransient<IHandleImportRequest>(serviceProvider =>
{
var featureFlags = serviceProvider.GetService<IOptions<FeatureFlags>>();
if (featureFlags is not null && featureFlags.Value.IsQueueActivated)
{
return new HandleCloudCancelImportRequest();
}
else
{
return new HandleLocalImportRequest();
}
});
Conclusion
See this GitHub issue for a deeper discussion about the impact of building the service provider during configuration — including a comment by David Fowler.
For feature flags, you can also use Microsoft’s library: https://learn.microsoft.com/en-us/azure/azure-app-configuration/use-feature-flags-dotnet-core
Even though examples target ASP.NET Core, the base package can be used independently: https://www.nuget.org/packages/Microsoft.FeatureManagement
Try to leave this world a little better than you found it. (Baden-Powell)