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

Enhance decorator support and syntax #880

Open
tillig opened this Issue Oct 27, 2017 · 22 comments

Comments

Projects
None yet
6 participants
@tillig
Contributor

tillig commented Oct 27, 2017

There are several issues around how we handle decorators and enhancements that are desired in that space. Updating the syntax and adding features sort of go hand-in-hand since, for the most part, it'll all require some changes to the internals and will likely introduce some overall breaking changes.

This issue is to round up discussions about what we want decorator syntax to look like and what additional features we want to support. After that's determined, let's get it done. Fixing up decorator syntax has been a long time coming.

Issues:

  • #529: Handle .As<T> and .AsImplementedInterfaces correctly for decoration targets
  • #727: Conditional decorator registration
  • #776: Composite pattern support
  • #874: Decorator registration without keys

I think @alexmg had a spike going with some ideas on an updated decorator syntax. I'll let him chime in as able. In the meantime, I'll close these other issues and we can handle it here.

@waynebrantley

This comment has been minimized.

waynebrantley commented Jan 19, 2018

@tillig Any movement on this? This is still quite a pain-point that I have had for a few years.

@tillig

This comment has been minimized.

Contributor

tillig commented Jan 19, 2018

Any "movement" will be updated in this issue. No need to ping; the point of the issue here is to ensure visibility into any progress. It is recognized as a pain point; it's also recognized as not a trivial thing to fix that will likely create breaking changes to the API. As such... well, there are only two project owners and there are a lot of issues. Huge speculative issues that will bring breaking changes are difficult to get momentum on.

@waynebrantley

This comment has been minimized.

waynebrantley commented Feb 3, 2018

Understood - I thought movement was happening.
I will just not use your open decoration pattern.
I constructed a list of everything that needs decorated and called my own lambda to determine decorators - sort that into a list of unique set of decorators and then decorate each set. I then can use your decorator method - because I know the last decorator in the list I can make it work with your keyed registration system.

This way I will not have huge possibilities to deal with and can get the lambda based conditional decorator support I need so bad!

Sorry for any inconvenience @tillig

@alexmg

This comment has been minimized.

Contributor

alexmg commented Feb 7, 2018

I had a chance on the weekend to play around with some ideas in a branch and have pushed the branch up to GitHub. The unit tests linked to below probably provide the best description of where the journey has taken me so far.

https://github.com/autofac/Autofac/blob/decorators/test/Autofac.Test/Features/Decorators/DecoratorRegistrationSourceTests.cs

For those who aren't up for reading through some unit tests I'll try to summarise the key points here.

The RegisterDecorated method registers an implementation type (ImplementorA) as a service type (IDecoratedService) and provides a hint that this service may be decorated. It essentially provides a mechanism to pin an IRegistrationSource implementation to that can create the decorator chain dynamically at runtime. If no decorators are applied the implementation type is returned when the service type is resolved.

You can resolve this service as though it was a regular TypedService so no special resolve methods are needed. It would be nice to add a decorator on top of any existing TypedService but doing so would ideally completely replace the original registration which currently isn't possible. I might follow that thread again to see what can be done.

builder.RegisterDecorated<ImplementorA, IDecoratedService>();

You can configure additional attributes of the registration such as lifetime scope, metadata, parameters and such.

builder.RegisterDecorated<ImplementorA, IDecoratedService>().SingleInstance();

The RegisterDecorator method registers an implementation type (DecoratorA) that will act as a decorator for a given service type (IDecoratedService). Decorators are applied in the order that they are registered.

builder.RegisterDecorated<ImplementorA, IDecoratedService>(); // May be decorated, but doesn't have to be.
builder.RegisterDecorator<DecoratorA, IDecoratedService>(); // Will be applied first
builder.RegisterDecorator<DecoratorB, IDecoratedService>(); // Will be applied last

You cannot configure additional attributes of the registration such as lifetime scope on the decorated registration. It will inherit the lifetime scope and other attributes such as metadata of the service it is decorating.

builder.RegisterDecorated<ImplementorA, IDecoratedService>(); // This method returns void.

The generic type constraints ensure that ImplementorA or DecoratorA actually implement IDecoratedService, but if these are not known ahead of time a Type can be provided instead.

builder.RegisterDecorated(typeof(ImplementorA), typeof(IDecoratedService));
builder.RegisterDecorator(typeof(DecoratorA), typeof(IDecoratedService));
builder.RegisterDecorator(typeof(DecoratorB), typeof(IDecoratedService));

It is possible to conditionally register decorators. In the example below, DecoratorA is not applied because its condition requires there to already be an existing decorator. The context is an instance of IDecoratorContext and is updated as each decorator is applied. That specific instance of the IDecoratorContext can also be injected into any of the decorators.

builder.RegisterDecorated<ImplementorA, IDecoratedService>();
builder.RegisterDecorator<DecoratorA, IDecoratedService>(context => context.AppliedDecorators.Any());
builder.RegisterDecorator<DecoratorB, IDecoratedService>();

The context also provides access to the last instance created in the decorator chain which is initially the inner implementation and then updated to decorators as they are applied. This could open up some interesting runtime decorator chain construction scenarios. I would be interested to know if people think this would be useful.

builder.RegisterDecorated<ImplementorA, IDecoratedService>();
builder.RegisterDecorator<DecoratorA, IDecoratedService>(ctx => ((IDecoratedService)ctx.CurrentInstance).Foo == "A");
builder.RegisterDecorator<DecoratorB, IDecoratedService>(ctx => ((IDecoratedService)ctx.CurrentInstance).Foo == "B");
builder.RegisterDecorator<DecoratorC, IDecoratedService>(ctx => ((IDecoratedService)ctx.CurrentInstance).Foo == "C");

The full list of applied decorator instances is available on the context. Maybe having a dedicated property for the implementation instance, and leaving accessing the last applied decorator as AppliedDecorators.LastOrDefault() would be better.

public interface IDecoratorContext
{
	Type ImplementationType { get; }
	Type ServiceType { get; }
	IEnumerable<Type> AppliedDecoratorTypes { get; }
	IEnumerable<object> AppliedDecorators { get; }
	object CurrentInstance { get; }
}

Any parameters that are configured on the RegisterDecorated are available to it as normal. Parameters provided at runtime are passed through the entire decorator chain.

var parameter = new NamedParameter("parameter", "ABC");
var instance = container.Resolve<IDecoratedService>(parameter); // Parameter will be passed to all decorators and implementation type.

As I mentioned in #824 (comment) I'm not sure this is actually a good idea but wanted to see if it was possible.

I'll continue to post updates here and welcome all feedback. It's early stages and there is no support for open generic decorators but it should work the same way except with trickier runtime type binding validation.

@drauch

This comment has been minimized.

drauch commented Feb 7, 2018

Hi! Great to hear that there is some action going on regarding decorator registrations 👍 Nice work!

Two feedback points from my side:

  1. Is there a way to register decorators without using the generic overload, something like:

builder.RegisterDecorator(ctx => new MyDecorator(1, 2, 3, ctx.CurrentInstance, 4, 5)).As<IMyInterface>();

?

  1. Why do we need to register the "library type" as RegisterDecorated? Why isn't it possible to decorate arbitrary registered types? We often have a use case that a library already provides some modules with registrations and we want to add a decorator to one of the classes (as an extension point mechanism). Most probably this library type is not registered as RegisterDecorated ... allowing to decorate any registered service would help a lot in this particular situations.

Best regards,
D.R.

@waynebrantley

This comment has been minimized.

waynebrantley commented Feb 7, 2018

Looks like you made some good progress!
For me - conditional decorators are not so much a 'runtime condition', but a registration condition.

Have a look at my comment #727 (comment)

So, at registration time, I will look at some attribute on the Query class - maybe look for [Permission] attribute or something. If it is there, I will decorate the handler with the permission decorator. If not, no need for it, etc.

I currently have this all working with open generics.
I pass my code typeof(IQuery<>) and typeof(IQueryHandler<,>) and a lambda.
From that, I find every implementation of IQuery<> and the IQueryHandler handler for that object. I then call the lambda with both of those types. The lambda returns a List that contains every decorator to use... like typeof(QueryPermissionDecorator<>). I then use that list and they keyed registration to make it happen.

I do not select decorators at resolution based on some condition - but rather at registration time.

Here is an example usage:

private void RegisterQueries(ContainerBuilder builder)
{
    var permissionDecorator = typeof(QueryHandlerPermissionDecorator<,>);

    //public delegate TResult Func<in T, out TResult>(T arg);
    List<Type> QueryDecoratorFunc(CustomRegistrationExtensions.CqrsType cqrsTypeData)
    {
        var decorators = new List<Type>();
        var permissionAttribute = cqrsTypeData.CommandOrQueryType.GetCustomAttribute<PermissionAttribute>();
        if (permissionAttribute != null && permissionAttribute.RightCheck != PermissionAttribute.RightChecking.Public)
            decorators.Add(permissionDecorator);
        return decorators;
    }

    CustomRegistrationExtensions.RegisterCqrsHandlersWithDynamicDecorators(builder, typeof(IQueryHandler<,>), typeof(IQuery<>), QueryDecoratorFunc, _assemblies);
}

@alexmg alexmg self-assigned this Mar 24, 2018

@alexmg

This comment has been minimized.

Contributor

alexmg commented Mar 24, 2018

Thanks for the feedback everyone. I have taken it on board and made some changes.

I managed to remove the requirement for using RegisterDecorated as a marker registration. You can register the implementation service as normal and then use the new decorator syntax for registering your decorators.

builder.RegisterType<ImplementorA>().As<IDecoratedService>();
builder.RegisterDecorator<DecoratorA, IDecoratedService>();

When adding a registration in a child lifetime scope, if there is a decorator already registered in a parent scope, then it will be applied to the new registration.

var builder = new ContainerBuilder();
builder.RegisterDecorator<DecoratorA, IDecoratedService>();
var container = builder.Build();

var scope = container.BeginLifetimeScope(b => b.RegisterType<ImplementorA>().As<IDecoratedService>());
var instance = scope.Resolve<IDecoratedService>();

Assert.IsType<DecoratorA>(instance);

A decorator can be registered in a child lifetime scope and will only be applied to resolutions within that scope.

var builder = new ContainerBuilder();
builder.RegisterType<ImplementorA>().As<IDecoratedService>();
var container = builder.Build();

var scope = container.BeginLifetimeScope(b => b.RegisterDecorator<DecoratorA, IDecoratedService>());

var scopedInstance = scope.Resolve<IDecoratedService>();
Assert.IsType<DecoratorA>(scopedInstance);

var rootInstance = container.Resolve<IDecoratedService>();
Assert.IsType<ImplementorA>(rootInstance);

You can now register decorators using a lambda syntax as shown below, where c is IComponentContext, p is IEnumerable<Parameter>, and i is IDecoratedService.

builder.RegisterType<ImplementorA>().As<IDecoratedService>();
builder.RegisterDecorator<IDecoratedService>((c, p, i) => new DecoratorA(i));

I also introduced the same functionality for open generic decorators (with the exception of lambda based registrations). There is no need to call a RegisterDecorated method for open generics either. The registrations are made as normal using the existing RegisterGeneric method.

var builder = new ContainerBuilder();
builder.RegisterGeneric(typeof(ImplementorA<>)).As(typeof(IDecoratedService<>);
builder.RegisterGenericDecorator(typeof(DecoratorA<>), typeof(IDecoratedService<>));
var container = builder.Build();

Assert.IsType<DecoratorA<int>>(container.Resolve<IDecoratedService<int>>());

The decorators (regular and open generic) work in conjunction with the existing relationship types like Func, Owned, Lazy and IEnumerable.

var builder = new ContainerBuilder();
builder.RegisterGeneric(typeof(ImplementorA<>)).As(typeof(IDecoratedService<>));
builder.RegisterGeneric(typeof(ImplementorB<>)).As(typeof(IDecoratedService<>));
builder.RegisterGenericDecorator(typeof(DecoratorA<>), typeof(IDecoratedService<>));
var container = builder.Build();

var services = container.Resolve<IEnumerable<IDecoratedService<int>>>();

Assert.Collection(
    services,
    s =>
    {
        Assert.IsType<DecoratorA<int>>(s);
        Assert.IsType<ImplementorA<int>>(s.Decorated);
    },
    s =>
    {
        Assert.IsType<DecoratorA<int>>(s);
        Assert.IsType<ImplementorB<int>>(s.Decorated);
    });

Decorator services are tracked when added to the component registry so that when resolving a service decoration is only attempted if the service is known to be decorated.

The new decorator support lives alongside the existing decorator implementation and there are no breaking changes in the API.

You can test out the updates by grabbing the 4.6.2-decorators-00449 package from our MyGet feed.

https://www.myget.org/F/autofac/api/v3/index.json

I would love for people to take this for a test and drive and help lookout for edge cases. Depending on feedback I will merge the changes from the decorators feature branch into the dev branch.

The DecoratorTests and OpenGenericDecoratorTests fixtures in the decorator branch are the currently best place to find additional usage examples.

https://github.com/autofac/Autofac/tree/decorators/test/Autofac.Test/Features/Decorators

@waynebrantley

This comment has been minimized.

waynebrantley commented Mar 24, 2018

I also introduced the same functionality for open generic decorators (with the exception of lambda based registrations). There is no RegisterDecorator method for open generics either. The registrations are made as normal using the existing RegisterGeneric method.

Why is this the case. Nearly 100% of my use of decorators are around open generics.

@alexmg

This comment has been minimized.

Contributor

alexmg commented Mar 25, 2018

That was a typo on my part @waynebrantley. It was meant to say there is no RegisterDecorated method (not RegisterDecorator) to indicate there was no need for special marker registrations for open generics either. I have fixed up the comment to avoid any further confusion.

As you can see in the later examples open generic decorators are most certainly included and work in exactly the same way, with the exception of not having a registration method that uses a lambda to return the decorator. You use the existing RegisterGeneric method to register an open generic, and the new RegisterGenericDecorator method that doesn't require from/to keys to register a decorator for it.

@waynebrantley

This comment has been minimized.

waynebrantley commented Mar 25, 2018

What you have done here looks like it will make conditional registration much easier - now that we don't have to know the 'last one' in advance.

Why no lambda for open generics? That is pretty much what I need for completely not needing additional code. For example, if the class that is the type for the open generic has an attribute of some type on it - I want to decorate it..otherwise I do not...etc. Is there a reason for not having the lambda?

@alexmg

This comment has been minimized.

Contributor

alexmg commented Apr 2, 2018

@waynebrantley I think you are referring to the lambda used to define the decorator condition and the open generic decorators definitely have that. This is where you would put your attribute based condition.

builder.RegisterGenericDecorator(typeof(DecoratorA<>), typeof(IDecoratedService<>), context => context.ImplementationType.GetTypeInfo().GetCustomAttribute<Attribute>() != null);

What they don't have is a lambda for declaring the actual decoration process, as the type of the instance that would be passed into the lambda is not know at the time of registration. In the example below for non open generic decorators, the instance parameter i can be passed into DecoratorA because the type is known in advance through the IDecoratedService type parameter on the RegisterDecorator method.

builder.RegisterDecorator<IDecoratedService>((c, p, i) => new DecoratorA(i));

You can't do that with open generic registrations because they are declared with Type parameters passed to the RegisterGenericDecorator method.

@waynebrantley

This comment has been minimized.

waynebrantley commented Apr 9, 2018

What is needed is to send the types of the things that make up the openGeneric into the lambda.
For example:
If typeof(IQueryHandler<,>) is the open generic decorator, I would want the two types of the two parameters that are used in the IQueryHandler to be passed in.

So a real example is this:

[SomeAttribute]
public class SomeQuery: IQuery<SomeResult> {}
public class SomeQueryHandler: IQueryHandler<SomeQuery, SomeResult> {}...

If the IQuery parameter to the IQueryHandler has [SomeAttribute] on it, then I want to decorate it with something extra. If it does not have the attribute I do not want to. So, to achieve this, the lambda would need pass in the two types SomeQuery and SomeResult

Am I making any sense?

@alexmg

This comment has been minimized.

Contributor

alexmg commented Aug 6, 2018

@waynebrantley You have access to the service type and implementation type via the decorator context and can extract the generic type arguments, look for attributes, or anything else that you like.

@alexmg

This comment has been minimized.

Contributor

alexmg commented Aug 26, 2018

I have pushed a pre-release 4.9.0-beta1 package to NuGet with the changes described above. There is an accompanying blog post that summarises much of the content from this thread. It would be awesome if those interested in these enhancements could take it for a spin and provide feedback.

@drauch

This comment has been minimized.

drauch commented Aug 26, 2018

I only had time to read the blog post, but I like it. Maybe always provide the initial object in the IDecoratedContext ... on one hand this can be misused for weird stuff, on the other hand you are a very basic library and maybe someone can make use of it in some scenario we can't even imagine now.

@fschmied

This comment has been minimized.

fschmied commented Aug 27, 2018

It would be awesome if those interested in these enhancements could take it for a spin and provide feedback.

I, too, have read the blog post, and I like the feature! It implements exactly what we need.

@snboisen

This comment has been minimized.

snboisen commented Aug 28, 2018

I'm not currently working on a project using Autofac, but from the blog post it sounds exactly like what I wanted back when I set up a couple of projects to use Autofac. Great job!

@alexmg

This comment has been minimized.

Contributor

alexmg commented Aug 29, 2018

Maybe always provide the initial object in the IDecoratedContext

@drauch I assume you mean the implementation instance that is initially available in the CurrentInstance property before the first decorator is applied. That sounds like a good idea.

@alexmg

This comment has been minimized.

Contributor

alexmg commented Aug 29, 2018

Thanks for the feedback @fschmied and @snboisen. If you get the chance to use the beta version please let us know if you come across any issues or have suggestions.

@waynebrantley

This comment has been minimized.

waynebrantley commented Aug 31, 2018

@alexmg this looks great. I will hook this into my CQRS project and see how it all ends up looking. I also have unit tests for the decorations - so I will exercise this against this new build. Thanks and I will be back in touch.

@alexmg

This comment has been minimized.

Contributor

alexmg commented Aug 31, 2018

Thanks @waynebrantley. That would be awesome.

@waynebrantley

This comment has been minimized.

waynebrantley commented Sep 10, 2018

@alexmg Sorry for the delay. I have not 'tested' functionality - a bit stuck on the api.

Here are the extensions I currently have to make this happen (based on your original code)

        public static IRegistrationBuilder<TLimit, TScanningActivatorData, TRegistrationStyle> AsClosedTypesOfWithDecorators<TLimit, TScanningActivatorData, TRegistrationStyle>(
            this IRegistrationBuilder<TLimit, TScanningActivatorData, TRegistrationStyle> registration,
            ContainerBuilder builder, Type commandType, params Type[] decorators) where TScanningActivatorData : ScanningActivatorData
        {
            if (commandType == null) throw new ArgumentNullException(nameof(commandType));
            if (decorators == null || decorators.Length == 0)
                return registration.AsClosedTypesOf(commandType);

            string actualHandler = Guid.NewGuid().ToString();
            var returnValue = registration
                .AsNamedClosedTypesOf(commandType, t => actualHandler);

            string lastHandler = actualHandler;
            var lastDecorator = decorators.Last();
            foreach (var decorator in decorators) //take all but last decorator...it cannot be keyed..
            {
                var newKey = Guid.NewGuid().ToString();
                var decoratorRegistration = builder.RegisterGenericDecorator(
                    decorator,
                    commandType, lastHandler);
                if (decorator != lastDecorator) //cannot key the last decorator...
                    decoratorRegistration.Keyed(newKey, commandType);
                lastHandler = newKey;
            }

            return returnValue;
        }

        // This is the important custom bit: Registering a named service during scanning.
        public static IRegistrationBuilder<TLimit, TScanningActivatorData, TRegistrationStyle>
            AsNamedClosedTypesOf<TLimit, TScanningActivatorData, TRegistrationStyle>(
            this IRegistrationBuilder<TLimit, TScanningActivatorData, TRegistrationStyle> registration,
            Type openGenericServiceType,
            Func<Type, object> keyFactory)
            where TScanningActivatorData : ScanningActivatorData
        {
            if (openGenericServiceType == null) throw new ArgumentNullException(nameof(openGenericServiceType));

            return registration
                .Where(candidateType => candidateType.IsClosedTypeOf(openGenericServiceType))
                .As(candidateType => candidateType.GetTypesThatClose(openGenericServiceType).Select(t => (Service)new KeyedService(keyFactory(t), t)));
        }

Here is example code that uses those extensions

builder.RegisterAssemblyTypes(typeof(SomeClass).Assembly)
       .AsClosedTypesOfWithDecorators(builder, typeof(ICommandHandler<>),
           typeof(CommandHandlerValidationDecorator<>), typeof(CommandHandlerEventDecorator<>));
builder.RegisterAssemblyTypes(typeof(SomeClass).Assembly)
       .AsClosedTypesOfWithDecorators(builder, typeof(IAsyncCommandHandler<,>),
           typeof(CommandHandlerValidationDecoratorAsync<,>), typeof(CommandHandlerEventDecoratorAsync<,>));
builder.RegisterAssemblyTypes(typeof(SomeClass).Assembly)
    .AsClosedTypesOfWithDecorators(builder, typeof(IAsyncCommandHandler<>),
        typeof(CommandHandlerValidationDecoratorAsync<>), typeof(CommandHandlerEventDecoratorAsync<>));

//and an example where there is no decorator - just the original handler
builder.RegisterAssemblyTypes(typeof(SomeClass).Assembly)
       .AsClosedTypesOfWithDecorators(builder, typeof(ICommandHandler<>));

Using your new code it would look like this:

                    builder.RegisterGenericDecorator(typeof(ICommandHandler<>), typeof(CommandHandlerValidationDecorator<>));
                    builder.RegisterGenericDecorator(typeof(ICommandHandler<>), typeof(CommandHandlerEventDecorator<>));
                    builder.RegisterGenericDecorator(typeof(IAsyncCommandHandler<,>), typeof(CommandHandlerValidationDecoratorAsync<,>));
                    builder.RegisterGenericDecorator(typeof(IAsyncCommandHandler<,>), typeof(CommandHandlerEventDecoratorAsync<,>));
                    builder.RegisterGenericDecorator(typeof(IAsyncCommandHandler<>), typeof(CommandHandlerValidationDecoratorAsync<>));
                    builder.RegisterGenericDecorator(typeof(IAsyncCommandHandler<>), typeof(CommandHandlerEventDecoratorAsync<>));

This brings up a few issues

  • Currently, it finds all ICommandHandlers<> in a list of assemblies and decorates those with a list of decorators. How do I do that now? Not sure what assemblies yours is operating on? I am a bit confused.
  • The last example is where there is no decorator - and it just hooks up the closed types - the original undercoated handler. How is that all handled?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment