C# 中,依赖注入(DI)的实现方式
前面介绍过 什么是控制反转(IoC)?什么是依赖注入(DI)?以及实现原理
在 C# 中,依赖注入(DI)的实现方式主要分为手动注入和通过 IoC 容器注入(如 .NET 自带的 Microsoft.Extensions.DependencyInjection
)。以下是具体代码示例,涵盖常用场景和最佳实践。
一、手动依赖注入(基础示例)
手动注入不依赖第三方容器,直接通过代码传递依赖,适合简单场景,核心是构造函数注入(最推荐的方式)。
1. 构造函数注入(推荐)
csharp
using System;// 1. 定义抽象依赖(接口)
public interface IMessageSender
{void Send(string message);
}// 2. 实现具体依赖(邮件发送)
public class EmailSender : IMessageSender
{public void Send(string message){Console.WriteLine($"[邮件发送] {message}");}
}// 3. 实现具体依赖(短信发送)
public class SmsSender : IMessageSender
{public void Send(string message){Console.WriteLine($"[短信发送] {message}");}
}// 4. 依赖方(通知服务):通过构造函数接收依赖
public class NotificationService
{private readonly IMessageSender _messageSender;// 构造函数注入:依赖由外部传入,且用 readonly 确保不可变public NotificationService(IMessageSender messageSender){_messageSender = messageSender ?? throw new ArgumentNullException(nameof(messageSender), "依赖不能为空"); // 校验依赖,避免空引用}public void NotifyUser(string username){_messageSender.Send($"用户 {username} 已收到通知");}
}// 5. 调用:手动注入依赖
class Program
{static void Main(){// 手动创建依赖实例IMessageSender emailSender = new EmailSender();// IMessageSender smsSender = new SmsSender(); // 可切换为短信发送// 注入到依赖方NotificationService notificationService = new NotificationService(emailSender);notificationService.NotifyUser("张三"); // 输出:[邮件发送] 用户 张三 已收到通知}
}
优势:
- 依赖在对象创建时就必须传入,确保对象初始化后即可正常工作(避免空引用)。
- 依赖不可变(
readonly
),避免运行时被篡改。
2. 属性注入(不推荐,仅特殊场景使用)
属性注入通过公共属性传递依赖,适合 “可选依赖”(非必须的功能),但可能导致对象创建后依赖未初始化的问题。
csharp
public class NotificationService
{// 属性注入:依赖通过属性设置(通常有默认值或允许为null)public IMessageSender MessageSender { get; set; } = new EmailSender(); // 默认值public void NotifyUser(string username){if (MessageSender == null)throw new InvalidOperationException("未设置消息发送器");MessageSender.Send($"用户 {username} 已收到通知");}
}// 调用
class Program
{static void Main(){var service = new NotificationService();service.MessageSender = new SmsSender(); // 通过属性注入依赖service.NotifyUser("李四"); // 输出:[短信发送] 用户 李四 已收到通知}
}
注意:属性注入可能导致 “对象已创建但依赖未设置” 的风险,除非有明确理由(如框架限制),否则优先用构造函数注入。
二、使用 .NET 自带的 DI 容器(Microsoft.Extensions.DependencyInjection
)
在 .NET Core/.NET 5+ 中,官方提供了 Microsoft.Extensions.DependencyInjection
容器,是企业级开发的首选。需先通过 NuGet 安装包(一般项目默认已引用):Install-Package Microsoft.Extensions.DependencyInjection
1. 基本用法(注册 + 解析)
csharp
using Microsoft.Extensions.DependencyInjection;
using System;// 复用上面的 IMessageSender、EmailSender、SmsSender、NotificationServiceclass Program
{static void Main(){// 1. 创建服务容器var serviceCollection = new ServiceCollection();// 2. 注册服务(关键步骤:告诉容器“抽象 -> 具体实现”的映射)// 注册 IMessageSender,指定实现为 EmailSenderserviceCollection.AddSingleton<IMessageSender, EmailSender>();// 注册 NotificationService(容器会自动注入其依赖 IMessageSender)serviceCollection.AddSingleton<NotificationService>();// 3. 构建服务提供器(容器的具体实现)var serviceProvider = serviceCollection.BuildServiceProvider();// 4. 从容器解析服务(自动处理依赖链)var notificationService = serviceProvider.GetRequiredService<NotificationService>();// 5. 使用服务notificationService.NotifyUser("王五"); // 输出:[邮件发送] 用户 王五 已收到通知}
}
2. 服务生命周期(3 种核心类型)
容器通过 “生命周期” 管理服务实例的创建和销毁,核心有 3 种:
生命周期 | 说明 | 适用场景 |
---|---|---|
Transient | 每次请求(GetService )创建新实例 | 轻量级、无状态服务(如工具类) |
Scoped | 每个 “作用域” 内创建一个实例(如 Web 请求) | 数据库上下文(DbContext) |
Singleton | 整个应用生命周期内只创建一个实例 | 全局配置、缓存服务 |
示例:验证生命周期差异
csharp
using Microsoft.Extensions.DependencyInjection;
using System;// 测试服务:记录实例ID,观察是否为同一实例
public class TestService
{public Guid Id { get; } = Guid.NewGuid(); // 实例化时生成唯一ID
}class Program
{static void Main(){var services = new ServiceCollection();// 注册3种生命周期的服务services.AddTransient<TestService>(); // Transientservices.AddScoped<TestService>(); // Scoped(需在作用域内解析)services.AddSingleton<TestService>(); // Singletonvar provider = services.BuildServiceProvider();// 1. 测试 Transient:每次获取都是新实例Console.WriteLine("Transient:");var t1 = provider.GetRequiredService<TestService>();var t2 = provider.GetRequiredService<TestService>();Console.WriteLine($"t1.Id == t2.Id? {t1.Id == t2.Id}"); // 输出:False// 2. 测试 Scoped:同一作用域内是同一实例,不同作用域不同Console.WriteLine("\nScoped:");using (var scope1 = provider.CreateScope()) // 创建作用域1{var s1 = scope1.ServiceProvider.GetRequiredService<TestService>();var s2 = scope1.ServiceProvider.GetRequiredService<TestService>();Console.WriteLine($"scope1内 s1.Id == s2.Id? {s1.Id == s2.Id}"); // True}using (var scope2 = provider.CreateScope()) // 创建作用域2{var s3 = scope2.ServiceProvider.GetRequiredService<TestService>();Console.WriteLine($"scope2内 s3.Id 与 scope1的s1不同? {true}"); // 必然不同}// 3. 测试 Singleton:全局唯一实例Console.WriteLine("\nSingleton:");var s1 = provider.GetRequiredService<TestService>();var s2 = provider.GetRequiredService<TestService>();Console.WriteLine($"s1.Id == s2.Id? {s1.Id == s2.Id}"); // 输出:True}
}
3. 依赖链解析(多层依赖)
容器会自动解析 “依赖的依赖”(递归解析),无需手动处理多层依赖关系。
csharp
using Microsoft.Extensions.DependencyInjection;
using System;// 第一层依赖:日志服务
public interface ILogger { void Log(string msg); }
public class ConsoleLogger : ILogger { public void Log(string msg) => Console.WriteLine($"[日志] {msg}"); }// 第二层依赖:用户仓储(依赖日志)
public interface IUserRepository
{ void Add(string username);
}
public class UserRepository : IUserRepository
{private readonly ILogger _logger;// 依赖 ILoggerpublic UserRepository(ILogger logger){_logger = logger;}public void Add(string username){_logger.Log($"用户 {username} 已添加到数据库");}
}// 第三层依赖:用户服务(依赖用户仓储)
public class UserService
{private readonly IUserRepository _userRepository;// 依赖 IUserRepository(其内部又依赖 ILogger)public UserService(IUserRepository userRepository){_userRepository = userRepository;}public void RegisterUser(string username){_userRepository.Add(username);}
}// 容器自动解析依赖链
class Program
{static void Main(){var services = new ServiceCollection();// 注册所有依赖(只需注册抽象与实现的映射,容器自动处理依赖链)services.AddSingleton<ILogger, ConsoleLogger>();services.AddSingleton<IUserRepository, UserRepository>();services.AddSingleton<UserService>();var provider = services.BuildServiceProvider();var userService = provider.GetRequiredService<UserService>();userService.RegisterUser("赵六"); // 输出:[日志] 用户 赵六 已添加到数据库(依赖链:UserService -> UserRepository -> ConsoleLogger)}
}
三、ASP.NET Core 中的依赖注入(实际应用)
在 ASP.NET Core 中,DI 容器已内置,通常在 Program.cs
中注册服务,在控制器 / 服务中通过构造函数注入使用。
1. 注册服务(Program.cs)
csharp
var builder = WebApplication.CreateBuilder(args);// 注册控制器(默认已包含)
builder.Services.AddControllers();// 注册自定义服务
builder.Services.AddScoped<IMessageSender, EmailSender>(); // 作用域生命周期(适合Web请求)
builder.Services.AddScoped<NotificationService>();var app = builder.Build();// 中间件配置...
app.MapControllers();
app.Run();
2. 在控制器中注入服务
csharp
using Microsoft.AspNetCore.Mvc;[ApiController]
[Route("api/[controller]")]
public class NotificationController : ControllerBase
{private readonly NotificationService _notificationService;// 控制器构造函数注入:容器自动传入 NotificationService(及其依赖)public NotificationController(NotificationService notificationService){_notificationService = notificationService;}[HttpGet("{username}")]public IActionResult Notify(string username){_notificationService.NotifyUser(username);return Ok("通知已发送");}
}
总结
- 核心方式:构造函数注入是首选,确保依赖不可变且初始化完整。
- 容器使用:.NET 自带的
Microsoft.Extensions.DependencyInjection
是主流选择,通过 “注册 - 解析” 管理服务,自动处理依赖链。 - 生命周期:根据服务特性选择 Transient/Scoped/Singleton,避免因生命周期错误导致的问题(如在 Singleton 中注入 Scoped 服务)。
以上示例覆盖了从基础手动注入到框架级容器使用的场景,可根据项目规模选择合适的方式。