Using contextual information for resolution predicate

124 views Asked by At

I have a service type ITestGuard which I would like to implement with either a FooTestGuard or NullTestGuard, depending on the expression tree that the instance is being injected into. Specifically, I want to supply FooTestGuard for all cases other than when one of the 'ancestors' of the resolution request is of type TestController.

I figured I could do this with the ExpressionBuilding event, using this sample as a guideline, adding a new Parent property to the DependencyContext and populating it through a recursive descent:

[DebuggerDisplay("DependencyContext (ServiceType: {ServiceType}, ImplementationType: {ImplementationType})")]
public class DependencyContext
{
    public static readonly DependencyContext Root = new DependencyContext();

    public DependencyContext(
        Type serviceType,
        Type implementationType,
        ParameterInfo parameter,
        DependencyContext parent = null)
    {
        ServiceType = serviceType;
        ImplementationType = implementationType;
        Parameter = parameter;
        Parent = parent;
    }

    private DependencyContext() { }

    public Type ServiceType { get; private set; }
    public Type ImplementationType { get; private set; }
    public ParameterInfo Parameter { get; private set; }
    public DependencyContext Parent { get; private set; }
}

public static class ContextDependentExtensions
{
    public static IEnumerable<DependencyContext> AncestorsAndSelf(this DependencyContext context)
    {
        while (true)
        {
            yield return context;
            if (context.Parent == null)
                yield break;
            context = context.Parent;
        }
    }

    public static void RegisterWithContext<TService>(this Container container,
        Func<DependencyContext, TService> contextBasedFactory) where TService : class
    {
        if (contextBasedFactory == null)
            throw new ArgumentNullException("contextBasedFactory");

        Func<TService> rootFactory = () => contextBasedFactory(DependencyContext.Root);
        container.Register(rootFactory, Lifestyle.Transient);

        // Allow the Func<DependencyContext, TService> to be injected into parent types.
        container.ExpressionBuilding += (sender, e) =>
        {
            if (e.RegisteredServiceType != typeof(TService))
            {
                var rewriter = new DependencyContextRewriter(
                    contextBasedFactory,
                    rootFactory,
                    e.RegisteredServiceType,
                    e.Expression);

                e.Expression = rewriter.Visit(e.Expression);
            }
        };
    }

    private sealed class DependencyContextRewriter : ExpressionVisitor
    {
        private readonly object _contextBasedFactory;
        private readonly object _rootFactory;
        private readonly Type _serviceType;
        private readonly Expression _expression;
        private readonly DependencyContext _parentContext;
        private readonly ParameterInfo _parameter;

        public DependencyContextRewriter(object contextBasedFactory,
            object rootFactory,
            Type serviceType,
            Expression expression,
            DependencyContext parentContext = null,
            ParameterInfo parameter = null)
        {
            _serviceType = serviceType;
            _contextBasedFactory = contextBasedFactory;
            _rootFactory = rootFactory;
            _expression = expression;
            _parentContext = parentContext;
            _parameter = parameter;
        }

        private Type ImplementationType
        {
            get
            {
                var expression = _expression as NewExpression;

                if (expression == null)
                    return _serviceType;

                return expression.Constructor.DeclaringType;
            }
        }

        protected override Expression VisitNew(NewExpression node)
        {
            var context = new DependencyContext(_serviceType, ImplementationType, _parameter, _parentContext);
            var parameters = node.Constructor.GetParameters();

            var rewritten = node.Arguments
                .Select((x, i) => new DependencyContextRewriter(_contextBasedFactory, _rootFactory, x.Type, x, context, parameters[i]).Visit(x));

            return node.Update(rewritten);
        }

        protected override Expression VisitInvocation(InvocationExpression node)
        {
            if (IsRootedContextBasedFactory(node))
                return Expression.Invoke(
                    Expression.Constant(_contextBasedFactory),
                    Expression.Constant(
                        new DependencyContext(
                            _serviceType,
                            ImplementationType,
                            _parameter,
                            new DependencyContext(_serviceType, ImplementationType, _parameter, _parentContext))));

            return base.VisitInvocation(node);
        }

        private bool IsRootedContextBasedFactory(InvocationExpression node)
        {
            var expression = node.Expression as ConstantExpression;

            if (expression == null)
                return false;

            return ReferenceEquals(expression.Value, _rootFactory);
        }
    }
}

However, what I'm seeing is that the context hierarchy is not fully populated when it gets passed to the delegate. I debugged the visitor while requesting a TestController, and followed it down to the VisitInvocation step for an ITestGuard. However, the IsRootedContextBasedFactory check returned false, which skipped the delegate substitution. I think this is because it had already been substituted on a previous call to ExpressionBuilt, which meant that the registered expression was no longer rootFactory and so the check failed.

How can I change this visitor so that it correctly passes in contextual information, including the dependency hierarchy, to the contextBasedFactory delegate?

1

There are 1 answers

6
Steven On BEST ANSWER

What you are trying to achieve can't be done using the ExpressionBuilding event. This event allow you to look at the complete object graph. It might seem to work when your complete object graph consists solely of transient registrations, but it will break immediately when any other lifestyle is used. It becomes impossible to 'look down' the object graph in case you're dealing with Expression trees.

The RegisterWithContext method is limited in by the structure of the built Expression tree, but even if the container would contain support to supply you with information about the registration's parents, this will never work out as you would expect.

The simplest demonstration of this is when the direct parent of your FooTestGuard is registered as singleton. Since Simple Injector guarantees a registration with the Singleton lifestyle to have at most one instance within the container instance. But it is impossible to give that single instance two different ITestGuard dependencies at the same time. To solve this, Simple Injector should either:

  1. Loosen the guarantees on singleton and create two instances of ITestGuard's parent, therefore breaking the promise to create only one instance.
  2. Stick with the guarantee of only creating one instance, which means that depending on which graph is resolved first that graph will either contain a FooTestGuard or a NullTestGuard.

I hope this simple example shows that both options will both be pretty bad solutions. And this is just a simple example. When working with other lifestyles or more complex object graphs it will be really easy to eventually fall into this trap and introduce bugs in your application.

Do note that this is not a limitation of Simple Injector, but a mathematical truth. Don't be misled that there is another DI library (read: Ninject) actually allows you to traverse the object graph up. You will encounter the same problems as I described here.

So instead of really complicating your configuration, you will be much better of using a custom proxy class that allows you to switch implementations at runtime:

public class TestGuardSelector : ITestGuard
{
    private readonly Func<bool> selector;
    private readonly ITestGuard trueGuard;
    private readonly ITestGuard falseGuard;

    public TestGuardSelector(Func<bool> selector, ITestGuard trueGuard,
        ITestGuard falseGuard) {
        this.selector = selector;
        this.trueGuard = trueGuard;
        this.falseGuard = falseGuard;
    }

    public object TestGuardMethod(object value) {
        // Forward the call
        return this.CurrentGuard.TestGuardMethod(value);
    }

    private ITestGuard CurrentGuard {
        get { return this.selector() ? this.trueGuard : this.falseGuard; }
    }
}

This proxy can be registered as follows:

container.RegisterSingle<ITestGuard>(new TestGuardSelector(
    () => HttpContext.Current.Request.Url.Contains(@"\Test\"),
    new FooTestGuard(),
    new NullTestGuard());