依赖注入
文献来源:《Pro ASP.NET Core MVC》 Adam Freeman 第18章 依赖注入
1 依赖注入原理
- 所有可能变化的地方都用接口
- 在使用接口的地方用什么实体类通过在
ConfigureService
中注册解决 - 注册的实体类需要指定在何种生命周期中有效
- Transient
- Scoped
- Singleton
2 接口
接口只定义契约,不定义实现。
//IRepository.cs
using System.Collections.Generic;
namespace DependencyInjection.Models{
public interface IRepository{
IEnumerable<Product> Products{get;}
Product this[string name]{get;}
void AddProduct(Product product);
void DeleteProduct(Product product);
}
}
//MemoryRepository.cs
using System.Collections.Generic;
namespace DependencyInjection.Models{
public class MemoryRepository:IRepository{
private Dictionary<string, Product> products;
public MemoryRepository(){
products = new Dictionary<string, Product>();
new List<Product{
new Product{Name="Kayak", Price=275M},
new Product{Name="Lifejacket", Price=48.95M},
new Product{Name="Soccer ball", Price=19.50M},
}.ForEach(p=>AddProduct(p));
}
public IEnumerable<Product> Products => products.Values;
public Product this[string name] => products[name];
public void AddProduct(Product product) => products[product.Name] = product;
public void DeleteProduct(Product product) => products.Remove(product.Name);
}
}
3 数据绑定页面
页面绑定代码使用@model和ViewBag
实现。
//Index.cshtml
@using DependencyInjection.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@model IEnumerable<Product>
@{layout=null;}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Dependency Injection</title>
<link rel="stylesheet" asp-href-include="lib/bootstrap/dist/css/*.min.css" />
</head>
<body class="panel-body">
@if(ViewData.Count>0){
<table class="table table-bordered table-condense table-striped">
@foreach(var kvp in ViewData){
<tr>
<td>@kvp.Key</td>
<td>@kvp.Value</td>
</tr>
}
</table>
}
<table class="table table-bordered table-condense table-striped">
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
@if(Model==null){
<tr><td colspan="3" class="text-center">No Model Data</td></tr>
}
else{
@foreach(var p in Model){
<tr>
<td>@p.Name</td>
<td>@string.Format("{0:C2}",p.Price)</td>
</tr>
}
}
</tbody>
</table>
</body>
</html>
该页面绑定了2组数据集
ViewData
集合@model IEnumerable<Product>
后台绑定
//HomeController.cs
//使用实体类的Products属性绑定页面
using Microsoft.AspNetCore.Mvc;
using DependencyInjection.Models;
public class HomeController:Controller{
public ViewResult Index() => View(new MemoryRepository().Products);
}
4 ASP.NET MVC定义依赖注入
4.1 接口依赖注入
- 单实例依赖注入
首先将后台处理由指定的实体类换成接口
//HomeController.cs
using Microsoft.AspNetCore.Mvc;
using DependencyInjection.Models;
using DependencyInjection.Infrastructure;
namespace DependencyInjection.Controllers{
//增加接口私有变量
private IRepository repository;
//通过构造函数传参至该私有变量
public HomeController(IRepository repo){
repository = repo;
}
//绑定接口的Products属性
//运行时由ASP.NET MVC框架确定使用何种实体类
public ViewResult Index()=>View(repository.Products);
}
然后配置接口与实体类之间的关系
//Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using DependencyInjection.Infrastructure;
using DependencyInjection.Model;
namespace DependencyInjection{
public void ConfigureServices(IServicesCollection services){
services.AddTransient<IRepository, MemoryRepository>();
...
}
...
}
services.AddTransient
告诉service provider
如何处理依赖。这里要注意一点,service provider
是整个MVC
框架服务的总提供者。
-
链式依赖
某实体类依赖于某接口,且该实体类中的成员变量又依赖于另一接口。
//MemoryRepository.cs
using DependencyInjection.Models{
public class MemoryRepository:IRepository{
private IModelStorage storage;//新增私有变量用于指定所属的仓库
public MemoryStorage(IModelStorage storage){
storage = modelStorage;
new List<Product>{
new Product{Name="Kayak",Price=275M},
new Product{Name="Lifejacket", Price=48.95M},
new Product{Name="Soccer ball", Price=19.50M}
}.ForEach(p=>AddProduct(p));
}
public IEnumerable<Product> Products=>storage.Items;
public Product this[string name] => storage[name];
public void AddProduct(Product product){
storage[product.Name] = product;
}
public void DeleteProduct(Product product){
storage.RemoveItem(product.Name);
}
}
}
在ConfigureServices
中注册。
//Startup.cs
...
namespace DependencyInjection{
public class Startup{
public void ConfigureServices(IServiceCollection services){
services.AddTransient<IRepository, MemoryRepository>();
services.AddTransient<IModelStorage, DictionaryStorage>();
...
}
...
}
}
4.2 实体类依赖注入
假设存在实体类ProductTotalizer
用于计算总价。
using System.Linq;
namespace DependencyInjection.Models{
public class ProductTotalizer{
public ProductTotalizer(IRepository repo){
Repository = repo;
}
//所述仓库用接口表示
public IRepository Repository {get;set;}
//总价
public decimal Total => Repository.Products.Sum(p=>p.Price);
}
}
在HomeController
中加入ProductTotalizer
变量。
...
public class HomeController:Controller{
private IRepository repository;
private ProductTotalizer totalizer;
public HomeController(IRepository repo, ProductTotalizer total){
repository = repo;
totalizer = total;
}
public viewResult Index(){
ViewBag.Total = totalizer.Total;
return View(repository.Products);
}
}
在Index.cshtml
中包含了对ViewBag
的显示,这里实际绑定了2个数据,第一个是ViewBag
,第二个是repository
。
为了明确在Controller中使用何种ProductTotalizer
(可能在后面的开发中,ProductTotalizer
是父类),可以对其进行强行指定。
//Startup.cs
...
public void ConfigureServices(IServicesCollection services){
...
services.AddTransient<ProductTotalizer>();
...
}
5 服务生命周期
类别 | 生命周期 |
---|---|
Transient | 只要有调用就创建新的对象。就算类型一样,但是对象不一样 |
Scoped | 在一个Controller 之中,只要类型一样,对象就一样。刷新Controller 之后对象变了,但是多个同一类型的对象还是同一个。 |
Singleton | 只要初始化了,就一直是这个对象,不管是否刷新Controller |
5.1 Transient
首先在service provider
中注册IRepository
的调用使用MemoryRepository
。
...
public void ConfigureServices(IServiceCollection services){
services.AddTransient<IRepository, MemoryRepository>();
...
}
...
在repository
中加入ToString()
函数,该函数的实现中包含了GUID
的生成。
//MemoryRepository.cs
using System.Collections.Generic;
namespace DependencyInjection.Models{
public class MemoryRepository:IRepository{
private IModelStorage storage;
private string guid = System.Guid.NewGuid().ToString();
public MemoryRepository(IModelStorage modelStore){
storage = modelStore;
...
}
...
public override string ToString(){
return guid;
}
}
}
在下图中可以看到,当HomeController
调用repository
时,service provider
会针对每一次调用都生成一个新的对象。
5.2 Scoped
其他的调用都一样,除了在service
中注册使用AddScoped
之外。
//Startup.cs
...
public void ConfigureServices(IServiceCollection services){
service.AddScoped<IRepository, MemoryRepository>();
...
}
5.3 Singleton
其他的调用都一样,除了在service
中注册使用AddSingleton
之外。
//Startup.cs
...
public void ConfigureServices(IServiceCollection services){
services.AddSingleton<IRepository, MemoryRepository>();
...
}
...
6 其他依赖注入
6.1 Action依赖注入
上述的依赖注入是针对整个Controller,但有的时候只想针对某一个Action进行依赖注入。此时只需要使用[FromServices]
即可。
using Microsoft.AspNetCore.Mvc;
using DependencyInjection.Models;
using DependencyInjection.Infrastructure;
namespace DependencyInjection.Controllers{
public class HomeController:Controller{
private IRepository repository;
public HomeController(IRepository repo){
repository = repo;
}
public ViewResult Index([FromServices]ProductTotalizer totalizer){
ViewBag.HomeController = repository.ToString();
ViewBag.Totalizer = totalizer.Repository.ToString();
return View(repository.Products);
}
}
}
上述代码表示在Index
中需要使用ProductTotalizer
时,从service provider
处获得。
6.2 手工进行依赖注入
除了上述的注册方式外,还有一些其他的注册方式。比如,当没有依赖出现时,而你又想获得一个接口的实例,该实例依赖于接口。在这种情况下,你可以直接通过service provider
来实现。
...
public class HomeController:Controller{
public ViewResult Index([FromServices]ProductTotalizer totalizer){
IRepository repository=
HttpContext.RequestServices.GetService<IRepository>();
...
}
}
...