-
C#中关于依赖注入——服务注册与使用
依赖注入——服务注册与使用
实例介绍
之前做电商后台用户管理模块时,控制器里直接new UserRepository()来操作数据库:
csharp
public class UsersController : Controller
{
private readonly UserRepository _userRepo = new UserRepository(); // 直接new实现类
// ...业务逻辑
}
后来要把SQL Server换成MySQL,得把所有控制器vb.net教程C#教程python教程SQL教程access 2010教程
里的new UserRepository()改成new MySqlUserRepository(),改了十几处才搞定;单元测试时只能连真实数据库,每次测试都要清理数据,慢得要死。用依赖注入重构后,控制器只依赖IUserRepository接口,实现类通过配置注册,换数据库只要改一行服务注册代码,单元测试用Mock模拟数据层,不用连真实数据库,效率提升了好几倍。这节就带你彻底搞懂ASP.NET Core的依赖注入,告别代码耦合的痛苦。
需求分析
依赖注入要解决“代码解耦、可测试、可配置、生命周期管理”的问题,具体需求如下:
1.服务注册:支持接口与实现类的绑定,三种生命周期(瞬时、作用域、单例);
2.构造函数注入:推荐的注入方式,类型安全,编译器自动检查依赖;
3.动态服务选择:根据配置选择不同的实现类(比如支付宝/微信支付);
4.生命周期管理:理解三种生命周期的区别,避免内存泄漏或数据共享问题;
5.单元测试支持:用Mock框架模拟服务,无需依赖真实实现;
6.第三方容器集成:支持Autofac等第三方DI容器(复杂场景);
7.最佳实践:遵循接口隔离、单一职责,避免服务定位器模式。
代码实现
前置条件:.NET 6+、ASP.NET Core MVC;需掌握接口与抽象、单元测试基础;推荐用Moq做Mock测试。
场景1:基础服务注册与构造函数注入
注册数据访问服务,控制器通过构造函数注入接口,实现代码解耦。
步骤1:定义接口与实现类(Repositories/IUserRepository.cs)
csharp
public interface IUserRepository
{
Task<List<User>> GetAllAsync();
Task<User?> GetByIdAsync(int id);
Task AddAsync(User user);
Task UpdateAsync(User user);
Task DeleteAsync(int id);
}
// SQL Server实现
public class SqlUserRepository : IUserRepository
{
private readonly AppDbContext _context;
// 构造函数注入数据库上下文(依赖注入的传递性)
public SqlUserRepository(AppDbContext context)
{
_context = context;
}
public async Task<List<User>> GetAllAsync() => await _context.Users.ToListAsync();
public async Task<User?> GetByIdAsync(int id) => await _context.Users.FindAsync(id);
public async Task AddAsync(User user)
{
_context.Users.Add(user);
await _context.SaveChangesAsync();
}
// ...其他方法实现
}
// MySQL实现(后续换数据库时只需注册这个实现)
public class MySqlUserRepository : IUserRepository
{
// ...MySQL相关实现
}
步骤2:注册服务(Program.cs)
csharp
var builder = WebApplication.CreateBuilder(args);
// 1. 注册数据库上下文(作用域生命周期,每个请求一个实例)
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// 2. 注册用户数据访问服务:接口绑定到SQL Server实现
// AddScoped:作用域生命周期,每个请求创建一个实例
builder.Services.AddScoped<IUserRepository, SqlUserRepository>();
// 3. 注册MVC服务
builder.Services.AddControllersWithViews();
var app = builder.Build();
// ...中间件配置
app.Run();
步骤3:控制器构造函数注入(Controllers/UsersController.cs)
csharp
public class UsersController : Controller
{
private readonly IUserRepository _userRepo;
private readonly ILogger<UsersController> _logger;
// 构造函数注入:依赖由DI容器自动提供,无需手动new
public UsersController(IUserRepository userRepo, ILogger<UsersController> logger)
{
_userRepo = userRepo;
_logger = logger;
}
public async Task<IActionResult> Index()
{
var users = await _userRepo.GetAllAsync(); // 调用接口方法,不依赖具体实现
_logger.LogInformation("获取用户列表,共{Count}条", users.Count);
return View(users);
}
}
场景2:生命周期管理(瞬时/作用域/单例)
理解三种生命周期的区别,避免误用导致的问题。
步骤1:定义测试服务(Services/ITestService.cs)
csharp
public interface ITestService
{
string GetInstanceId(); // 返回实例的HashCode,用于区分不同实例
}
public class TestService : ITestService
{
private readonly string _instanceId;
public TestService()
{
_instanceId = Guid.NewGuid().ToString().Substring(0, 8); // 简化的实例ID
}
public string GetInstanceId() => _instanceId;
}
步骤2:注册不同生命周期的服务(Program.cs)
csharp
// 1. 瞬时生命周期:每次请求创建新实例(适合无状态、轻量级服务)
builder.Services.AddTransient<ITestService, TestService>();
// 2. 作用域生命周期:每个请求创建一个实例(适合数据库上下文、数据访问服务)
// builder.Services.AddScoped<ITestService, TestService>();
// 3. 单例生命周期:整个应用生命周期只创建一个实例(适合配置、缓存服务)
// builder.Services.AddSingleton<ITestService, TestService>();
步骤3:控制器测试生命周期(Controllers/HomeController.cs)
csharp
public class HomeController : Controller
{
private readonly ITestService _service1;
private readonly ITestService _service2;
public HomeController(ITestService service1, ITestService service2)
{
_service1 = service1;
_service2 = service2;
}
public IActionResult Index()
{
// 打印两个实例的ID,看是否相同
ViewBag.InstanceId1 = _service1.GetInstanceId();
ViewBag.InstanceId2 = _service2.GetInstanceId();
return View();
}
}
测试结果:
瞬时:每次请求的两个实例ID都不同;
作用域:同一个请求的两个实例ID相同,不同请求不同;
单例:所有请求的实例ID都相同。
场景3:动态服务选择(工厂注入)
根据配置选择不同的支付服务实现类,无需修改业务代码。
步骤1:定义支付接口与实现(Services/IPaymentService.cs)
csharp
public interface IPaymentService
{
Task<string> CreateOrderAsync(decimal amount);
}
public class AlipayService : IPaymentService
{
public async Task<string> CreateOrderAsync(decimal amount)
{
// 模拟支付宝创建订单
await Task.Delay(100);
return $"支付宝订单:{Guid.NewGuid()},金额:{amount}";
}
}
public class WechatPayService : IPaymentService
{
public async Task<string> CreateOrderAsync(decimal amount)
{
// 模拟微信支付创建订单
await Task.Delay(100);
return $"微信支付订单:{Guid.NewGuid()},金额:{amount}";
}
}
步骤2:注册支付服务工厂(Program.cs)
csharp
// 1. 注册所有支付实现类
builder.Services.AddTransient<AlipayService>();
builder.Services.AddTransient<WechatPayService>();
// 2. 注册工厂:根据配置选择支付服务
builder.Services.AddScoped<Func<string, IPaymentService>>(serviceProvider => paymentType =>
{
switch (paymentType.ToLower())
{
case "alipay":
return serviceProvider.GetRequiredService<AlipayService>();
case "wechatpay":
return serviceProvider.GetRequiredService<WechatPayService>();
default:
throw new ArgumentException($"不支持的支付类型:{paymentType}");
}
});
// 3. 从配置读取默认支付类型
builder.Services.AddScoped<IPaymentService>(serviceProvider =>
{
var config = serviceProvider.GetRequiredService<IConfiguration>();
var defaultPaymentType = config["Payment:DefaultType"] ?? "alipay";
var factory = serviceProvider.GetRequiredService<Func<string, IPaymentService>>();
return factory(defaultPaymentType);
});
步骤3:配置文件(appsettings.json)
json
{
"Payment": {
"DefaultType": "wechatpay" // 切换支付类型只需改这里
}
}
步骤4:控制器使用支付服务(Controllers/OrdersController.cs)
csharp
public class OrdersController : Controller
{
private readonly IPaymentService _paymentService;
private readonly Func<string, IPaymentService> _paymentFactory;
// 注入默认支付服务和工厂
public OrdersController(IPaymentService paymentService, Func<string, IPaymentService> paymentFactory)
{
_paymentService = paymentService;
_paymentFactory = paymentFactory;
}
public async Task<IActionResult> CreateOrder(decimal amount)
{
// 使用默认支付服务
var defaultOrder = await _paymentService.CreateOrderAsync(amount);
// 手动选择支付宝
var alipayOrder = await _paymentFactory("alipay").CreateOrderAsync(amount);
// ...业务逻辑
return Ok(new { defaultOrder, alipayOrder });
}
}
逐行讲解
场景1:基础服务注册
1.AddScoped<IUserRepository, SqlUserRepository>():
o第一个参数是服务类型(接口),第二个是实现类型;
oAddScoped表示作用域生命周期,每个HTTP请求创建一个实例,避免不同请求共享数据;
2.构造函数注入:
o控制器的构造函数接收IUserRepository接口,DI容器自动提供实现类实例;
o类型安全:如果缺少依赖,编译时会报错,而不是运行时;
3.解耦好处:换MySQL实现时,只需把SqlUserRepository改成MySqlUserRepository,控制器代码无需修改。
场景2:生命周期管理
1.瞬时(AddTransient):每次请求依赖时创建新实例,适合无状态、轻量级服务(比如工具类);
2.作用域(AddScoped):每个请求一个实例,适合数据库上下文、数据访问服务(避免不同请求共享DB上下文导致的并发问题);
3.单例(AddSingleton):整个应用生命周期一个实例,适合配置、缓存服务(注意线程安全,避免共享可变数据);
4.内存泄漏风险:单例服务如果依赖作用域服务,会导致作用域服务无法释放,引发内存泄漏,要避免这种情况。
场景3:工厂注入
1.Func<string, IPaymentService>:工厂委托,根据字符串参数返回对应的服务实例;
2.配置驱动:从appsettings.json读取默认支付类型,无需修改代码即可切换实现;
3.灵活扩展:新增支付方式(比如银联),只需添加实现类,修改工厂的switch语句,业务代码无需改动。
基础知识拓展
-
依赖注入的核心概念
控制反转(IOC):传统方式是类自己创建依赖,IOC是容器创建依赖并注入到类中,控制权反转;
依赖注入(DI):IOC的一种实现方式,通过构造函数、属性或方法注入依赖;
服务容器:ASP.NET Core内置的DI容器,负责注册服务、解析依赖、管理生命周期。 - 注入方式对比
| 注入方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 构造函数注入 | 类型安全,依赖明确 | 构造函数参数可能过多 | 必须的依赖(推荐) |
| 属性注入 | 可选依赖,构造函数简洁 | 依赖不明确,可能为空 | 可选依赖(比如日志) |
| 方法注入 | 适合批量注入,灵活 | 代码繁琐 | 框架扩展(比如中间件) |
属性注入示例:
csharp
public class UsersController : Controller
{
[FromServices] // 属性注入标记
public ILogger<UsersController> Logger { get; set; } = null!;
}
-
第三方DI容器集成(Autofac)
复杂场景(比如AOP、批量注册)可以用Autofac:
csharp
// 安装Autofac.AspNetCore NuGet包
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
builder.Host.ConfigureContainer<ContainerBuilder>(builder =>
{
// 批量注册程序集内的服务
builder.RegisterAssemblyTypes(typeof(Program).Assembly)
.Where(t => t.Name.EndsWith("Repository"))
.AsImplementedInterfaces()
.InstancePerLifetimeScope(); // 对应作用域生命周期
});
-
单元测试模拟服务(Moq)
用Moq模拟IUserRepository,测试控制器逻辑:
csharp
// 安装Moq、xUnit NuGet包
public class UsersControllerTests
{
[Fact]
public async Task Index_Returns_All_Users()
{
// 1. Mock IUserRepository
var mockRepo = new Mock<IUserRepository>();
mockRepo.Setup(repo => repo.GetAllAsync())
.ReturnsAsync(new List<User> { new User { Id = 1, UserName = "test" } });
// 2. 创建控制器,注入Mock服务
var controller = new UsersController(mockRepo.Object);
// 3. 测试动作
var result = await controller.Index();
// 4. 验证结果
var viewResult = Assert.IsType<ViewResult>(result);
var model = Assert.IsAssignableFrom<List<User>>(viewResult.Model);
Assert.Single(model);
}
}
总结
依赖注入是ASP.NET Core的核心,它的价值在于代码解耦、可测试、可配置,让你的代码更灵活、更易于维护。关键要点:
1.生命周期选择:作用域生命周期是最常用的(数据库上下文、数据访问),单例要注意线程安全;
2.构造函数注入优先:类型安全,依赖明确,是推荐的注入方式;
3.接口隔离:服务接口要小而专,避免大接口导致的依赖冗余;
4.避免服务定位器:不要用serviceProvider.GetService()直接在业务代码中获取服务,破坏依赖注入的原则;
5.单元测试友好:依赖注入让Mock测试成为可能,无需依赖真实实现,测试更高效。
比如电商后台中,用依赖注入后,数据访问、支付、缓存等服务都可以独立扩展和测试,换数据库、换支付方式只需改配置或服务注册,业务代码无需改动,大大提升了开发效率和代码质量。掌握依赖注入,你写的代码会更专业、更健壮!
来源:https://www.xin3721.com/ArticlecSharp/c49475.html










