1. 程式人生 > >SignalR中的依賴註入

SignalR中的依賴註入

rar try mic lang net vol 需要 ioc scom

什麽是依賴註入?

如果你已經熟悉依賴註入可以跳過此節。

依賴註入 (DI) 模式下,對象並不為自身的依賴負責。 下邊的例子是一個主動 DI. 假設你有個對象需要消息日誌。你可能定義了一個日誌接口:

C#
interface ILogger 
{
    void LogMessage(string message);
}

在你的對象中,你可以創建一個 ILogger來記錄消息。

C#
// 不用依賴註入。
class SomeComponent
{
    ILogger _logger = new FileLogger(@"C:\logs\log.txt");

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

可以工作,但不是最好的設計。如果你想將FileLogger換成其它的ILogger 實現, 你就得修改 SomeComponent。假如有一堆的對象使用 FileLogger, 你就得將所有的對象都改一遍,或者學決定將 FileLogger形成單例模式,你依舊需要整個程序的修改。

更好的做法是將 ILogger i註入到對象,比如通過構造函數:

C#
// 使用依賴註入.
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 的實現而不更改依賴的哪個對象。

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

這個模式叫 構造函數註入. 另一種模式是設置註入,在需要的地方可以通過設置器方法或屬性來設置依賴。

SignalR中簡單依賴註入

細看一下聊天程序教程 Getting Started with SignalR. 下邊是這個程序的Hub類:

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

假設你想把聊天的信息在發送前先存下來。你可以定義一個接口來抽象這些功能,然後使用 DI 把這個接口註入到ChatHub 類中。

C#
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 應用並不直接創建hub; SignalR 會為你創建。默認情況下,SignalR 期望一個有參數的構造方法。然而你可以很容易的註冊一個函數來創建這個hub 實例,然後用這個函數來實現 DI. 調用GlobalHost.DependencyResolver.Register來註冊這個函數。

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

	App.MapSignalR();

	// ...
}

現在SignalR就會在你需要創建 ChatHub 實例的時候來調用這個匿名函數。

IoC 容器

上邊的代碼在簡單的場合下已經不錯了,但你還是得這樣寫:

C#
... new ChatHub(new ChatMessageRepository()) ...

在一個復雜的應用有很多的依賴。In a complex application with many dependencies, you might need to write a lot of this "wiring" code. This code can be hard to maintain, especially if dependencies are nested. It is also hard to unit test.

One solution is to use an IoC container. An IoC container is a software component that is responsible for managing dependencies.You register types with the container, and then use the container to create objects. The container automatically figures out the dependency relations. Many IoC containers also allow you to control things like object lifetime and scope.

Note

"IoC" stands for "inversion of control", which is a general pattern where a framework calls into application code. An IoC container constructs your objects for you, which "inverts" the usual flow of control.

Using IoC Containers in SignalR

The Chat application is probably too simple to benefit from an IoC container. Instead, let‘s look at the StockTicker sample.

The StockTicker sample defines two main classes:

  • StockTickerHub: The hub class, which manages client connections.
  • StockTicker: A singleton that holds stock prices and periodically updates them.

StockTickerHub holds a reference to the StockTicker singleton, while StockTicker holds a reference to the IHubConnectionContext for the StockTickerHub. It uses this interface to communicate with StockTickerHub instances. (For more information, see Server Broadcast with ASP.NET SignalR.)

We can use an IoC container to untangle these dependencies a bit. First, let‘s simplify the StockTickerHub and StockTicker classes. In the following code, I‘ve commented out the parts that we don‘t need.

Remove the parameterless constructor from StockTickerHub. Instead, we will always use DI to create the hub.

C#
[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;
    }

    // ...

For StockTicker, remove the singleton instance. Later, we‘ll use the IoC container to control the StockTicker lifetime. Also, make the constructor public.

C#
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;
    //    }
    //}

Next, we can refactor the code by creating an interface for StockTicker. We‘ll use this interface to decouple the StockTickerHub from the StockTicker class.

Visual Studio makes this kind of refactoring easy. Open the file StockTicker.cs, right-click on the StockTicker class declaration, and select Refactor ...Extract Interface.

技術分享

In the Extract Interface dialog, click Select All. Leave the other defaults. Click OK.

技術分享

Visual Studio creates a new interface named IStockTicker, and also changes StockTicker to derive from IStockTicker.

Open the file IStockTicker.cs and change the interface to public.

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

In the StockTickerHub class, change the two instances of StockTicker to IStockTicker:

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

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

Creating an IStockTicker interface isn‘t strictly necessary, but I wanted to show how DI can help to reduce coupling between components in your application.

Add the Ninject Library

There are many open-source IoC containers for .NET. For this tutorial, I‘ll use Ninject. (Other popular libraries include Castle Windsor, Spring.Net,Autofac, Unity, and StructureMap.)

Use NuGet Package Manager to install the Ninject library. In Visual Studio, from the Tools menu select Library Package Manager | Package Manager Console. In the Package Manager Console window, enter the following command:

PowerShell
Install-Package Ninject -Version 3.0.1.10

Replace the SignalR Dependency Resolver

To use Ninject within SignalR, create a class that derives from DefaultDependencyResolver.

C#
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));
    }
}

This class overrides the GetService and GetServices methods of DefaultDependencyResolver. SignalR calls these methods to create various objects at runtime, including hub instances, as well as various services used internally by SignalR.

  • The GetService method creates a single instance of a type. Override this method to call the Ninject kernel‘s TryGet method. If that method returns null, fall back to the default resolver.
  • The GetServices method creates a collection of objects of a specified type. Override this method to concatenate the results from Ninject with the results from the default resolver.

Configure Ninject Bindings

Now we‘ll use Ninject to declare type bindings.

Open your application‘s Startup.cs class (that you either created manually as per the package instructions in readme.txt, or that was created by adding authentication to your project). In the Startup.Configuration method, create the Ninject container, which Ninject calls the kernel.

C#
var kernel = new StandardKernel();

Create an instance of our custom dependency resolver:

C#
var resolver = new NinjectSignalRDependencyResolver(kernel);

Create a binding for IStockTicker as follows:

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

This code is saying two things. First, whenever the application needs an IStockTicker, the kernel should create an instance of StockTicker. Second, the StockTicker class should be a created as a singleton object. Ninject will create one instance of the object, and return the same instance for each request.

Create a binding for IHubConnectionContext as follows:

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

This code creatres an anonymous function that returns an IHubConnection. The WhenInjectedInto method tells Ninject to use this function only when creating IStockTicker instances. The reason is that SignalR creates IHubConnectionContext instances internally, and we don‘t want to override how SignalR creates them. This function only applies to our StockTicker class.

Pass the dependency resolver into the MapSignalR method by adding a hub configuration:

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

Update the Startup.ConfigureSignalR method in the sample‘s Startup class with the new parameter:

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

Now SignalR will use the resolver specified in MapSignalR, instead of the default resolver.

Here is the complete code listing for Startup.Configuration.

C#
public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        // For more information on how to configure your application, visit http://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);
    }
}

To run the StockTicker application in Visual Studio, press F5. In the browser window, navigate to http://localhost:*port*/SignalR.Sample/StockTicker.html.

技術分享

The application has exactly the same functionality as before. (For a description, see Server Broadcast with ASP.NET SignalR.) We haven‘t changed the behavior; just made the code easier to test, maintain, and evolve.

SignalR中的依賴註入