読者です 読者をやめる 読者になる 読者になる

yamamoWorks

.NET技術を中心に気まぐれに更新していきます

IConstructorSelectorPolicyでコンストラクタ引数の型解決に介入する

Unityを使ってASP.NET MVCリポジトリパターンを実装する時のお話です。

KVSのデータを操作するこんなインターフェイスと実装があるとします。

public interface IKeyValueStore
{
Task StoreAsync(string key, object value);
Task GetAsync(string key);
}

public class AzureTableStore : IKeyValueStore
{
private strint tableName;

public AzureTableStore(string tableName)
{
this.tableName = tableName;
}
実装クラスのコンストラクタ引数で格納先が指定できるとします。
var shopStore = new AzureTableStore("shops");
var productStore = new AzureTableStore("products");
Unityを使うことを考えてASP.NET MVCのControllerを実装します。
public class HomeController : Controller
{
public HomeController(IKeyValueStore shopStore, IKeyValueStore productStore)
{
}
Unityへの型の登録を記述します。
public static class UnityConfig
{
public static void RegisterComponents()
{
var container = new UnityContainer();

container.RegisterType(
new InjectionConstructor("shop"));

container.RegisterType
(
new InjectionConstructor("product"));
で、実行してみると…
HomeControllerコンストラクタのshopStore、productStoreには両方ともTableNameが"product"のAzureTableStoreインスタンスが渡ってきます。

まぁ、同じ型を登録してるから上書きされて"product"になるのは納得できる。
では、どうするか。

それぞれのインターフェイスを用意してそれぞれ登録すると…
interface IShopStore : IKeyValueStore {}
interface IProductStore : IKeyValueStore {}

public class AzureTableStore : IShopStore, IProductStore {...}

public HomeController(IShopStore shopStore, IProductStore productStore) {...}

container.RegisterType(new InjectionConstructor("shop"));
container.RegisterType(new InjectionConstructor("product"));
これも結果は同じ。
両方ともTableNameが"product"のAzureTableStoreインスタンスが渡ってきます。
ちょっと不自然な動きに思えるが、そういうものらしい。

ならば実装クラスを別々に…
interface IShopStore : IKeyValueStore {}
interface IProductStore : IKeyValueStore {}

public class ShopStore : AzureTableStore, IShopStore {
public ShopStore() : base("shop") {...}

public class ProductStore : AzureTableStore, IProductStore {
public ProductStore() : base("product") {...}

public HomeController(IShopStore shopStore, IProductStore productStore) {...}

container.RegisterType();
container.RegisterType();
これならHomeControllerコンストラクタのshopStore、productStoreにはShopStoreとProductStoreのインスタンスが渡ってきます。
でも、インターフェイスとクラスが無駄に増えた。


次は考え方を変えて、HomeControllerをUnityに登録してみます。
container.RegisterType(
"shopStore",
new InjectionConstructor("shop"));

container.RegisterType
(
"productStore",
new InjectionConstructor("product"));

container.RegisterType(
new InjectionConstructor(
new ResolvedParameter
("shopStore"),
new ResolvedParameter
("productStore")));
ResolvedParameterでHomeControllerインスタンスが生成される時のコンストラクタ引数を指定できます。
また、型の登録時に名前を付けられるので"shopStore"と"productStore"とし、ResolvedParameterに指定します。
これでHomeControllerコンストラクタの第1引数がnew AzureTableStore("shop")、第2引数にnew AzureTableStore("product")でインスタンスが生成されます。

これがUnityの正しい使い方なのかしら?
でも、Controllerが沢山あったら登録するのが怠いよね...

という事で、この辺りの動作を変更してやります。
UnityにはIConstructorSelectorPolicyインターフェイスがあり、実装すべきはCreateResolverメソッドで、既定の動作となるDefaultUnityConstructorSelectorPolicyクラスは以下のように実装されてます。 ※Codeplex参照
protected override IDependencyResolverPolicy CreateResolver(ParameterInfo parameter)
{
var attrs = parameter.GetCustomAttributes(false)
 .OfType().ToList();
if(attrs.Count > 0)
{
return attrs[0].CreateResolver(parameter.ParameterType);
}
return new NamedTypeDependencyResolverPolicy(parameter.ParameterType, null);
}
最後のNamedTypeDependencyResolverPolicyをnewする際に第2引数に渡す文字列が型の解決で名前として使用されるので、ParameterInfo.Nameを渡しちゃいます。
ParameterInfo.Nameはコンストラクタ引数の変数名です。つまり ctor(string x, int y)であれば"x"や"y"です。
つまり、コンストラクタ引数の変数名で型登録時の名前を指定するという少し無茶な仕様となります。
class CustomUnityConstructorSelectorPolicy : DefaultUnityConstructorSelectorPolicy
{
protected override IDependencyResolverPolicy CreateResolver(ParameterInfo parameter)
{
// 今回はDependencyResolutionAttributeの処理は無視
return new NamedTypeDependencyResolverPolicy(
parameter.ParameterType,
parameter.Name);
}
}
あとは用意したCustomUnityConstructorSelectorPolicyが既定の動作となるようにゴニョゴニョと…
class CustomUnityContainerExtension : UnityContainerExtension
{
protected override void Initialize()
{
Context.Policies.SetDefault(
new CustomUnityConstructorSelectorPolicy());
}
}

public static class UnityConfig
{
public static void RegisterComponents()
{
var container = new UnityContainer();

container.AddNewExtension();

container.RegisterType
(
"shopStore",
new InjectionConstructor("shop"));

container.RegisterType
(
"productStore",
new InjectionConstructor("product"));

DependencyResolver.SetResolver(new UnityDependencyResolver(container));
}
}

public class HomeController : Controller
{
public HomeController(IKeyValueStore shopStore, IKeyValueStore productStore)これで、Controllerのインスタンスが生成される時にIKeyValueStore型で変数名と同じ名前で登録されているクラスを探して解決されるようになります。
変数名で指定するのは俺々ルール感たっぷりですが、コード的にはスッキリしました。