Description
Background and motivation
Architectures and principles can differ from project to project and when it comes to centralizing some aspects of your services the DI container is often a first point to handle these aspects.
While I can understand that the extensions team doesn't want to add a central abstraction and implementation for systems like property injection 1234, I cannot fully support this decision. Microsoft libraries often rather try to be not "opinionated" but it seems in this case we are lacking the right extension point for developers to add support for such a mechanism without bloating the core. It seems like an extreme measure to fully swap out the DI container because some additional customization aspects are needed.
So I was thinking of an alternative approach to tackle this topic: What if the default DI container would provide an extension point where developers or library authors can hook into the service creation? Such a callback would open the door for a nice range of extensions:
- Injecting dependencies via properties.
- Calling initialization methods after the constructor if certain interfaces are implemented (similar to Angulars OnInit)
- Providing interceptors/decorators to services 5.
This is a similar strategy like EF Core6 follows. With interceptors the main library can be augmented and extended with new aspects without fully exchanging central bits (like the requiring to exchange the provider).
API Proposal
I am not fully sure about the API design itself, I'm open for any alternative approach providing the same result.
As main API proposal I would see: Add an option for a callback which the DI container calls when a service was realized.
namespace Microsoft.Extensions.DependencyInjection;
public delegate object? ServiceRealizedCallback(IServiceProvider serviceProvider, ServiceDescriptor
serviceDescriptor, object? realizedService);
public class ServiceProviderOptions
{
public ServiceRealizedCallback? OnServiceRealized { get; set; }
}
API Usage
interface IServiceInit { void OnInit(); }
class MyServiceWithPropertyDependencies : IServiceInit
{
public required IOtherService OtherService {get;set;}
public IOptionalService? OptionalService {get;set;}
void OnInit() { OptionalService ??= OtherService?.OptionalService; }
}
var services = new ServiceCollection();
services.AddSingleton<MyServiceWithPropertyDependencies>();
services.AddSingleton<IOtherService, OtherService>();
var serviceProvider = services.BuildServiceProvider(new ServiceProviderOptions
{
OnServiceRealized = (sp, desc, obj)
{
if (obj != null)
{
obj = ApplyPropertyDependencyInjection(sp, desc, obj);
}
if (obj is IServiceInit init)
{
init.OnInit();
}
return obj;
};
});
var instance = serviceProviderFactory.GetRequiredService<MyServiceWithPropertyDependencies>();
// instance.OtherService != null
// instance.OptionalService == null
Alternative Designs
Ad 1: The ServiceProviderEngine
and default implementation could be made public or be exposed with another abstraction. I don't like this approach so much as it pulls quite many internals but its still a legit one. It might make it more complicated for others to extend the default behavior as the whole engine needs to be substituted.
Ad 2: Instead of a simple delegate we could also define an interface and allow multiple "interceptors" to be registered. This would allow an easier extension through extension methods and external libraries:
namespace Microsoft.Extensions.DependencyInjection;
public interface IServiceRealizationInterceptor
{
object? OnServiceRealized(IServiceProvider serviceProvider, ServiceDescriptor
serviceDescriptor, object? realizedService);
}
public class ServiceProviderOptions
{
public IList<IServiceRealizationInterceptor> Interceptors { get; } = new List<IServiceRealizationInterceptor>();
}
While from an API design it might be better for extension, it has more performance and complexity implications (order of interceptors, performance with looping and interface calls, chicken-egg problem if people want to use DI already in the interceptors).
Risks
The DI container is a perfromance sensitive aspect in most applications. Poorly written interceptors could result in a performance degredation which is percieved as bad performance of the DI framework itself.
Unexperienced users might not understand that misbehaviors might be caused by interceptors resulting in additional issues being raised against the core library while 3rd party extensions are to blame. With the .net hostbuilders its easy to hide things behind the scenes.