-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
Add decorator extension methods to IServiceCollection #36021
Comments
Yeah... The current implementation is a giant hack around the container's current limitations and there are some confusing aspects around the API, like "what lifetime should the decorator have?". I've settled on copying the lifetime of the decoratee, but some people have argued that there are cases where this isn't the best solution. Agreed on all the perf concerns. I've basically just kept things as simple as possible (code-wise) since it's mostly a one-time cost at startup and no one has complained (yet). Most of them should be pretty easy to fix, i.e. remove LINQ usage, but some things are just fundamentally hard to do on top of the current feature set. I'd be really interested in hearing your take on the last bullet point, as an example. |
I think people are wrong there, the most correct thing is to preserve the lifetime.
People wouldn't complain but I would 😄. More seriously, we're been taking a look at how much time we spend re-enumerating the IServiceCollection at startup and it's kinda crazy.
Agreed but I think doing this without it being a container native feature is appealing.
I think that limitation is fine. The alternatives I can think of have to do with storing that list of decorators in a side object and writing a custom IServiceProviderFactory that unwraps them but that seems like overkill. |
cc @aspnet/di-council |
@aspnet/di-council To get the discussion going I'll give you a brief explanation of how this is implemented in LightInject. Decorator characteristics
Example public class FooDecorator : IFoo
{
public FooDecorator(IFoo decoratee, IBar bar)
{
}
} RegistrationLet's take a look at the following registrations. container.RegisterScoped<IFoo, Foo>();
container.Decorate<IFoo, FooDecorator>();
container.Decorate<IFoo, AnotherFooDecorator>(); The decorators are applied in the registration order meaning that the decorator chain here will be
Please note that service registration is completely decoupled from the decorators meaning that the following will also work. container.Decorate<IFoo, FooDecorator>();
container.Decorate<IFoo, AnotherFooDecorator>();
container.RegisterScoped<IFoo, Foo>(); LifetimeDecorators follow the same lifetime as the service being decorated which is a very important aspect of decorators. Allowing a decorator to have a different lifetime/lifecycle than the service being decorated could lead to some serious confusion among users. If we think of decorators as being the manifestation of the Open/Closed principle, we want to be able to add new functionality/aspect without altering the behavor of the service being decorated. If in any case we should require different lifetimes, and mind that these cases should be extremely rare, we can always do "manual" decoration in the factory delegate used during registration. So my advice here is to keep it simple and let decorators follow the lifetime of the decorated service. SummaryThis covers the very basics of decorator support in LightInject and I have intentionally not gone into more advanced scenarios like decorating open generic types which is where things really starts to get complicated from an implementation standpoint. I would also advice against such advanced features as it would make it substantially harder to confirm to the MS.DI abstraction |
@seesharper See the code I linked to (@khellang's code) which implements decorators on top of the |
If the Decorators implemented externally as extensions, does it mean that DI adapters treat them as normal services and register as usual? It may cause a confusion:
|
Could you provide a link to the relevant documentation? This term is rather ambiguous and would be nice to know exactly what is being discussed. Is this akin to interception? |
So, essentially decorators are what Unity and Castle call interception, or are these separate services? In other words, if I resolve IFoo, will container return three services with two of them being decorators, or it will return one latest decorator with others and original service chained down the line? |
I'm not super familiar with it, but I always thought interception was some sort of AOP thing where proxies were generated to intercept calls to resolved instances. I guess you could look at it as interception, but with way less magic.
The latter. You'll get a chain of wrapped interceptors. |
Well, if this is the later than it would be rather challenging to implement for multiple, chained decorators. Not impossible, but very challenging. At present Unity does not implement multilevel interception. I like the idea though |
I do not think you could register decorators as normal services. The adapter has to recognize these registrations are decorators and create relevant internal registrations (configure interception in case of Unity). The entry in a collection should unambiguously identify it as a decorator. |
The OP shows an implementation of this on top of the existing abstractions as extensions. |
I was talking about implementing decoration in the container itself. Done at the container level it would eliminate all these scans and speed things up a bit. |
You could say that decorators are a way of implementing AOP (Aspect Oriented Programming) where each decorator is responsible for an "aspect" such as logging, profiling ,caching, circuit breaking and other cross cutting concerns. The decorator pattern really starts to shine when dealing with reusable open generic types. As an example, a common way of implementing the command part of the CQRS pattern is to have an public interface ICommandHandler<in TCommand>
{
Task HandleAsync(TCommand command, CancellationToken cancellationToken = default);
} public class SaveCustomerCommandHandler : ICommandHandler<SaveCustomerCommand>
{
private readonly IDbConnection dbConnection;
public SaveCustomerCommandHandler(IDbConnection dbConnection)
{
this.dbConnection = dbConnection;
}
public async Task HandleAsync(SaveCustomerCommand command, CancellationToken cancellationToken = default(CancellationToken))
{
// Save the customer to the database
}
} Now let's imagine that we have many of these command handlers and we want to make sure that we execute them in the context of a transaction. We could start the transaction in each command handler or we could use the decorator pattern to wrap all command handlers in a transaction like this. public class TransactionalCommandHandler<TCommand> : ICommandHandler<TCommand>
{
private readonly IDbConnection dbConnection;
private readonly ICommandHandler<TCommand> commandHandler;
public TransactionalCommandHandler(IDbConnection dbConnection, ICommandHandler<TCommand> commandHandler)
{
this.dbConnection = dbConnection;
this.commandHandler = commandHandler;
}
public async Task HandleAsync(TCommand command, CancellationToken cancellationToken)
{
using (var transaction = dbConnection.BeginTransaction())
{
await commandHandler.HandleAsync(command, cancellationToken);
transaction.Commit();
}
}
} To decorate each command handler with the container.Decorate(typeof(ICommandHandler<>), typeof(TransactionalCommandHandler<>)) And Viola, all command handlers are now executed in a transaction. We could do the same for logging, profiling and other cross cutting concerns. @khellang You can decorate open generics in Scrutor as well? But as mentioned, most people think of AOP as magic stuff involving generating proxies and implementing interceptors. I have a fair bit of experience with how much magic it takes since I've written an interception library (LightInject.Interception). The difference is that the "decorator" is implemented at runtime (proxy) and the method calls are forwarded to the interceptor. The downside of this is that it is in many cases difficult to debug and to grasp for new developers. We will also take a performance hit since all method arguments are passed to the interceptor as |
The Decorators in DryIoc described here: https://github.com/dadhi/DryIoc/blob/master/docs/DryIoc.Docs/Decorators.md In general the feature is similar to LightInject and combining with other library features may enable more things. The important point is that Decorator registrations are distinguished from the "normal" Service registrations. This info is preserved by container and used to enable nesting, special diagnostics, control the caching, etc. For the whole picture DryIoc has a 3rd kind of registrations - Wrappers, e.g. |
So for external containers the decorator will be created by the external container and the decorated type (original registration) will be created using Can you chain together decorators? Does it support more advanced scenarios like where the decorator can have |
Correct. The decorated type (decoratee) won't be resolved from the external container, but all its dependencies will. This is also problematic for disposal, since the decorated instance won't be tracked by the container for disposal 😞 |
While I like the notion that it doesn't require any container authors to change anything, it also might be nice to allow containers that already implement decorators to do so natively. Similar to the way I'm also having a tough time working out mentally: What does it mean if some decorators are registered with these extension methods while others may be registered via the underlying container? For example: public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IFoo, Foo>();
services.Decorate<IFoo, Foo2>();
}
public void ConfigureContainer(ContainerBuilder builder)
{
builder.RegisterDecorator<LoggingDecorator, IFoo>();
} At least in Autofac, there's a whole resolution context around decoration so folks can intercept when something is getting decorated and do some complex things. I'm not clear how that would interact by some decoration being handled externally and some internally, like if there'd be some weirdness around things getting double decorated or possibly decorated in the wrong order. I think if there was some way to map the |
This is a problem. My concerns are:
|
The entire reason this is done is because of recurrent references (if I understand the term correctly). Decorators always get an instance of "itself" injected, i.e. you have an |
I couldn't figure out the best area label to add to this issue. Please help me learn by adding exactly one area label. |
Hi, The problem with the decorator pattern is that the decorator class must exist in the source, and it must implement the same interfaces as the decorated class. So for each decorated class, a decorator class is needed. Actually if several decorated classes implement the same interface then only one decorator class is needed for them, but a real class is still needed in the source. Proxy pattern is better for cross cutting concerns (transaction, trace, etc...). Like for the decorator pattern, one proxy instance is created for each target instance but it is generated at runtime, no additional class is needed. For example : public interface IFoo
{
void DoSomething();
}
public class Foo : IFoo
{
public override void DoSomething()
{
// code
}
}
public interface IBar
{
void DoSomethingElse();
}
public class Bar : IBar
{
public override void DoSomethingElse()
{
// code
}
} To apply transaction around method invocation : public class Proxy : System.Reflection.DispatchProxy
{
private object target;
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
{
using(var tx = new TransactionScope())
{
object ret = targetMethod.Invoke(target, args);
tx.Complete();
return ret;
}
}
public static T For<T>(T target)
{
object proxy = System.Reflection.DispatchProxy.Create<T, Proxy>();
((Proxy)proxy).target = target;
return (T)proxy;
}
} Service collection registration : IServiceCollection sc = new ServiceCollection();
sc.AddSingleton<IFoo>(Proxy.For<IFoo>(new Foo());
sc.AddSingleton<IBar>(Proxy.For<IBar>(new Bar()); Suggested in #45760 : IServiceCollection sc = new ServiceCollection();
sc.AddSingleton<IFoo, Foo>(fooInstance => Proxy.For<IFoo>(fooInstance));
sc.AddSingleton<IBar, Bar>(barInstance => Proxy.For<IBar>(barInstance)); The idea is to be involved in the service registration, a One limitation : the
A inheritance based proxy would be much better. With appropriate custom attributes ( public class Foo : IFoo
{
[Transactional]
public override void Bar()
{
// code
}
} Better design : the proxy would apply a chain of interceptors (transactional interceptor, trace interceptor, etc...), the chain is based on the custom attributes discovered on target's methods. No custom attribute on any method for a given class means that there is no need for a proxy. This introspection has to be done only once. But a developer should not have to create its own proxies and interceptors for common use cases... This interception support should be built-in : built-in custom attributes for common use cases (transaction, trace....),built-in proxy creation with a chain of interceptors and the possibility to create custom interception logic. It has been a proven solution in Java for 10 years. It would be similar to Aspnet public interface IInterceptorAttribute: System.Attribute
{
Object AroundInvoke(InvocationContext ctx);
}
public class TransactionalAttribute : IInterceptorAttribute
{
public override Object AroundInvoke(InvocationContext ctx)
{
// do something before
object ret = ctx.Proceed();
// do something after
return ret;
}
} With an async version of course. |
This isn't something we would build into the default container. The bar is really high for new features, especially ones like this. I'd look at other containers that may have this built in |
Might be a relevant addition to the conversation. This is a super simple method for a decorate extension that is borrowed from how HttpClient and HttpClientFactory work.
Borrowed from this blog which goes into further detail: Things it does: |
@Tiberriver256, it's good to see that we have nearly the same implementation. I have the following suggestions:
Although the above code and the suggestions are probably clear, I've included my own extension for reference. (It fulfills all the above, including the checkboxes.) /// <summary>
/// Helps register decorator implementations that wrap existing ones in the container.
/// </summary>
internal static class DecoratorRegistrationExtensions
{
/// <summary>
/// Registers a <typeparamref name="TService"/> decorator on top of the previous registration of that type.
/// </summary>
/// <param name="lifetime">If no lifetime is provided, the lifetime of the previous registration is used.</param>
public static IServiceCollection AddDecorator<TService, TImplementation>(
this IServiceCollection services,
ServiceLifetime? lifetime = null)
where TService : class
where TImplementation : TService
{
var decoratorFactory = ActivatorUtilities.CreateFactory(typeof(TImplementation),
new[] { typeof(TService) });
return AddDecorator<TService>(
services,
(serviceProvider, decoratedInstance) =>
(TService)decoratorFactory(serviceProvider, new object[] { decoratedInstance }),
lifetime);
}
/// <summary>
/// Registers a <typeparamref name="TService"/> decorator on top of the previous registration of that type.
/// </summary>
/// <param name="decoratorFactory">Constructs a new instance based on the the instance to decorate and the <see cref="IServiceProvider"/>.</param>
/// <param name="lifetime">If no lifetime is provided, the lifetime of the previous registration is used.</param>
public static IServiceCollection AddDecorator<TService>(
this IServiceCollection services,
Func<IServiceProvider, TService, TService> decoratorFactory,
ServiceLifetime? lifetime = null)
where TService : class
{
// By convention, the last registration wins
var previousRegistration = services.LastOrDefault(
descriptor => descriptor.ServiceType == typeof(TService));
if (previousRegistration is null)
throw new InvalidOperationException($"Tried to register a decorator for type {typeof(TService).Name} when no such type was registered.");
// Get a factory to produce the original implementation
var decoratedServiceFactory = previousRegistration.ImplementationFactory;
if (decoratedServiceFactory is null && previousRegistration.ImplementationInstance != null)
decoratedServiceFactory = _ => previousRegistration.ImplementationInstance;
if (decoratedServiceFactory is null && previousRegistration.ImplementationType != null)
decoratedServiceFactory = serviceProvider => ActivatorUtilities.CreateInstance(
serviceProvider, previousRegistration.ImplementationType, Array.Empty<object>());
if (decoratedServiceFactory is null) // Should be impossible
throw new Exception($"Tried to register a decorator for type {typeof(TService).Name}, but the registration being wrapped specified no implementation at all.");
var registration = new ServiceDescriptor(
typeof(TService), CreateDecorator, lifetime ?? previousRegistration.Lifetime);
services.Add(registration);
return services;
// Local function that creates the decorator instance
TService CreateDecorator(IServiceProvider serviceProvider)
{
var decoratedInstance = (TService)decoratedServiceFactory(serviceProvider);
var decorator = decoratorFactory(serviceProvider, decoratedInstance);
return decorator;
}
}
} |
Any update on this? Would this come in .NET 7? |
The milestone is marked as "future", so no, this is not happening for .NET 7. |
@davidfowl thanks |
I see lots of code snippets here :) which is the recommended implementation until this is officially supported? |
Scrutor has about 65M downloads in the meantime 😅 |
AB#1244416
Currently Scrutor has these:
https://github.com/khellang/Scrutor/blob/338a59333c7eafe25d5aefdd22434653c83eb9ab/src/Scrutor/ServiceCollectionExtensions.Decoration.cs#L10
I think we should consider adding something similar in the box for a couple of reasons:
This needs to be built in a way that doesn't require container authors to change anything (that's why extension methods are being proposed).
cc @khellang Since he may want to contribute this 😉.
Some things I am concerned about in the above implementation:
ActivatorUtilities.GetServiceOrCreateInstance
should be swapped withCreateFactory
.Works
The text was updated successfully, but these errors were encountered: