-
Notifications
You must be signed in to change notification settings - Fork 529
Contextual Binding
One of the more powerful (and complex) features of Ninject is its contextual binding system. We mentioned earlier that you can register more than one binding for a type. There are two primary reasons one might want to register multiple type bindings:-
In this case, the appropriate implementation that should be used depends on the target context for which the service type is being resolved and the binding metadata on each binding. Up until this point, we’ve only been talking about default bindings — bindings that are used unconditionally, in any context. However, unless you make one or more of the bindings conditional, you’ll get an exception at runtime indicating that the appropriate binding to use in a given context is ambiguous (you’ll run into the same case if you do a kernel.Get<T>()
and there are multiple possible results for the given service type T
).
This mechanism allows you to get a set of related service implementations at the same time – this is not contextual binding. You use this mechanism by requesting a IEnumerable<T>
, T[]
array or List<T>
in the target – see Multi-injection
class Warrior
{
readonly IEnumerable<IWeapon> _weapons;
public Warrior( IEnumerable<IWeapon> weapons) {
_weapons=weapons;
}
public void Attack(string victim) {
foreach(var weapon in _weapons)
Console.WriteLine(weapon.Hit(victim));
}
}
Named bindings are the simplest (and most common) form of conditional binding, and are used as follows:
- In your type bindings, you register multiple type bindings for the same service type, but add binding metadata so you’ll be able to indicate which one is appropriate in your condition later:-
Bind<IWeapon>().To<Shuriken>().Named("Strong");
Bind<IWeapon>().To<Dagger>().Named("Weak");
2. At the target location, you indicate the name you want to use in the given context:-
class WeakAttack {
readonly IWeapon _weapon;
public WeakAttack([Named("Weak")] IWeapon weakWeapon){
_weapon = weakWeapon;
}
public void Attack(string victim){
Console.WriteLine(_weapon.Hit(victim));
}
}
3. alternately, you can programmatically request a specifically named instance of a service directly (but be careful – doing so directly is typically an instance of the Service Location antipattern) by doing:
kernel.Get<IWeapon>("Weak");
The Named Bindings in the previous section are built-in and have associated overloads and attribute types for a very simple reason – they’re a very common use case that can often satisfy the requirements of most straightforward applications. However they’re just a specific application of a more general concept: Constrained Resolution.
Constrained resolution involves the following elements:
- binding metadata: extra information we include in a type binding
- the target context: the receiver which the service is being resolved for
- One or more of the following
- constraint predicates on the target using the binding metadata to take into consideration when matching a resolution target against a set of bindings
- constraints on the type binding which take into account the target context
Constraining the binding selection via ConstraintAttribute
-derived attributes on the injection target
ConstraintAttribute
is an abstract attribute class from which one can derive your own constraints which filter the set of eligible bindings for injection into a target. Example usage is as follows:
// will work just as well without this line, but it's more correct and important for IntelliSense etc.
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter,
AllowMultiple = true, Inherited = true)]
public class Swimmer : ConstraintAttribute {
public bool Matches(IBindingMetadata metadata) {
return metadata.Has("CanSwim") && metadata.Get<bool>("CanSwim");
}
}
class WarriorsModule : Ninject.Modules.NinjectModule {
public override void Load() {
Bind<IWarrior>().To<Ninja>();
Bind<IWarrior>().To<Samurai>().WithMetadata("CanSwim", false);
Bind<IWarrior>().To<SpecialNinja>().WithMetadata("CanSwim", true);
}
}
class AmphibiousAttack {
public AmphibiousAttack([Swimmer]IWarrior warrior) {
Assert.IsType<SpecialNinja>(warrior);
}
}
The above is a basic example of using the metadata to guide the selection of the appropriate binding. Here are some other examples:
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter,
AllowMultiple = true, Inherited = true)]
public class NonSwimmer : ConstraintAttribute {
public override bool Matches(IBindingMetadata metadata) {
return metadata.Has("CanSwim") && !metadata.Get<bool>("CanSwim");
}
}
class OnLandAttack {
public OnLandAttack([NonSwimmer]IWarrior warrior) {
Assert.IsType<Samurai>(warrior);
}
}
Since the conditionals are attached to the target, if no conditional is specified on the target then all bindings will be used. The following code will throw an ActivationException
because the results are ambiguous.
class JustAttack {
// Note: This will fail because we have three matching bindings.
public JustAttack(IWarrior warrior) {
}
}
If this is not what you want then see the next section on binding-based conditionals.
Note that one is allowed to have multiple attributes on a target – e.g., you can combine the built-in [Named("X")]
with a custom constraint such as [CanSwim]
above and it’ll add the two conditions together.
A key issue with explicitly managing binding metadata as string->object
dictionary mappings is that things can easily go wrong with magic strings – as a result it’s generally recommended to prefer tagging things with attributes in preference whereever possible.
An important point about previous section regarding Attribute
-driven constraints at the target side is that a sensible default approach is to use Constructor Injection and not have any container-dependencies in your business logic unless you have good reason. This is why constraints are more typically specified alongside the type bindings, although Ninject doesn’t mind either way – it’ll even let you constrain from the target side and the type binding side at the same time. Examples of constraints on a type binding are (specifying the same constraints as the previous section but on the bindings instead):
Bind<IWarrior>().To<Samurai>().WhenInjectedInto(typeof(OnLandAttack));
Bind<IWarrior>().To<SpecialNinja>().WhenInjectedInto(typeof(AmphibiousAttack));
There are a multiple constraint options, all specified via a When...
option at the end of the type binding. The simplest form are ones driven by straightforward marker attributes, without any need to derive from base classes etc.
class SwimmerNeeded : Attribute{}
class ClimberNeeded : Attribute{}
This allows one to define constraints based on any of the following:
- the Target: the parameter being injected into
- the Member: the property, method or constructor itself
- the Class: the class Type within which the Member or Target is defined within
class WarriorsModule : Ninject.Modules.NinjectModule {
public override void Load() {
Bind<IWarrior>().To<Ninja>();
Bind<IWarrior>().To<Samurai>().WhenClassHas<ClimberNeeded>();
Bind<IWarrior>().To<Samurai>().WhenTargetHas<ClimberNeeded>();
Bind<IWarrior>().To<SpecialNinja>().WhenMemberHas<SwimmerNeeded>();
}
}
Then the right things will happen when one resolves services such as:
class MultiAttack {
public MultiAttack([ClimberNeeded] IWarrior MountainWarrior) {
}
[Inject, SwimmerNeeded]
IWarrior OffShoreWarrior { get; set; }
[Inject]
IWarrior AnyOldWarrior {get;set;}
}
[ClimberNeeded]
class MountainousAttack {
[Inject, SwimmerNeeded]
IWarrior HighlandLakeSwimmer { get; set; }
[Inject]
IWarrior StandardMountainWarrior { get; set; }
}
Specifying constraints on the type binding using arbitrary elements of the resolution request context
All of the .WhenXXX()
helpers in the preceding section are just built-in specific applications of the generalized .When(Func<IRequest,bool>)
method, which one can use as follows:
Bind<IWarrior>().To<Samurai>().When(request => request.Target.Member.Name.StartsWith("Climbing"));
Bind<IWarrior>().To<Samurai>().When(request => request.Target.Type.Namespace.StartsWith("Samurais.Climbing"));
As with all of Ninject, it’s highly recommended to look at the tests
They go through all the possibilities in detail, and are extremely short and readable – try it; you won’t regret looking and you’re bound to learn something along the way!
Another way of doing contextual resolution is to have the factory take the context into consideration. For example, people often use a pattern as follows to do logging:
class ClassThatLogs {
ILog _log = LogFactory.CreateLog(typeof(ClassThatLogs));
// Sometimes one sees reflection based approaches too, which have efficiency issues
}
In the above, each logger
- needs to be customized for the context into which it is cut and pasted
- is being located rather than injected (aka Service Location antipattern)
An alternate approach is to make the Factory Method’s generated result contextual is to bind as follows:
Bind<ILog>().ToMethod( context => LogFactory.CreateLog( context.Request.Target.Member.DeclaringType ) );
Which enables having the loggers injected as follows:
class ClassThatLogs {
readonly ILog _log;
public ClassThatLogs(ILog log){
_log = log;
}
}
The above are just examples. If you are really doing logging with Ninject in the picture, please refer to Ninject.Extensions.Logging which an extension for that very purpose.
There are .Depth
, .ParentContext
and .ParentRequest
members on the IRequest passed to the When
predicate which are used to define rules based on the hierarchy of requests in a service resolution where nesting takes place. See The Activation Process for more details on this.
Some articles on topics related to nested resolution:
Continue reading: Conventions-Based Binding
Licensed under Apache 2 License
Contents
- Home
- Why Use Ninject
- Getting Started
- Dependency Injection By Hand
- Dependency Injection With Ninject
- Injection Patterns
- Multi Injection
- Object Scopes
- Modules and the Kernel
- Providers, Factory Methods and the Activation Context
- The Activation Process
- How Injection Works
- Contextual Binding
- Conventions-Based Binding