.NET 依赖注入 续

续写 .NET 依赖注入 ------ 服务注册-CSDN博客

一、生命周期

在 .NET(特别是 ASP.NET Core)中,依赖注入(DI)是框架的核心功能之一。理解服务的‌**生命周期(Service Lifetime)**‌对于构建高性能、线程安全且无内存泄漏的应用至关重要。

.NET 内置的 DI 容器主要支持三种服务生命周期:‌单例(Singleton) ‌、‌瞬时(Transient) ‌和 ‌‌作用域(Scoped)。

单例

定义:一个应用程序的生命周期只会创建一次对象,后续每次使用都将复用该对象。

适用场景:无状态且线程安全的服务;全局配置服务(如 IConfiguration);缓存服务(如 IMemoryCache);日志记录器(ILogger)等。

cs 复制代码
// 注册方法
services.AddSingleton<IMyService, MyService>();

瞬时

定义:每个方法调用周期内只创建一次对象。

适用场景:轻量级、无状态的服务;简单的工具类、计算器、字符串处理器等。

cs 复制代码
// 注册方法
services.AddTransient<IMyService, MyService>();

作用域

定义:每个 HTTP 请求期间创建一次。

适用场景:需要在单次请求中保持状态一致性的服务(如工作单元 Unit of Work);用户会话相关的临时数据服务。

cs 复制代码
// 注册方法
services.AddScoped<IMyService, MyService>();

作用域的验证

通过服务实例的 HashCode 进行观察对比三种服务生命周期区别,分别注册三个不同的接口或使用工厂模式:

cs 复制代码
// 定义标记接口
public interface ITransientService { int Id { get; } }
public interface IScopedService { int Id { get; } }
public interface ISingletonService { int Id { get; } }
cs 复制代码
// 实现类
public class TransientService : ITransientService { public int Id => this.GetHashCode(); }
public class ScopedService : IScopedService { public int Id => this.GetHashCode(); }
public class SingletonService : ISingletonService { public int Id => this.GetHashCode(); }
cs 复制代码
using Microsoft.Extensions.DependencyInjection;
using System;

class Program
{
    static void Main(string[] args)
    {
        var services = new ServiceCollection();
        
        // 注册服务
        services.AddTransient<ITransientService, TransientService>();
        services.AddScoped<IScopedService, ScopedService>();
        services.AddSingleton<ISingletonService, SingletonService>();

        var provider = services.BuildServiceProvider();

        Console.WriteLine("=== 第一次解析 ===");
        var t1 = provider.GetRequiredService<ITransientService>();
        var s1 = provider.GetRequiredService<IScopedService>(); // 注意:根容器解析 Scoped 会有警告,最好用 Scope
        var si1 = provider.GetRequiredService<ISingletonService>();

        Console.WriteLine($"Transient 1: {t1.Id}");
        Console.WriteLine($"Singleton 1: {si1.Id}");
        
        // 创建第一个作用域 (模拟请求 1)
        Console.WriteLine("\n=== 作用域 1 (Request 1) ===");
        using (var scope1 = provider.CreateScope())
        {
            var s1_inScope = scope1.ServiceProvider.GetRequiredService<IScopedService>();
            var t1_inScope = scope1.ServiceProvider.GetRequiredService<ITransientService>();
            var si1_inScope = scope1.ServiceProvider.GetRequiredService<ISingletonService>();

            Console.WriteLine($"Scoped 1-1:    {s1_inScope.Id}");
            Console.WriteLine($"Transient 1-1: {t1_inScope.Id}");
            Console.WriteLine($"Singleton 1-1: {si1_inScope.Id}");

            // 在同一作用域内再次解析
            var s1_inScope_2 = scope1.ServiceProvider.GetRequiredService<IScopedService>();
            var t1_inScope_2 = scope1.ServiceProvider.GetRequiredService<ITransientService>();
            
            Console.WriteLine($"Scoped 1-2:    {s1_inScope_2.Id} (应与 Scoped 1-1 相同)");
            Console.WriteLine($"Transient 1-2: {t1_inScope_2.Id} (应与 Transient 1-1 不同)");
        }

        // 创建第二个作用域 (模拟请求 2)
        Console.WriteLine("\n=== 作用域 2 (Request 2) ===");
        using (var scope2 = provider.CreateScope())
        {
            var s2_inScope = scope2.ServiceProvider.GetRequiredService<IScopedService>();
            var si2_inScope = scope2.ServiceProvider.GetRequiredService<ISingletonService>();

            Console.WriteLine($"Scoped 2-1:    {s2_inScope.Id} (应与 Scoped 1-1 不同)");
            Console.WriteLine($"Singleton 2-1: {si2_inScope.Id} (应与 Singleton 1-1 相同)");
        }
        
        Console.ReadKey();
    }
}

二、服务描述

在 .NET 依赖注入(DI)体系中,ServiceDescriptor 是服务注册的核心数据单元。它充当了"服务契约"与"具体实现"之间的桥梁,记录了容器在解析服务时所需的所有元数据,包括服务类型、实现类型(或实例/工厂)以及生命周期。

调用 services.AddSingleton<IMyService, MyService>() 时,框架底层实际上是在 IServiceCollection中添加了一个描述该注册的 ServiceDescriptor 对象。

ServiceDescriptor 关键属性

属性 类型 说明
‌ServiceType‌ Type ‌服务契约 ‌。通常是接口或抽象基类(如 IMyService),也是消费者通过 GetRequiredService<T>() 请求的类型。
‌ImplementationType‌ Type ‌具体实现 ‌。当使用类型映射注册时(如 AddTransient<IMy, My>),此处存储具体类类型(如 My)。容器将使用反射调用其构造函数来创建实例。
‌ImplementationInstance‌ object ‌预创建实例 ‌。当使用实例注册时(如 AddSingleton<IMy>(new My())),此处存储具体的对象实例。容器直接返回此对象,不再创建新实例。
‌ImplementationFactory‌ Func<IServiceProvider, object> ‌工厂方法 ‌。当使用工厂注册时(如 AddScoped<IMy>(sp => new My(...))),此处存储委托。每次需要解析服务时,容器会执行此委托来获取实例。
‌Lifetime‌ ServiceLifetime ‌生命周期 ‌。枚举值,决定实例的共享范围: • Transient(瞬时) • Scoped(作用域) • Singleton(单例)

三种注册方式

基于类型的注册

容器在解析时,会通过反射查找 MyService 的构造函数,并递归解析其依赖项来创建新实例

cs 复制代码
services.AddTransient<IMyService, MyService>();

基于实例的注册

cs 复制代码
var myInstance = new MyService();
services.AddSingleton<IMyService>(myInstance);

容器直接返回 myInstance。‌

注意 ‌:容器不会负责释放由用户手动创建并传入的实例(除非该实例实现了 IDisposable 且容器在某些特定配置下管理它,但通常建议让容器管理生命周期)。

基于工厂的注册

cs 复制代码
services.AddScoped<IMyService>(sp => {
    var config = sp.GetRequiredService<IConfiguration>();
    return new MyService(config["Key"]);
});

三、批量注册

手动逐个注册服务(如 services.AddScoped<IMyService, MyService>())在项目规模扩大后会变得繁琐且易错。通过‌批量注册 ‌可以自动化这一过程,核心思路是利用反射扫描程序集,根据特定规则生成 ServiceDescriptor 并添加到 IServiceCollection 中。

根据名称匹配

这是最常见的自动注册方式,通常基于"类名"与"接口名"的命名约定。例如:UserService 自动注册为 IUserService。

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

public static class ServiceCollectionExtensions
{
    /// <summary>
    /// 基于名称约定自动注册服务
    /// 规则:类名以 "Service" 结尾,自动注册到 I{ClassName} 接口
    /// </summary>
    public static IServiceCollection AddServicesByConvention(this IServiceCollection services, Assembly assembly = null)
    {
        // 默认扫描调用方的程序集,也可指定特定程序集
        assembly ??= Assembly.GetCallingAssembly();

        var types = assembly.GetTypes()
            .Where(t => t.IsClass && !t.IsAbstract && t.IsPublic && !t.ContainsGenericParameters)
            .Where(t => t.Name.EndsWith("Service", StringComparison.OrdinalIgnoreCase));

        foreach (var implType in types)
        {
            // 寻找对应的接口:例如 UserService -> IUserService
            var interfaceName = $"I{implType.Name}";
            var interfaceType = implType.GetInterfaces()
                .FirstOrDefault(i => i.Name == interfaceName);

            if (interfaceType != null)
            {
                // 默认注册为 Scoped,可根据需求调整或增加特性标记来动态决定生命周期
                services.AddScoped(interfaceType, implType);
            }
            else
            {
                // 可选策略:如果没有找到特定接口,可以注册自身或跳过
                // services.AddScoped(implType, implType); 
            }
        }

        return services;
    }
}
cs 复制代码
// 在 Program.cs 中
builder.Services.AddServicesByConvention(typeof(Program).Assembly);

根据标记注册

名称约定虽然方便,但不够灵活(例如无法区分生命周期,或类名不符合规范)。使用自定义 Attribute(特性)可以更精确地控制哪些类需要注册以及它们的生命周期。

定义特性

cs 复制代码
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class AutoRegisterAttribute : Attribute
{
    public ServiceLifetime Lifetime { get; set; } = ServiceLifetime.Scoped;
    public Type[] Services { get; set; } // 可选:指定注册到哪些接口,默认为所有实现接口

    public AutoRegisterAttribute(ServiceLifetime lifetime = ServiceLifetime.Scoped)
    {
        Lifetime = lifetime;
    }
}

实现注册扩展方法

cs 复制代码
public static class ServiceCollectionMarkerExtensions
{
    public static IServiceCollection AddServicesByMarker(this IServiceCollection services, Assembly assembly = null)
    {
        assembly ??= Assembly.GetCallingAssembly();

        var types = assembly.GetTypes()
            .Where(t => t.IsClass && !t.IsAbstract && t.IsPublic)
            .Where(t => t.GetCustomAttribute<AutoRegisterAttribute>() != null);

        foreach (var implType in types)
        {
            var attribute = implType.GetCustomAttribute<AutoRegisterAttribute>();
            
            // 确定要注册的接口列表
            var interfacesToRegister = attribute.Services?.Length > 0 
                ? attribute.Services 
                : implType.GetInterfaces();

            foreach (var serviceType in interfacesToRegister)
            {
                // 根据特性中指定的生命周期进行注册
                var descriptor = new ServiceDescriptor(serviceType, implType, attribute.Lifetime);
                services.Add(descriptor);
            }
        }

        return services;
    }
}

在业务类上使用标记

cs 复制代码
[AutoRegister(ServiceLifetime.Singleton)]
public class CacheService : ICacheService
{
    // ...
}

[AutoRegister(ServiceLifetime.Transient)]
public class EmailSender : IEmailSender
{
    // ...
}
cs 复制代码
builder.Services.AddServicesByMarker(typeof(Program).Assembly);

配置扩展方法

为了保持 Program.cs 的整洁,建议将批量注册逻辑封装为独立的扩展方法,甚至支持从配置文件或模块化架构中动态加载。

支持多程序集扫描

cs 复制代码
public static class ServiceCollectionBatchExtensions
{
    /// <summary>
    /// 批量注册多个程序集中的服务
    /// </summary>
    public static IServiceCollection AddBatchServices(this IServiceCollection services, params Assembly[] assemblies)
    {
        if (assemblies == null || assemblies.Length == 0)
        {
            assemblies = new[] { Assembly.GetCallingAssembly() };
        }

        foreach (var assembly in assemblies)
        {
            // 这里可以组合多种策略,例如先按标记,再按约定
            
            // 策略1: 基于标记注册
            services.RegisterByMarker(assembly);
            
            // 策略2: 基于约定注册 (作为补充)
            services.RegisterByConvention(assembly);
        }

        return services;
    }

    private static IServiceCollection RegisterByMarker(this IServiceCollection services, Assembly assembly)
    {
        // 复用之前的标记注册逻辑
        var types = assembly.GetTypes()
            .Where(t => t.IsClass && !t.IsAbstract && t.IsPublic && t.GetCustomAttribute<AutoRegisterAttribute>() != null);

        foreach (var implType in types)
        {
            var attr = implType.GetCustomAttribute<AutoRegisterAttribute>();
            foreach (var iface in implType.GetInterfaces())
            {
                services.Add(new ServiceDescriptor(iface, implType, attr.Lifetime));
            }
        }
        return services;
    }
    
    private static IServiceCollection RegisterByConvention(this IServiceCollection services, Assembly assembly)
    {
         // 复用之前的约定注册逻辑
         var types = assembly.GetTypes()
            .Where(t => t.IsClass && !t.IsAbstract && t.IsPublic && t.Name.EndsWith("Service", StringComparison.OrdinalIgnoreCase));

        foreach (var implType in types)
        {
            var interfaceName = $"I{implType.Name}";
            var interfaceType = implType.GetInterfaces().FirstOrDefault(i => i.Name == interfaceName);
            if (interfaceType != null)
            {
                services.AddScoped(interfaceType, implType);
            }
        }
        return services;
    }
}

在 Program.cs 中的最终调用

cs 复制代码
var builder = WebApplication.CreateBuilder(args);

// 一行代码完成所有服务的批量注册
// 可以传入当前程序集,也可以传入其他包含服务的程序集
builder.Services.AddBatchServices(
    typeof(Program).Assembly, 
    typeof(SomeOtherModule.Service).Assembly
);

var app = builder.Build();
app.Run();
相关推荐
淡海水5 小时前
02-03-原理篇-大对象堆
unity·c#·游戏引擎·.net·gc
LateFrames9 小时前
WPF 升级选型:用 .NET 8 还是 .NET 10
.net·wpf·.net8·.net10
淡海水1 天前
02-01-原理篇-Mark-Sweep与变种算法
算法·unity·c#·游戏引擎·.net·gc
波力海苔夹心脆6752 天前
C# 值类型与引用类型详解:存储位置、赋值机制、参数传递、相等比较、装箱拆箱与常见陷阱
开发语言·jvm·经验分享·笔记·c#·.net
淡海水2 天前
01-04-认知篇-Unity内存全景
unity·c#·游戏引擎·.net·gc
JosieBook2 天前
【.NET工程师】.NET 工程师成长体系(精要版)
.net
淡海水3 天前
01-03-认知篇-C#内存模型深度解析
unity·c#·游戏引擎·.net·gc
AKULAKK4 天前
关于在Ghost版Win7上运行.net 2程序遇到的问题以及解决办法
.net·win7
波力海苔夹心脆6754 天前
C# 序列化与反序列化详解:System.Text.Json、Newtonsoft.Json、XmlSerializer 用法、特性选项与安全实践
经验分享·后端·c#·json·.net