Asp.Net Core学习随笔
学习自BLBL杨中科老师
环境:SQLServer,控制台(.net),Asp.net core,EF core
依赖注入(Dependency Injection)
依赖注入是实现控制反转(Inversion Of Control 即IOC)的一种方式(还有一种叫服务定位器的实现,但是不如依赖注入好用),软件开发中实现解耦常用的方式.
比如吃饭
1. 传统写法(没有DI,紧耦合)
// 你(相当于程序中的类)自己做饭
class 我 {void 吃饭() {炒锅 锅 = new 炒锅(); // 自己造锅锅.炒菜("番茄炒蛋"); // 强依赖炒锅}
}
问题:
- 如果想吃火锅,必须重写整个
吃饭()
方法 - 锅坏了要自己修(修改代码)
2. 依赖注入写法(解耦)
// 定义「烹饪工具」接口
interface 烹饪工具 {void 做菜(string 菜名);
}// 实现不同工具
class 炒锅 : 烹饪工具 {public void 做菜(string 菜名) => Console.WriteLine($"用炒锅做{菜名}");
}class 火锅 : 烹饪工具 {public void 做菜(string 菜名) => Console.WriteLine($"用火锅煮{菜名}");
}// 你通过外部获得工具(依赖注入)
class 我 {private 烹饪工具 _工具;// 构造函数注入public 我(烹饪工具 工具) { _工具 = 工具; // 工具由外部提供}void 吃饭() {_工具.做菜("番茄炒蛋"); // 不关心具体用什么工具}
}
// (外部决定给你什么工具)
var 我 = new 我(new 火锅()); // 想吃火锅
我.吃饭(); // 输出:用火锅煮番茄炒蛋// 换工具只需改一行代码
var 我 = new 我(new 炒锅()); // 改吃炒菜
我.吃饭(); // 输出:用炒锅做番茄炒蛋
编程概念 | 生活化比喻 | 好处 |
---|---|---|
依赖注入(DI) | 外卖员递给你餐具 | 想吃什么换什么工具 |
控制反转(IoC) | 不用自己买锅,外卖提供 | 控制权交给外部 |
接口 | 「能烹饪」的标准 | 保证工具都能做饭 |
为什么这样做?
- 易扩展:新增空气炸锅只需实现
烹饪工具
接口,不用改我
的代码 - 易测试:测试时可以传入「假锅」(Mock对象)
- 解耦:你和具体锅具零耦合,只管吃饭不关心锅哪来的
可能你还是觉得自己做饭好(有时候也确实不使用这些复杂的概念比较好,但这往往是一些很小的项目),但软件开发(尤其是后端开发往往要长期维护,往往都是比较大的项目,需求改动较为频繁的项目)随着项目越来越复杂,这种依赖注入的好处就越来越明显(就比如你不可能所有的事都亲力亲为,但是如果约定好一个规范,那么别人的东西你也可以拿过来就用).如果暂时不理解也没关系!
服务定位器
先提一下服务定位器的实现,要引入
Microsoft.Extensions.DependencyInjection 这个包已经实现了服务定位器,我们直接拿来用
你可能感到奇怪:为什么不直接讲依赖注入?我的理解:先从服务器讲起逐渐过渡到依赖注入的这个过程,比较循序渐进,同时很多概念是相通的.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;namespace Asp.net_Study
{internal class Program{static void Main(string[] args){//想创建一个集合对象ServiceCollection serviceCollection = new ServiceCollection();//将你的服务注册到集合serviceCollection.AddTransient<TestServiceImpl>();//使用这个集合构建一个服务提供对象,别忘了使用using自动释放资源using (ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider()){//使用这个服务提供对象的GetService方法获取你刚才注册的服务var t = serviceProvider.GetService<TestServiceImpl>();//获取了服务对象,该干什么干什么t.Name = "Tom";t.SayHello();}}}public class TestServiceImpl{public string Name { get; set; }public void SayHello(){Console.WriteLine($"Hello,my name is {Name} ");}}
}
对象生命周期(Lifetime)
生命周期类型 | 行为描述 | 适用场景 |
---|---|---|
Transient | 每次请求都创建新实例 | 轻量级、无状态服务(如工具类) |
Scoped | 同一Web请求内共享实例(不同请求不同实例) | 数据库上下文(如EF Core的DbContext) |
Singleton | 整个应用生命周期内只创建一个实例(所有请求共享) | 配置服务、缓存管理器 |
注册服务可以注册上述三种不同类型的服务
Transient
刚才的例子就是Transient,因为我使用了AddTransient
static void Main(string[] args){//想创建一个集合对象ServiceCollection serviceCollection = new ServiceCollection();//将你的服务注册到集合serviceCollection.AddTransient<TestServiceImpl>();//使用这个集合构建一个服务提供对象,别忘了使用using自动释放资源using (ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider()){//使用这个服务提供对象的GetService方法获取你刚才注册的服务var t = serviceProvider.GetService<TestServiceImpl>();//获取了服务对象,该干什么干什么t.Name = "Tom";var t1 = serviceProvider.GetService<TestServiceImpl>();t1.Name = "Jack";t.SayHello();t1.SayHello();}}
可见确实给我们提供了两个实例,不然
t.SayHello();得到的结果也应该是Jack.
Singleton
单例没啥好说的
static void Main(string[] args){//想创建一个集合对象ServiceCollection serviceCollection = new ServiceCollection();//将你的服务注册到集合serviceCollection.AddSingleton<TestServiceImpl>();//使用这个集合构建一个服务提供对象,别忘了使用using自动释放资源using (ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider()){//使用这个服务提供对象的GetService方法获取你刚才注册的服务var t = serviceProvider.GetService<TestServiceImpl>();//获取了服务对象,该干什么干什么t.Name = "Tom";var t1 = serviceProvider.GetService<TestServiceImpl>();t1.Name = "Jack";t.SayHello();t1.SayHello();}}
Scope
简单点说就是某个范围能确保获取的服务是一致的,出了范围就不是了
static void Main(string[] args){//想创建一个集合对象ServiceCollection serviceCollection = new ServiceCollection();//将你的服务注册到集合serviceCollection.AddScoped<TestServiceImpl>();//使用这个集合构建一个服务提供对象,别忘了使用using自动释放资源using (ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider()){// using开辟的范围内能保证获取一致的对象using (IServiceScope scope = serviceProvider.CreateScope()){//t t1是一个对象var t = scope.ServiceProvider.GetService<TestServiceImpl>();var t1 = scope.ServiceProvider.GetService<TestServiceImpl>();t.Name = "Tom";t1.Name = "Jack";t.SayHello();t1.SayHello();}using (IServiceScope scope1 = serviceProvider.CreateScope()){var t = scope1.ServiceProvider.GetService<TestServiceImpl>();var t1 = scope1.ServiceProvider.GetService<TestServiceImpl>();t.Name = "张三";t1.Name = "李四";t.SayHello();t1.SayHello();}}}
使用接口替代实际的类型
可以看到现在我们的代码一下子就非常灵活了,因为我们现在除了注册哪里使用了实际的类型,其他的地方我们都是使用的接口,要是需求有变动,我们只需要重新实现该接口,替换实现类型就可以.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;namespace Asp.net_Study
{internal class Program{static void Main(string[] args){//想创建一个集合对象ServiceCollection serviceCollection = new ServiceCollection();//实现服务类型和实现类型分离serviceCollection.AddScoped<ITestServiceImpl, TestServiceImpl>();//使用这个集合构建一个服务提供对象,别忘了使用using自动释放资源using (ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider()){using (IServiceScope scope = serviceProvider.CreateScope()){ITestServiceImpl t = scope.ServiceProvider.GetService<ITestServiceImpl>();t.Name = "Tom";t.SayHello();}}}}public class TestServiceImpl : ITestServiceImpl{public string Name { get; set; }public void SayHello(){Console.WriteLine($"Hello,my name is {Name} ");}}public interface ITestServiceImpl{string Name { get; set; }void SayHello();}
}
注册的方法多种多样,也可以同一个服务注册多个实现,那么理所当然的获取服务的方法也五花八门.这里不多说,用到那个查一下就好.
依赖注入的传染性
一个服务如果依赖另一个服务,直接声明然后构造函数传入你需要的服务就可以,而无需考虑它从哪来.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;namespace Asp.net_Study
{internal class Program{static void Main(string[] args){//想创建一个集合对象ServiceCollection serviceCollection = new ServiceCollection();//实现服务类型和实现类型分离serviceCollection.AddScoped<ILog, Log>();serviceCollection.AddScoped<ISave, Save>();//使用这个集合构建一个服务提供对象,别忘了使用using自动释放资源using (ServiceProvider serviceProvider = serviceCollection.BuildServiceProvider()){ISave save = serviceProvider.GetService<ISave>();save.SaveInfo();}}}public interface ILog{void LogInfo(string message);}public class Log : ILog{public void LogInfo(string message){Console.WriteLine(message);}}public interface ISave{void SaveInfo();}public class Save : ISave{private readonly ILog _log;public Save(ILog log){this._log = log;}public void SaveInfo(){_log.LogInfo("Log something");Console.WriteLine("SaveInfo");}}
}
依赖注入核心概念
1. 服务(Service)
- 定义:任何可以被注入使用的类或接口(如数据库访问类、日志工具、业务逻辑类)
public interface ILoggerService {void Log(string message);
}public class ConsoleLogger : ILoggerService {public void Log(string message) => Console.WriteLine(message);
}
2. 注册服务(Service Registration)
- 作用:告诉DI容器如何创建服务实例
- ASP.NET Core 注册方式:
// Program.cs
builder.Services.AddScoped<ILoggerService, ConsoleLogger>(); // 注册接口和实现类
3. 查询服务(Service Resolution)
- 方式:通过构造函数、
IServiceProvider
或[FromServices]
获取服务实例
public class MyController : ControllerBase {private readonly ILoggerService _logger;// 构造函数注入(最常用)public MyController(ILoggerService logger) {_logger = logger;}[HttpGet]public IActionResult Get([FromServices] ILoggerService logger) {logger.Log("使用FromServices注入");return Ok();}
}
4. 对象生命周期(Lifetime)
生命周期类型 | 行为描述 | 适用场景 |
---|---|---|
Transient | 每次请求都创建新实例 | 轻量级、无状态服务(如工具类) |
Scoped | 同一Web请求内共享实例(不同请求不同实例) | 数据库上下文(如EF Core的DbContext) |
Singleton | 整个应用生命周期内只创建一个实例(所有请求共享) | 配置服务、缓存管理器 |