In this post I will show an example of how you can extend the standard features of the
Unity IoC container. I will show how an object is created in Unity "from the inside". I'll tell you about Unity Extensions, Strategies & Policies.
Suppose there is a Persistence component in our application that is responsible for saving objects. It is described by the IPersistence interface and has implementations - FilePersistence, DbPersistence, WsPersistence, InMemoryPersistence.
In the classic version, at the beginning of the application, we register the desired implementation in Unity and then, by calling Resolve for IPersistence, we always get it.
IUnityContainer uc = new UnityContainer();
uc.RegisterType<IPersistence, FilePersistence>();
IPersistence p = uc.Resolve<IPersistence>();
p.Add(obj);
* This source code was highlighted with Source Code Highlighter .
')
But what to do if the necessary implementation can change during the operation of the application. For example, is it specified in a config file, or should the network be unavailable, should FilePersistence be automatically used?
Unity has the ability to register dependencies by name. Example:
uc.RegisterType<IPersistence, InMemoryPersistence>( "none" );
uc.RegisterType<IPersistence, FilePersistence>( "file" );
uc.RegisterType<IPersistence, DbPersistence>( "db" );
uc.RegisterType<IPersistence, WsPersistence>( "ws" );
IPersistence p = uc.Resolve<IPersistence>( "file" ); // file .
IPersistence p = uc.Resolve<IPersistence>( "db" ); // db.
* This source code was highlighted with Source Code Highlighter .
It remains to ensure that when receiving an implementation without a name, Unity will somehow determine which implementation we need.
Let us have a delegate that we pass to Unity, which defines the desired implementation name.
Example:
uc.SetNameResolver<IPersistence>(GetPersistenceImplementationName);
IPersistence p = uc.Resolve<IPersistence>(); // , GetPersistenceImplementationName
* This source code was highlighted with Source Code Highlighter .
There is no standard way in Unity for this. But we will solve the problem by writing our own extension.
Unity extensions
The Unity extension is a class inherited from UnityContainerExtension. It has the extension context (ExtensionContext) and the virtual methods Initialize () and Remove () (respectively, are called when the extension is initialized and deleted).
Extensions are added through the methods of the AddNewExtension and AddExtension container, removed through RemoveAllExtensions.
public class NameResolverExtension : UnityContainerExtension
{
protected override void Initialize()
{
}
protected override void Remove()
{
}
public NameResolverExtension()
: base ()
{
}
}
uc.AddNewExtension<NameResolverExtension>();
* This source code was highlighted with Source Code Highlighter .
For an extension to be configured, it must implement the configurator interface inherited from IUnityContainerExtensionConfigurator. Configuring occurs through the method of container Configure.
//
public delegate string NameResolverDelegate(Type typeToBuild);
// -
public interface INameResolverExtensionConfigurator : IUnityContainerExtensionConfigurator
{
INameResolverExtensionConfigurator RegisterDelegatedName<TTypeToBuild>(
NameResolverDelegate resolver);
}
static private string GetPersistenceImplementationName(Type typeToBuild)
{
// ...
return "db" ;
}
uc.Configure<INameResolverExtensionConfigurator>()
.RegisterDelegatedName<IPersistence>(GetPersistenceImplementationName);
* This source code was highlighted with Source Code Highlighter .
Strategy
Each registered type in Unity has its own build key (buildKey). It consists of a registered type and the name under which it was registered.
The Unity Resolve process is implemented using strategies.
Strategy is a class that implements the IBuilderStrategy interface. It has four methods: PreBuildUp, PostBuildUp, PreTearDown, PostTearDown.
When calling Resolve:
- A list of registered strategies is created;
- The build-key of the required type and the context of construction (BuilderContext) are formed;
- The context is sequentially processed by strategies until one of them sets the BuildComplete flag to true.
In Unity, there are 4 predefined strategies that are invoked for each Resolve:
- BuildKeyMappingStrategy. Replaces the build key in the context of the type being searched for with the implementation key. In fact, all the resolving is happening here;
- LifetimeStrategy. Checks the presence of the implementation in the Lifetime Manager;
- ArrayResolutionStrategy. Resolving array dependencies
- BuildPlanStrategy. Creating an instance of the implementation (if it has not yet been created) and automatically resolving its dependencies.
We will write our strategist, which will replace in the build key an empty name for the desired implementation.
internal class ResolveNameBuilderStrategy : BuilderStrategy
{
private NamedTypeBuildKey ReplaceBuildKeyName(IBuilderContext context, NamedTypeBuildKey buildKey)
{
}
public override void PreBuildUp(IBuilderContext context)
{
if (context.BuildKey is NamedTypeBuildKey)
context.BuildKey = ReplaceBuildKeyName(context, (NamedTypeBuildKey)(context.BuildKey));
}
public ResolveNameBuilderStrategy()
: base ()
{
}
}
* This source code was highlighted with Source Code Highlighter .
Because Since the search key implementation is searched for in the BuildKeyMappingStrategy strategy, we need to register our strategy so that it is executed before BuildKeyMappingStrategy. Strategies are sorted according to the stage that was specified during registration.
In total there are 7 stages - Setup, TypeMapping, Lifetime, PreCreation, Creation, Initialization, PostInitialization. BuildKeyMappingStrategy is registered at the TypeMapping stage, which means we will register our strategy at Setup. Registration will take place in the Initialize method of our extension.
public class NameResolverExtension : UnityContainerExtension, INameResolverExtensionConfigurator
{
protected override void Initialize()
{
Context.Strategies.AddNew<ResolveNameBuilderStrategy>(UnityBuildStage.Setup);
}
}
* This source code was highlighted with Source Code Highlighter .
Policies
Another important mechanism in Unity is policy.
A policy is an interface inherited from IBuilderPolicy and a class that implements it.
In the policy interface, you can define methods for any action. IBuilderPolicy itself is empty.
The strategy can obtain from the BuilderContext a policy for a given type using a build key.
Create your own policy to get the new name for the build-key.
internal interface IResolveNamePolicy : IBuilderPolicy
{
string ResolveName(NamedTypeBuildKey buildKey);
}
* This source code was highlighted with Source Code Highlighter .
We use it in our strategy.
internal class ResolveNameBuilderStrategy : BuilderStrategy
{
private NamedTypeBuildKey ReplaceBuildKeyName(
IBuilderContext context, NamedTypeBuildKey buildKey)
{
IResolveNamePolicy policy = context.Policies.Get<IResolveNamePolicy>(buildKey);
if (policy != null )
return new NamedTypeBuildKey(buildKey.Type, policy.ResolveName(buildKey));
return buildKey;
}
}
* This source code was highlighted with Source Code Highlighter .
You can add strategies in the extension through context. A policy is added for a specific key, or as a default policy.
We implement the policy of getting the name through the delegate:
internal class ResolveNamePolicyDelegated : IResolveNamePolicy
{
protected readonly NameResolverDelegate Resolver;
public ResolveNamePolicyDelegated(NameResolverDelegate resolver)
: base ()
{
Resolver = resolver;
}
public string ResolveName(NamedTypeBuildKey buildKey)
{
return Resolver(buildKey.Type);
}
}
* This source code was highlighted with Source Code Highlighter .
For the policy interface, the IResolveNamePolicy can be somewhat implemented, for example, through a delegate, through an interface, through a call to a config.
We will add a policy for a specific build-key when configuring our extension.
public class NameResolverExtension : UnityContainerExtension, INameResolverExtensionConfigurator
{
public INameResolverExtensionConfigurator RegisterDelegatedName<TTypeToBuild>(NameResolverDelegate resolver)
{
Context.Policies.Set<IResolveNamePolicy>(
new ResolveNamePolicyDelegated(resolver),
NamedTypeBuildKey.Make<TTypeToBuild>());
return this ;
}
}
* This source code was highlighted with Source Code Highlighter .
Result
Our extension is ready.
Now we can do this:
IUnityContainer uc = new UnityContainer();
uc.RegisterType<IPersistence, InMemoryPersistence>( "none" );
uc.RegisterType<IPersistence, FilePersistence>( "file" );
uc.RegisterType<IPersistence, DbPersistence>( "db" );
uc.RegisterType<IPersistence, WsPersistence>( "ws" );
uc.AddNewExtension<NameResolverExtension>();
uc.Configure<INameResolverExtensionConfigurator>().RegisterDelegatedName<IPersistence>(GetPersistenceImplementationName);
IPersistence p = uc.Resolve<IPersistence>();
p.Add( new Object()); // , GetPersistenceImplementationName
* This source code was highlighted with Source Code Highlighter .
You can create a class helper for IUnityContainer so that you can write “SetNameResolver” as you would like in the beginning.
Now when calling Resolve:
- The first is our strategy;
- It gets the policy for the desired build key;
- If a policy exists for this build-key, then the build-key is replaced in the context with a key with the name from the policy;
- Further, Resolve works in the same way as before, but it creates an object not for an unnamed key, but for a key with a new name.
The source code is
here.