解读ASP.NET 5 & MVC6系列(7):依赖注入

在前面的章节(Middleware章节)中,咱们提到了依赖注入功能(Dependency Injection),ASP.NET 5正式将依赖注入进行了全功能的实现,以便开发人员可以开发更具弹性的组件程序,MVC6也利用了依赖注入的功能从新对Controller和View的服务注入功能进行了从新设计;将来的依赖注入功能还可能提供更多的API,全部若是尚未开始接触依赖注入的话,就得好好学一下了。html

在以前版本的依赖注入功能里,依赖注入的入口有MVC中的IControllerFactory和Web API中的IHttpControllerActivator中,在新版ASP.NET5中,依赖注入变成了最底层的基础支撑,MVC、Routing、SignalR、Entity Framrwork等都依赖于依赖注入的IServiceProvider接口,针对该接口微软给出了默认的实现ServiceProvider,以及Ninject和AutoFac版本的包装,固然你也可使用其它第三方的依赖注入容器,如Castle Windsor等;一旦应用了第三方容器,全部的依赖解析都会被路由到该第三方容器上。git

针对通用的依赖类型的解析与建立,微软默认定义了4种类别的生命周期,分别以下:github

类型 描述
Instance 任什么时候间都只能使用特定的实例对象,开发人员须要负责该对象的初始化工做。
Transient 每次都从新建立一个实例。
Singleton 建立一个单例,之后每次调用的时候都返回该单例对象。
Scoped 在当前做用域内,无论调用多少次,都是一个实例,换了做用域就会再次建立实例,相似于特定做用内的单例。

类型注册与示例

依赖注入类型的注册通常是在程序启动的入口中,如Startup.cs中的ConfigureServices中,该类的主要目的就是注册依赖注入的类型。因为依赖注入的主要体现是接口编程,因此本例中,我以接口和实现类的方式来举例。web

首先声明一个接口ITodoRepository和实现类TodoRepository1,代码以下:编程

public interface ITodoRepository
{
    IEnumerable<TodoItem> AllItems { get; }
    void Add(TodoItem item);
    TodoItem GetById(int id);
    bool TryDelete(int id);
}

public class TodoItem
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class TodoRepository : ITodoRepository
{
    readonly List<TodoItem> _items = new List<TodoItem>();

    public IEnumerable<TodoItem> AllItems
    {
        get { return _items; }
    }

    public TodoItem GetById(int id)
    {
        return _items.FirstOrDefault(x => x.Id == id);
    }

    public void Add(TodoItem item)
    {
        item.Id = 1 + _items.Max(x => (int?)x.Id) ?? 0;
        _items.Add(item);
    }

    public bool TryDelete(int id)
    {
        var item = GetById(id);

        if (item == null) { return false; }

        _items.Remove(item);

        return true;
    }
}

为了演示不一样的声明周期类型,建议多实现几个类,好比TodoRepository二、TodoRepository三、TodoRepository4等,以便进行演示。app

而后在ConfigureServices方法内注册接口ITodoRepository类型和对应的实现类,本例中根据不一样的生命周期注册了不一样的实现类,具体示例以下:框架

//注册单例模式,整个应用程序周期内ITodoRepository接口的示例都是TodoRepository1的一个单例实例
services.AddSingleton<ITodoRepository, TodoRepository1>();
services.AddSingleton(typeof(ITodoRepository), typeof(TodoRepository1));  // 等价形式

//注册特定实例模型,整个应用程序周期内ITodoRepository接口的示例都是固定初始化好的一个单例实例

TodoRepository2
services.AddInstance<ITodoRepository>(new TodoRepository2());
services.AddInstance(typeof(ITodoRepository), new TodoRepository2());  // 等价形式

//注册做用域型的类型,在特定做用域内ITodoRepository的示例是TodoRepository3
services.AddScoped<ITodoRepository, TodoRepository3>();
services.AddScoped(typeof(ITodoRepository), typeof(TodoRepository3));// 等价形式

//获取该ITodoRepository实例时,每次都要实例化一次TodoRepository4类
services.AddTransient<ITodoRepository, TodoRepository4>();
services.AddTransient(typeof(ITodoRepository), typeof(TodoRepository));// 等价形式

//若是要注入的类没有接口,那你能够直接注入自身类型,好比:
services.AddTransient<LoggingHelper>();

依赖注入的在MVC中的使用方式目前有三种,分别是Controller的构造函数、属性以及View中的Inject形式。其中构造函数注入和以前的MVC中的是同样的,示例代码以下:ide

public class TodoController : Controller
{
    private readonly ITodoRepository _repository;

    /// 依赖注入框架会自动找到ITodoRepository实现类的示例,赋值给该构造函数
    public TodoController(ITodoRepository repository)
    {
        _repository = repository;
    }

    [HttpGet]
    public IEnumerable<TodoItem> GetAll()
    {
        return _repository.AllItems;  //这里就可使用该对象了
    }
}

属性注入,则是经过在属性上加一个[FromServices]属性便可实现自动获取实例。函数

public class TodoController : Controller
{
    // 依赖注入框架会自动找到ITodoRepository实现类的示例,赋值给该属性
    [FromServices]
    public ITodoRepository Repository { get; set; }

    [HttpGet]
    public IEnumerable<TodoItem> GetAll()
    {
        return Repository.AllItems;
    }
}

注意:这种方式,目前只适用于Controller以及子类,不适用于普通类
同时:经过这种方式,你能够获取到更多的系统实例对象,如ActionContextHttpContextHttpRequestHttpResponseViewDataDictionary、以及ActionBindingContext源码分析

在视图中,则能够经过@inject关键字来实现注入类型的实例提取,示例以下:

@using WebApplication1
@inject ITodoRepository repository
<div>
    @repository.AllItems.Count()
</div>

而最通常的使用方式,则是获取IServiceProvider的实例,获取该IServiceProvider实例的方式目前有以下几种(但范围不一样):

var provider1 = this.Request.HttpContext.ApplicationServices; 当前应用程序里注册的Service
var provider2 = Context.RequestServices;  // Controller中,当前请求做用域内注册的Service
var provider3 = Resolver; //Controller中

而后经过GetService和GetRequiredService方法来获取指定类型的实例,示例以下:

var _repository1 = provider1.GetService(typeof(ITodoRepository));
var _repository2 = provider1.GetService<LoggingHelper>();//等价形式
//上述2个对象可能为空

var _repository3 = provider1.GetRequiredService(typeof(ITodoRepository));
var _repository4 = provider1.GetRequiredService<LoggingHelper>();//等价形式
//上述2个对象确定不为空,由于若是为空的话,会自动抛异常出来

普通类的依赖注入

在新版的ASP.NET5中,不只支持上面咱们所说的接口类的依赖注入,还支持普通的类型的依赖注入,好比咱们生命一个普通类,示例以下:

public class AppSettings
{
    public string SiteTitle { get; set; }
}

上述普通类要保证有无参数构造函数,那么注册的用法,就应该像以下这样:

services.Configure<AppSettings>(app =>
{
    app.SiteTitle = "111";
});

使用的时候,则须要获取IOptions<AppSettings>类型的实例,而后其Options属性便是AppSettings的实例,代码以下:

var appSettings = app.ApplicationServices.GetRequiredService<IOptions<AppSettings>>().Options;

固然,咱们也能够在视图中,使用@inject语法来获取实例,示例代码以下:

@inject IOptions<AppSettings> AppSettings

<title>@AppSettings.Options.SiteTitle</title>

基于Scope生命周期的依赖注入

普通的Scope依赖注入

基于Scope做用域的实例在建立的时候须要先建立做用域,而后在该做用域内再获取特定的实例,咱们看看一个示例并对其进行验证。首先,注册依赖注入类型,代码以下:

services.AddScoped<ITodoRepository, TodoRepository>();

而后建立做用域,并在该做用域内获取实例:

var serviceProvider = Resolver;

var scopeFactory = serviceProvider.GetService<IServiceScopeFactory>(); //获取Scope工厂类
using (var scope = scopeFactory.CreateScope())  // 建立一个Scope做用域
{
    var containerScopedService = serviceProvider.GetService<ITodoRepository>();  //获取普通的实例
    var scopedService1 = scope.ServiceProvider.GetService<ITodoRepository>(); //获取当前Scope的实例
    Thread.Sleep(200);
    var scopedService2 = scope.ServiceProvider.GetService<ITodoRepository>(); //获取当前Scope的实例

    Console.WriteLine(containerScopedService == scopedService1); // 输出:False
    Console.WriteLine(scopedService1 == scopedService2); //输出:True
}

另外,Scope也能够进行嵌套,嵌套的内外做用域所获取的实例也是不相同的,实例代码以下:

var serviceProvider = Resolver;

var outerScopeFactory = serviceProvider.GetService<IServiceScopeFactory>();
using (var outerScope = outerScopeFactory.CreateScope()) //外部Scope做用域
{
    var innerScopeFactory = outerScope.ServiceProvider.GetService<IServiceScopeFactory>();
    using (var innerScope = innerScopeFactory.CreateScope()) //内部Scope做用域
    {
        var outerScopedService = outerScope.ServiceProvider.GetService<ITodoRepository>();
        var innerScopedService = innerScope.ServiceProvider.GetService<ITodoRepository>();

        Console.WriteLine(outerScopedService == innerScopedService); // 输出:False
    }
}

基于HTTP请求的Scope依赖注入

在以前不少流行的DI容器中,针对每一个请求,在该请求做用域内保留一个单实例对象是很流行的,也就是在每次请求期间一个类型的对象实例只会建立一次,这样能够大大提升性能。

在ASP.NET5中,基于HTTP请求的Scope依赖注入是经过一个ContainerMiddleware来实现的,调用该Middleware时,会建立一个限定做用域的DI容器,用于替换当前请求中已有的默认DI容器。在该管线中,全部后续的Middleware都会使用这个新的DI容器,在请求走完整个Pipeline管线之后,该ContainerMiddleware的做用就结束了,此时做用域会被销毁,而且在该做用域内建立的实例对象也都会销毁释放。

ContainerMiddleware的时序图以下所示:

具体的使用方式以下:

app.Use(new Func<RequestDelegate, RequestDelegate>(nextApp => new ContainerMiddleware(nextApp, app.ApplicationServices).Invoke));

普通类的依赖注入处理

目前普通类的依赖注入,只支持构造函数,好比咱们定于一个TestService类,代码以下:

public class TestService
{
    private ITodoRepository _repository;
    public TestService(ITodoRepository r)
    {
        _repository = r;
    }

    public void Show()
    {
        Console.WriteLine(_repository.AllItems);
    }
}

经过在构造函数里传入ITodoRepository类的参数来使用该实例,使用的时候须要先将该类注册到DI容器中,代码以下:

services.AddScoped<ITodoRepository, TodoRepository>();
services.AddSingleton<TestService>();

而后调用以下语句便可使用:

var service = serviceProvider.GetRequiredService<TestService>();

另外,须要注意,在目前的状况下,不能使用[FromServices]来使用依赖注入功能,好比,以下代码在获取TestService2实例的过程当中会出现错误:

public class TestService2
{
    [FromServices]
    public ITodoRepository Repository { get; set; }
    public void Show()
    {
        Console.WriteLine(Repository.AllItems);
    }
}

普通类中获取HttpContext实例

在MVC6中,咱们没办法经过HttpContent.Current来获取上下文对象了,因此在普通类中使用的时候就会出问题,要想在普通类中使用该上下文对象,须要经过依赖注入来获取HttpContext实例,微软在ASP.NET5中,提供了IHttpContextAccessor接口用于获取该上下文对象。也就是说,咱们能够将该类型的参数放在构造函数中,以获取上下文实例,代码以下:

public class TestService3
{
    private IHttpContextAccessor _httpContextAccessor;
    public TestService3(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public void Show()
    {
        var httpContext = _httpContextAccessor.HttpContext;//获取上下文对象实例
        Console.WriteLine(httpContext.Request.Host.Value);
    }
}

而使用的时候,则直接经过以下语句就能够了,代码以下:

var service = serviceProvider.GetRequiredService<TestService3>();
service.Show();

提示:普通类的构造函数中,能够传入多个DI容器支持的数据相似做为参数。

使用第三方DI容器

目前,.NETCore不支持,只能在全功能版的.NET framework上才能使用,因此使用的时候须要注意一下。第三方DI容器的替换一般是在Startup.cs的Configure方法中进行的,在方法的开始处进行替换,以便后续的Middleware会使用相关的依赖注入功能。

首先要引入第三方的容器,以Autofac为例,引入Microsoft.Framework.DependencyInjection.Autofac,而后加入以下示例中的替换代码便可:

app.UseServices(services =>
{
    services.AddMvc();// AddMvc要在这里注册
    var builder = new ContainerBuilder();// 构造容器构建类
    builder.Populate(services);//将现有的Services路由到Autofac的管理集合中
    IContainer container = builder.Build();
    return container.Resolve<IServiceProvider>();//返回AutoFac实现的IServiceProvider
});

注意,使用上述方法的时候,要把Mvc的注册代码services.AddMvc();必需要从ConfigureServices中挪到该表达式内,不然会报异常,等待微软解决。

另外,还有一个方式,微软目前的实例项目中尚未公开,经过分析一些代码,咱们能够发现,在Microsoft.AspNet.Hosting程序中的StartupLoader.cs负责程序入口点的执行,在该文件中,咱们知道首先是调用Startup.cs中的ConfigureServices方法,而后再调用Configure方法;咱们能够看到示例中的ConfigureServices的返回值是void类型的,但在源码分析中发现,在根据约定解析ConfigureServices方法的时候,其首先判断有没有返回类型是IServiceProvider的,若是有则执行该方法,用使用该返回中返回的新IServiceProvider实例;没有的话,再继续查找void类型的ConfigureServices方法。因此,咱们能够经过这种方式,来替换第三方的DI容器,实例代码以下:

// 须要先删除void类型的ConfigureServices方法
public IServiceProvider ConfigureServices(IServiceCollection services)
{
    var builder = new ContainerBuilder();  // 构造容器构建类
    builder.Populate(services);  //将现有的Services路由到Autofac的管理集合中
    IContainer container = builder.Build();
    return container.Resolve<IServiceProvider>(); //返回AutoFac实现的IServiceProvider
}

这样,你就能够像以往同样,使用Autofac的方式进行依赖类型的管理了,示例以下:

public class AutofacModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.Register(c => new Logger())
            .As<ILogger>()
            .InstancePerLifetimeScope();

        builder.Register(c => new ValuesService(c.Resolve<ILogger>()))
            .As<IValuesService>()
            .InstancePerLifetimeScope();
    }
}

地址:https://github.com/aspnet/Hosting/blob/dev/src/Microsoft.AspNet.Hosting/Startup/StartupLoader.cs
另一个关于Autofac集成的案例:http://alexmg.com/autofac-4-0-alpha-1-for-asp-net-5-0-beta-3/

最佳实践

在使用依赖注入的的时候,咱们应该遵照以下最佳实践。

  1. 作任何事情以前,务必在程序入口点提早注册全部的依赖类型。
  2. 避免直接使用IServiceProvider接口,相反,在构造函数里显式添加须要依赖的类型便可,让依赖注入引擎本身来解析实例,一旦依赖很难管理的话,就使用抽象工厂。
  3. 基于接口进行编程,而不是基于实现进行编程。

参考1:http://social.technet.microsoft.com/wiki/contents/articles/28875.dependency-injection-in-asp-net-vnext.aspx
参考2:http://blogs.msdn.com/b/webdev/archive/2014/06/17/dependency-injection-in-asp-net-vnext.aspx

同步与推荐

本文已同步至目录索引:解读ASP.NET 5 & MVC6系列

相关文章
相关标签/搜索