[API Proposal]: OnBeforeBuild Delegate - Add a hook into the ServiceCollection, allowing execution of code just before building the ServiceProvider
thomhurst opened this issue · 15 comments
Background and motivation
While the Microsoft Dependency Injection container works extremely well for most use cases, it has a couple of weaknesses compared to some other containers.
One area where I've specifically found that it is weak in, is supporting Decorator classes.
While it is currently possible to support decorators, it is dependent on the ordering of registrations. Meaning you can't register a decorator class before registering the original implementation. This means that if someone is unaware of this order, they could start refactoring the Dependency Injection setup, moving certain registrations into different locations, and this could end up breaking your application. Therefore this means this is delicate, flaky code.
A Dependency Injection container shouldn't really concern itself with the ordering of registrations in my opinion.
By exposing a delegate on the IServiceCollection, called something like OnBeforeBuild, we can add hooks that will be invoked just before building the ServiceProvider.
This means that regardless of when we called (for example) an extension method called .AddDecorator<IInterface, TDecorator>(), which under the hood used this hook, it wouldn't break, because it would end up always being invoked at the latest stage possible.
This hook could also enable more advanced use-cases, such as scanning the collection before building, and utilising that data in any custom logic.
My proposed API would look like this:
+ public delegate void OnBeforeBuild(IServiceCollection serviceCollection);
public interface IServiceCollection : IList<ServiceDescriptor>
{
+ OnBeforeBuild OnBeforeBuild { get; set; }
}Then where we are given a ServiceCollection, we can use this hook. We can then implement extension methods to abstract away logic like
public static class DependencyInjectionExtensions
{
public static IServiceCollection AddSingletonDecorator<TService, TDecorator>(this IServiceCollection services)
where TService : class
where TDecorator : class, TService
{
services.OnBeforeBuild += collection => collection.AddSingleton<TService, TDecorator>();
return services;
}
}API Proposal
+ public delegate void OnBeforeBuild(IServiceCollection serviceCollection);
public interface IServiceCollection : IList<ServiceDescriptor>
{
+ OnBeforeBuild OnBeforeBuild { get; set; }
}API Usage
services.OnBeforeBuild += collection => collection.AddSingleton<TService, TDecorator>();Alternative Designs
No response
Risks
Building the ServiceProvider would be marginally slower. But this is a one-time process for most applications.
This shouldn't cause any breaking changes, as it just exposes a new delegate, and doesn't change any existing behaviour.
Tagging subscribers to this area: @dotnet/area-extensions-dependencyinjection
See info in area-owners.md if you want to be subscribed.
Issue Details
Background and motivation
While the Microsoft Dependency Injection container works extremely well for most use cases, it has a couple of weaknesses compared to some other containers.
One area where I've specifically found that it is weak in, is supporting Decorator classes.
While it is currently possible to support decorators, it is dependent on the ordering of registrations. Meaning you can't register a decorator class before registering the original implementation. This means that if someone is unaware of this order, they could start refactoring the Dependency Injection setup, moving certain registrations into different locations, and this could end up breaking your application. Therefore this means this is delicate, flaky code.
A Dependency Injection container shouldn't really concern itself with the ordering of registrations in my opinion.
By exposing a delegate on the IServiceCollection, called something like OnBeforeBuild, we can add hooks that will be invoked just before building the ServiceProvider.
This means that regardless of when we called (for example) an extension method called .AddDecorator<IInterface, TDecorator>(), which under the hood used this hook, it wouldn't break, because it would end up always being invoked at the latest stage possible.
This hook could also enable more advanced use-cases, such as scanning the collection before building, and utilising that data in any custom logic.
My proposed API would look like this:
+ public delegate void OnBeforeBuild(IServiceCollection serviceCollection);
public interface IServiceCollection : IList<ServiceDescriptor>
{
+ OnBeforeBuild OnBeforeBuild { get; set; }
}Then where we are given a ServiceCollection, we can use this hook. We can then implement extension methods to abstract away logic like
public static class DependencyInjectionExtensions
{
public static IServiceCollection AddSingletonDecorator<TService, TDecorator>(this IServiceCollection services)
where TService : class
where TDecorator : class, TService
{
services.OnBeforeBuild += collection => collection.AddSingleton<TService, TDecorator>();
return services;
}
}API Proposal
+ public delegate void OnBeforeBuild(IServiceCollection serviceCollection);
public interface IServiceCollection : IList<ServiceDescriptor>
{
+ OnBeforeBuild OnBeforeBuild { get; set; }
}API Usage
services.OnBeforeBuild += collection => collection.AddSingleton<TService, TDecorator>();Alternative Designs
No response
Risks
Building the ServiceProvider would be marginally slower. But this is a one-time process for most applications.
This shouldn't cause any breaking changes, as it just exposes a new delegate, and doesn't change any existing behaviour.
| Author: | thomhurst |
|---|---|
| Assignees: | - |
| Labels: |
|
| Milestone: | - |
, it is dependent on the ordering of registrations. Meaning you can't register a decorator class before registering the original implementation.
Can you provide an example or pseudo-code of the implementation of the event -- does the decorator class in your case need to enumerate existing services and then add a decorator for some of them and then perhaps remove the original?
, it is dependent on the ordering of registrations. Meaning you can't register a decorator class before registering the original implementation.
Can you provide an example or pseudo-code of the implementation of the event -- does the decorator class in your case need to enumerate existing services and then add a decorator for some of them and then perhaps remove the original?
Yeah exactly. Apologies I'm on mobile but it'd be something like
services.OnBeforeBuild += collection =>
{
var myService = collection.First(x => x.ServiceType == typeof(IMyInterface));
collection.Remove(myService);
collection.AddTransient<IMyInterface>(sp => new MyDecorator(ActivatorUtilities.CreateInstance(myService.ImplementationType, sp)));
}
I do think this feature has merit; moving to future based on schedule.
This shouldn't cause any breaking changes
Adding a property to the existing IServiceCollection interface is a breaking change.
Oh will it? If it wasn't referenced before how does it break stuff?
This is "DISALLOWED: Adding a member to an interface" in .NET API changes that affect compatibility.
IServiceCollection is implemented by at least the following classes, and the new property would need to be added to each of them:
- Microsoft.Extensions.DependencyInjection.ServiceCollection
- Microsoft.Maui.Hosting.Internal.MauiServiceCollection (via Microsoft.Maui.Hosting.IMauiServiceCollection)
- Microsoft.Extensions.DependencyInjection.DefaultHttpClientBuilderServiceCollection
- Lamar.ServiceRegistry
- AutoDI.AutoDIServiceCollection
- Prism.Ioc.ServiceCollection
- Microsoft.KernelMemory.AppBuilders.ServiceCollectionPool
An interface default method is not feasible in IServiceCollection because the Microsoft.Extensions.DependencyInjection.Abstractions package targets .NET Framework too and is supported there for use with ASP.NET Core 2.1.
If you instead did
namespace Microsoft.Extensions.DependencyInjection
{
// Keep the existing interface as is.
public interface IServiceCollection : IList<ServiceDescriptor>
{
}
+ public delegate void OnBeforeBuild(IServiceCollection serviceCollection);
+ // It doesn't seem useful to be able to implement IServiceCollectionBeforeBuild
+ // without IServiceCollection, because the IServiceCollection is passed as
+ // a parameter to the delegate anyway.
+ public interface IServiceCollectionBeforeBuild : IServiceCollection
+ {
+ OnBeforeBuild OnBeforeBuild { get; set; }
+ }
- public partial class ServiceCollection : IServiceCollection
+ public partial class ServiceCollection : IServiceCollection, IServiceCollectionBeforeBuild
{
+ public OnBeforeBuild OnBeforeBuild { get; set; }
}
}then this would not be a breaking change according to those rules, but there could still be compatibility problems if a component attempts to register its delegate in IServiceCollectionBeforeBuild but the application uses a DI container library that does not support this feature.
The thing is, a major version gets released each year anyway. By versioning standards, breaking changes are to be expected in new major versions
This would be on the options instead of on the service collection (if we decided to do anything here)
This would be on the options instead of on the service collection (if we decided to do anything here)
Allowing delegates? That'd limit libraries which wouldn't really solve registering decorators. Extensions are usually on the collection so libraries don't have access to the options
Feature that change the core interfaces are a much bigger scope as all of the existing implementers of IServiceCollection need to buy into this new primitive. It requires getting buy in and sign off from those parties. Features of the built-in container are smaller in scope and can be executed on without that level of buy in (if the core team agrees though).
If you want it to be in the first category then you need to do the work to get the DI council to buy into your feature and agree to implement it everywhere.
PS: I'm not convinced this feature has reached that level of significance, but I am just one person 😄
Fair. I'm not saying its a huge feature, but it definitely helps enable the decorator pattern which COULD have big implications.
And it basically enables a new feature that isn't possible right now.
Also, I understand the interface implementation problem, but I'd argue how often those interfaces are used outside of the MEDI types. I guarantee minimally.
Also sorry if that came across argumentatively, I don't mean that, but just trying to convey that I don't think it's affect many people
Also, I understand the interface implementation problem, but I'd argue how often those interfaces are used outside of the MEDI types. I guarantee minimally.
Doesn’t change the stance on how we approach DI features.
but it definitely helps enable the decorator pattern which COULD have big implications.
@thomhurst
Fair. I'm not saying its a huge feature, but it definitely helps enable the decorator pattern which COULD have big implications.
A decorator pattern based on an OnBeforeBuild phase would be nice when wanting to decorate many types in the container with some kind of generic behavior; where most of those types are actually third-party to you and wired up inside other extension methods and builders. They might even be implementation details you really shouldn't be messing with unless you strictly know what you're doing. (E.g. Application Insights and similar telemetry solutions hooking themselves into everything all over the place, is this kind of thing.)
But that's not the type of decoration you are discussing here, I think.
What you're talking about is just composing a service from layers of decorators.
You can already register decorated types in IServiceCollection like that in a nice and declarative way with some elbow grease.
All it requires is a set of extension methods that internally register a factory delegate which constructs the concrete implementation types in order and injects them into the constructor of the next type using an ObjectFactory from ActivatorUtilities.CreateFactory.
The end result of that can be a fluent API like:
services
.AddDecoratedScoped<IFooService, BaseFoo>()
.DecorateWith<FooLoggingDecorator>()
.DecorateWith<FooCachingDecorator>()
.DecorateWith<FooEtcEtcDecorator>();