加载中,请稍等...

行业资讯


.NET Core 依赖注入实战手册:构造函数、属性注入与项目应用

引用链接:https://mp.weixin.qq.com/s/0o6WTPjKDx-DE-vvawTBhg?scene=1&click_id=1
一、引言:为什么需要依赖注入?

在现代软件开发中,构建可维护、可测试、可扩展的应用程序是核心目标之一。随着应用程序复杂度的增加,类与类之间的依赖关系变得错综复杂。传统的“硬编码”依赖方式(即在类内部直接 new 一个依赖对象)导致了高耦合、低内聚,给单元测试、重构和维护带来了巨大挑战。

依赖注入(Dependency Injection, DI)是一种设计模式,它通过将依赖项从类的内部创建转移到外部注入,实现了“控制反转”(Inversion of Control, IoC),从而降低了类之间的耦合度,提高了代码的灵活性和可测试性。

.NET Core 从设计之初就内置了强大的依赖注入容器,使得开发者可以轻松地实现依赖管理。本文将深入探讨 .NET Core 中的依赖注入机制,涵盖服务生命周期、注册方式、使用场景,并通过详细示例展示构造函数注入、属性注入、方法注入等常见模式

二、.NET Core 依赖注入基础概念

1. 什么是依赖注入?

依赖注入是一种实现控制反转(IoC)的技术。在传统编程中,一个类如果需要使用另一个类的功能,通常会自己创建该类的实例:

public class OrderService{    private readonly EmailService _emailService;    public OrderService()    {        _emailService = new EmailService(); // 硬编码依赖    }    public void ProcessOrder(Order order)    {        // 处理订单逻辑        _emailService.SendConfirmation(order.CustomerEmail);    }}

这种方式的问题在于:

  • OrderService 与 EmailService 紧耦合。

  • 无法在测试中替换 EmailService 为模拟对象(Mock)。

  • 如果 EmailService 的构造函数发生变化,OrderService 也需要修改。

使用依赖注入后:

public class OrderService{    private readonly IEmailService _emailService;    public OrderService(IEmailService emailService)    {        _emailService = emailService;    }    public void ProcessOrder(Order order)    {        _emailService.SendConfirmation(order.CustomerEmail);    }}

依赖由外部注入,OrderService 不再关心 IEmailService 的具体实现。

2. 依赖注入的三种主要方式

  1. 构造函数注入(Constructor Injection):最常用、最推荐的方式。依赖通过构造函数传入。

  2. 属性注入(Property Injection):依赖通过公共属性注入。

  3. 方法注入(Method Injection):依赖通过方法参数传入。

3. 服务生命周期

.NET Core DI 容器支持三种服务生命周期:

生命周期
描述
使用场景
Singleton
容器在整个应用程序生命周期中只创建一个实例。
全局状态管理、配置服务、日志服务。
Scoped
在每个请求(或作用域)内创建一个实例。
Web 应用中,每个 HTTP 请求使用一个实例。
Transient
每次请求服务时都创建一个新实例。
轻量级、无状态的服务。

注意:在 ASP.NET Core 应用中,Scoped 通常对应一个 HTTP 请求的生命周期。

三、.NET Core DI 容器配置

1. 配置服务容器

在 Program.cs(.NET 6+)或 Startup.cs(.NET 5 及更早)中配置服务:

// Program.cs (.NET 6+)var builder = WebApplication.CreateBuilder(args);// 添加服务到 DI 容器builder.Services.AddTransient<IEmailServiceEmailService>();builder.Services.AddScoped<IOrderServiceOrderService>();builder.Services.AddSingleton<ILoggerLogger>();var app = builder.Build();// 配置中间件管道app.MapGet("/", () => "Hello World!");app.Run();

2. 服务注册方法

  • AddTransient<TService, TImplementation>():注册瞬态服务。

  • AddScoped<TService, TImplementation>():注册作用域服务。

  • AddSingleton<TService, TImplementation>():注册单例服务。

  • AddSingleton<TService>(instance):注册一个已创建的单例实例。

  • AddSingleton<TService>(factory):通过工厂方法创建单例。

 

四、构造函数注入(Constructor Injection)

这是最推荐、最常用的注入方式。

示例

// 服务接口public interface IEmailService{    void SendConfirmation(string email);}// 服务实现public class EmailService : IEmailService{    public void SendConfirmation(string email)    {        Console.WriteLine($"发送确认邮件到: {email}");    }}// 使用服务的类public class OrderService{    private readonly IEmailService _emailService;    // 构造函数注入    public OrderService(IEmailService emailService)    {        _emailService = emailService;    }    public void ProcessOrder(Order order)    {        // 处理订单        Console.WriteLine("订单处理中...");        _emailService.SendConfirmation(order.CustomerEmail);    }}// Order 类public class Order{    public int Id { getset; }    public string CustomerEmail { getset; }}

在 Program.cs 中注册服务:

builder.Services.AddTransient<IEmailServiceEmailService>();builder.Services.AddScoped<IOrderServiceOrderService>();

优点

  • 依赖关系清晰,易于理解。

  • 所有依赖在对象创建时必须提供,避免了空引用异常。

  • 易于进行单元测试(可通过 Mock 框架注入模拟对象)。

五、属性注入(Property Injection)

属性注入允许通过公共属性注入依赖。虽然 .NET Core 内置的 DI 容器不直接支持属性注入,但我们可以通过以下方式实现:

1. 使用 IServiceProvider 手动解析(不推荐)

public class OrderService{    public IEmailService EmailService { getset; }    public void ProcessOrder(Order order)    {        // 手动从容器解析(不推荐,破坏了 DI 原则)        EmailService ??= ServiceProvider.GetService<IEmailService>();        EmailService.SendConfirmation(order.CustomerEmail);    }    // 需要外部设置 ServiceProvider    public IServiceProvider ServiceProvider { getset; }}

这种方式破坏了依赖注入的初衷,不推荐使用。

2. 使用第三方容器(如 Autofac)

Autofac 等第三方 DI 容器原生支持属性注入。

安装 Autofac.Extensions.DependencyInjection

dotnet add package Autofac.Extensions.DependencyInjection

配置 Autofac

// Program.csusing Autofac;using Autofac.Extensions.DependencyInjection;var builder = WebApplication.CreateBuilder(args);// 使用 Autofac 作为容器builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());// 注册服务(Autofac 方式)builder.Host.ConfigureContainer<ContainerBuilder>(containerBuilder =>{    containerBuilder.RegisterType<EmailService>().As<IEmailService>().InstancePerLifetimeScope();    containerBuilder.RegisterType<OrderService>().PropertiesAutowired(); // 启用属性注入});var app = builder.Build();app.Run();

启用属性注入的类

public class OrderService{    // [Dependency] 特性标记属性需要注入    public IEmailService EmailService { getset; }    public void ProcessOrder(Order order)    {        EmailService.SendConfirmation(order.CustomerEmail);    }}

属性注入的优缺点

优点
缺点
可选依赖:某些属性可能不是必须的。
隐藏依赖:类的依赖关系不明显。
灵活性高:可以在运行时动态注入。
可能导致空引用异常(属性未注入)。
适合遗留代码改造。
不利于单元测试(需要模拟整个容器)。

建议:尽量使用构造函数注入。只有在确实需要可选依赖或与第三方框架集成时才考虑属性注入。

六、方法注入(Method Injection)

方法注入通过方法参数传入依赖。适用于某些特定场景,如策略模式、工厂方法等。

示例

public interface IPaymentProcessor{    void ProcessPayment(decimal amount);}public class CreditCardProcessor : IPaymentProcessor{    public void ProcessPayment(decimal amount)    {        Console.WriteLine($"使用信用卡支付 {amount:C}");    }}public class PayPalProcessor : IPaymentProcessor{    public void ProcessPayment(decimal amount)    {        Console.WriteLine($"使用 PayPal 支付 {amount:C}");    }}public class OrderService{    // 方法注入:依赖通过参数传入    public void ProcessOrder(Order order, IPaymentProcessor paymentProcessor)    {        Console.WriteLine("订单处理中...");        paymentProcessor.ProcessPayment(order.Amount);    }}

在 Program.cs 中:

builder.Services.AddTransient<IPaymentProcessorCreditCardProcessor>();// 或注册多个实现

调用时:

var orderService = app.Services.GetRequiredService<OrderService>();var paymentProcessor = app.Services.GetRequiredService<IPaymentProcessor>();orderService.ProcessOrder(order, paymentProcessor);

优点

  • 灵活性高,可以根据上下文选择不同的实现。

  • 适合动态决策场景。

缺点

  • 调用方需要知道并获取依赖。

  • 增加了调用复杂度。

七、服务生命周期详解

1. Singleton(单例)

builder.Services.AddSingleton<ILoggerLogger>();
  • 整个应用程序生命周期中只有一个实例。

  • 所有请求共享同一个实例。

  • 注意:避免在单例中持有作用域或瞬态服务的引用,否则会导致“捕获服务”问题。

2. Scoped(作用域)

builder.Services.AddScoped<IOrderRepositoryOrderRepository>();
  • 在每个作用域(如 HTTP 请求)内创建一个实例。

  • 同一请求内的所有服务共享同一个实例。

  • 在非 Web 应用中,需要手动创建作用域。

using (var scope = app.Services.CreateScope()){    var service = scope.ServiceProvider.GetRequiredService<IOrderService>();    // 使用 service}

3. Transient(瞬态)

builder.Services.AddTransient<IValidatorOrderValidator>();
  • 每次请求服务时都创建新实例。

  • 适合无状态、轻量级服务。

生命周期选择建议

  • Singleton:全局配置、缓存、日志记录器。

  • Scoped:数据库上下文(如 DbContext)、用户会话服务。

  • Transient:验证器、转换器、工具类。

八、高级用法

1. 注册多个实现

builder.Services.AddTransient<INotificationServiceEmailNotificationService>();builder.Services.AddTransient<INotificationServiceSmsNotificationService>();

注入所有实现:

public class NotificationService{    private readonly IEnumerable<INotificationService> _notificationServices;    public NotificationService(IEnumerable<INotificationService> notificationServices)    {        _notificationServices = notificationServices;    }    public void NotifyAll(string message)    {        foreach (var service in _notificationServices)        {            service.Send(message);        }    }}

2. 工厂模式注入

builder.Services.AddSingleton<Func<stringINotificationService>>(provider => key =>{    return key switch    {        "email" => provider.GetService<EmailNotificationService>(),        "sms" => provider.GetService<SmsNotificationService>(),        _ => throw new ArgumentException("Unknown notification type")    };});

使用:

public class OrderService{    private readonly Func<string, INotificationService> _notificationFactory;    public OrderService(Func<string, INotificationService> notificationFactory)    {        _notificationFactory = notificationFactory;    }    public void ProcessOrder(Order order)    {        var emailService = _notificationFactory("email");        emailService.Send($"订单 {order.Id} 已处理");    }}

3. 命名注册与 Keyed Services(.NET 8+)

.NET 8 引入了 Keyed Services,支持按名称或键注册服务。

// 注册builder.Services.AddKeyedSingleton<INotificationService, EmailNotificationService>("email");builder.Services.AddKeyedSingleton<INotificationService, SmsNotificationService>("sms");// 注入public class OrderService{    private readonly INotificationService _emailService;    private readonly INotificationService _smsService;    public OrderService(        [FromKeyedServices("email")] INotificationService emailService,        [FromKeyedServices("sms")] INotificationService smsService)    {        _emailService = emailService;        _smsService = smsService;    }}

九、完整 Demo 项目

项目结构
DemoApp/
├── Services/
│   ├── IEmailService.cs
│   ├── EmailService.cs
│   ├── IOrderService.cs
│   └── OrderService.cs
├── Models/
│   └── Order.cs
├── Controllers/
│   └── OrderController.cs
└── Program.cs

代码实现

Models/Order.cs

public class Order{    public int Id { getset; }    public string CustomerName { getset; }    public string CustomerEmail { getset; }    public decimal Amount { getset; }}

Services/IEmailService.cs

public interface IEmailService{    void SendConfirmation(string email, string orderId);}

Services/EmailService.cs

public class EmailService : IEmailService{    private readonly ILogger<EmailService> _logger;    public EmailService(ILogger<EmailService> logger)    {        _logger = logger;    }    public void SendConfirmation(string email, string orderId)    {        _logger.LogInformation($"发送订单确认邮件到 {email},订单号:{orderId}");        // 模拟发送邮件        Console.WriteLine($"邮件已发送至 {email},订单 {orderId}");    }}

Services/IOrderService.cs

public interface IOrderService{    Order CreateOrder(string customerName, string customerEmail, decimal amount);    void ProcessOrder(int orderId);}

Services/OrderService.cs

public class OrderService : IOrderService{    private readonly IEmailService _emailService;    private readonly ILogger<OrderService> _logger;    private static List<Order> _orders = new();    public OrderService(IEmailService emailService, ILogger<OrderService> logger)    {        _emailService = emailService;        _logger = logger;    }    public Order CreateOrder(string customerName, string customerEmail, decimal amount)    {        var order = new Order        {            Id = _orders.Count + 1,            CustomerName = customerName,            CustomerEmail = customerEmail,            Amount = amount        };        _orders.Add(order);        _logger.LogInformation($"创建订单:{order.Id}");        return order;    }    public void ProcessOrder(int orderId)    {        var order = _orders.FirstOrDefault(o => o.Id == orderId);        if (order == null)        {            _logger.LogWarning($"订单 {orderId} 未找到");            return;        }        _logger.LogInformation($"处理订单 {orderId}");        // 模拟处理逻辑        Thread.Sleep(100); // 模拟耗时操作        _emailService.SendConfirmation(order.CustomerEmail, order.Id.ToString());        _logger.LogInformation($"订单 {orderId} 处理完成");    }}

Controllers/OrderController.cs

[ApiController][Route("api/[controller]")]public class OrderController : ControllerBase{    private readonly IOrderService _orderService;    public OrderController(IOrderService orderService)    {        _orderService = orderService;    }    [HttpPost]    public IActionResult CreateOrder([FromBody] CreateOrderRequest request)    {        var order = _orderService.CreateOrder(request.CustomerName, request.CustomerEmail, request.Amount);        return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order);    }    [HttpGet("{id}")]    public IActionResult GetOrder(int id)    {        var order = _orderService.GetOrder(id); // 假设 IOrderService 有此方法        if (order == nullreturn NotFound();        return Ok(order);    }    [HttpPost("{id}/process")]    public IActionResult ProcessOrder(int id)    {        _orderService.ProcessOrder(id);        return Ok(new { message = "订单处理已启动" });    }}public class CreateOrderRequest{    public string CustomerName { getset; }    public string CustomerEmail { getset; }    public decimal Amount { getset; }}

Program.cs

var builder = WebApplication.CreateBuilder(args);// 添加服务builder.Services.AddControllers();builder.Services.AddEndpointsApiExplorer();builder.Services.AddSwaggerGen();// 注册自定义服务builder.Services.AddTransient<IEmailServiceEmailService>();builder.Services.AddScoped<IOrderServiceOrderService>();// 添加日志服务(内置)builder.Services.AddLogging();var app = builder.Build();// 配置中间件if (app.Environment.IsDevelopment()){    app.UseSwagger();    app.UseSwaggerUI();}app.UseHttpsRedirection();app.UseAuthorization();app.MapControllers();app.Run();

运行与测试

  1. 启动应用。

  2. 使用 Swagger 或 Postman 测试:

    • POST /api/order 创建订单。

    • POST /api/order/{id}/process 处理订单,观察控制台输出的邮件发送信息。

十、最佳实践与常见陷阱

最佳实践

  1. 优先使用构造函数注入。

  2. 合理选择服务生命周期。

  3. 接口隔离:定义细粒度接口。

  4. 避免在构造函数中执行耗时操作。

  5. 使用 ILogger<T> 而不是静态日志。

常见陷阱

  1. 服务捕获(Service Capturing):

    • 错误:在 Singleton 中注入 Scoped 服务。

    • 后果:Scoped 服务被提升为 Singleton,导致状态污染。

    • 解决:通过工厂模式或在作用域内解析。

  2. 内存泄漏:

    • 错误:在 Scoped 或 Transient 服务中持有大对象引用。

    • 解决:及时释放资源,使用 IDisposable

  3. 循环依赖:

    • A 依赖 B,B 依赖 A。

    • 解决:重构设计,使用中介者模式或事件总线。

十一、总结

.NET Core 内置的依赖注入容器功能强大且易于使用。通过合理使用构造函数注入、理解服务生命周期、避免常见陷阱,可以构建出高内聚、低耦合、易于测试和维护的应用程序。

虽然属性注入在某些场景下有用,但应谨慎使用,优先考虑构造函数注入。对于复杂需求,可以结合 Autofac 等第三方容器。

依赖注入不仅是技术,更是一种设计思想。掌握它,将使你的 .NET Core 应用更加健壮和灵活。

联系我们采购

采购流程

1、邀请注册账号
联系客户经理提供公司名称或个人姓名及手机号,生成邀请链接,使用此链接注册阿里云会员
联系客户经理二维码
2、关联成为VIP客户
使用收到的邀请链接注册并按提供信息一致营业执照或支付宝完成帐号实名认证,完成帐号注册
注册成为VIP客户二维码
3、阿里云官网下订单
登录阿里云官网下产品订单,开启上您的云之旅,有消费找客户经理要优惠哦
阿里云下单优惠二维码