次の方法で共有


SignalR 1.x の依存関係挿入

投稿者: Patrick Fletcher

警告

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

依存関係の挿入は、オブジェクト間のハードコーディングされた依存関係を解除する方法であり、(モック オブジェクトを使用して) テストしたり、実行時の動作を変更したりするために、オブジェクトの依存関係を簡単に置き換えることができます。 このチュートリアルでは、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 を呼び出して関数を登録します。

protected void Application_Start()
{
    GlobalHost.DependencyResolver.Register(
        typeof(ChatHub), 
        () => new ChatHub(new ChatMessageRepository()));

    RouteTable.Routes.MapHubs();

    // ...
}

これで、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 クラスを簡略化しましょう。 次のコードでは、不要な部分をコメントアウトしました。

パラメーターなしのコンストラクターを StockTicker から削除します。 代わりに、常に 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 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 displaying over Visual Studio Code, with the Refactor and Extract Interface options being highlighted.

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

Screenshot of the Extract Interface dialog with the Select All option being highlighted and O K option being displayed.

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 を使用して型バインドを宣言します。

ファイル RegisterHubs.csを開きます。 RegisterHubs.Start メソッドで、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<IHubConnectionContext>().ToMethod(context =>
    resolver.Resolve<IConnectionManager>().GetHubContext<StockTickerHub>().Clients
).WhenInjectedInto<IStockTicker>();

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

依存関係リゾルバーを MapHubs メソッドに渡します。

RouteTable.Routes.MapHubs(config);

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

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

public static class RegisterHubs
{
    public static void Start()
    {
        var kernel = new StandardKernel();
        var resolver = new NinjectSignalRDependencyResolver(kernel);

        kernel.Bind<IStockTicker>()
            .To<Microsoft.AspNet.SignalR.StockTicker.StockTicker>()
            .InSingletonScope();

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

        var config = new HubConfiguration()
        {
            Resolver = resolver
        };

        // Register the default hubs route: ~/signalr/hubs
        RouteTable.Routes.MapHubs(config);
    }
}

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

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

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