ASP.NET Core从底层上设计的,支持和使用依赖注入。ASP.NET Core应用可以利用内置的框架服务将它们注入到 Startup
类的方法中,并且应用程序服务能够配置注入。ASP.NET提供的默认服务容器提供了一个最小功能集并且不打算取代其他容器。
依赖注入(Dependency Injection,DI)是一种在对象及其它们的合作者或者依赖项之间获得松散耦合的技术。不是直接实例化一个合作者或者使用静态引用,类用来执行其动作的对象以某种方式提供给该类。通常,类会通过它们的构造函数声明其需要的依赖项,允许它们遵循 显示依赖原则 (Explicit Dependencies Principle)。这种方法被称为“构造函数注入(Constructor Injection)”。
当类采用DI思想来设计时,它们会耦合更加松散,因为它们没有对它们的合作者采用直接,硬编码依赖项,这遵循 依赖倒置原则
(Dependency Inversion Principle),其中指出“高级模块不应该依赖于低级模块;两者都应该依赖抽象。“类要求在类被构造时向其提供抽象(通常是 interfaces
),而不是引用特定的实现。将依赖提取到接口中并提供这些接口的实现作为参数也是 策略设计模式
(Strategy design pattern)的一个示例。
当一个系统被设计去使用DI,很多类通过它们的构造函数(或属性)请求其依赖关系,有一个专门用于创建这些类及其相关依赖关系是很有帮助的。这些类被称为 容器 或者更具体地, 控制反转 (Inversion of Control,IoC)容器或者依赖注入容器(Dependency Injection,DI)。一个容器本质上是一个工厂,负责提供从它请求的类型的实例。如果一个给定的类型声明它具有依赖关系,并且容器已经被配置来提供依赖类型, 它会创建依赖关系,作为创建所请求实例的一部分。以这种方式,可以向类提供提供复杂的依赖关系图而不需要任何硬编码的对象构造函数。除了使用它们的依赖关系创建对象之外,容器通常还管理应用程序中的对象生命周期。
ASP.NET Core包含一个简单的内置容器(由 IServiceProvider
接口表示),默认支持构造函数注入,并且ASP.NET可通过DI使某些服务可用。ASP.NET的容器指的是它管理的类型为 services
, services
是指由ASP.NET Core的IoC容器所管理的类型,你在你的应用程序的 Startup
类中的 ConfigureServices
方法中配置内置的容器服务。
Startup
类中的 ConfigureServices
方法负责定义应用程序将会使用的服务,包含平台功能比如Entity Framework Core和ASP.NET Core MVC。最初,提供给 ConfigureServices
的 IServiceCollection
只定义了一些服务。下面是一个如何使用一些扩展方法比如 AddDbContext
, AddIdentity
和 AddMvc
来向容器中添加额外服务的例子。
// 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<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); services.AddIdentity<ApplicationUser, IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders(); services.AddMvc(); // Add application services. services.AddTransient<IEmailSender, AuthMessageSender>(); services.AddTransient<ISmsSender, AuthMessageSender>(); }
ASP.NET提供的功能和中间件,比如MVC,遵循一个约定 - 使用单一的Add ServiceName 扩展方法来注册所有该功能所需的服务。
你也可以在 Startup
方法中通过它们的参数列表来请求一些框架提供的服务。
你可以注册你自己的应用程序服务。第一个泛型类型表示将要从容器中请求的类型(通常是一个接口)。第二个泛型类型表示将会由容器实例化并且用于完成这些请求的具体类型。
services.AddTransient<IEmailSender, AuthMessageSender>(); services.AddTransient<ISmsSender, AuthMessageSender>();
注:每一个 services.Add<ServiceName>
扩展方法添加(或可能配置)服务。比如, services.AddMvc()
添加一个MVC需要的服务。
AddTransient
方法用于将抽象类型映射到为每一个需要它的对象单独实例化的具体服务。这就是称为服务的 生命周期
,并额外的生命周期选项在下面描述。为你注册的每一个服务选择合适的生命周期是很重要的。
是否应该向请求它的每个类提供一个新的服务实例?或者在一个给定的Web请求中使用一个实例?或者应该在应用程序生命周期中使用单个实例?
下面的代码中,具体的数据访问机制被抽象在遵循 仓储模式(repository pattern)
的 IUserRepository
接口后面。 IUserRepository
的实例是通过构造函数请求并分配给一个私有字段,然后用来访问所需的用户。
public class UserController : Controller { private readonly IUserRepository _userRepository; private readonly ILogger<UserController> _logger; public UserController(IUserRepository userRepository, ILogger<UserController> logger) { _userRepository = userRepository; _logger = logger; } [HttpGet] public IEnumerable<UserItem> GetAll() { _logger.LogInformation("LoggingEvents.LIST_ITEMS","Listing all items"); return _userRepository.GetAll(); }
IUserRepository
定义了控制器需要使用 User
实例的几个方法。
public interface IUserRepository { void Add(UserItem item); IEnumerable<UserItem> GetAll(); UserItem Find(string key); UserItem Remove(string key); void Update(UserItem item); }
这个接口在运行时使用一个具体的 UserRepository
类型来实现。
注:在 UserRepository
类中使用的DI的方式是一个你可以在你的应用程序服务遵循的通用模型,不只是在”仓储库”或者数据访问类中。
public class UserRepository : IUserRepository { private readonly UserContext _context; //<== public class UserContext : DbContext public _UserRepository(UserContext context) //<== { _context = context; //<== } public async Task<IEnumerable<User>> GetAll() { return await _context.Users.ToListAsync(); } public async Task<User> Get(int id) { return await _context.Users.SingleOrDefaultAsync(u => u.ID == id); } public async void Add(User user) { _context.Add(user); await _context.SaveChangesAsync(); } public async void Update(User user) { _context.Update(user); await _context.SaveChangesAsync(); } public async void Delete(int id) { var user = _context.Users.SingleOrDefault(u => u.ID == id); _context.Users.Remove(user); await _context.SaveChangesAsync(); } public bool UserExists(int id) { return _context.Users.Any(e => e.ID == id); } }
注: UserRepository
的构造函数需要一个 DbContext
。依赖注入用于像这样的链式方式并不少见,每个请求依次请求它的依赖关系。容器负责解析所有的依赖关系并返回一个完全解析后的服务。
注:创建所请求的对象和它所需的所有对象,以及那些需要的所有对象,称为 对象图(object graph) 。同样,必须解析的依赖关系的集合通常称为 依赖树(dependency tree) 或 依赖图(dependency graph)
UserRepository
和 DbContext
都必须在 Statup
类中的 ConfigureServices
方法的服务容器中注册。 DbContext
通过对扩展方法 AddDbContext<T>
的调用进行配置。以下代码显示了 UserRepository
类型的注册。
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { string connectionString = Configuration.GetConnectionString("MyConnection"); services.AddDbContext<UserContext>(options => options.UseMySQL(connectionString)); // Add framework services. services.AddMvc(); // Register application services. // Add our repository type services.AddScoped<IUserRepository, UserRepository>(); }
Entity Framework上下文应该使用使用 Scoped
生命周期添加到服务容器中。如果你使用如下所示的帮助方法,这是自动处理的。使用Entity Framework存储库应使用相同的生命周期。
ASP.NET服务可以配置成如下的生命周期:
Transient生命周期服务在它们每次请求时被创建。这种生命周期适合于轻量级,无状态的服务。
Scoped生命周期服务在每次请求被创建一次。
Singleton生命周期服务在它们第一次被请求时被创建(或者如果你在 ConfigureServices
运行时指定了一个实例)并且每个后续请求将使用相同的实例。如果你的应用程序需要单例行为,推荐让服务容器来管理服务的生命周期而不是实现在你的类中实现单例模式和管理对象的生命周期。