依赖注入的核心概念
依赖注入(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
语句包裹。