依赖注入的核心概念
依赖注入(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):
-
安装 Autofac 包:
Install-Package Autofac.Mvc5
-
在
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
)。
关键区别
- 配置入口:.NET Core 在
Program.cs
,.NET Framework 在Global.asax
。 - 容器依赖:.NET Core 内置 DI 容器,.NET Framework 需引入第三方库。
- 控制器注册:.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>
注入服务到过滤器
过滤器默认不支持构造函数注入,需通过TypeFilter
或ServiceFilter
实现依赖注入。
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
语句包裹。