本篇将介绍Asp.Net Core中一个非常重要的特性:依赖注入,并展示其简单用法。
第一部分、概念介绍
Dependency Injection:又称依赖注入,简称DI。在以前的开发方式中,层与层之间、类与类之间都是通过new一个对方的实例进行相互调用,这样在开发过程中有一个好处,可以清晰的知道在使用哪个具体的实现。随着软件体积越来越庞大,逻辑越来越复杂,当需要更换实现方式,或者依赖第三方系统的某些接口时,这种相互之间持有具体实现的方式不再合适。为了应对这种情况,就要采用契约式编程:相互之间依赖于规定好的契约(接口),不依赖于具体的实现。这样带来的好处是相互之间的依赖变得非常简单,又称松耦合。至于契约和具体实现的映射关系,则会通过配置的方式在程序启动时由运行时确定下来。这就会用到DI。
第二部分、DI的注册与注入
借用这个系列之前的框架结构,添加如下接口和实现类
1 using System.Collections.Generic; 2 using WebApiFrame.Models; 3 4 namespace WebApiFrame.Repositories 5 { 6 public interface IUserRepository 7 { 8 IEnumerableGetAll(); 9 10 User GetById(int id); 11 } 12 }
1 using System.Collections.Generic; 2 using System.Linq; 3 using WebApiFrame.Models; 4 5 namespace WebApiFrame.Repositories 6 { 7 public class UserRepository : IUserRepository 8 { 9 private IListlist = new List () 10 { 11 new User(){ Id = 1, Name = "name:1", Sex = "Male" }, 12 new User(){ Id = 2, Name = "name:2", Sex = "Female" }, 13 new User(){ Id = 3, Name = "name:3", Sex = "Male" }, 14 }; 15 16 public IEnumerable GetAll() 17 { 18 return list; 19 } 20 21 public User GetById(int id) 22 { 23 return list.FirstOrDefault(i => i.Id == id); 24 } 25 } 26 }
一、注册
修改 Startup.cs 的ConfigureServices方法,将上面的接口和实现类注入到DI容器里
1 public void ConfigureServices(IServiceCollection services) 2 { 3 // 注入MVC框架 4 services.AddMvc(); 5 6 // 注册接口和实现类的映射关系 7 services.AddScoped(); 8 }
修改 UsersController.cs 的构造函数和Action方法
1 using System; 2 using Microsoft.AspNetCore.Mvc; 3 using WebApiFrame.Models; 4 using WebApiFrame.Repositories; 5 6 namespace WebApiFrame.Controllers 7 { 8 [Route("api/[controller]")] 9 public class UsersController : Controller 10 { 11 private readonly IUserRepository userRepository; 12 13 public UsersController(IUserRepository userRepo) 14 { 15 userRepository = userRepo; 16 } 17 18 [HttpGet] 19 public IActionResult GetAll() 20 { 21 var list = userRepository.GetAll(); 22 return new ObjectResult(list); 23 } 24 25 [HttpGet("{id}")] 26 public IActionResult Get(int id) 27 { 28 var user = userRepository.GetById(id); 29 return new ObjectResult(user); 30 } 31 32 #region 其他方法 33 // ...... 34 #endregion 35 } 36 }
启动程序,分别访问地址 http://localhost:5000/api/users 和 http://localhost:5000/api/users/1 ,页面将展示正确的数据。
从上面的例子可以看到,在 Startup.cs 的ConfigureServices的方法里,通过参数的AddScoped方法,指定接口和实现类的映射关系,注册到DI容器里。在控制器里,通过构造方法将具体的实现注入到对应的接口上,即可在控制器里直接调用了。
除了在ConfigureServices方法里进行注册外,还可以在Main函数里进行注册。注释掉 Startup.cs ConfigureServices方法里的注入代码,在 Program.cs 的Main函数里添加注入方法
1 using Microsoft.AspNetCore.Hosting; 2 using Microsoft.Extensions.DependencyInjection; 3 using WebApiFrame.Repositories; 4 5 namespace WebApiFrame 6 { 7 public class Program 8 { 9 public static void Main(string[] args) 10 { 11 var host = new WebHostBuilder() 12 .UseKestrel() 13 .ConfigureServices(services=> 14 { 15 // 注册接口和实现类的映射关系 16 services.AddScoped(); 17 }) 18 .UseStartup() 19 .Build(); 20 21 host.Run(); 22 } 23 } 24 }
此方法等效于 Startup.cs 的ConfigureServices方法。
二、注入
添加三个测试接口和实现类
1 namespace WebApiFrame 2 { 3 public interface ITestOne 4 { 5 6 } 7 8 public class TestOne : ITestOne 9 { 10 11 } 12 }
1 namespace WebApiFrame 2 { 3 public interface ITestTwo 4 { 5 6 } 7 8 public class TestTwo : ITestTwo 9 { 10 11 } 12 }
1 namespace WebApiFrame 2 { 3 public interface ITestThree 4 { 5 6 } 7 8 public class TestThree : ITestThree 9 { 10 11 } 12 }
修改 Startup.cs 的ConfigureServices方法,将接口和实现类的映射关系注册到DI容器
1 public void ConfigureServices(IServiceCollection services) 2 { 3 // 注入MVC框架 4 services.AddMvc(); 5 6 // 注册接口和实现类的映射关系 7 services.AddScoped(); 8 services.AddScoped(); 9 services.AddScoped(); 10 }
添加 DemoController.cs 类
1 using System.Threading.Tasks; 2 using Microsoft.AspNetCore.Http; 3 using Microsoft.AspNetCore.Mvc; 4 5 namespace WebApiFrame 6 { 7 [Route("[controller]")] 8 public class DemoController : Controller 9 { 10 private readonly ITestOne _testOne; 11 private readonly ITestTwo _testTwo; 12 private readonly ITestThree _testThree; 13 14 public DemoController(ITestOne testOne, ITestTwo testTwo, ITestThree testThree) 15 { 16 _testOne = testOne; 17 _testTwo = testTwo; 18 _testThree = testThree; 19 } 20 21 [HttpGet("index")] 22 public async Task Index() 23 { 24 HttpContext.Response.ContentType = "text/html"; 25 await HttpContext.Response.WriteAsync($"ITestOne => {_testOne}
"); 26 await HttpContext.Response.WriteAsync($"ITestTwo => {_testTwo}
"); 27 await HttpContext.Response.WriteAsync($"ITestThree => {_testThree}
"); 28 } 29 } 30 }
启动程序,访问地址 http://localhost:5000/demo/index ,页面显示了每个接口对应的实现类
通常依赖注入的方式有三种:构造函数注入、属性注入、方法注入。在Asp.Net Core里,采用的是构造函数注入。
在以前的Asp.Net MVC版本里,控制器必须有一个无参的构造函数,供框架在运行时调用创建控制器实例,在Asp.Net Core里,这不是必须的了。当访问控制器的Action方法时,框架会依据注册的映射关系生成对应的实例,通过控制器的构造函数参数注入到控制器中,并创建控制器实例。
三、构造函数的选择
上一个例子展示了在.Net Core里采用构造函数注入的方式实现依赖注入。当构造函数有多个,并且参数列表不同时,框架又会采用哪一个构造函数创建实例呢?
为了更好的演示,新建一个.Net Core控制台程序,引用下面两个nuget包。DI容器正是通过这两个包来实现的。
"Microsoft.Extensions.DependencyInjection": "1.0.0" "Microsoft.Extensions.DependencyInjection.Abstractions": "1.0.0"
同样新建四个测试接口和实现类,并在Main函数添加注册代码。最终代码如下
1 using Microsoft.Extensions.DependencyInjection; 2 using System; 3 4 namespace DiApplicationTest 5 { 6 public class Program 7 { 8 public static void Main(string[] args) 9 { 10 IServiceCollection services = new ServiceCollection(); 11 services.AddScoped() 12 .AddScoped () 13 .AddScoped () 14 .AddScoped () 15 .BuildServiceProvider() 16 .GetService (); 17 18 Console.ReadLine(); 19 } 20 } 21 22 public interface ITestOne { } 23 public interface ITestTwo { } 24 public interface ITestThree { } 25 26 public class TestOne : ITestOne { } 27 public class TestTwo : ITestTwo { } 28 public class TestThree : ITestThree { } 29 30 public interface ITestApp { } 31 public class TestApp : ITestApp 32 { 33 public TestApp(ITestOne testOne, ITestTwo testTwo, ITestThree testThree) 34 { 35 Console.WriteLine($"TestApp({testOne}, {testTwo}, {testThree})"); 36 } 37 } 38 }
启动调试,在cmd窗口可以看见打印内容
这里注册了四个接口和对应的实现类,其中一个接口的实现类 TestApp.cs 拥有一个三个参数的构造函数,这三个参数类型分别是其他三个接口。通过GetServices方法通过唯一的一个构造函数创建了 TestApp.cs 的一个实例。
接下来在 TestApp.cs 里添加一个有两个参数的构造函数,同时修改Main函数内容,去掉一个接口的注册
1 public class TestApp : ITestApp 2 { 3 public TestApp(ITestOne testOne, ITestTwo testTwo) 4 { 5 Console.WriteLine($"TestApp({testOne}, {testTwo})"); 6 } 7 8 public TestApp(ITestOne testOne, ITestTwo testTwo, ITestThree testThree) 9 { 10 Console.WriteLine($"TestApp({testOne}, {testTwo}, {testThree})"); 11 } 12 }
1 public static void Main(string[] args) 2 { 3 IServiceCollection services = new ServiceCollection(); 4 services.AddScoped() 5 .AddScoped () 6 //.AddScoped () 7 .AddScoped() 8 .BuildServiceProvider() 9 .GetService (); 10 11 Console.ReadLine(); 12 }
再次启动调试,查看cmd窗口打印内容
当有多个构造函数时,框架会选择参数都是有效注入接口的构造函数创建实例。在上面这个例子里, ITestThree.cs 和 TestThree.cs 的映射关系没有注册到DI容器里,框架在选择有效的构造函数时,会过滤掉含有ITestThree接口类型的参数的构造函数。
接下来在 TestApp.cs 再添加一个构造函数。为了方便起见,我给每个构造函数添加了编号标识一下。
1 public class TestApp : ITestApp 2 { 3 // No.1 4 public TestApp(ITestOne testOne) 5 { 6 Console.WriteLine($"TestApp({testOne})"); 7 } 8 9 // No.2 10 public TestApp(ITestOne testOne, ITestTwo testTwo) 11 { 12 Console.WriteLine($"TestApp({testOne}, {testTwo})"); 13 } 14 15 // No.3 16 public TestApp(ITestOne testOne, ITestTwo testTwo, ITestThree testThree) 17 { 18 Console.WriteLine($"TestApp({testOne}, {testTwo}, {testThree})"); 19 } 20 }
再次启动调试,查看cmd窗口打印内容
结果显示框架选择了No.2号构造函数。框架会选择参数列表集合是其他所有有效的构造函数的参数列表集合的超集的构造函数。在这个例子里,有No.1和No.2两个有效的构造函数,No.2的参数列表集合为[ITestOne, ITestTwo],No.1的参数列表集合为[ITestOne],No.2是No.1的超集,所以框架选择了No.2构造函数创建实例。
接下来修改下 TestApp.cs 的构造函数,取消Main函数里 ITestThree.cs 注册代码的注释
1 public class TestApp : ITestApp 2 { 3 // No.2 4 public TestApp(ITestOne testOne, ITestTwo testTwo) 5 { 6 Console.WriteLine($"TestApp({testOne}, {testTwo})"); 7 } 8 9 // No.4 10 public TestApp(ITestTwo testTwo, ITestThree testThree) 11 { 12 Console.WriteLine($"TestApp({testTwo}, {testThree})"); 13 } 14 }
启动调试,发现会抛出一个 System.InvalidOperationException 异常,异常内容表明框架无法选择一个正确的构造函数,不能创建实例。
在这个例子里,两个构造函数的参数列表集合分别为[ITestOne, ITestTwo]和[ITestTwo, ITestThree],因为谁也无法是对方的超集,所以框架不能继续创建实例。
总之,框架在选择构造函数时,会依次遵循以下两点规则:
1. 使用有效的构造函数创建实例
2. 如果有效的构造函数有多个,选择参数列表集合是其他所有构造函数参数列表集合的超集的构造函数创建实例
如果以上两点都不满足,则抛出 System.InvalidOperationException 异常。
四、Asp.Net Core默认注册的服务接口
框架提供了但不限于以下几个接口,某些接口可以直接在构造函数和 Startup.cs 的方法里注入使用
第三部分、生命周期管理
框架对注入的接口创建的实例有一套生命周期的管理机制,决定了将采用什么样的创建和回收实例。
下面通过一个例子演示这三种方式的区别
在第二部分的第二点的例子里添加以下几个接口和实现类
1 using System; 2 3 namespace WebApiFrame 4 { 5 public interface ITest 6 { 7 Guid TargetId { get; } 8 } 9 10 public interface ITestTransient : ITest { } 11 public interface ITestScoped : ITest { } 12 public interface ITestSingleton : ITest { } 13 14 public class TestInstance : ITestTransient, ITestScoped, ITestSingleton 15 { 16 public Guid TargetId 17 { 18 get 19 { 20 return _targetId; 21 } 22 } 23 24 private Guid _targetId { get; set; } 25 26 public TestInstance() 27 { 28 _targetId = Guid.NewGuid(); 29 } 30 } 31 }
1 namespace WebApiFrame 2 { 3 public class TestService 4 { 5 public ITestTransient TestTransient { get; } 6 public ITestScoped TestScoped { get; } 7 public ITestSingleton TestSingleton { get; } 8 9 public TestService(ITestTransient testTransient, ITestScoped testScoped, ITestSingleton testSingleton) 10 { 11 TestTransient = testTransient; 12 TestScoped = testScoped; 13 TestSingleton = testSingleton; 14 } 15 } 16 }
修改 Startup.cs 的ConfigureServices方法里添加注册内容
1 public void ConfigureServices(IServiceCollection services) 2 { 3 // 注入MVC框架 4 services.AddMvc(); 5 6 // 注册接口和实现类的映射关系 7 services.AddTransient(); 8 services.AddScoped(); 9 services.AddSingleton(); 10 services.AddTransient(); 11 }
修改 DemoController.cs 内容
1 using System.Threading.Tasks; 2 using Microsoft.AspNetCore.Http; 3 using Microsoft.AspNetCore.Mvc; 4 5 namespace WebApiFrame 6 { 7 [Route("[controller]")] 8 public class DemoController : Controller 9 { 10 public ITestTransient _testTransient { get; } 11 public ITestScoped _testScoped { get; } 12 public ITestSingleton _testSingleton { get; } 13 public TestService _testService { get; } 14 15 public DemoController(ITestTransient testTransient, ITestScoped testScoped, ITestSingleton testSingleton, TestService testService) 16 { 17 _testTransient = testTransient; 18 _testScoped = testScoped; 19 _testSingleton = testSingleton; 20 _testService = testService; 21 } 22 23 [HttpGet("index")] 24 public async Task Index() 25 { 26 HttpContext.Response.ContentType = "text/html"; 27 await HttpContext.Response.WriteAsync($"Controller Log
"); 28 await HttpContext.Response.WriteAsync($"Transient => {_testTransient.TargetId.ToString()}
"); 29 await HttpContext.Response.WriteAsync($"Scoped => {_testScoped.TargetId.ToString()}
"); 30 await HttpContext.Response.WriteAsync($"Singleton => {_testSingleton.TargetId.ToString()}
"); 31 32 await HttpContext.Response.WriteAsync($"Service Log
"); 33 await HttpContext.Response.WriteAsync($"Transient => {_testService.TestTransient.TargetId.ToString()}
"); 34 await HttpContext.Response.WriteAsync($"Scoped => {_testService.TestScoped.TargetId.ToString()}
"); 35 await HttpContext.Response.WriteAsync($"Singleton => {_testService.TestSingleton.TargetId.ToString()}
"); 36 } 37 } 38 }
启动调试,连续两次访问地址 http://localhost:5000/demo/index ,查看页面内容
对比内容可以发现,在同一个请求里,Transient对应的GUID都是不一致的,Scoped对应的GUID是一致的。而在不同的请求里,Scoped对应的GUID是不一致的。在两个请求里,Singleton对应的GUID都是一致的。
第三部分、第三方DI容器
除了使用框架默认的DI容器外,还可以引入其他第三方的DI容器。下面以Autofac为例,进行简单的演示。
引入Autofac的nuget包
"Autofac.Extensions.DependencyInjection": "4.0.0-rc3-309"
在上面的例子的基础上修改 Startup.cs 的ConfigureServices方法,引入autofac的DI容器,修改方法返回值
1 public IServiceProvider ConfigureServices(IServiceCollection services) 2 { 3 // 注入MVC框架 4 services.AddMvc(); 5 6 // autofac容器 7 var containerBuilder = new ContainerBuilder(); 8 containerBuilder.RegisterType().As ().InstancePerDependency(); 9 containerBuilder.RegisterType ().As ().InstancePerLifetimeScope(); 10 containerBuilder.RegisterType ().As ().SingleInstance(); 11 containerBuilder.RegisterType ().AsSelf().InstancePerDependency(); 12 containerBuilder.Populate(services); 13 14 var container = containerBuilder.Build(); 15 return container.Resolve (); 16 }
启动调试,再次访问地址 http://localhost:5000/demo/index ,会得到上个例子同样的效果。