
Unity Expansion
In this post I will show an example of how to 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.
Let's say in our application there is a Persistence component 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, calling Resolve for IPersistence, we always get it.
But what to do if the necessary implementation can change during the application. For example, is it set in the config file, or if the network is unavailable, should you use FilePersistence automatically?
Unity has the ability to register dependencies by name. Example:
It remains to ensure that when receiving an implementation without a name, Unity somehow determines what kind of implementation we need.
Let us have a delegate, which we pass to Unity, which defines the desired implementation name.
Example:
There is no standard way in Unity for this. But we will solve the problem by writing our extension.
The Unity extension is a class inherited from UnityContainerExtension. It has an extension context (ExtensionContext) and virtual methods Initialize () and Remove () (respectively called when initializing and removing the extension).
Extensions are added through the container methods AddNewExtension and AddExtension, removed through RemoveAllExtensions.
For an extension to be configurable, it must implement a configurator interface inherited from IUnityContainerExtensionConfigurator. Configuration takes place via the Configure container method.
Each registered type in Unity has its own build key (buildKey). It consists of the 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:
Unity has 4 predefined strategies that are called for each Resolve:
We will write our own strategy, which will replace the empty name in the build key with the name of the desired implementation.
Because If the search for implementation by the build key occurs in the BuildKeyMappingStrategy strategy, then we must register our strategy so that it runs before BuildKeyMappingStrategy. Strategies are sorted depending on the stage that was specified during registration.
There are 7 stages in total - 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 occur in the Initialize method of our extension.
Another important mechanism in Unity is policies.
A policy is an interface inherited from IBuilderPolicy and a class that implements it.
In the policy interface, you can define methods for any actions. IBuilderPolicy itself is empty.
A strategy can get a policy for a given type from the BuilderContext using the build key.
Let's create our policy to get a new name using the build key.
We use it in our strategy.
You can add strategies in the extension through context. The policy is added for a specific key, or as a default policy.
We implement the policy of getting the name through the delegate:
There can be several implementations for the IResolveNamePolicy policy interface, for example, through a delegate, through an interface, through accessing the config.
We will add a policy for a specific build key when configuring our extension.
Our extension is ready.
Now we can do this:
You can create a class helper for IUnityContainer so that you can write "SetNameResolver", as you wanted at the beginning.
Now when calling Resolve:
Source code is here.
Let's say in our application there is a Persistence component 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, calling Resolve for IPersistence, we always get it.
IUnityContainer uc = new UnityContainer();
uc.RegisterType();
IPersistence p = uc.Resolve();
p.Add(obj);
* This source code was highlighted with Source Code Highlighter.
But what to do if the necessary implementation can change during the application. For example, is it set in the config file, or if the network is unavailable, should you use FilePersistence automatically?
Unity has the ability to register dependencies by name. Example:
uc.RegisterType("none");
uc.RegisterType("file");
uc.RegisterType("db");
uc.RegisterType("ws");
IPersistence p = uc.Resolve("file"); // Получили file реализацию.
IPersistence p = uc.Resolve("db"); // Получили dbреализацию.
* This source code was highlighted with Source Code Highlighter.
It remains to ensure that when receiving an implementation without a name, Unity somehow determines what kind of implementation we need.
Let us have a delegate, which we pass to Unity, which defines the desired implementation name.
Example:
uc.SetNameResolver(GetPersistenceImplementationName);
IPersistence p = uc.Resolve(); // Тут мы получим ту реализацию, имя который вернул 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 extension.
Unity extensions
The Unity extension is a class inherited from UnityContainerExtension. It has an extension context (ExtensionContext) and virtual methods Initialize () and Remove () (respectively called when initializing and removing the extension).
Extensions are added through the container methods AddNewExtension and AddExtension, removed through RemoveAllExtensions.
public class NameResolverExtension : UnityContainerExtension
{
protected override void Initialize()
{
}
protected override void Remove()
{
}
public NameResolverExtension()
: base()
{
}
}
uc.AddNewExtension();
* This source code was highlighted with Source Code Highlighter.
For an extension to be configurable, it must implement a configurator interface inherited from IUnityContainerExtensionConfigurator. Configuration takes place via the Configure container method.
// Наш делегат для получения имени
public delegate string NameResolverDelegate(Type typeToBuild);
// Интерфейс-конфигуратор
public interface INameResolverExtensionConfigurator : IUnityContainerExtensionConfigurator
{
INameResolverExtensionConfigurator RegisterDelegatedName(
NameResolverDelegate resolver);
}
static private string GetPersistenceImplementationName(Type typeToBuild)
{
// На самом деле тут мы должны читать конфиг...
return "db";
}
uc.Configure()
.RegisterDelegatedName(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 the 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 desired type and the build context (BuilderContext) are generated;
- The context is sequentially processed by strategies until one of them sets the BuildComplete flag to true.
Unity has 4 predefined strategies that are called for each Resolve:
- BuildKeyMappingStrategy. Replaces the build key in the context from the type you are looking for with the implementation key. In fact, all resolving happens here;
- LifetimeStrategy. Checks for an implementation in the Lifetime Manager;
- ArrayResolutionStrategy. Resolution of array dependencies;
- BuildPlanStrategy. Creating an instance of the implementation (if it has not already been created) and automatically resolving its dependencies.
We will write our own strategy, which will replace the empty name in the build key with the name of 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 If the search for implementation by the build key occurs in the BuildKeyMappingStrategy strategy, then we must register our strategy so that it runs before BuildKeyMappingStrategy. Strategies are sorted depending on the stage that was specified during registration.
There are 7 stages in total - 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 occur in the Initialize method of our extension.
public class NameResolverExtension : UnityContainerExtension, INameResolverExtensionConfigurator
{
protected override void Initialize()
{
Context.Strategies.AddNew(UnityBuildStage.Setup);
}
}
* This source code was highlighted with Source Code Highlighter.
Policies
Another important mechanism in Unity is policies.
A policy is an interface inherited from IBuilderPolicy and a class that implements it.
In the policy interface, you can define methods for any actions. IBuilderPolicy itself is empty.
A strategy can get a policy for a given type from the BuilderContext using the build key.
Let's create our policy to get a new name using 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(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. The 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.
There can be several implementations for the IResolveNamePolicy policy interface, for example, through a delegate, through an interface, through accessing the config.
We will add a policy for a specific build key when configuring our extension.
public class NameResolverExtension : UnityContainerExtension, INameResolverExtensionConfigurator
{
public INameResolverExtensionConfigurator RegisterDelegatedName(NameResolverDelegate resolver)
{
Context.Policies.Set(
new ResolveNamePolicyDelegated(resolver),
NamedTypeBuildKey.Make());
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("none");
uc.RegisterType("file");
uc.RegisterType("db");
uc.RegisterType("ws");
uc.AddNewExtension();
uc.Configure().RegisterDelegatedName(GetPersistenceImplementationName);
IPersistence p = uc.Resolve();
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 wanted at the beginning.
Now when calling Resolve:
- Our strategy is the first to launch;
- She receives 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 the key with the name from the policy;
- Further, Resolve works as before, but it creates the object not for the nameless key, but for the key with a new name.
Source code is here.