ASP.NET Core3基础:02. 依赖注入
简介
首先先介绍依赖注入相关的两个名词:
- DI:依赖注入,inversion of control
- IoC:控制反转,Dependency Injection
依赖注入简单来说就是把以前通过 new
创建对象,改成我们需要对象时,由框架给我们提供。这样的好处有很多,例如实现解耦、统一管理依赖(特别依赖链非常复杂时)、更方便进行单元测试等。
可以理解成,IoC是一种概念,而DI可以看成是实现IoC的一种方法。
应用程序中的依赖关系方向应该是抽象的方向,而不是实现详细信息的方向。 大部分应用程序都是这样编写的:编译时依赖关系顺着运行时执行的方向流动,从而生成一个直接依赖项关系图。 也就是说,如果模块 A 调用模块 B 中的函数,而模块 B 又调用模块 C 中的函数,则编译时 A 取决于 B,而 B 又取决于 C,如图中所示。
应用依赖关系反转原则后,A 可以调用 B 实现的抽象上的方法,让 A 可以在运行时调用 B,而 B 又在编译时依赖于 A 控制的接口(因此,典型的编译时依赖项发生反转)。 运行时,程序执行的流程保持不变,但接口引入意味着可以轻松插入这些接口的不同实现。
举一个生活中的例子:插座的插口有国标、欧标等,这就可以看成是接口(interface),一种协议,通过这个协议,不同生产电器的厂家的只要实现了这个协议,就可以接入任何品牌的插座接口,这样就实现了松耦合,即两者都依赖于抽象,不依赖具体实现。
依赖项反转是生成松散耦合应用程序的关键一环,因为可以将实现详细信息编写为依赖并实现更高级别的抽象,而不是相反。 因此,生成的应用程序的可测试性、模块化程度以及可维护性更高。 遵循依赖关系反转原则可实现依赖关系注入。
关于设计模式的6大原则,网上有很多资料,可以搜一下。
ASP.NET Core 3中的依赖注入
一、配置依赖注入
创建ASP.NET Core 3 API类型的项目时,已经默认就配置好了依赖注入框架,使用时只需要在 Startup.cs 文件的 ConfigureServices 方法中配置即可:
例如我们有个服务 IMyService
,并且有个这个接口的实现类 MyService
:
public void ConfigureServices(IServiceCollection services) { services.AddScoped<IMyService, MyService>(); }
二、获取依赖注入
获取依赖注入有多种方法:
- 在控制器的构造方法获取依赖注入:只需要把要注入的服务接口写到构造方法中即可
public class HomeController: Controller { private readonly IMyService _myService; public HomeController(IMyService myService) { _myService = myService; } }
- 在控制器的action获取注入:只要把参数标记为
[FromServices]
public class HomeController: Controller { public HomeController( ) { } public int Get([FromServices]IMyService myService) { } }
依赖注入配置的各种姿势
上面的例子时最最简单的依赖注入方法,但是其实依赖注入还有很多需要注意的地方:
一、单例(Singleton)、范围(Scoped)、暂时(Transient)
在配置依赖注入时,有3中比较常用的注入类型,分别是:
- Transient:暂时,即每次获取到的都是一个新的对象(可以通过
GetHashCode
方法进行验证) - Scoped:范围,即在一个范围内,多次获取到的是同一个对象,不同范围是不同的对象。比如说在API项目中,如果把服务配置为Scoped,则每次HTTP请求期间得到的都是同一个对象
- Singleton:单例,即在Application的生命周期内,这个对象都是同一个。
在实际使用中用哪种方式需要根据具体的情况而定,没有哪一种好哪一种不好。
还是用上面那个IMyService的接口做例子,下面代码展示如何配置:
public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IMyService, MyService>( ); services.AddScoped<IMyService, MyService>( ); services.AddTransient<IMyService, MyService>( ); }
二、各种配置的方法
注册的时候并不不止可以用泛型的方法进行注册,也可以通过其他方式注册:
三、尝试注入、删除注入、替换注入、多注入
如果不确定之前是否已经注入过这个类型的,并且希望已经注入的话不注入,没有注入的话才进行注入时,可以使用尝试注入:
// 之前没有注入过 IMyService 类型的对象,这次注入成功 services.TryAddScoped<IMyService, MyService>( ); // 这里再尝试注入 MyService2 到 IMyService 时,并不会注入,容器中还是 MyService services.TryAddScoped<IMyService, MyService2>( );
如果希望删除已经注入的服务,则可以调用:
services.RemoveAll<IMyService>();
如果喜欢替换被注入的服务,则可以调用:
// 注册Service1 services.AddSingleton<IMyService, MyService1>( ); // 替换注册,替换上面的IMyService注册的MyService1,改为MyService2 services.Replace(ServiceDescriptor.Singleton<IMyService, MyService2>( ));
如果希望注册多个实现类到同一个服务:
// 注册多个接口实现 public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IMyService, MyService1>( ); services.AddSingleton<IMyService, MyService2>( ); } // 使用时可以使用IEnumerable进行接受多个 public int GetServiceList([FromServices]IEnumerable<IMyServices> services) { }
示例项目
紧接着上一篇的API框架,继续完善,最后会演示一个学生信息登记的页面:
- 首先新建一个Services的文件夹(真是项目这个会分离出来当作独立的项目),并创建一个
IStudentService
的接口和StudentService
的类,并且继承IStudentService
接口。这个接口暂时只有一个获取全部学生信息的方法:GetAllStudent
。 - 再新建一个文件夹Model(同样真实的项目会分离出来),并且在这个文件夹创建一个学生的实体类
Student
,这个类包含 Name 和 Age 两个属性。
// 抽象接口 public interface IStudentService { List<Student> GetAllStudent( ); } // 具体的实现类 public class StudentService : IStudentService { public List<Student> GetAllStudent( ) { // 这里为了方便,直接写死 return new List<Student>( ) { new Student(){ID = 1, Name = "张三", Age = 10 }, new Student(){ID = 2, Name = "李四", Age = 12 }, new Student(){ID = 3, Name = "王五", Age = 14 } }; } } // 学生的实体类 public class Student { public int ID { get; set; } /// <summary> /// 姓名 /// </summary> public string Name { get; set; } /// <summary> /// 年龄 /// </summary> public int Age { get; set; } }
- 然后在Ccontroller文件夹下面新建一个 StudentController 的控制器,这个控制器调用 IStudentService 的 GetAllStudent 方法,把数据返回到前端
public class StudentController : Controller { [Route("/student/getall")] public List<Student> GetAllStudent([FromServices] IStudentService student) { // 通过容器注入 IStudentService 服务,并且调用 GetAllStudent 方法 return student.GetAllStudent( ); } }
- 最后,我们需要把 IStudentService 配置到容器中,让 StudentController 在获取的时候能获取到服务的实例:
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddControllers( ); // 配置服务 IStudentService 到 StudentService services.AddScoped<IStudentService, StudentService>( ); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment( )) { app.UseDeveloperExceptionPage( ); } app.UseRouting( ); app.UseAuthorization( ); app.UseEndpoints(endpoints => { endpoints.MapControllers( ); }); } }
运行项目,并且导航到 http://localhost:5000/student/getall
结果如下
最后的项目结构
总结
这篇大概把mvc默认框架下的依赖注入部分常用的方法列举出来了,并且通过一个例子演示了如何使用依赖注入服务,如果需要更详细了解依赖注入可以查看官方文档:https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-3.1#register-groups-of-services-with-extension-methods
当然依赖注入还可以进行注入配置文件,让程序可以运动在不同的环境下,这个将在后面会有一篇单独的说明配置文件如何注入、热更新等。
下一篇会是使用actofac增强注入,autofac可以实现更多的注入方式,例如注入到属性的对象等。