Search code examples
c#dependency-injectiondecoratorautofaccircular-dependency

Decorator for multiple interfaces - a circular dependency riddle in Autofac


I'm coming from Ninject but I decided to give Autofac a try since it seems more actively developed. So far I can say that registering decorators is not as easy as in Ninject using .WhenInjectedExactlyInto syntax. Anyway please bear with me as I am an Autofac newbie.

Here's the problem:

I have type A implementing interface IA that is decorated by A_Decorator. A_Decorator implements interfaces IA and IB, and in turn should be decorated by AB_Decorator that also implements both IA and IB. AB_Decorator takes two dependencies of types IA and IB (so it is a decorator for both) but they both should resolve to the same instance of A_Decorator. It looks something like this: AB_Decorator(A_Decorator(A) as IA, A_Decorator(A) as IB). When requesting service of type IA or type IB from Autofac container, they should reference a single AB_Decorator instance.

It is a bit tricky to describe by word, but here's the simplest code sample I could come up with that shows this situation (I've added instance IDs and trace messages to constructors to see what's going on):

using System;
using Autofac;

namespace AutofacExample
{
    internal interface IA { }

    internal interface IB { }

    class A : IA
    {
        static int _instanceCounter;
        readonly int Id = ++_instanceCounter;

        public A()
        {
            Console.WriteLine(this);
        }

        public override string ToString()
        {
            return $"{GetType().Name}[{nameof(Id)}={Id}]";
        }
    }

    class A_Decorator : IA, IB
    {
        static int _instanceCounter = 10;
        readonly int Id = ++_instanceCounter;

        /* decorated1 should reference instance of A */

        public A_Decorator(IA decoratedA)
        {
            Console.WriteLine($"{this}({nameof(decoratedA)}={decoratedA})");
        }

        public override string ToString()
        {
            return $"{GetType().Name}[{nameof(Id)}={Id}]";
        }
    }

    class AB_Decorator : IA, IB
    {
        static int _instanceCounter = 100;
        readonly int Id = ++_instanceCounter;

        /* Both decorated1 and decorated2 should reference the same instance of A_Decorator */

        public AB_Decorator(IA decoratedA, IB decoratedB)
        {
            Console.WriteLine($"{this}({nameof(decoratedA)}={decoratedA}, {nameof(decoratedB)}={decoratedB})");
        }

        public override string ToString()
        {
            return $"{GetType().Name}[{nameof(Id)}={Id}]";
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            ContainerBuilder builder = new ContainerBuilder();

            builder
                .RegisterType<A>()
                .Named<IA>(nameof(A))
                .SingleInstance();

            builder
                .RegisterType<A_Decorator>()
                .Named<IA>(nameof(A_Decorator))
                .Named<IB>(nameof(A_Decorator))
                .SingleInstance();

            builder
                .RegisterType<AB_Decorator>()
                .Named<IA>(nameof(AB_Decorator))
                .Named<IB>(nameof(AB_Decorator))
                .SingleInstance();

            /* A is decorated by A_Decorator as IA */
            builder
                .RegisterDecorator<IA>(
                    (c, decorated) =>
                        c.ResolveNamed<IA>(nameof(A_Decorator), TypedParameter.From(decorated)),
                    nameof(A))
                //.Keyed<IA>("innerA")
                //.Keyed<IB>("innerB")
                .SingleInstance();

            /* Trying to register AB_Decorator as IA creates circular dependency */
            //builder
            //    .RegisterDecorator<IA>(
            //        (c, decorated) =>
            //            c.ResolveNamed<IA>(nameof(AB_Decorator), TypedParameter.From(decorated)),
            //        "innerA")
            //    .SingleInstance();

            /* A_Decorator is decorated by AB_Decorator as IB */
            builder
                .RegisterDecorator<IB>(
                        (c, decorated) =>
                            c.ResolveNamed<IB>(nameof(AB_Decorator), TypedParameter.From(decorated)),
                        nameof(A_Decorator) /* "innerB" */)
                    .SingleInstance();

            IContainer container = builder.Build();

            IA a = container.Resolve<IA>();
            IB b = container.Resolve<IB>();

            Console.WriteLine($"{nameof(a)} == {nameof(b)} ? {ReferenceEquals(a, b)}");
            Console.WriteLine($"{nameof(a)} is {a.GetType().Name}");
            Console.WriteLine($"{nameof(b)} is {b.GetType().Name}");
        }
    }
}

Unfortunately requesting instance of IA gives me A_Decorator, while for IB I get AB_Decorator. Trying to uncomment the extra decorator registration block causes circular dependency exception (DependencyResolutionException: Circular component dependency detected: System.Object -> AutofacExample.AB_Decorator -> System.Object -> AutofacExample.AB_Decorator) and I can't make it work trying various combinations of the named registrations.

Does anyone know a solution to this? Thanks in advance.


Solution

  • Problem

    The problem lies with the decorator registrations for AB_Decorator. Specifically the lambda function resolving the AB_Decorator:

    ( c, decorated ) => c.ResolveNamed<IA>( nameof( AB_Decorator ), TypedParameter.From( decorated ) );
    

    The constructor for AB_Decorator takes in 2 arguments that should both be the same instance of A_Decorator which is being supplied to the lambda as decorated. However, decorated is being passed as a parameter only once via TypedParameter.From( decorated ). Therefore Autofac is going to try to resolve the second parameter via the container.

    Now the registrations for IB show that we should get a singleton instance with A_Decorator wrapped in AB_Decorator. So to resolve IB, the container must construct AB_Decorator. There's the problem, we're currently trying to resolve AB_Decorator as IA, but we need an IB to fulfill the constructor arguments for the AB_Decorator being constructed for IA. And IB is registered in the container as AB_Decorator. So you get:

    AB_Decorator(A_Decorator(A) as IA, AB_Decorator(A_Decorator(A) as IA, AB_Decorator(etc...))
    

    Solution

    We need to pass decorated into both parameters when resolving AB_Decorator. Like this:

    builder
        .RegisterDecorator<IA>(
            ( c, decorated ) =>
    
                c.ResolveNamed<IA>( nameof( AB_Decorator ),
                    new TypedParameter( typeof( IA ), decorated ),
                    new TypedParameter( typeof( IB ), decorated )
                )
            ,"innerA"
        )
        .SingleInstance();
    
    
    builder
        .RegisterDecorator<IB>(
            ( c, decorated ) =>
    
                c.ResolveNamed<IB>( nameof( AB_Decorator ),
                    new TypedParameter( typeof( IA ), decorated ),
                    new TypedParameter( typeof( IB ), decorated )
                )
            , nameof( A_Decorator ) /* "innerB" */
        )
        .SingleInstance();
    

    Now we are sending decorated which is A_Decorator to both the IA and the IB parameters. Directly constructing the TypedParameter instances allows me to specify the type I want the instance to fulfill in the parameters list for, in this case, AB_Decorator.