In my application, I want to construct the following object graphs using my DI Container, Simple Injector:
new Mode1(
new CommonBuilder(
new Grouping(
new GroupingStrategy1())), // NOTE: Strategy 1
new CustomBuilder());
new Mode2(
new CommonBuilder(
new Grouping(
new GroupingStrategy2())), // NOTE: Strategy 2
new CustomBuilder());
The following classes represent the graphs above:
public class Mode1 : IMode
{
private readonly ICommonBuilder commonBuilder;
private readonly ICustomBuilder customBuilder;
public Mode1(ICommonBuilder commonBuilder, ICustomBuilder ICustomBuilder customBuilder)
{
this.commonBuilder = commonBuilder;
this.customBuilder = customBuilder;
}
public void Run()
{
this.commonBuilder.Build();
this.customBuilder.Build();
//some code specific to Mode1
}
}
public class Mode2 : IMode
{
//same code as in Mode1
public void Run()
{
this.commonBuilder.Build();
this.customBuilder.Build();
//some code specific to Mode2
}
}
With CommonBuilder
and Grouping
being:
public class CommonBuilder : ICommonBuilder
{
private readonly IGrouping grouping;
public CommonBuilder(IGrouping grouping)
{
this.grouping = grouping;
}
public void Build()
{
this.grouping.Group();
}
}
public class Grouping : IGrouping
{
//Grouping strategy should be binded based on mode it is running
private readonly IGroupingStrategy groupingStrategy;
public Grouping(IGroupingStrategy groupingStrategy)
{
this.groupingStrategy = groupingStrategy;
}
public void Group()
{
this.groupingStrategy.Execute();
}
}
I am using Simple Injector for DI in my project. As shown above, I've go 2 modes of code which are called as per user preference, I've got common code for each mode (which I don't want to duplicate), I want to bind my grouping strategy (I've go 2 grouping strategies, one for each mode) in my common code based on the mode of execution. I've come across solution to use factories and switch between bindings at run time, but I'don't want go with that solution as I've same scenario at multiple places in my code (I'll end up creating multiple factories).
Can anyone suggest how to do the binding in cleaner way
You can use Context-Based Injection. However, because the context-based injection based on consumer of the dependency's consumer (or parents of their parents) can lead to all sorts of complications and subtle bugs (especially when caching lifestyles such as Scoped
and Singleton
are involved), Simple Injector's API limits you to looking one level up.
There are several ways to work around this seeming limitation in Simple Injector. The first thing you should do is take a step back and see whether or not you can simplify your design, as these kinds of requirements often (but not always) come from design inefficiencies. One such issue is Liskov Substitution Principle (LSP) violations. From this perspective, it's good to ask yourself the question, would Mode1
break when it gets injected with a Grouping
that contains the strategy for Mode2
? If the answer is yes, you are likely violating the LSP and you should first and foremost try to fix that problem first. When fixed, you'll likely see your configuration problems go away as well.
If you determined the design doesn't violate LSP, the second-best option is to burn type information about the consumer's consumer directly into the graph. Here's a quick example:
var container = new Container();
container.Collection.Append<IMode, Mode1>();
container.Collection.Append<IMode, Mode2>();
container.RegisterConditional(
typeof(ICommonBuilder),
c => typeof(CommonBuilder<>).MakeGenericType(c.Consumer.ImplementationType),
Lifestyle.Transient,
c => true);
container.RegisterConditional(
typeof(IGrouping),
c => typeof(Grouping<>).MakeGenericType(c.Consumer.ImplementationType),
Lifestyle.Transient,
c => true);
container.RegisterConditional<IGroupingStrategy, Strategy1>(
c => typeof(Model1) == c.Consumer.ImplementationType
.GetGenericArguments().Single() // Consumer.Consumer
.GetGenericArguments().Single(); // Consumer.Consumer.Consumer
container.RegisterConditional<IGroupingStrategy, Strategy2>(
c => typeof(Mode2)) == c.Consumer.ImplementationType
.GetGenericArguments().Single()
.GetGenericArguments().Single();
In this example, instead of using a non-generic Grouping
class, a new Grouping<T>
class is created, and the same is done for CommonBuilder<T>
. These classes can be a sub class of the non-generic Grouping
and CommonBuilder
classes, placed in your Composition Root, so you don't have to change your application code for this:
class Grouping<T> : Grouping // inherit from base class
{
public Grouping(IGroupingStrategy strategy) : base(strategy) { }
}
class CommonBuilder<T> : CommonBuilder // inherit from base class
{
public CommonBuilder(IGrouping grouping) : base(grouping) { }
}
Using this generic CommonBuilder<T>
,1 you make a registration where the T
becomes the type of the consumer it is injected into. In other words, Mode1
will be injected with a CommonBuilder<Mode1>
and Mode2
will get a CommonBuilder<Mode2>
. This is identical to what is common when registering ILogger
implementations as shown in the documentation. Because of the generic typing, however, CommonBuilder<Mode1>
will be injected with an Grouping<CommonBuilder<Mode1>>
.
These registrations aren't really conditional, rather contextual. The injected type changes based on its consumer. This construct, however, makes the type information of IGrouping
's consumer available in the constructed object graph. This allows the conditional registrations for IGroupingStrategy
to be applied based on that type information. This is what happens inside the registration's predicate:
c => typeof(Mode2)) == c.Consumer.ImplementationType // = Grouping<CommonBuilder<Mode2>>
.GetGenericArguments().Single() // = CommonBuilder<Mode2>
.GetGenericArguments().Single(); // = Mode2
In other words, if we can change the IGrouping
implementation in such way that its implementation's type (Grouping<T>
) provides information about its consumers (the IMode
implementations). This way the conditional registration of IGroupingStrategy
can use that information about it's consumer's consumer.
Here the registration requests the consumer's implementation type (which will be either Grouping<Mode1>
or Grouping<Mode2>
) and will grab the single generic argument from that implementation (which will be either Mode1
or Mode2
). In other words this allows us to get the consumer's consumer. This can be matched with the expected type to return either true
or false
.
Although this seems a bit awkward and complex, advantage of this model is that the complete object graph is known to Simple Injector, which allows it to analyze and verify the object graph. It also allows Auto-Wiring to take place. In other words, if IGrouping
or IGroupingStrategy
implementations have (other) dependencies, Simple Injector will automatically inject them and verify their correctness. It also allows you to visualize the object graph without losing any information. For instance, this is the graph that Simple Injector will show if you hover over it in the Visual Studio debugger:
Mode1(
CommonBuilder<Mode1>(
Grouping<CommonBuilder<Mode1>>(
Strategy1())))
The apparent downside of this approach is that if either CommonBuilder<T>
or Grouping<T>
are registered as singleton, there will now be a single instance per closed-generic type. This means that CommonBuilder<Mode1>
will be a different instance than CommonBuilder<Mode2>
.
Alternatively, you can also just make the CommonBuilder
registration conditional as follows:
var container = new Container();
container.Collection.Append<IMode, Mode1>();
container.Collection.Append<IMode, Mode2>();
container.RegisterConditional<ICommonBuilder>(
Lifestyle.Transient.CreateRegistration(
() => new CommonBuilder(new Grouping(new Strategy1())),
container),
c => c.Consumer.ImplementationType == typeof(Mode1));
container.RegisterConditional<ICommonBuilder>(
Lifestyle.Transient.CreateRegistration(
() => new CommonBuilder(new Grouping(new Strategy2())),
container),
c => c.Consumer.ImplementationType == typeof(Mode2));
This is a bit simpler than the previous approach, but it disables Auto-Wiring. In this case CommonBuilder
and its dependencies are wired by hand. When the object graph is simple (doesn't contain many dependencies), this method can be good enough. When dependencies are added to either CommonBuilder
, Grouping
or the strategies, however, this can cause high maintenance and might hide bugs as Simple Injector will be unable to verify the dependency graph on your behalf.
Please do be aware of the following statement in the documentation about the RegisterConditional
methods:
The predicates are only used during object graph compilation and the predicate’s result is burned in the structure of returned object graph. For a requested type, the exact same graph will be created on every subsequent call. This disallows changing the graph based on runtime conditions.