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

Enhance decorator support and syntax #880

Closed
tillig opened this issue Oct 27, 2017 · 41 comments
Closed

Enhance decorator support and syntax #880

tillig opened this issue Oct 27, 2017 · 41 comments
Assignees
Labels

Comments

@tillig
Copy link
Member

@tillig 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
Copy link

@waynebrantley 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
Copy link
Member Author

@tillig 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
Copy link

@waynebrantley 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
Copy link
Member

@alexmg 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
Copy link

@drauch 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
Copy link

@waynebrantley 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
Copy link
Member

@alexmg 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
Copy link

@waynebrantley 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
Copy link
Member

@alexmg 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
Copy link

@waynebrantley 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
Copy link
Member

@alexmg 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
Copy link

@waynebrantley 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
Copy link
Member

@alexmg 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
Copy link
Member

@alexmg 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
Copy link

@drauch 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
Copy link

@fschmied 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
Copy link

@snboisen 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
Copy link
Member

@alexmg 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
Copy link
Member

@alexmg 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
Copy link

@waynebrantley 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
Copy link
Member

@alexmg alexmg commented Aug 31, 2018

Thanks @waynebrantley. That would be awesome.

@waynebrantley
Copy link

@waynebrantley 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?
@waynebrantley
Copy link

@waynebrantley waynebrantley commented Nov 26, 2018

@alexmg Anything else you need?

@dotnetjunkie
Copy link

@dotnetjunkie dotnetjunkie commented Dec 12, 2018

I've tested the RegisterGenericDecorator method for chapter 13 of my book and it has proven to work very well. This is a fantastic improvement to the library, which I hope can be RTMed soon.

Great work.

@waynebrantley
Copy link

@waynebrantley waynebrantley commented Dec 15, 2018

@dotnetjunkie All this is of course around the CQRS pattern and I used your blog for inspiration several years ago. I would like to see the book and have some potentially really cool things I could offer for it.

@alexmg
Copy link
Member

@alexmg alexmg commented Feb 10, 2019

@dotnetjunkie Thank you. Knowing you have tested this for the book gives me confidence that the changes are working as expected.

@alexmg
Copy link
Member

@alexmg alexmg commented Feb 10, 2019

This is now available in the 4.9.0 release. Thank you to everyone for your feedback and contributions.

@alexmg alexmg closed this Feb 10, 2019
@waynebrantley
Copy link

@waynebrantley waynebrantley commented Feb 10, 2019

@alexmg Well, glad it is released - however, as you can see I had a few issues on how we could use your new API to accomplish what is necessary. Any comments?

(Thanks for getting this great feature out)

@tillig
Copy link
Member Author

@tillig tillig commented Feb 10, 2019

@waynebrantley You mentioned earlier you haven't actually tried the new API - have you tried it and still found problems? You posted some fairly complex scenarios with no repro, it might be worth trying the new system to see how it works. Worst case, you can still use the old syntax for the time being.

If you find a specific shortcoming that we can repro and address, maybe file an issue with that?

@waynebrantley
Copy link

@waynebrantley waynebrantley commented Feb 11, 2019

@tillig - I have not "run tests" or "run code" against the new API - because I cannot see a way to use it! My questions were about how to use the API - more about how does the current API you added help achieve the original goals. Once I could actually code the registrations - I can definitely run my existing test suite on it.

The two examples I gave were

  • How do you specify what assemblies the generic decorator searches with this syntax:
    builder.RegisterGenericDecorator(typeof(ICommandHandler<>), typeof(CommandHandlerValidationDecorator<>));

  • How do you just 'hookup' just a handler without a decorator? Or this is used solely do decorate after you register the original handler? I used to do it like below (with the code I added), so how do you do it now?

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

@tillig tillig commented Feb 11, 2019

The registration for a decorator doesn't search assemblies. It decorates things you have registered in the container. What you put in the container is up to you.

RegisterAssemblyTypes will scan the assemblies you pass it, but that is a separate thing from registering the decorators.

Here's what I'd recommend:

Look at the unit tests. The are some for decorating open generics and some without open generics. These show a pretty good range of usage of the new syntax.

Register the base open generics services with assembly scanning as usual. Use the new syntax to attach the decorators using examples from the unit tests.

Part of the help we need here might actually require a bit of spelunking on your part. We're doing our best to make things better, but it'd help if you could dive in the deep end, use intellisense and the available source, and hammer on it a bit. It may well turn out you can't do what you want or that it's hard; that's valuable feedback, too.

If that doesn't work, you're welcome to leave that code up there and we can get to it in time, but it likely won't be soon, and I apologize up front for that.

@waynebrantley
Copy link

@waynebrantley waynebrantley commented Feb 11, 2019

Ok, that makes sense...I was just coming at it from the "way I had to do it before". I will dig and report back.

@ArtemBan
Copy link

@ArtemBan ArtemBan commented Mar 20, 2019

Still no compositor pattern support?

@alexmg
Copy link
Member

@alexmg alexmg commented Mar 26, 2019

Hi @ArtemBan. Unfortunately, composite pattern support did not make it across the line. I am still interested in supporting it though and will open a separate issue for that feature.

@khamza85
Copy link

@khamza85 khamza85 commented Apr 19, 2019

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.

Consider multiple service implementations. Since the decorator has to inject the decoratee itself (Meta won't work), how is it possible to get inherited metadata?

Also, would be reasonable to inject the decoratee with Implicit Relationship Type? I often want to add cross-cutting concern for lazy, func or even child scoped support. In such case, the previous problem with Meta drops off.

@khamza85
Copy link

@khamza85 khamza85 commented Apr 19, 2019

Another observation. Generic IDecoratorContext support. Something like

   public interface IDecoratorContext<TImplementation, TService> {
        IReadOnlyList<TService> AppliedDecorators { get; }
        IReadOnlyList<Type> AppliedDecoratorTypes { get; } // redundant?
        TService CurrentInstance { get; }
    }

Sorry for flooding closed issue.

@tillig
Copy link
Member Author

@tillig tillig commented Apr 19, 2019

If there's an enhancement you'd like I'd recommend opening a new issue. Include a description of the enhancement and some examples of how you imagine it'd be used. We can start the discussion on it from there.

@alexmg
Copy link
Member

@alexmg alexmg commented Apr 22, 2019

@khamza85 The AppliedDecoratorTypes is technically redundant but was added for convenience to cater for the common usage scenario of checking if a particular decorator Type has been applied.

It simply allows for this:

var decorated = context.AppliedDecoratorTypes.Contains(typeof(MyDecorator));

Instead of this:

var decorated = context.AppliedDecorators.Select(d => d.GetType()).Any(d => d == typeof(MyDecorator));

It seemed worthwhile to make a common scenario a little easier to code.

@tillig
Copy link
Member Author

@tillig tillig commented Sep 24, 2020

We have a failing unit test, currently skipped that points to #529 and #880. It uses the older keyed-decorator syntax, like this:

public class DecoratingServiceThatHasDefaultImplementation
{
    private readonly IContainer _container;

    public DecoratingServiceThatHasDefaultImplementation()
    {
        const string from = "from";
        var builder = new ContainerBuilder();

        builder.RegisterType<Implementer1>().As<IService>().Named<IService>(from);
        builder.RegisterDecorator<IService>(s => new Decorator(s), from);

        _container = builder.Build();
    }

    [Fact(Skip = "Issue #529 => #880")]
    public void InstanceWithDefaultImplementationIsDecorated()
    {
        var decorator = _container.Resolve<IService>();
        Assert.IsType<Decorator>(decorator);
        Assert.IsType<Implementer1>(((Decorator)decorator).Decorated);
    }
}

This still doesn't pass in the v6 codeline, but... I'm wondering if this is interesting at all to solve. If we change it to use the new syntax, it passes:

public class DecoratingServiceThatHasDefaultImplementation
{
    private readonly IContainer _container;

    public DecoratingServiceThatHasDefaultImplementation()
    {
        const string from = "from";
        var builder = new ContainerBuilder();

        builder.RegisterType<Implementer1>().As<IService>().Named<IService>(from);

        // This is the change:
        builder.RegisterDecorator<Decorator, IService>();

        _container = builder.Build();
    }

    [Fact]
    public void InstanceWithDefaultImplementationIsDecorated()
    {
        var decorator = _container.Resolve<IService>();
        Assert.IsType<Decorator>(decorator);
        Assert.IsType<Implementer1>(((Decorator)decorator).Decorated);
    }
}

I'm thinking the best course would be to just drop this test. It isn't a problem with the new decorator syntax and, if we don't intend on fixing the old syntax, there's no value in trucking the test around being skipped forever.

/cc @alexmg @alistairjevans

@alistairjevans
Copy link
Member

@alistairjevans alistairjevans commented Sep 25, 2020

Agreed that dropping the test for the old syntax (but perhaps leaving in a test using the new syntax?).

The old syntax doesn't use the improved decorator pipeline behaviour anyway, so should be strongly discouraged.

@tillig
Copy link
Member Author

@tillig tillig commented Sep 25, 2020

I'll commit a test for this using the new syntax and remove the old test. I would wager we already have a test like this for the new stuff but... I'll look.

tillig added a commit that referenced this issue Sep 25, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
10 participants
You can’t perform that action at this time.