团队开发框架实战—ASP.NET Core的依赖注入
ASP.NET Core 1.0在设计上原生就支持和有效利用依赖注入。在Startup类中,应用可以通过将框架内嵌服务注入到方法中来使用他们;另一方面,你也可以配置服务来注入使用。默认的服务容器只提供了最小的特性集合,所以并不打算取代其他的IoC容器。
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddDbContext<ActDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("SqlServerConnection")));
services.AddMvc();
services.AddTransient<IUnitOfWork, ActUnitOfWork>();
services.AddTransient<IRepositoryBase<Role>, ActRepositoryBase<Role>>();
services.AddTransient<IRoleAppService, RoleAppService>();
AutoMapper.Configuration.Configure();
}
什么是依赖注入DI
依赖注入是为了达到解耦对象和其依赖的一项技术。一个类为了完成自身某些操作所需的对象是通过某种方式提供的,而不是使用静态引用或者直接实例化。通常情况下,类通过构造器来声明其依赖,遵循 显式依赖原则 。这种方式称作构造器注入。
当以DI思想来设计类时,这些类更加松耦合,因为他们不直接硬编码的依赖其合作者。这遵循了依赖倒置原则,即高层模块不应依赖底层模块,两者都应依赖抽象。类在构建时所需是抽象(如接口interface),而不是具体的实现。把依赖抽离成接口,把这些接口的实现作为参数也是 策略设计模式 的例子。
当一个系统使用DI来设计时,很多类通过构造器或者属性来添加依赖,这样就很方便有一个专门的类来创建这些类以及他们相关的依赖。这样的类称之为“容器”或者“IoC容器”或“DI容器”。一个容器本质上是一个工厂,来给请求者提供类型实例。如果给定类型声明了自身依赖,容器也配置了来提供这些依赖类型,那么它会创建这些依赖作为请求实例的一部分。通过这种方式可以为 类提供复杂的依赖图,而不需要任何硬编码的对象依赖。除了创建依赖对象外,容器一般还管理应用内的对象生命周期。
ASP.NET Core 1.0提供了一个简单的内置容器(以IServiceProvider为代表),默认支持构造器注入,这样ASP.NET可以通过DI使某些服务可用。ASP.NET把它所管理的类型称之为服务。本文的剩下部分,服务即指ASP.NET IoC容器所管理的类型。你可以在Startup类中的ConfigureServices 方法中配置内置的容器服务。
Note:Martin Fowler写过一篇很详细的依赖反转的 文章 。微软对此也有很棒的描述 连接 。
总结一下IOC和DI
IOC:Inversion of Control,即“控制反转”,他不是什么新的技术,而是一种设计思想。
通常我们是这么理解,我们一般的设计思想是在对象内部直接控制,而IOC是将设计好的对象交给容器控制,而不是传统的在对象内部直接控制。
打个比方:我们租房子,在我们和房主之间插入了一个中间人(房介),我们只需要跟房介提出我们的要求,比如房子要三室一厅、卧室向阳、房东是女的(_ )、楼层不要太低、遮光不要太长等等等等,然后房介就会按照我们的要求给我们提供一个房产信息,我们满意就跟租赁、入住,如果我们不满意(抛出异常),房介就会帮我们做后续处理。整个过程不再是由我们控制,而是由房介这么一个容器去控制。所有的类都会在spring容器中登记,告诉spring你是个什么东西,你需要什么东西,然后spring会在系统运行到适当的时候,把你要的东西主动给你,同时也把你交给其他需要你的东西。所有的类的创建、销毁都由 spring来控制,也就是说控制对象生存周期的不再是引用它的对象,而是spring。对于某个具体的对象而言,以前是它控制其他对象,现在是所有对象都被spring控制,所以这叫控制反转。
DI:IOC的一个重点是在系统运行中,动态的向某个对象提供它所需要的其他对象。这一点是通过DI(Dependency Injection,依赖注入)来实现的。比如对象A需要操作数据库,以前我们总是要在A中自己编写代码来获得一个Connection对象,有了 spring我们就只需要告诉spring,A中需要一个Connection,至于这个Connection怎么构造,何时构造,A不需要知道。在系统运行时,spring会在适当的时候制造一个Connection,然后像打针一样,注射到A当中,这样就完成了对各个对象之间关系的控制。A需要依赖 Connection才能正常运行,而这个Connection是由spring注入到A中的,依赖注入的名字就这么来的。那么DI是如何实现的呢? 有一个重要特征是反射(reflection),它允许程序在运行的时候动态的生成对象、执行对象的方法、改变对象的属性,spring就是通过反射来实现注入的。
使用框架提供的服务
ConfigureServices方法负责定义应用使用的服务,包括平台特性服务如EF和ASP.NET MVC。最初提供给ConfigureServices的IServiceCollection只有少数服务。默认web模板提供了怎么通过扩展方法来添加额外服务到容器的例子,如AddEntityFramework, AddIdentity, 和AddMVC。
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddEntityFramework()
.AddSqlServer()
.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddMvc();
// Add application services.
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
}
ASP.NET提供的特性和中间件遵循使用一个AddService扩展方法的约定,来注册该特性使用的所需的所有服务。
Note:你可以在Startup方法中请求某些framework-provided服务,详见应用启动 Application Startup
当然,除了配置框架提供的各种服务,你也可以配置自己定义的服务。
注册自定义服务
在默认web模板中,有如下两个服务被添加到IServiceCollection中
// Add application services.
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
AddTransient方法将抽象类型映射为实体服务,对于每个请求这都单独实例化,这称作服务的生命周期。额外生命周期选项如下。对于每个注册的服务选择合适的生命周期是很重要的。是对每个请求类都提供一个新的实例化服务?还是在给定web请求内只实例化一次?还是在应用周期内只有单例?
在本文的例子中,有个简单的CharacterController来显示Character姓名,在Index方法中显示已存储的Character(如果没有则创建)。虽然注册了EF服务,但本例持久化没有使用数据库。具体的数据获取服务抽象到了ICharacterRepository接口实现中,这遵从了 仓储模式 。在构造器中请求ICharacterRepository参数,并将其赋给私有变量,来根据需要获取Character。
using System.Linq;
using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Models;
using Microsoft.AspNet.Mvc;
namespace DependencyInjectionSample.Controllers
{
public class CharactersController : Controller
{
private readonly ICharacterRepository _characterRepository;
public CharactersController(ICharacterRepository characterRepository)
{
_characterRepository = characterRepository;
}
// GET: /characters/
public IActionResult Index()
{
var characters = _characterRepository.ListAll();
if (!characters.Any())
{
_characterRepository.Add(new Character("Darth Maul"));
_characterRepository.Add(new Character("Darth Vader"));
_characterRepository.Add(new Character("Yoda"));
_characterRepository.Add(new Character("Mace Windu"));
characters = _characterRepository.ListAll();
}
return View(characters);
}
}
接口ICharacterRepository只简单定义了两个方法,Controller通过其来操作Charcter实例。
using System.Collections.Generic;
using DependencyInjectionSample.Models;
namespace DependencyInjectionSample.Interfaces
{
public interface ICharacterRepository
{
IEnumerable<Character> ListAll();
void Add(Character character);
}
}
接口有具体类型CharacterRepository来实现,在运行时被使用。
Note:CharacterRepository类只是使用DI的普通例子,你可以对应用所有的服务使用DI,而不仅仅是“仓储”和数据获取类。
using System.Collections.Generic;
using System.Linq;
using DependencyInjectionSample.Interfaces;
namespace DependencyInjectionSample.Models
{
public class CharacterRepository : ICharacterRepository
{
private readonly ApplicationDbContext _dbContext;
public CharacterRepository(ApplicationDbContext dbContext)
{
_dbContext = dbContext;
}
public IEnumerable<Character> ListAll()
{
return _dbContext.Characters.AsEnumerable();
}
public void Add(Character character)
{
_dbContext.Characters.Add(character);
_dbContext.SaveChanges();
}
}
}
请注意,CharacterRepository在其构造器中请求了ApplicationDbContext实例。这种链式的依赖注入是很常见的,被依赖本身又有自己的依赖。容器来负责已树形的方式来解析所有这些依赖,并返回解析完成的服务。
Note:创建请求对象,以及其依赖,其依赖的依赖,有时这被称之为 对象图 。同样的,需要解析的对象集合称之为 依赖树 或者 依赖图 。
ICharacterRepository和ApplicationDbContext都必须在ConfigureServices中注册。ApplicationDbContext是通过扩展方法AddEntityFramework来配置,它包括添加DbContext (AddDbContext )的一个扩展。仓储的注视在在ConfigureServices方法的结尾。
services.AddTransient<ISmsSender, AuthMessageSender>();
services.AddScoped<ICharacterRepository, CharacterRepository>();
// Show different lifetime options
services.AddTransient<IOperationTransient, Operation>();
EF contexts需要使用scoped生命周期来添加到服务容器。如果你使用了上面的helper方法,这是已经处理好的。使用EF的仓储服务应该使用同样的生命周期。
警告: 主要不安全的来源是通过单例来解析Scoped生命周期服务服务。这样做的后果,很有可能在处理后续请求时使用的服务的状态是错误的。