首页 > 代码库 > SignalR中的依赖注入

SignalR中的依赖注入

什么是依赖注入?

如果你已经熟悉依赖注入可以跳过此节。

依赖注入 (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中的依赖注入