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?
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 builtExpression
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 theSingleton
lifestyle to have at most one instance within the container instance. But it is impossible to give that single instance two differentITestGuard
dependencies at the same time. To solve this, Simple Injector should either:ITestGuard
's parent, therefore breaking the promise to create only one instance.FooTestGuard
or aNullTestGuard
.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:
This proxy can be registered as follows: