VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > c#编程 >
  • 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语句,业务代码无需改动。
基础知识拓展

  1. 依赖注入的核心概念
    控制反转(IOC):传统方式是类自己创建依赖,IOC是容器创建依赖并注入到类中,控制权反转;
    依赖注入(DI):IOC的一种实现方式,通过构造函数、属性或方法注入依赖;
    服务容器:ASP.NET Core内置的DI容器,负责注册服务、解析依赖、管理生命周期。
  2. 注入方式对比
注入方式 优点 缺点 适用场景
构造函数注入 类型安全,依赖明确 构造函数参数可能过多 必须的依赖(推荐)
属性注入 可选依赖,构造函数简洁 依赖不明确,可能为空 可选依赖(比如日志)
方法注入 适合批量注入,灵活 代码繁琐 框架扩展(比如中间件)

属性注入示例:
csharp

	public class UsersController : Controller
	{
	[FromServices] // 属性注入标记
	public ILogger<UsersController> Logger { get; set; } = null!;
	}
  1. 第三方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(); // 对应作用域生命周期
	});
  1. 单元测试模拟服务(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


相关教程