khellang / Scrutor

Assembly scanning and decoration extensions for Microsoft.Extensions.DependencyInjection
MIT License
3.64k stars 239 forks source link

Make decorated classes directly referenceable #184

Open adj123 opened 2 years ago

adj123 commented 2 years ago

Hi, I have possibly a slightly unusual case where I have some class which needs to be decorated twice, with the inner one registered as a hosted service:

public interface IMyExternalService { }
public class MyExternalServiceHttpApi : IMyExternalService { }
public class CachingLayerOverMyExternalService : IMyExternalService, IHostedService { }
public class SomethingElseDecoratingMyExternalService : IMyExternalService { }

services.AddSingleton<IMyExternalService, MyExternalServiceHttpApi>();
services.Decorate<IMyExternalService, CachingLayerOverMyExternalService>();
services.AddHostedService(sp => (CachingLayerOverMyExternalService)sp.GetRequiredService<IMyExternalService>()); // <-- Doesn't work
services.Decorate<IMyExternalService, SomethingElseDecoratingMyExternalService>();

The AddHostedService line doesn't work because by the time the function executes, IMyExternalService is already decorated with SomethingElseDecoratingMyExternalService. My only workaround at the moment is to publicly expose the underlying IMyExternalService in this decorator, but this is a hack - it's not part of the decorator's functionality, it's only there to facilitate the DI mechanism which happens to be used to compose all the classes.

I need some handle to be able to reference that inner decorator somehow for my AddHostedService call - eg. perhaps if Decorate also registered a transient service pointer to each decorated type, like if I could do:

services.Decorate<IMyExternalService, CachingLayerOverMyExternalService>();
services.AddHostedService(sp => sp.GetRequiredService<Decorator<CachingLayerOverMyExternalService>>().Value); // <-- Added automatically by the preceding .Decorate

I imagine the general problem here isn't limited to hosted services and this solution would help for cases when the decorator implements arbitrary multiple interfaces which it also needs to be registered against.

khellang commented 1 year ago

Hey @adj123! 👋🏻

I think this might be solvable by using the new "keyed services" in .NET 8. I might introduce an overload that takes a key when decorating, which will let you refer to that specific decorator when resolving using the key.

Do you think this could work?

adj123 commented 1 year ago

Hey, I haven't had a chance to play around with these yet, but from their description it does sound like this approach would work well as a solution to this problem!

hankovich commented 11 months ago

@khellang do you have some estimation when this keyed services feature will be available? I'm eagerly waiting for it

adj123 commented 5 months ago

Alternatively, after looking at the implementation further, I've realised we actually already do register a handle to the decorated service, we just don't expose it - the DecoratedType service descriptor. So a clean way to do this would be to add a wrapper like

public sealed record DecoratedService<TService>(Type Type);

public static TService GetRequiredService<TService>(this IServiceProvider provider, DecoratedService<TService> serviceType) =>
    (TService)provider.GetRequiredService(serviceType.Type);

and new overloads like

public static IServiceCollection Decorate<TService, TDecorator>(this IServiceCollection services, out DecoratedService<TService> decorated)
{
    ... decorate ...
    DecoratedType decoratedType = ...;
    decorated = new(decoratedType);
}

and then we can call it with something like (to use my earlier example):

services.Decorate<IMyExternalService, CachingLayerOverMyExternalService>(out var cachingLayer);
services.AddHostedService(sp => sp.GetRequiredService(cachingLayer));
services.Decorate<IMyExternalService, SomethingElseDecoratingMyExternalService>();

How does that sound?