MVC 依赖注入(DI)与服务全解析(附避坑实战)

发布于:2025-09-12 ⋅ 阅读:(20) ⋅ 点赞:(0)

依赖注入的核心概念

依赖注入(DI)是一种设计模式,通过将对象的依赖关系从内部创建转移到外部传递,实现解耦。在 MVC 框架中,DI 容器负责管理对象的生命周期和依赖关系,开发者只需声明依赖,容器自动完成注入。

生命周期配置

不同框架的生命周期命名可能不同,但核心分为三类:

  • 瞬态(Transient):每次请求都创建新实例,适合无状态的轻量级服务。
  • 作用域(Scoped):同一请求内共享实例,常见于 HTTP 上下文相关的服务(如数据库连接)。
  • 单例(Singleton):全局共享一个实例,适用于耗时资源(如配置中心)。

错误示例:将数据库上下文误注册为单例,导致多用户数据混乱。

// 错误:DbContext 应使用 Scoped 而非 Singleton
services.AddSingleton<AppDbContext>();

注入方式对比

  • 构造函数注入:强类型,显式声明依赖,推荐作为首选。
  • 属性注入:灵活性高,但可能隐藏依赖关系,需谨慎使用。
  • 方法注入:适用于临时依赖,常见于工厂模式。

推荐代码示例:

public class OrderService
{
    private readonly IPaymentGateway _gateway;
    // 构造函数注入
    public OrderService(IPaymentGateway gateway)
    {
        _gateway = gateway;
    }
}

常见问题与解决方案

循环依赖:A 依赖 B,B 又依赖 A。可通过提取公共逻辑到第三类或改用方法注入解决。
过度注入:构造函数参数过多(如超过 5 个),需拆分职责或引入聚合服务。

测试中的应用

通过模拟依赖项(Mock)实现单元测试隔离。例如使用 Moq 框架:

var mockGateway = new Mock<IPaymentGateway>();
mockGateway.Setup(g => g.Process(It.IsAny<decimal>())).Returns(true);
var service = new OrderService(mockGateway.Object);

框架差异示例

  • ASP.NET Core:内置 DI 容器,通过 IServiceCollection 配置。
  • Spring Boot:使用 @Autowired 注解实现注入。
  • Laravel:通过服务容器绑定依赖,支持自动解析。

最佳实践

  • 优先选择构造函数注入,明确依赖关系。
  • 根据业务需求严格匹配生命周期,避免跨请求状态污染。
  • 定期检查容器配置,移除未使用的服务以减少开销。

通过合理使用 DI,可显著提升代码的可维护性和可测试性,减少模块间的耦合度。### 依赖注入在 .NET Framework 与 .NET Core 中的配置差异

基础准备:定义服务接口与实现

定义服务接口与实现类,确保接口与实现分离,便于解耦和测试。以下是一个示例:

// 服务接口(定义契约)
public interface IProductService
{
    List<Product> GetHotProducts(int count);
    Product GetById(int id);
}

// 服务实现(具体逻辑)
public class ProductService : IProductService
{
    private readonly AppDbContext _dbContext;

    // 构造函数注入依赖(DbContext 也是服务)
    public ProductService(AppDbContext dbContext)
    {
        _dbContext = dbContext;
    }

    public List<Product> GetHotProducts(int count)
    {
        return _dbContext.Products
            .Where(p => p.IsHot && p.IsActive)
            .Take(count)
            .ToList();
    }

    public Product GetById(int id)
    {
        return _dbContext.Products.Find(id);
    }
}
.NET Core/.NET 5+ 配置

.NET Core 内置 DI 容器,配置入口在 Program.cs 文件中,通过 IServiceCollection 注册服务:

var builder = WebApplication.CreateBuilder(args);

// 添加 MVC 控制器与视图支持
builder.Services.AddControllersWithViews();

// 注册数据库上下文(Scoped 生命周期)
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// 注册自定义服务(推荐接口+实现)
builder.Services.AddScoped<IProductService, ProductService>();

// 直接注册实现类(无接口时用)
builder.Services.AddTransient<LogService>();

// 注册配置类(从 appsettings.json 绑定)
builder.Services.Configure<AppSettings>(
    builder.Configuration.GetSection("AppSettings"));

var app = builder.Build();
// ... 中间件配置 ...
app.Run();
.NET Framework 配置(使用 Autofac)

.NET Framework 原生 DI 功能较弱,需借助第三方容器(如 Autofac):

  1. 安装 Autofac 包:

    Install-Package Autofac.Mvc5
    
  2. Global.asax 中配置:

    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            // 初始化 Autofac 容器
            var builder = new ContainerBuilder();
    
            // 注册控制器(Autofac 需显式注册控制器)
            builder.RegisterControllers(typeof(MvcApplication).Assembly);
    
            // 注册数据库上下文(InstancePerRequest 对应 .NET Core 的 Scoped)
            builder.RegisterType<AppDbContext>()
                .InstancePerRequest();
    
            // 注册自定义服务
            builder.RegisterType<ProductService>()
                .As<IProductService>()
                .InstancePerRequest();
    
            // 设置 MVC 的依赖解析器
            var container = builder.Build();
            DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
    
            // 其他 MVC 初始化
            AreaRegistration.RegisterAllAreas();
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        }
    }
    
生命周期对比
  • .NET Core:提供 Transient(每次请求新建)、Scoped(每次请求单例)、Singleton(全局单例)三种生命周期。
  • Autofac:提供 InstancePerRequest(类似 Scoped)、InstancePerDependency(类似 Transient)、SingleInstance(类似 Singleton)。
关键区别
  1. 配置入口:.NET Core 在 Program.cs,.NET Framework 在 Global.asax
  2. 容器依赖:.NET Core 内置 DI 容器,.NET Framework 需引入第三方库。
  3. 控制器注册:.NET Core 自动注册控制器,Autofac 需显式注册。

注入服务到控制器

在控制器中使用构造函数注入是最推荐的方式。通过私有只读字段存储服务实例,确保依赖项通过构造函数传入,避免手动实例化服务。

public class ProductsController : Controller
{
    private readonly IProductService _productService;
    private readonly IOptions<AppSettings> _appSettings;

    public ProductsController(
        IProductService productService,
        IOptions<AppSettings> appSettings)
    {
        _productService = productService;
        _appSettings = appSettings;
    }

    public ActionResult HotProducts()
    {
        int hotCount = _appSettings.Value.HotProductCount;
        var hotProducts = _productService.GetHotProducts(hotCount);
        return View(hotProducts);
    }
}

注入服务到视图

视图中的服务注入适用于简单场景,避免使视图逻辑过于复杂。使用@inject指令声明服务,直接在视图中使用。

@model List<Product>
@inject IProductService ProductService
@inject IOptions<AppSettings> AppSettings

<h3>热门商品(共 @AppSettings.Value.HotProductCount 个)</h3>
<ul>
    @foreach (var product in Model)
    {
        <li>@product.Name - ¥@product.Price</li>
    }
</ul>

<p>本月热销:@ProductService.GetHotProducts(1).FirstOrDefault()?.Name</p>

注入服务到过滤器

过滤器默认不支持构造函数注入,需通过TypeFilterServiceFilter实现依赖注入。

public class LogFilter : IActionFilter
{
    private readonly LogService _logService;

    public LogFilter(LogService logService)
    {
        _logService = logService;
    }

    public void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var controller = filterContext.Controller.ToString();
        var action = filterContext.ActionDescriptor.ActionName;
        _logService.WriteLog($"请求:{controller}/{action}");
    }

    public void OnActionExecuted(ActionExecutedContext filterContext) { }
}

在控制器或方法上使用TypeFilter

[TypeFilter(typeof(LogFilter))]
public class ProductsController : Controller
{
    // 控制器逻辑
}

全局注册过滤器:

builder.Services.AddControllersWithViews(options =>
{
    options.Filters.Add<TypeFilter<LogFilter>>();
});

Singleton、Scoped、Transient 核心区别

Singleton
实例在第一次请求时创建,整个应用生命周期内保持唯一。适用于无状态服务,如全局配置、工具类。

builder.Services.AddSingleton<IMailService, MailService>();

Scoped
每个请求范围内创建一个实例,同一请求内多次注入共享同一实例。适用于有状态服务,如数据库上下文(DbContext)、用户会话。

builder.Services.AddScoped<IProductService, ProductService>();

Transient
每次注入或获取服务时都创建新实例。适用于轻量级、无状态服务,如日志记录器、验证器。

builder.Services.AddTransient<ILoginValidator, LoginValidator>();

常见错误案例与解决方案

错误1:Singleton 依赖 Scoped 服务
问题:Singleton 长期持有 Scoped 服务(如 DbContext),导致内存泄漏和数据不一致。
错误代码示例:

public class SingletonService : ISingletonService
{
    private readonly AppDbContext _dbContext;
    public SingletonService(AppDbContext dbContext) => _dbContext = dbContext;
    public void DoWork() => var data = _dbContext.Products.ToList();
}
// ❌ 错误注册
builder.Services.AddSingleton<ISingletonService, SingletonService>();
builder.Services.AddScoped<AppDbContext>();

解决方案
通过 IServiceScopeFactory 创建临时作用域:

public class SingletonService : ISingletonService
{
    private readonly IServiceScopeFactory _scopeFactory;
    public SingletonService(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory;

    public void DoWork()
    {
        using (var scope = _scopeFactory.CreateScope())
        {
            var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            var data = dbContext.Products.ToList(); // 正确释放
        }
    }
}

错误2:Transient 用于有状态服务
问题:每次注入生成新实例,导致状态丢失(如购物车数据)。
错误代码示例:

public class CartService : ICartService
{
    public List<CartItem> Items { get; set; } = new();
    public void AddItem(CartItem item) => Items.Add(item);
}
// ❌ 错误注册
builder.Services.AddTransient<ICartService, CartService>();

// 控制器中状态丢失
_cartService1.AddItem(new CartItem { Id = 1 });
var count = _cartService2.Items.Count; // 结果为 0

解决方案
改用 Scoped 生命周期:

builder.Services.AddScoped<ICartService, CartService>();

生命周期选择原则

  • 无状态且全局共享:Singleton
  • 请求内有状态或需隔离:Scoped
  • 短暂、无状态且轻量:Transient

避免跨生命周期依赖(如 Singleton 直接依赖 Scoped),优先通过工厂模式或作用域隔离解决。

自定义缓存过滤器的实现步骤

定义缓存服务接口与实现
public interface ICacheService
{
    T Get<T>(string key);
    void Set<T>(string key, T value, TimeSpan expiration);
    void Remove(string key);
}

public class MemoryCacheService : ICacheService
{
    private readonly IMemoryCache _memoryCache;

    public MemoryCacheService(IMemoryCache memoryCache)
    {
        _memoryCache = memoryCache;
    }

    public T Get<T>(string key) => _memoryCache.TryGetValue(key, out T value) ? value : default;
    public void Set<T>(string key, T value, TimeSpan expiration) => _memoryCache.Set(key, value, expiration);
    public void Remove(string key) => _memoryCache.Remove(key);
}
创建自定义缓存过滤器
public class CustomCacheFilter : IActionFilter
{
    private readonly ICacheService _cacheService;
    private readonly string _cacheKey;
    private readonly int _expirationMinutes;

    public CustomCacheFilter(ICacheService cacheService, string cacheKey, int expirationMinutes)
    {
        _cacheService = cacheService;
        _cacheKey = cacheKey;
        _expirationMinutes = expirationMinutes;
    }

    public void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var cacheData = _cacheService.Get<object>(_cacheKey);
        if (cacheData != null)
        {
            filterContext.Result = new ViewResult { ViewData = (ViewDataDictionary)cacheData };
        }
    }

    public void OnActionExecuted(ActionExecutedContext filterContext)
    {
        if (filterContext.Result is ViewResult viewResult && 
            _cacheService.Get<object>(_cacheKey) == null)
        {
            _cacheService.Set(_cacheKey, viewResult.ViewData, TimeSpan.FromMinutes(_expirationMinutes));
        }
    }
}
服务注册与配置
builder.Services.AddMemoryCache();
builder.Services.AddScoped<ICacheService, MemoryCacheService>();
在控制器中使用过滤器
[TypeFilter(typeof(CustomCacheFilter), Arguments = new object[] { "HotProductsCache", 10 })]
public ActionResult HotProducts()
{
    var hotProducts = _productService.GetHotProducts(8);
    return View(hotProducts);
}

关键注意事项

  • 过滤器构造函数注入的服务需通过DI容器注册
  • TypeFilter用于传递运行时参数(如cacheKey
  • OnActionExecuting中若命中缓存会直接短路请求
  • OnActionExecuted仅在首次未命中缓存时执行存储

坑 1:手动 new 服务实例(绕过 DI 容器,依赖无法注入)

在 ASP.NET Core 中,依赖注入(DI)是核心机制,手动通过 new 创建服务实例会导致依赖链断裂。例如 ProductService 需要 DbContext,但手动实例化时无法自动注入依赖,导致编译错误或运行时异常。

正确做法:始终通过构造函数注入服务,禁止手动 new

public class ProductsController : Controller
{
    private readonly IProductService _productService;

    public ProductsController(IProductService productService)
    {
        _productService = productService;
    }

    public ActionResult Index()
    {
        var products = _productService.GetHotProducts(8);
        return View(products);
    }
}

坑 2:控制器构造函数参数过多(“构造函数爆炸”)

当控制器依赖过多服务时,构造函数会变得冗长且难以维护。例如 OrderController 依赖 5 个服务,导致代码臃肿。

解决方案:使用聚合服务封装相关依赖

// 定义聚合服务
public class OrderAggregateService
{
    public IOrderService OrderService { get; }
    public IProductService ProductService { get; }
    public ICartService CartService { get; }

    public OrderAggregateService(
        IOrderService orderService,
        IProductService productService,
        ICartService cartService)
    {
        OrderService = orderService;
        ProductService = productService;
        CartService = cartService;
    }
}

// 注册聚合服务
services.AddScoped<OrderAggregateService>();

// 简化后的控制器
public class OrderController : Controller
{
    private readonly OrderAggregateService _aggregateService;
    private readonly IUserService _userService;

    public OrderController(
        OrderAggregateService aggregateService,
        IUserService userService)
    {
        _aggregateService = aggregateService;
        _userService = userService;
    }

    public ActionResult Create()
    {
        var products = _aggregateService.ProductService.GetAll();
        // 其他逻辑
    }
}

坑 3:循环依赖问题

当服务 A 依赖服务 B,同时服务 B 又依赖服务 A 时,会导致 DI 容器无法解析。

解决方案

  • 重构设计,通过引入第三个服务(如中介者模式)解耦循环依赖。
  • 必要时使用 IServiceProvider.GetRequiredService 延迟解析(需谨慎)。

坑 4:未正确管理服务生命周期

误用 Singleton 生命周期注册需要请求作用域的服务(如 DbContext),会导致内存泄漏或数据污染。

生命周期选择指南

  • Transient:每次请求创建新实例(轻量级无状态服务)。
  • Scoped:同一请求内共享实例(如 DbContext)。
  • Singleton:全局单例(配置类服务)。

坑 5:过度依赖 DI 容器

在非 DI 管理的类(如静态类或实体类)中强行使用 DI,会导致设计混乱。

解决方案

  • 遵循“构造函数注入”原则,避免在非 DI 上下文中解析服务。
  • 对于需要服务的实体类,可采用“领域事件”模式解耦。

坑 6:忽略 IDisposable 服务的释放

未正确处理实现了 IDisposable 的服务(如文件流、数据库连接),可能导致资源泄漏。

正确做法

  • DI 容器会自动释放 Scoped/Transient 服务的 IDisposable 实例。
  • 手动创建的 IDisposable 对象需使用 using 语句包裹。

网站公告

今日签到

点亮在社区的每一天
去签到