Share via


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");
    }
}

這可運作,但不是最佳設計。 如果您想要以另一個 ILogger 實作取代 FileLogger ,則必須修改 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);

此模式稱為建 構函式插入。 另一個模式是 setter 插入,您可以在其中透過 setter 方法或屬性來設定相依性。

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()) ...

在具有許多相依性的複雜應用程式中,您可能需要撰寫許多「連接」程式碼。 此程式碼可能難以維護,特別是如果相依性是巢狀的。 也很難進行單元測試。

其中一個解決方案是使用 IoC 容器。 IoC 容器是負責管理相依性的軟體元件。您會向容器註冊類型,然後使用容器來建立物件。 容器會自動找出相依性關聯。 許多 IoC 容器也可讓您控制物件存留期和範圍等專案。

注意

「IoC」 代表「控制反轉」,這是架構呼叫應用程式程式碼的一般模式。 IoC 容器會為您建構物件,這會「反轉」一般控制流程。

在 SignalR 中使用 IoC 容器

聊天應用程式可能太簡單,無法受益于 IoC 容器。 相反地,讓我們看看 StockTicker 範例。

StockTicker 範例會定義兩個主要類別:

  • StockTickerHub:管理用戶端連線的中樞類別。
  • StockTicker:保留股票價格並定期更新它們的單一。

StockTickerHub 會保留單一的 StockTicker 參考,同時 StockTicker 保留 的 IHubConnectionCoNtextStockTickerHub 參考。 它會使用此介面來與 StockTickerHub 實例通訊。 (如需詳細資訊,請參閱使用 ASP.NET SignalR.) 進行伺服器廣播

我們可以使用 IoC 容器將這些相依性取消糾纏一些。 首先,讓我們簡化 StockTickerHubStockTicker 類別。 在下列程式碼中,我已批註化我們不需要的部分。

從 移除無參數建構函 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 介面來重構程式碼。 我們將使用此介面將 與 StockTicker 類別分離 StockTickerHub

Visual Studio 讓這類重構變得容易。 開啟 StockTicker.cs 檔案,以滑鼠右鍵按一下 StockTicker 類別宣告,然後選取 [重構 ... 擷取介面

在 Visual Studio 程式碼上按一下滑鼠右鍵下拉式功能表的螢幕擷取畫面,其中已醒目提示 [Refractor] 和 [擷取介面] 選項。

在 [ 擷取介面 ] 對話方塊中,按一下 [ 全選]。 其他部分保留預設值。 按一下 [確定]。

[擷取介面] 對話方塊的螢幕擷取畫面,其中已醒目提示 [全選] 選項,並已選取所有可用的選項。

Visual Studio 會建立名為 IStockTicker 的新介面,也會變更 StockTicker 為衍生自 IStockTicker

開啟 IStockTicker.cs 檔案,並將介面變更為 public

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

在 類別中 StockTickerHub ,將 的 StockTicker 兩個實例變更為 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。 (其他熱門程式庫包括Spring.NetAutofacUnityStructureMap.)

使用 NuGet 套件管理員來安裝 Ninject 程式庫。 在 Visual Studio 中,從 [工具] 功能表選取[NuGet 套件管理員套件管理員>主控台]。 在 [Package Manager Console] 視窗中,輸入下列命令:

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方法會建立類型的單一實例。 覆寫此方法以呼叫 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.

此程式碼正在說兩件事。 首先,每當應用程式需要 IStockTicker 時,核心應該建立 的 StockTicker 實例。 其次,類別 StockTicker 應該是建立為單一物件。 Ninject 會建立物件的一個實例,並針對每個要求傳回相同的實例。

建立 IHubConnectionCoNtext 的系結,如下所示:

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

此程式碼會建立傳回 IHubConnection的匿名函式。 WhenInjectedInto方法會指示 Ninject 只在建立 IStockTicker 實例時使用此函式。 原因是 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

Internet Explorer 瀏覽器視窗的螢幕擷取畫面,其中顯示 A S P 點 NET Signal R Stock Ticker 範例網頁。

應用程式的功能與之前完全相同。 (如需描述,請參閱 伺服器廣播與 ASP.NET SignalR.) 我們尚未變更行為;只是讓程式碼更容易測試、維護和演進。