续写 .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();