web-dev-qa-db-ja.com

Autofac-コンテナーを渡さずにランタイムパラメーターを解決する

コンストラクターで2つのパラメーターを受け取る、より単純な「ServiceHelper」クラスがあります。

public ServiceHelper(ILogger<ServiceHelper> log, string serviceName)

(Autofacがうまく提供しているNLogのILogger汎用ラッパー。serviceNameは、実行時に提供する必要があるコントロールするWindowsサービスの名前です。)

Autofacを使用して、異なるクラス名で実行時にこのクラスの新しいインスタンスを作成する方法について頭を悩ましています。実行時に異なるサービス名を指定する必要があるため、このようなものはもちろん機能しません:

builder.RegisterType<ServiceHelper>().As<IServiceHelper>().WithParameter(new NamedParameter("serviceName", null)).InstancePerDependency();

私が読んだことから、コンテナを渡し、Resolveを手動で正しく呼び出すのは悪い習慣です(サービスロケータはAutoFacが警告する「アンチパターン」)、またはそれですか?もしそうしたら

container.Resolve<ServiceHelper>(new NamedParameter("serviceName", "some service name"));

しかし、これまでのところ、Autofacにコンテナをクラスにインジェクトさせる方法がよくわかりません。このように、自分自身を正確に登録する必要がありますか?そして、私のクラスはコンストラクタにIContainerを必要としますか? (これは、コンストラクター注入を使用したC#サービスにあります)

builder.RegisterType<Container>().As<IContainer>().InstancePerDependency();

デリゲートファクトリについても読みましたが、コンテナを渡す必要がなくなるわけではありません。

実際、ServiceHelperを使用するほとんどのクラスは、特定のサービス名に1つまたは2つのServiceHelperを必要とするだけなので、予想外のserviceNameパラメーターで数千を作成しているのではなく、これはちょっと頭を痛めています。

21
Jeremy

はい、コンテナをあらゆる場所に渡すことはアンチパターンです。

次のようなファクトリを使用することで回避できます。

(注:この回答のすべてのコードはテストされていません。VisualStudioを搭載していないマシンのテキストエディターで作成しています)

public interface IServiceHelperFactory
{
    IServiceHelper CreateServiceHelper(string serviceName);
}

public class ServiceHelperFactory : IServiceHelperFactory
{
    private IContainer container;

    public ServiceHelperFactory(IContainer container)
    {
        this.container = container;
    }

    public IServiceHelper CreateServiceHelper(string serviceName)
    {
        return container.Resolve<ServiceHelper>(new NamedParameter("serviceName", serviceName));
    }
}

起動時に、他のすべてと同様に、AutofacにServiceHelperFactoryを登録します。

builder.RegisterType<ServiceHelperFactory>().As<IServiceHelperFactory>();

次に、ServiceHelperが必要な場合は、コンストラクター注入を介してファクトリーを取得できます。

public class SomeClass : ISomeClass
{
    private IServiceHelperFactory factory;

    public SomeClass(IServiceHelperFactory factory)
    {
        this.factory = factory;
    }

    public void ThisMethodCreatesTheServiceHelper()
    {
        var helper = this.factory.CreateServiceHelper("some service name");
    }
}

Autofacを使用したコンストラクターインジェクションによってファクトリ自体を作成することにより、自分でコンテナを渡す必要なく、ファクトリがコンテナについて認識していることを確認できます。

一見したところ、このソリューションは、コンテナを直接渡すこととほとんど変わりません。しかし、利点は、アプリがまだコンテナーから分離されていることです-コンテナーが認識されている唯一の場所(スタートアップを除く)は工場内にあります。


編集:

忘れました上で言ったように、これはVisual Studioのないマシンで書いているので、サンプルコードをテストすることはできません。
あなたのコメントを読みましたが、Autofacを使用してコンテナ自体を登録しようとしたときに同様の問題があったことを思い出します。

私の問題は、ビルダーをコンテナーに登録する必要があることでした。
しかし、コンテナインスタンスを登録するには、builder.Build() ...を呼び出す必要があります。これは、コンテナを作成します。つまり、後でビルダーにデータを登録することはできません。
エラーメッセージは覚えていませんが、同じ問題が発生していると思います。

私が見つけた解決策は、2番目のビルダーを作成し、そこにコンテナーを登録し、2番目のビルダーを使用して唯一のコンテナーを更新することでした。

以下は、私のオープンソースプロジェクトの1つからの作業コードです。

起動時に、コンテナを登録します:

var builder = new ContainerBuilder();

// register stuff here

var container = builder.Build();

// register the container
var builder2 = new ContainerBuilder();
builder2.RegisterInstance<IContainer>(container);
builder2.Update(container);

...次に使用されます WindowServiceによって新しいWPFウィンドウを作成

public class WindowService : IWindowService
{
    private readonly IContainer container;

    public WindowService(IContainer container)
    {
        this.container = container;
    }

    public T GetWindow<T>() where T : MetroWindow
    {
        return (T)this.container.Resolve<T>();
    }
}
21

私は上記のアプローチの道を進み、うまくいきましたが、IContainerの「Resolve <>」メソッドが拡張メソッドであるため、ユニットテストが不可能であることがわかりました。また、コンテナを渡さないことについてのすべての話で、「正しい」と感じることもありませんでした。

私は図面に戻り、Autofac Delegate Factoriesを使用してオブジェクトをインスタンス化する「正しい」方法を見つけました http://docs.autofac.org/en/latest/advanced/delegate-factories.html

7
raterus

解決は、ルートコンポジションオブジェクトに対してのみ発生する必要があります。 resolveの呼び出しは、オブジェクトの「更新」とほぼ同じです。これは匂いテストです。解像度が動的でオンザフライでしか決定できない場合もありますが、most依存関係は決定論的であり、事前に登録できます。 Autofacでこれを行う方法が課題です。 @Christian Spechtが受賞した回答は良い回答ですが、すべてが実行時に決定されると仮定しています。

設計時に依存関係チェーンを定義するには、SO topic Autofacサブ依存関係チェーン登録 ...を参照してください。

3
barrypicker