.NET学习-依赖注入、配置系统、日志系统

依赖注入

首先理解"控制反转"的思想

它是把 "创建 / 管理依赖对象的控制权",从 "使用依赖的类内部" 转移到 "外部容器 / 框架"。

生活例子:你要实现吃饭的功能,自己来做饭吃,就相当于控制权在自己手里,要多辣自己都可以控制;而点外卖的话,控制权不在自己手里,称为"控制反转",具体的口味都是由商家决定的。

"依赖注入"是实现"控制反转"的方式

可以这样理解,依赖就是需要的东西,比如美食,将需要的东西注入进来,而不是自己去创建。需要什么直接去要,自己不用实现。

依赖注入代码示例:

先定义一个简单的日志服务:

cs 复制代码
// 1. 服务接口(被依赖的对象)
public interface ILogService
{
    void Log(string message); // 日志方法
}

// 2. 服务实现类
public class ConsoleLogService : ILogService
{
    // 实现:控制台输出日志
    public void Log(string message)
    {
        Console.WriteLine($"【控制台日志】{message}");
    }
}

依赖注入实现:

cs 复制代码
using System;
using Microsoft.Extensions.DependencyInjection; // .NET DI核心命名空间

// 业务类:依赖ILogService(被动接收注入)
public class OrderService
{
    // 1. 声明依赖:通过构造函数告诉容器"我需要ILogService"
    private readonly ILogService _logService;
    public OrderService(ILogService logService)
    {
        _logService = logService; // 容器注入的实例
    }

    // 业务方法:使用依赖的服务
    public void CreateOrder(int orderId)
    {
        _logService.Log($"创建订单:{orderId}"); // 用注入的日志服务
    }
}

class Program
{
    static void Main()
    {
        // 步骤1:注册服务(把服务映射关系添加到容器)
        var services = new ServiceCollection();
        // 注册:ILogService接口 → ConsoleLogService实现类(瞬时生命周期)
        services.AddTransient<ILogService, ConsoleLogService>();
        // 注册业务类OrderService
        services.AddTransient<OrderService>();

        // 步骤2:构建DI容器(IServiceProvider)
        using var serviceProvider = services.BuildServiceProvider();

        // 步骤3:从容器解析OrderService(容器自动注入ILogService)
        var orderService = serviceProvider.GetRequiredService<OrderService>();
        
        // 步骤4:调用业务方法(使用注入的日志服务)
        orderService.CreateOrder(1001);
    }
}

输出结果:

cs 复制代码
【控制台日志】创建订单:1001

💡服务定位器

"服务定位器"是实现"控制反转"思想的另一种方式

核心逻辑是:通过一个「全局可访问的 "定位器"(通常是.NETIServiceProvider)」,让类主动从容器中获取所需的服务实例,而非被动接收注入("依赖注入" 是被动)。

简单说:

  • 依赖注入(DI)是 "容器把服务送上门"(被动接收);
  • 服务定位器是 "类自己去容器里拿服务"(主动获取)。

优点是:灵活(可动态获取服务)

缺点:依赖关系 "隐藏"(代码中看不到类依赖哪些服务)

代码示例:

cs 复制代码
using System;
using Microsoft.Extensions.DependencyInjection;

// 业务类:主动从定位器(IServiceProvider)获取ILogService
public class OrderService
{
    // 1. 持有定位器(IServiceProvider)引用
    private readonly IServiceProvider _serviceLocator;
    public OrderService(IServiceProvider serviceLocator)
    {
        _serviceLocator = serviceLocator; // 注入定位器(仅这一步是DI)
    }

    public void CreateOrder(int orderId)
    {
        // 2. 主动从定位器中获取ILogService(服务定位器核心逻辑)
        var logService = _serviceLocator.GetRequiredService<ILogService>();
        logService.Log($"创建订单:{orderId}");
    }
}

class Program
{
    static void Main()
    {
        // 步骤1:注册服务(和DI示例完全一样)
        var services = new ServiceCollection();
        services.AddTransient<ILogService, ConsoleLogService>();
        services.AddTransient<OrderService>();

        // 步骤2:构建容器(定位器本身)
        using var serviceProvider = services.BuildServiceProvider();

        // 步骤3:解析OrderService(容器注入IServiceProvider)
        var orderService = serviceProvider.GetRequiredService<OrderService>();
        
        // 步骤4:调用业务方法(主动获取日志服务)
        orderService.CreateOrder(1002);
    }
}

相关概念

服务 :即对象(提供特定功能的对象)

在 DI 中通常以「接口 + 实现类」的形式存在(也可直接是具体类)

如上例中,ConsoleLogService 和ILogService 就是服务,具体的说前者是服务实现类,后者是服务接口。

注册服务:将服务映射关系告知容器的过程

核心定义:把「服务接口与实现类的对应关系」、「服务的生命周期」这两类信息,提交给服务容器的操作,相当于给容器 "下达指令"。

核心目的:让服务容器知道 "当需要某个服务接口的实例时,应该创建哪个实现类的对象,以及这个对象的生命周期如何"。

cs 复制代码
// 注册:ILogService(接口)→ ConsoleLogService(实现类),生命周期为瞬态
services.AddTransient<ILogService, ConsoleLogService>();

服务容器:负责管理注册的服务(DI 的 "核心管家")

核心定义:.NET 中对应IServiceProvider(构建后)/IServiceCollection(注册时),是存储所有注册服务信息、管理服务实例创建 / 销毁 / 依赖解析的核心容器。

如上例:运行阶段容器:IServiceProvider

注册阶段容器:ServiceCollection

核心管理职责(不止是 "存储",更核心是 "管理"):

存储:保存所有服务的注册信息(接口 - 实现类映射、生命周期);

创建:根据注册信息,在需要时创建服务实例;

解析:自动解析服务的依赖链(如 A 依赖 B,B 依赖 C,容器会自动创建 C→B→A);

销毁:根据服务生命周期,在合适时机销毁服务实例,释放资源。

查询服务:创建对象及关联对象(也叫 "服务解析")

核心定义:从服务容器中获取已注册服务实例的过程,容器在这个过程中会自动完成「当前服务对象创建」+「关联依赖对象创建与注入」,最终返回可用的服务实例。

核心细节: 不是简单的 "取对象",而是 "按需创建 + 依赖组装":如果服务 A 依赖服务 B,容器会先创建服务 B,再将 B 注入到 A 中,最终返回完整可用的 A 实例;

核心方法:.NET 中常用GetRequiredService<T>()(强制获取,无实例抛异常)、GetService<T>()(可选获取,无实例返回 null)。

cs 复制代码
// 这行代码就是「查询服务」的核心实现
//从IServiceProvider(运行阶段容器)中,查询并获取OrderService类型的服务实例;
var orderService = serviceProvider.GetRequiredService<OrderService>();

对象生命周期:Transient (瞬态);Scoped(范围);Singleton(单例)

  • 核心定义:服务实例的 "存活规则",决定了服务实例的「创建时机」和「销毁时机」,.NET DI 内置 3 种核心生命周期,适配不同场景需求。

  • 三种生命周期核心拆解(通俗化):

    生命周期类型 核心特点(创建 / 销毁) 通俗类比 适用场景
    Transient(瞬态) 每次查询 / 注入都创建新实例,无主动销毁(由 GC 回收) 餐厅的一次性餐具,每次使用都换新的 无状态工具类(如日志、仓储、辅助计算)
    Scoped(范围) 同一 "作用域" 内(如ASP.NET Core 单个请求)创建1 个实例,作用域结束时销毁 餐厅的一桌餐具,同一桌顾客共用一套,用餐结束后回收 ASP.NET Core 请求相关服务(如用户上下文、请求仓储)
    Singleton(单例) 整个应用程序生命周期内只创建1 个实例,应用停止时销毁 餐厅的冰箱,所有厨师共用一个,餐厅关门后才停用 全局共享无状态服务(如配置、缓存、日志工厂)

根据类型获取 / 注册服务:

有两种类型:服务类型 vs 实现类型"

两者的关系:可以 "不同",也可以 "相同" ,推荐不同

如果一个服务是用接口+实现类 实现的,那么服务类型和实现类型分别是接口和实现类

如果一个服务就是 用一个类实现的,那么服务类型和实现类型就均是这个类。

比如之前的日志服务:

ILogService(接口,约定 "要做日志功能");

实现类型:ConsoleLogService(类,实际写日志到控制台)。

代码里的注册就是 "把招牌和干活的人绑定": 运行 // 服务类型(ILogService) → 实现类型(ConsoleLogService) services.AddTransient<ILogService, ConsoleLogService>();

cs 复制代码
// 服务类型(ILogService) → 实现类型(ConsoleLogService)
services.AddTransient<ILogService, ConsoleLogService>();

两者相同(直接注册类,不用接口):如果不写接口,直接注册一个类(比如OrderService): 服务类型 = 实现类型,服务类型和实现类型都是都是OrderService;

cs 复制代码
OrderService services.AddTransient<OrderService>(); 

为什么 "建议面向接口编程(服务类型用接口)"?

用餐厅类比:如果招牌是 "川菜馆"(接口,只说 "做川菜"),实际干活的可以是张师傅、李师傅(不同实现类)------想换厨师,直接换干活的人,不用换门口的招牌(业务代码不用改)。

但如果招牌直接写 "张师傅川菜馆"(服务类型用具体类),想换李师傅就得把招牌也换成 "李师傅川菜馆"(业务代码必须改)。 所以用接口当服务类型,换实现更灵活,业务代码不用动(这就是 "面向接口编程" 的好处)。

".NET 的 DependencyInjection 组件,包含 ServiceLocator 的功能"

.NET 官方的这个 "DependencyInjection 组件"(就是我们一直用的ServiceCollection+IServiceProvider),不止能做 DI(被动注入),还能做服务定位器(主动拿服务)

比如:

cs 复制代码
// 这行就是"服务定位器"的功能(主动从容器拿服务)
var orderService = serviceProvider.GetRequiredService<OrderService>();

所以这个组件是 "一专多能"------ 名字叫 "依赖注入",但把 "服务定位器" 的功能也包进去了,既支持被动注入,也支持主动取服务。

初步使用

.NET 的 "依赖注入" 功能不是所有项目都默认自带的(比如控制台项目),这个命令是从官方 "软件商店"(NuGet)下载并安装 DI 所需的所有工具类(比如ServiceCollectionIServiceProvider),让你的项目能用上 DI。

cs 复制代码
Install-Package Microsoft.Extensions.DependencyInjection

"using + 命名空间" 是告诉 C# 代码:"我要用到刚才安装的 DI 工具包里的类了",这样后续写ServiceCollectionIServiceProvider时,代码才认识这些类型(不会报错)。

cs 复制代码
using Microsoft.Extensions.DependencyInjection

ServiceCollectionIServiceProvider的关系

这是把 "服务清单" 变成 "可用的容器"

  • 先理解ServiceCollection:它是 DI 的 "服务清单"------ 你先把要管理的服务(比如之前的ILogServiceOrderService)都 "登记" 到这个清单里(就像做饭前先列一个 "要准备的食材清单")。
  • 再理解BuildServiceProvider():把这个 "服务清单" 变成能实际用的 DI 容器(IServiceProvider(类比:拿着食材清单去超市,把所有食材买回来,放到厨房储物柜里 ------ 这个储物柜就是 "容器",能直接拿里面的食材做饭)。
  • 最终效果:这个容器(IServiceProvider)可以取出 "清单里登记过的服务"(比如之前代码里用serviceProvider.GetRequiredService<OrderService>()拿服务)。
cs 复制代码
// 步骤3:在"清单"里登记服务
services.AddTransient<ILogService, ConsoleLogService>();
// 步骤3:把"清单"变成可用的容器
using var serviceProvider = services.BuildServiceProvider();
// 步骤3:从容器里取服务(用之前登记的东西)
var orderService = serviceProvider.GetRequiredService<OrderService>();

初步实操

先定义一个服务:由接口和实现类组成:

cs 复制代码
  //服务接口
  public interface ITestService 
  {
      public string Name { get; set; }
      public void SayHi();
  }

  //服务实现类
  public class TestServiceImpl : ITestService
  {
      public string Name { get; set; }
      public void SayHi()
      {
          Console.WriteLine($"Hi,Im{Name}");
      }
  }

普通方法调用这个服务:

cs 复制代码
 static void Main(string[] args)
 {
     ITestService p1 = new TestServiceImpl();
     p1.Name = "Tom";
     p1.SayHi();
     Console.Read();
 }

使用服务定位器:

cs 复制代码
 static void Main(string[] args)
 {
     //初始化服务注册集合
     ServiceCollection services = new ServiceCollection();
     //注册服务(写一个购物清单)
     services.AddTransient<TestServiceImpl>();
     // 构建正式DI容器(将清单物品买来放仓库里)
     using (ServiceProvider serviceProvider = services.BuildServiceProvider())
     {
         //解析服务(从容器取实例)
         TestServiceImpl p1 = serviceProvider.GetService<TestServiceImpl>();
         p1.Name = "boy";
         p1.SayHi();
     }
 }

生命周期-Singleton

cs 复制代码
            services.AddSingleton<TestServiceImpl>();           

             using (ServiceProvider serviceProvider = services.BuildServiceProvider())
            {
                解析服务(从容器取实例)
                TestServiceImpl p1 = serviceProvider.GetService<TestServiceImpl>();
                p1.Name = "boy";
                p1.SayHi();

                TestServiceImpl p2 = serviceProvider.GetService<TestServiceImpl>();
                p2.Name = "jack";
                p2.SayHi();

                p1.SayHi();//如果他的结果为Jack,说明是同一对象
                Console.WriteLine(object.ReferenceEquals(p1, p2));//Ture
                object.ReferenceEquals这个方法用于判断2个实例是否为同一对象
            }

如果是上个生命周期,则为false

三种生命周期-Scope

cs 复制代码
services.AddScoped<TestServiceImpl>();

//Scope的写法有不同处,1、需要创建范围。2、解析的时候,不是直接用容器了,而是用scope中的容器
using (ServiceProvider serviceProvider = services.BuildServiceProvider())
{
    using (IServiceScope scope1 = serviceProvider.CreateScope())//初始化一个范围。
    {
        //TestServiceImpl p3 = serviceProvider.GetService<TestServiceImpl>();//通常是这样写,
        //但在Scope里,是用范围实例.ServiceProvider(注意这个不是实例,是类)
        TestServiceImpl p3 = scope1.ServiceProvider.GetService<TestServiceImpl>();
        p3.Name = "any";
        p3.SayHi();
    }
}

创建了2个scope,验证里面的对象是否为同一对象,结果:不是

cs 复制代码
using (ServiceProvider serviceProvider = services.BuildServiceProvider())
{
    TestServiceImpl t;
    using (IServiceScope scope1 = serviceProvider.CreateScope())//初始化一个范围。
    {
        //TestServiceImpl p3 = serviceProvider.GetService<TestServiceImpl>();//通常是这样写,
        //但在Scope里,是用范围实例.ServiceProvider(注意这个不是实例,是类)
        TestServiceImpl p3 = scope1.ServiceProvider.GetService<TestServiceImpl>();
        p3.Name = "any";
        p3.SayHi();
        t = p3;
    }

    using (IServiceScope scope2 = serviceProvider.CreateScope())//初始化一个范围。
    {
        //TestServiceImpl p3 = serviceProvider.GetService<TestServiceImpl>();//通常是这样写,
        //但在Scope里,是用范围实例.ServiceProvider(注意这个不是实例,是类)
        TestServiceImpl p3 = scope2.ServiceProvider.GetService<TestServiceImpl>();
        p3.Name = "any";
        p3.SayHi();
        Console.WriteLine(object.ReferenceEquals(t, p3));
    }
}

实现 IDisposable 的类,容器会自动调用 Dispose

通俗讲

IDisposable 是 "资源清洁工" 接口 ------ 如果你的类用了文件、数据库连接 这些 "用完必须手动关闭" 的资源,就实现这个接口,把 "关资源" 的代码写在Dispose方法里。

而.NET DI 容器是个 "贴心管家":如果这个类是Scoped/Transient生命周期,等它 "离开自己的使用范围"(比如 Scoped 的作用域结束、Transient 用完被回收),容器会自动帮你调用 Dispose 方法 ,不用你手动写using或者调用 Dispose 了。

cs 复制代码
using Microsoft.Extensions.DependencyInjection;
using System;

namespace DILifeCycle
{
    // 实现IDisposable的"文件服务"(用了文件资源,需要清理)
    public class FileService : IDisposable
    {
        public FileService()
        {
            Console.WriteLine("FileService被创建了(打开了文件资源)");
        }

        // 资源清理逻辑写在这里
        public void Dispose()
        {
            Console.WriteLine("FileService的Dispose被自动调用了(关闭了文件资源)");
        }
    }

    class Program
    {
        static void Main()
        {
            var services = new ServiceCollection();
            // 注册为Scoped(作用域内生效)
            services.AddScoped<FileService>();

            using var provider = services.BuildServiceProvider();
            using (var scope = provider.CreateScope()) // 作用域开始
            {
                var fileService = scope.ServiceProvider.GetRequiredService<FileService>();
                Console.WriteLine("正在使用FileService...");
            } // 作用域结束 → 容器自动调用Dispose

            Console.Read();
        }
    }
}
cs 复制代码
FileService被创建了(打开了文件资源)
正在使用FileService...
FileService的Dispose被自动调用了(关闭了文件资源)

别在 "长生命周期对象" 里引用 "短生命周期对象"

通俗讲

生命周期有 "长短排名":Singleton(全局唯一,跟程序同生共死) > Scoped(比如一个请求活一个) > Transient(用一次就扔)

如果 "长寿的"(比如 Singleton)引用了 "短命的"(比如 Scoped),等 "短命的" 被销毁后,"长寿的" 还拿着它的引用 ------ 再用就会拿到 "已经死了的对象",ASP.NET Core 里会直接抛异常阻止这种危险操作。

3、不要在长生命周期的对象中引用比它短的生命周期的对象。在ASP.NET Core 中,这样做默认会抛异常。

4、生命周期的选择:如果类无状态,建议为 Singleton;如果类有状态,且有 Scope 控制,建议为 Scoped,因为通常这种 Scope 控制下的代码都是运行在同一个线程中的,没有并发修改的问题;在使用 Transient 的时候要谨慎。

5、.NET 注册服务的重载方法很多,看着文档琢磨吧。

正式操作

配置系统

日志系统

相关推荐
西岸行者5 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意5 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码5 天前
嵌入式学习路线
学习
毛小茛5 天前
计算机系统概论——校验码
学习
babe小鑫5 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms5 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下5 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。5 天前
2026.2.25监控学习
学习
im_AMBER5 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J5 天前
从“Hello World“ 开始 C++
c语言·c++·学习