次の方法で共有


SignalR の依存関係挿入

投稿者: Patrick Fletcher

警告

このドキュメントは、最新版の SignalR は対象としていません。 ASP.NET Core SignalR を参照してください。

このトピックで使用されるソフトウェアのバージョン

このトピックの以前のバージョン

SignalR の以前のバージョンについては、「SignalR の以前のバージョン」を参照してください。

質問とコメント

このチュートリアルの感想、改善できる内容に関するフィードバックをページの下部にあるコメントでお寄せください。 チュートリアルに直接関連しない質問がある場合は、ASP.NET SignalR フォーラムまたは StackOverflow.com に投稿できます。

依存関係の挿入は、オブジェクト間のハードコーディングされた依存関係を解除する方法であり、(モック オブジェクトを使用して) テストしたり、実行時の動作を変更したりするために、オブジェクトの依存関係を簡単に置き換えることができます。 このチュートリアルでは、SignalR ハブで依存関係の挿入を実行する方法について示します。 また、SignalR で IoC コンテナーを使用する方法についても示します。 IoC コンテナーは、依存関係の挿入のための一般的なフレームワークです。

依存関係の挿入とは

依存関係の挿入に既に慣れている場合は、このセクションをスキップしてください。

依存関係の挿入 (DI) とは、オブジェクトによって独自の依存関係が作成されないパターンです。 DI の動機付けとなる簡単な例を次に示します。 メッセージをログに記録する必要があるオブジェクトがあるとします。 次のようなログ インターフェイスを定義できます。

interface ILogger 
{
    void LogMessage(string message);
}

オブジェクトでは、ILogger を作成してメッセージをログに記録できます。

// Without dependency injection.
class SomeComponent
{
    ILogger _logger = new FileLogger(@"C:\logs\log.txt");

    public void DoSomething()
    {
        _logger.LogMessage("DoSomething");
    }
}

これは機能しますが、最適な設計ではありません。 FileLogger を別の ILogger 実装に置き換える場合は、SomeComponent を変更する必要があります。 他の多くのオブジェクトで FileLogger が使用されていると仮定すると、それらすべてを変更する必要があります。 または、FileLogger をシングルトンにする場合は、アプリケーション全体で変更を行う必要もあります。

より適切な方法は、たとえばコンストラクター引数を使用して ILogger をオブジェクトに "挿入" することです。

// With dependency injection.
class SomeComponent
{
    ILogger _logger;

    // Inject ILogger into the object.
    public SomeComponent(ILogger logger)
    {
        if (logger == null)
        {
            throw new NullReferenceException("logger");
        }
        _logger = logger;
    }

    public void DoSomething()
    {
        _logger.LogMessage("DoSomething");
    }
}

これで、使用する ILogger の選択がオブジェクトによって行われないようになりました。 実装に依存するオブジェクトを変更せずに、ILogger 実装を切り替えることができます。

var logger = new TraceLogger(@"C:\logs\log.etl");
var someComponent = new SomeComponent(logger);

このパターンはコンストラクター挿入 と呼ばれます。 もう 1 つのパターンはセッター挿入です。セッター メソッドまたはプロパティを使用して依存関係を設定します。

SignalR での単純な依存関係の挿入

SignalR の概要に関するチュートリアルのチャット アプリケーションについて考えてみましょう。 そのアプリケーションのハブ クラスを次に示します。

public class ChatHub : Hub
{
    public void Send(string name, string message)
    {
        Clients.All.addMessage(name, message);
    }
}

チャット メッセージを送信する前にサーバーに保存するとします。 この機能を抽象化するインターフェイスを定義し、DI を使用してインターフェイスを ChatHub クラスに挿入できます。

public interface IChatRepository
{
    void Add(string name, string message);
    // Other methods not shown.
}

public class ChatHub : Hub
{
    private IChatRepository _repository;

    public ChatHub(IChatRepository repository)
    {
        _repository = repository;
    }

    public void Send(string name, string message)
    {
        _repository.Add(name, message);
        Clients.All.addMessage(name, message);
    }

唯一の問題は、SignalR アプリケーションによってハブが直接作成されるのではないということです。ユーザーに代わって SignalR が作成します。 既定では、SignalR では、ハブ クラスにパラメーターなしのコンストラクターが必要です。 ただし、ハブ インスタンスを作成する関数を簡単に登録し、この関数を使用して DI を実行できます。 GlobalHost.DependencyResolver.Register を呼び出して関数を登録します。

public void Configuration(IAppBuilder app)
{
    GlobalHost.DependencyResolver.Register(
        typeof(ChatHub), 
        () => new ChatHub(new ChatMessageRepository()));

    App.MapSignalR();

    // ...
}

これで、SignalR は ChatHub インスタンスを作成する必要があるときに常にこの匿名関数を呼び出すようになりました。

IoC コンテナー

前のコードは、単純なケースでは問題ありません。 しかし、これを記述しなければなりませんでした。

... new ChatHub(new ChatMessageRepository()) ...

依存関係が多い複雑なアプリケーションでは、この "配線" コードの多くを記述する必要がある場合があります。 このコードは、特に依存関係が入れ子になっている場合、保守するのが難しい場合があります。 単体テストも困難です。

1 つの解決策は、IoC コンテナーを使用することです。 IoC コンテナーは、依存関係を管理するソフトウェア コンポーネントです。コンテナーに型を登録し、コンテナーを使用してオブジェクトを作成します。 コンテナーは、依存関係を自動的に把握します。 多くの IoC コンテナーでは、オブジェクトの有効期間やスコープなどを制御することもできます。

Note

"IoC" とは、フレームワークがアプリケーション コードを呼び出す一般的なパターンである "Inversion of Control" (制御の反転) の略です。 IoC コンテナーによってオブジェクトが作成され、通常の制御フローが "反転" されます。

SignalR での IoC コンテナーの使用

チャット アプリケーションは、IoC コンテナーを利用するには単純すぎる可能性があります。 代わりに、StockTicker サンプルを見てみましょう。

StockTicker サンプルでは、次の 2 つのメイン クラスを定義します。

  • StockTickerHub: クライアント接続を管理するハブ クラス。
  • StockTicker: 株価を保持し、定期的に更新するシングルトン。

StockTickerHubStockTicker シングルトンへの参照を保持し、StockTickerIHubConnectionContextStockTickerHub への参照を保持します。 このインターフェイスを使用して StockTickerHub インスタンスと通信します。 (詳細については、「ASP.NET SignalR を使用したサーバー ブロードキャスト」を参照してください。)

IoC コンテナーを使用して、これらの依存関係を少し簡潔にすることができます。 まず、StockTickerHub クラスと StockTicker クラスを簡略化しましょう。 次のコードでは、不要な部分をコメントアウトしました。

パラメーターなしのコンストラクターを StockTickerHub から削除します。 代わりに、常に DI を使用してハブを作成します。

[HubName("stockTicker")]
public class StockTickerHub : Hub
{
    private readonly StockTicker _stockTicker;

    //public StockTickerHub() : this(StockTicker.Instance) { }

    public StockTickerHub(StockTicker stockTicker)
    {
        if (stockTicker == null)
        {
            throw new ArgumentNullException("stockTicker");
        }
        _stockTicker = stockTicker;
    }

    // ...

StockTicker の場合は、シングルトン インスタンスを削除します。 後で、IoC コンテナーを使用して StockTicker の有効期間を制御します。 また、コンストラクターをパブリックにします。

public class StockTicker
{
    //private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(
    //    () => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));

    // Important! Make this constructor public.
    public StockTicker(IHubConnectionContext<dynamic> clients)
    {
        if (clients == null)
        {
            throw new ArgumentNullException("clients");
        }

        Clients = clients;
        LoadDefaultStocks();
    }

    //public static StockTicker Instance
    //{
    //    get
    //    {
    //        return _instance.Value;
    //    }
    //}

次に、StockTicker のインターフェイスを作成してコードをリファクタリングできます。 このインターフェイスを使用して、StockTickerHubStockTicker クラスから分離します。

Visual Studio を使用すると、この種のリファクタリングが簡単になります。 ファイル StockTicker.cs を開き、StockTicker クラス宣言を右クリックし、[リファクター] ...[インターフェイスの抽出] を選択します。

Screenshot of the right-click dropdown menu over Visual Studio code with the Refractor and Extract Interface options being highlighted.

[インターフェイスの抽出] ダイアログで、[すべて選択] をクリックします。 他の既定値はそのままにします。 OK をクリックします。

Screenshot of the Extract Interface dialog with the Select All option being highlighted, with all the available options being selected.

Visual Studio によって、IStockTicker という新しいインターフェイスが作成されます。また、IStockTicker から派生するように StockTicker が変更されます。

ファイル IStockTicker.cs を開き、インターフェイスをパブリックに変更します。

public interface IStockTicker
{
    void CloseMarket();
    IEnumerable<Stock> GetAllStocks();
    MarketState MarketState { get; }
    void OpenMarket();
    void Reset();
}

StockTickerHub クラスで、StockTicker の 2 つのインスタンスを IStockTicker に変更します。

[HubName("stockTicker")]
public class StockTickerHub : Hub
{
    private readonly IStockTicker _stockTicker;

    public StockTickerHub(IStockTicker stockTicker)
    {
        if (stockTicker == null)
        {
            throw new ArgumentNullException("stockTicker");
        }
        _stockTicker = stockTicker;
    }

IStockTicker インターフェイスの作成は厳密には必要ではありませんが、アプリケーション内のコンポーネント間の結合を減らすために DI がどのように役立つかを示したいと思いました。

Ninject ライブラリを追加する

.NET 用の多くのオープン ソース IoC コンテナーがあります。 このチュートリアルでは、Ninject を使用します。 (その他の一般的なライブラリには、Castle WindsorSpring.NetAutofacUnityStructureMap などがあります。)

NuGet パッケージ マネージャーを使用して、Ninject ライブラリをインストールします。 Visual Studio の [ツール] メニューで、[NuGet パッケージ マネージャー]>[パッケージ マネージャー コンソール] を選択します。 [パッケージ マネージャー コンソール] ウィンドウで、次のコマンドを入力します。

Install-Package Ninject -Version 3.0.1.10

SignalR 依存関係リゾルバーを置き換える

SignalR 内で Ninject を使用するには、DefaultDependencyResolver から派生するクラスを作成します。

internal class NinjectSignalRDependencyResolver : DefaultDependencyResolver
{
    private readonly IKernel _kernel;
    public NinjectSignalRDependencyResolver(IKernel kernel)
    {
        _kernel = kernel;
    }

    public override object GetService(Type serviceType)
    {
        return _kernel.TryGet(serviceType) ?? base.GetService(serviceType);
    }

    public override IEnumerable<object> GetServices(Type serviceType)
    {
        return _kernel.GetAll(serviceType).Concat(base.GetServices(serviceType));
    }
}

このクラスは、DefaultDependencyResolverGetServiceGetServices メソッドをオーバーライドします。 SignalR は、これらのメソッドを呼び出して、ハブ インスタンスや、SignalR によって内部的に使用されるさまざまなサービスを含むさまざまなオブジェクトを実行時に作成します。

  • GetService メソッドは、型の 1 つのインスタンスを作成します。 このメソッドをオーバーライドして、Ninject カーネルの TryGet メソッドを呼び出します。 そのメソッドによって null が返された場合は、既定のリゾルバーにフォールバックします。
  • GetServices メソッドは、指定した型のオブジェクトのコレクションを作成します。 このメソッドをオーバーライドして、Ninject の結果を既定のリゾルバーの結果と連結します。

Ninject バインドを構成する

次に、Ninject を使用して型バインドを宣言します。

アプリケーションの Startup.cs クラスを開きます (readme.txt のパッケージの指示に従って手動で作成したか、プロジェクトに認証を追加して作成されたもの)。 Startup.Configuration メソッドで、Ninject コンテナーを作成します。このコンテナーでは、Ninject によってカーネルが呼び出されます。

var kernel = new StandardKernel();

カスタム依存関係リゾルバーのインスタンスを作成します。

var resolver = new NinjectSignalRDependencyResolver(kernel);

IStockTicker のバインドを作成します。

kernel.Bind<IStockTicker>()
    .To<Microsoft.AspNet.SignalR.StockTicker.StockTicker>()  // Bind to StockTicker.
    .InSingletonScope();  // Make it a singleton object.

このコードは 2 つのことを言っています。 1 つ目は、アプリケーションで IStockTicker が必要な場合は常に、カーネルで StockTicker のインスタンスを作成する必要があるということ。 2 つ目は、StockTicker クラスはシングルトン オブジェクトとして作成する必要があるということです。 Ninject はオブジェクトの 1 つのインスタンスを作成し、要求ごとに同じインスタンスを返します。

IHubConnectionContext のバインドを次のように作成します。

kernel.Bind(typeof(IHubConnectionContext<dynamic>)).ToMethod(context =>
                    resolver.Resolve<IConnectionManager>().GetHubContext<StockTickerHub>().Clients
                     ).WhenInjectedInto<IStockTicker>();

このコードは、IHubConnection を返す匿名関数を作成します。 WhenInjectedInto メソッドは、IStockTicker インスタンスの作成時にのみこの関数を使用するように Ninject に指示します。 その理由は、SignalR によって IHubConnectionContext インスタンスが内部的に作成されるため、SignalR での作成方法をオーバーライドしないようにするためです。 この関数は StockTicker クラスにのみ適用されます。

ハブ構成を追加して、依存関係リゾルバーを MapSignalR メソッドに渡します。

var config = new HubConfiguration();
config.Resolver = resolver;
Microsoft.AspNet.SignalR.StockTicker.Startup.ConfigureSignalR(app, config);

サンプルの Startup クラスの Startup.ConfigureSignalR メソッドを新しいパラメーターで更新します。

public static void ConfigureSignalR(IAppBuilder app, HubConfiguration config)
{
    app.MapSignalR(config);
}

SignalR では、既定のリゾルバーではなく、MapSignalR で指定されたリゾルバーが使用されます。

Startup.Configuration の完成したコードは次のようになります。

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=316888

        var kernel = new StandardKernel();
        var resolver = new NinjectSignalRDependencyResolver(kernel);

        kernel.Bind<IStockTicker>()
            .To<Microsoft.AspNet.SignalR.StockTicker.StockTicker>()  // Bind to StockTicker.
            .InSingletonScope();  // Make it a singleton object.

        kernel.Bind(typeof(IHubConnectionContext<dynamic>)).ToMethod(context =>
                resolver.Resolve<IConnectionManager>().GetHubContext<StockTickerHub>().Clients
                    ).WhenInjectedInto<IStockTicker>();

        var config = new HubConfiguration();
        config.Resolver = resolver;
        Microsoft.AspNet.SignalR.StockTicker.Startup.ConfigureSignalR(app, config);
    }
}

Visual Studio で StockTicker アプリケーションを実行するには、F5 キーを押します。 ブラウザー ウィンドウで、http://localhost:*port*/SignalR.Sample/StockTicker.html に移動します。

Screenshot of an Internet Explorer browser window, displaying the A S P dot NET Signal R Stock Ticker Sample webpage.

アプリケーションの機能は以前とまったく同じです。 (説明については、「ASP.NET SignalR を使用したサーバー ブロードキャスト」を参照してください。)動作は変更されていません。コードのテスト、保守、発展が容易になりました。