Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[API Proposal]: OnBeforeBuild Delegate - Add a hook into the ServiceCollection, allowing execution of code just before building the ServiceProvider #84847

Open
thomhurst opened this issue Apr 14, 2023 · 14 comments
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-Extensions-DependencyInjection needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration
Milestone

Comments

@thomhurst
Copy link

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.

@thomhurst thomhurst added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Apr 14, 2023
@ghost ghost added the untriaged New issue has not been triaged by the area owner label Apr 14, 2023
@ghost
Copy link

ghost commented Apr 14, 2023

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:

api-suggestion, untriaged, area-Extensions-DependencyInjection

Milestone: -

@steveharter
Copy link
Member

, 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?

@thomhurst
Copy link
Author

thomhurst commented May 23, 2023

, 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)));
}

@steveharter steveharter added needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration and removed untriaged New issue has not been triaged by the area owner labels Jun 14, 2023
@steveharter
Copy link
Member

I do think this feature has merit; moving to future based on schedule.

@KalleOlaviNiemitalo
Copy link

This shouldn't cause any breaking changes

Adding a property to the existing IServiceCollection interface is a breaking change.

@thomhurst
Copy link
Author

Oh will it? If it wasn't referenced before how does it break stuff?

@KalleOlaviNiemitalo
Copy link

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:

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.

@KalleOlaviNiemitalo
Copy link

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.

@thomhurst
Copy link
Author

The thing is, a major version gets released each year anyway. By versioning standards, breaking changes are to be expected in new major versions

@davidfowl
Copy link
Member

This would be on the options instead of on the service collection (if we decided to do anything here)

@thomhurst
Copy link
Author

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

@davidfowl
Copy link
Member

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 😄

@thomhurst
Copy link
Author

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

@davidfowl
Copy link
Member

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.

#36021

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-Extensions-DependencyInjection needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration
Projects
None yet
Development

No branches or pull requests

4 participants