使用Visual Studio Code开发Asp.Net Core WebApi学习笔记(六)-- 依赖注入
本篇将介绍Asp.Net Core中一个非常重要的特性:依赖注入,并展示其简单用法。
第一部分、概念介绍
Dependency Injection:又称依赖注入,简称DI。在以前的开发方式中,层与层之间、类与类之间都是通过new一个对方的实例进行相互调用,这样在开发过程中有一个好处,可以清晰的知道在使用哪个具体的实现。随着软件体积越来越庞大,逻辑越来越复杂,当需要更换实现方式,或者依赖第三方系统的某些接口时,这种相互之间持有具体实现的方式不再合适。为了应对这种情况,就要采用契约式编程:相互之间依赖于规定好的契约(接口),不依赖于具体的实现。这样带来的好处是相互之间的依赖变得非常简单,又称松耦合。至于契约和具体实现的映射关系,则会通过配置的方式在程序启动时由运行时确定下来。这就会用到DI。
第二部分、DI的注册与注入
借用这个系列之前的框架结构,添加如下接口和实现类
- using System.Collections.Generic;
- using WebApiFrame.Models;
- namespace WebApiFrame.Repositories
- {
- public interface IUserRepository
- {
- IEnumerable<User> GetAll();
- User GetById(int id);
- }
- }
IUserRepository.cs
- using System.Collections.Generic;
- using System.Linq;
- using WebApiFrame.Models;
- namespace WebApiFrame.Repositories
- {
- public class UserRepository : IUserRepository
- {
- private IList<User> list = new List<User>()
- {
- new User(){ Id = , Name = "name:1", Sex = "Male" },
- new User(){ Id = , Name = "name:2", Sex = "Female" },
- new User(){ Id = , Name = "name:3", Sex = "Male" },
- };
- public IEnumerable<User> GetAll()
- {
- return list;
- }
- public User GetById(int id)
- {
- return list.FirstOrDefault(i => i.Id == id);
- }
- }
- }
UserRepository.cs
一、注册
修改 Startup.cs 的ConfigureServices方法,将上面的接口和实现类注入到DI容器里
- public void ConfigureServices(IServiceCollection services)
- {
- // 注入MVC框架
- services.AddMvc();
- // 注册接口和实现类的映射关系
- services.AddScoped<IUserRepository, UserRepository>();
- }
修改 UsersController.cs 的构造函数和Action方法
- using System;
- using Microsoft.AspNetCore.Mvc;
- using WebApiFrame.Models;
- using WebApiFrame.Repositories;
- namespace WebApiFrame.Controllers
- {
- [Route("api/[controller]")]
- public class UsersController : Controller
- {
- private readonly IUserRepository userRepository;
- public UsersController(IUserRepository userRepo)
- {
- userRepository = userRepo;
- }
- [HttpGet]
- public IActionResult GetAll()
- {
- var list = userRepository.GetAll();
- return new ObjectResult(list);
- }
- [HttpGet("{id}")]
- public IActionResult Get(int id)
- {
- var user = userRepository.GetById(id);
- return new ObjectResult(user);
- }
- #region 其他方法
- // ......
- #endregion
- }
- }
启动程序,分别访问地址 http://localhost:5000/api/users 和 http://localhost:5000/api/users/1 ,页面将展示正确的数据。
从上面的例子可以看到,在 Startup.cs 的ConfigureServices的方法里,通过参数的AddScoped方法,指定接口和实现类的映射关系,注册到DI容器里。在控制器里,通过构造方法将具体的实现注入到对应的接口上,即可在控制器里直接调用了。
除了在ConfigureServices方法里进行注册外,还可以在Main函数里进行注册。注释掉 Startup.cs ConfigureServices方法里的注入代码,在 Program.cs 的Main函数里添加注入方法
- using Microsoft.AspNetCore.Hosting;
- using Microsoft.Extensions.DependencyInjection;
- using WebApiFrame.Repositories;
- namespace WebApiFrame
- {
- public class Program
- {
- public static void Main(string[] args)
- {
- var host = new WebHostBuilder()
- .UseKestrel()
- .ConfigureServices(services=>
- {
- // 注册接口和实现类的映射关系
- services.AddScoped<IUserRepository, UserRepository>();
- })
- .UseStartup<Startup>()
- .Build();
- host.Run();
- }
- }
- }
此方法等效于 Startup.cs 的ConfigureServices方法。
二、注入
添加三个测试接口和实现类
- namespace WebApiFrame
- {
- public interface ITestOne
- {
- }
- public class TestOne : ITestOne
- {
- }
- }
ITestOne.cs
- namespace WebApiFrame
- {
- public interface ITestTwo
- {
- }
- public class TestTwo : ITestTwo
- {
- }
- }
ITestTwo.cs
- namespace WebApiFrame
- {
- public interface ITestThree
- {
- }
- public class TestThree : ITestThree
- {
- }
- }
ITestThree.cs
修改 Startup.cs 的ConfigureServices方法,将接口和实现类的映射关系注册到DI容器
- public void ConfigureServices(IServiceCollection services)
- {
- // 注入MVC框架
- services.AddMvc();
- // 注册接口和实现类的映射关系
- services.AddScoped<ITestOne, TestOne>();
- services.AddScoped<ITestTwo, TestTwo>();
- services.AddScoped<ITestThree, TestThree>();
- }
添加 DemoController.cs 类
- using System.Threading.Tasks;
- using Microsoft.AspNetCore.Http;
- using Microsoft.AspNetCore.Mvc;
- namespace WebApiFrame
- {
- [Route("[controller]")]
- public class DemoController : Controller
- {
- private readonly ITestOne _testOne;
- private readonly ITestTwo _testTwo;
- private readonly ITestThree _testThree;
- public DemoController(ITestOne testOne, ITestTwo testTwo, ITestThree testThree)
- {
- _testOne = testOne;
- _testTwo = testTwo;
- _testThree = testThree;
- }
- [HttpGet("index")]
- public async Task Index()
- {
- HttpContext.Response.ContentType = "text/html";
- await HttpContext.Response.WriteAsync($"<h1>ITestOne => {_testOne}</h1>");
- await HttpContext.Response.WriteAsync($"<h1>ITestTwo => {_testTwo}</h1>");
- await HttpContext.Response.WriteAsync($"<h1>ITestThree => {_testThree}</h1>");
- }
- }
- }
启动程序,访问地址 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函数添加注册代码。最终代码如下
- using Microsoft.Extensions.DependencyInjection;
- using System;
- namespace DiApplicationTest
- {
- public class Program
- {
- public static void Main(string[] args)
- {
- IServiceCollection services = new ServiceCollection();
- services.AddScoped<ITestOne, TestOne>()
- .AddScoped<ITestTwo, TestTwo>()
- .AddScoped<ITestThree, TestThree>()
- .AddScoped<ITestApp, TestApp>()
- .BuildServiceProvider()
- .GetService<ITestApp>();
- Console.ReadLine();
- }
- }
- public interface ITestOne { }
- public interface ITestTwo { }
- public interface ITestThree { }
- public class TestOne : ITestOne { }
- public class TestTwo : ITestTwo { }
- public class TestThree : ITestThree { }
- public interface ITestApp { }
- public class TestApp : ITestApp
- {
- public TestApp(ITestOne testOne, ITestTwo testTwo, ITestThree testThree)
- {
- Console.WriteLine($"TestApp({testOne}, {testTwo}, {testThree})");
- }
- }
- }
启动调试,在cmd窗口可以看见打印内容
这里注册了四个接口和对应的实现类,其中一个接口的实现类 TestApp.cs 拥有一个三个参数的构造函数,这三个参数类型分别是其他三个接口。通过GetServices方法通过唯一的一个构造函数创建了 TestApp.cs 的一个实例。
接下来在 TestApp.cs 里添加一个有两个参数的构造函数,同时修改Main函数内容,去掉一个接口的注册
- public class TestApp : ITestApp
- {
- public TestApp(ITestOne testOne, ITestTwo testTwo)
- {
- Console.WriteLine($"TestApp({testOne}, {testTwo})");
- }
- public TestApp(ITestOne testOne, ITestTwo testTwo, ITestThree testThree)
- {
- Console.WriteLine($"TestApp({testOne}, {testTwo}, {testThree})");
- }
- }
- public static void Main(string[] args)
- {
- IServiceCollection services = new ServiceCollection();
- services.AddScoped<ITestOne, TestOne>()
- .AddScoped<ITestTwo, TestTwo>()
- //.AddScoped<ITestThree, TestThree>()
- .AddScoped<ITestApp, TestApp>()
- .BuildServiceProvider()
- .GetService<ITestApp>();
- Console.ReadLine();
- }
再次启动调试,查看cmd窗口打印内容
当有多个构造函数时,框架会选择参数都是有效注入接口的构造函数创建实例。在上面这个例子里, ITestThree.cs 和 TestThree.cs 的映射关系没有注册到DI容器里,框架在选择有效的构造函数时,会过滤掉含有ITestThree接口类型的参数的构造函数。
接下来在 TestApp.cs 再添加一个构造函数。为了方便起见,我给每个构造函数添加了编号标识一下。
- public class TestApp : ITestApp
- {
- // No.1
- public TestApp(ITestOne testOne)
- {
- Console.WriteLine($"TestApp({testOne})");
- }
- // No.2
- public TestApp(ITestOne testOne, ITestTwo testTwo)
- {
- Console.WriteLine($"TestApp({testOne}, {testTwo})");
- }
- // No.3
- public TestApp(ITestOne testOne, ITestTwo testTwo, ITestThree testThree)
- {
- Console.WriteLine($"TestApp({testOne}, {testTwo}, {testThree})");
- }
- }
再次启动调试,查看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 注册代码的注释
- public class TestApp : ITestApp
- {
- // No.2
- public TestApp(ITestOne testOne, ITestTwo testTwo)
- {
- Console.WriteLine($"TestApp({testOne}, {testTwo})");
- }
- // No.4
- public TestApp(ITestTwo testTwo, ITestThree testThree)
- {
- Console.WriteLine($"TestApp({testTwo}, {testThree})");
- }
- }
启动调试,发现会抛出一个 System.InvalidOperationException 异常,异常内容表明框架无法选择一个正确的构造函数,不能创建实例。
在这个例子里,两个构造函数的参数列表集合分别为[ITestOne, ITestTwo]和[ITestTwo, ITestThree],因为谁也无法是对方的超集,所以框架不能继续创建实例。
总之,框架在选择构造函数时,会依次遵循以下两点规则:
1. 使用有效的构造函数创建实例
2. 如果有效的构造函数有多个,选择参数列表集合是其他所有构造函数参数列表集合的超集的构造函数创建实例
如果以上两点都不满足,则抛出 System.InvalidOperationException 异常。
四、Asp.Net Core默认注册的服务接口
框架提供了但不限于以下几个接口,某些接口可以直接在构造函数和 Startup.cs 的方法里注入使用
第三部分、生命周期管理
框架对注入的接口创建的实例有一套生命周期的管理机制,决定了将采用什么样的创建和回收实例。
下面通过一个例子演示这三种方式的区别
在第二部分的第二点的例子里添加以下几个接口和实现类
- using System;
- namespace WebApiFrame
- {
- public interface ITest
- {
- Guid TargetId { get; }
- }
- public interface ITestTransient : ITest { }
- public interface ITestScoped : ITest { }
- public interface ITestSingleton : ITest { }
- public class TestInstance : ITestTransient, ITestScoped, ITestSingleton
- {
- public Guid TargetId
- {
- get
- {
- return _targetId;
- }
- }
- private Guid _targetId { get; set; }
- public TestInstance()
- {
- _targetId = Guid.NewGuid();
- }
- }
- }
ITest.cs
- namespace WebApiFrame
- {
- public class TestService
- {
- public ITestTransient TestTransient { get; }
- public ITestScoped TestScoped { get; }
- public ITestSingleton TestSingleton { get; }
- public TestService(ITestTransient testTransient, ITestScoped testScoped, ITestSingleton testSingleton)
- {
- TestTransient = testTransient;
- TestScoped = testScoped;
- TestSingleton = testSingleton;
- }
- }
- }
TestService.cs
修改 Startup.cs 的ConfigureServices方法里添加注册内容
- public void ConfigureServices(IServiceCollection services)
- {
- // 注入MVC框架
- services.AddMvc();
- // 注册接口和实现类的映射关系
- services.AddTransient<ITestTransient, TestInstance>();
- services.AddScoped<ITestScoped, TestInstance>();
- services.AddSingleton<ITestSingleton, TestInstance>();
- services.AddTransient<TestService, TestService>();
- }
修改 DemoController.cs 内容
- using System.Threading.Tasks;
- using Microsoft.AspNetCore.Http;
- using Microsoft.AspNetCore.Mvc;
- namespace WebApiFrame
- {
- [Route("[controller]")]
- public class DemoController : Controller
- {
- public ITestTransient _testTransient { get; }
- public ITestScoped _testScoped { get; }
- public ITestSingleton _testSingleton { get; }
- public TestService _testService { get; }
- public DemoController(ITestTransient testTransient, ITestScoped testScoped, ITestSingleton testSingleton, TestService testService)
- {
- _testTransient = testTransient;
- _testScoped = testScoped;
- _testSingleton = testSingleton;
- _testService = testService;
- }
- [HttpGet("index")]
- public async Task Index()
- {
- HttpContext.Response.ContentType = "text/html";
- await HttpContext.Response.WriteAsync($"<h1>Controller Log</h1>");
- await HttpContext.Response.WriteAsync($"<h6>Transient => {_testTransient.TargetId.ToString()}</h6>");
- await HttpContext.Response.WriteAsync($"<h6>Scoped => {_testScoped.TargetId.ToString()}</h6>");
- await HttpContext.Response.WriteAsync($"<h6>Singleton => {_testSingleton.TargetId.ToString()}</h6>");
- await HttpContext.Response.WriteAsync($"<h1>Service Log</h1>");
- await HttpContext.Response.WriteAsync($"<h6>Transient => {_testService.TestTransient.TargetId.ToString()}</h6>");
- await HttpContext.Response.WriteAsync($"<h6>Scoped => {_testService.TestScoped.TargetId.ToString()}</h6>");
- await HttpContext.Response.WriteAsync($"<h6>Singleton => {_testService.TestSingleton.TargetId.ToString()}</h6>");
- }
- }
- }
启动调试,连续两次访问地址 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容器,修改方法返回值
- public IServiceProvider ConfigureServices(IServiceCollection services)
- {
- // 注入MVC框架
- services.AddMvc();
- // autofac容器
- var containerBuilder = new ContainerBuilder();
- containerBuilder.RegisterType<TestInstance>().As<ITestTransient>().InstancePerDependency();
- containerBuilder.RegisterType<TestInstance>().As<ITestScoped>().InstancePerLifetimeScope();
- containerBuilder.RegisterType<TestInstance>().As<ITestSingleton>().SingleInstance();
- containerBuilder.RegisterType<TestService>().AsSelf().InstancePerDependency();
- containerBuilder.Populate(services);
- var container = containerBuilder.Build();
- return container.Resolve<IServiceProvider>();
- }
启动调试,再次访问地址 http://localhost:5000/demo/index ,会得到上个例子同样的效果。
使用Visual Studio Code开发Asp.Net Core WebApi学习笔记(六)-- 依赖注入的更多相关文章
- 使用Visual Studio Code开发Asp.Net Core WebApi学习笔记(一)-- 起步
本文记录了在Windows环境下安装Visual Studio Code开发工具..Net Core 1.0 SDK和开发一个简单的Web-Demo网站的全过程. 一.安装Visual Studio ...
- 使用Visual Studio Code开发Asp.Net Core WebApi学习笔记(八)-- 多环境开发
本篇将演示Asp.Net Core如何在多环境下进行开发适配. 在一个正规的开发流程里,软件开发部署将要经过三个阶段:开发.测试.上线,对应了三个环境:开发.测试.生产.在不同的环境里,需要编写不同的 ...
- 使用Visual Studio Code开发Asp.Net Core WebApi学习笔记(十)-- 发布(Windows)
本篇将在这个系列演示的例子上继续记录Asp.Net Core在Windows上发布的过程. Asp.Net Core在Windows上可以采用两种运行方式.一种是自托管运行,另一种是发布到IIS托管运 ...
- 使用Visual Studio Code开发Asp.Net Core WebApi学习笔记(九)-- 单元测试
本篇将结合这个系列的例子的基础上演示在Asp.Net Core里如何使用XUnit结合Moq进行单元测试,同时对整个项目进行集成测试. 第一部分.XUnit 修改 Project.json 文件内容, ...
- 使用Visual Studio Code开发Asp.Net Core WebApi学习笔记(三)-- Logger
本篇是在上一篇的基础上添加日志功能,并记录NLog在Asp.Net Core里的使用方法. 第一部分:默认Logger支持 一.project.json添加日志包引用,并在cmd窗口使用 dotnet ...
- [转]使用Visual Studio Code开发Asp.Net Core WebApi学习笔记(三)-- Logger
本文转自:https://www.cnblogs.com/niklai/p/5662094.html 本篇是在上一篇的基础上添加日志功能,并记录NLog在Asp.Net Core里的使用方法. 第一部 ...
- 使用Visual Studio Code开发Asp.Net Core WebApi学习笔记(七)-- 结构化配置
本篇将记录.Net Core里颇有特色的结构化配置的使用方法. 相比较之前通过Web.Config或者App.Config配置文件里使用xml节点定义配置内容的方式,.Net Core在配置系统上发生 ...
- 使用Visual Studio Code开发Asp.Net Core WebApi学习笔记(五)-- Filter
在上一篇里,介绍了中间件的相关内容和使用方法.本篇将介绍Asp.Net Core MVC框架的过滤器的相关内容和使用方法,并简单说明一下与中间件的区别. 第一部分.MVC框架内置过滤器 下图展示了As ...
- 使用Visual Studio Code开发Asp.Net Core WebApi学习笔记(四)-- Middleware
本文记录了Asp.Net管道模型和Asp.Net Core的Middleware模型的对比,并在上一篇的基础上增加Middleware功能支持. 在演示Middleware功能之前,先要了解一下Asp ...
随机推荐
- Bridge桥接模式
当我们的功能要在多个维度进行扩展时,各个维度之间可以交叉组合,就可以考虑使用桥接模式. 将抽象部分与实现部分分离,使它们都可以独立的变化. ...
- 跨域请求 & jsonp
0 什么是跨域请求 在一个域名下请求另外一个域名下的资源,就是跨域请求.example 1:比如:我当前的域名是http://che.pingan.com.我现在要去请求http://www.cnbl ...
- SCI期刊的审稿流程
审稿中涉及到的人: EIC-Editor in Chief 主编, 此人很重要,有稿件最终决定权. ADM- (可能是) Administrator 应该是协助主编日常工作的. AE-Associat ...
- sql server远程访问Oracle数据库
在sql server上新建了连接服务器后 在指定的链接服务器上执行指定的传递查询. 该服务器是 OLE DB 数据源. OPENQUERY 可以在查询的 FROM 子句中引用,就好象它是一个表名. ...
- iOS 模态视图
模态视图不是专门的某个类,而是通过视图控制器的presentViewController方法弹出的视图,我们称为模态视图. 模态视图出现的场景一般是临时弹出的窗口,譬如:登录窗口: 模态视图弹出时通过 ...
- Fiddler录制jmeter脚本,干货分享
我们知道以前jmeter的脚本来源有三个,手动书写.badboy录制.自带的录制功能(jmeter3.0该功能还比较好),目前我们又多了一个fiddler生成,自上次分享出来fiddler ...
- LICEcap GIF 屏幕录制工具
LICEcap 是一款屏幕录制工具,支持导出git动画图片格式,简单好用.大小只有几百KB 运行之后,可以随意调整大小,右下角有开始/停止按钮. 压缩包:http://files.cnb ...
- 在代码中调用 mvc 4 api
mvc 4 api 的调用有很多种,最常见也最简单的一种是 用 ajax 的方式在前端界面中调用, 如果是在后台代码中调用 ,是要复杂一些,以下是 以 post 的方式调用 api 的封装好的方法: ...
- 学习总结 初步了解HTML课程
HTML 内容(超文本标记语言) CSS 网页美化 Javascript 脚本语言 <html> --开始标签 <head> 网页上的控制信息 < ...
- Activity的四种launchMode 详细分析
launchMode在多个Activity跳转的过程中扮演着重要的角色,它可以决定是否生成新的Activity实例,是否重用已存在的 Activity实例,是否和其他Activity实例公用一个tas ...