ASP.NET Core 依赖注入机制详解
在 ASP.NET Core 架构中,依赖注入(DI)被视为核心设计理念,内置框架天然支持这一模式。应用程序不仅能使用框架提供的服务,也能灵活注册自定义服务。内置的 IoC 容器虽然功能精简,但足以满足多数场景,并非设计用来取代 Autofac 等第三方容器。
1. 理解依赖注入核心概念
依赖注入是一项实现对象与其依赖项之间松耦合的技术。核心思想是:类不应在内部直接实例化依赖对象或使用静态引用,而应由外部将所需依赖"注入"进来。最常见的实现方式是通过构造函数声明依赖,这被称为"构造函数注入"。
采用 DI 设计的类,由于没有硬编码依赖的具体实现,耦合度显著降低。这符合"依赖倒置原则"(DIP):高层模块不应依赖低层模块,两者都应依赖于抽象。类在构造时接受抽象(如接口)作为参数,而非具体实现,这正是"策略设计模式"的体现。
负责管理和提供这些依赖的组件称为"容器"(Container),也称为"控制反转容器"(IoC Container)或"DI容器"。容器本质上是一个工厂,负责实例化被请求的类型。如果某类型声明了依赖,且容器已配置好这些依赖的映射关系,容器会在创建该类型实例时,自动解决其依赖链。此外,容器通常还管理着对象实例的生命周期。
ASP.NET Core 提供了一个内建的轻量级容器,默认支持构造函数注入。容器管理的类型被称为"服务"(Services),这些服务的配置通常在 Startup 类的 ConfigureServices 方法中完成。
2. 使用框架内置服务
ConfigureServices 方法的核心任务是注册应用程序运行所需的服务,包括框架自身功能(如 Entity Framework Core、ASP.NET Core MVC)和用户自定义的业务服务。除了 IServiceCollection 提供的基本方法,框架提供了丰富的扩展方法(如 AddDbContext、AddMvc、AddTransient 等)来批量注册服务:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddDbContext<AccessManagementContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),
providerOptions => providerOptions.EnableRetryOnFailure()));
services.AddTransient<ICompanyServices, CompanyServices>();
}
框架功能遵循约定,通常通过单一的 Add[ServiceName] 扩展方法完成所需服务的全部注册。
3. 注册和管理自定义服务
注册自定义服务的语法为 services.AddTransient<IInterface, Implementation>();。第一个泛型参数是服务请求类型(通常是接口),第二个泛型参数是实现类型,由容器负责实例化。
AddTransient 方法将抽象类型映射到具体实现,每次请求该服务时都会创建一个新的实例。正确选择服务的生命周期至关重要,下文将详述。
以下是一个完整的服务注册与使用示例:
1. 接口定义
public interface IAccountServices
{
Task<List<AccountViewModel>> GetList();
}
2. 实现类
public class AccountServices : IAccountServices
{
private readonly AccessManagementContext _context;
public AccountServices(AccessManagementContext context)
{
_context = context; // 构造函数注入 DbContext
}
public async Task<List<Account>> GetList()
{
return await _context.Account.ToListAsync();
}
}
3. 在 Startup 中注册
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddDbContext<AccessManagementContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),
providerOptions => providerOptions.EnableRetryOnFailure()));
services.AddTransient<IAccountServices, AccountServices>();
}
4. 在 Controller 中注入
public class AccountController : Controller
{
private readonly IAccountServices _accountServices;
public AccountController(IAccountServices accountServices)
{
_accountServices = accountServices;
}
public async Task<ActionResult> Index()
{
var viewModels = await _accountServices.GetList();
return View(viewModels);
}
}
4. 服务生命周期与注册选项详解
ASP.NET Core 容器支持三种主要生命周期模式:
- Transient(瞬态):每次请求服务时都会创建一个新的实例。适用于轻量级、无状态的服务。
- Scoped(作用域):在同一个作用域(通常是一个 HTTP 请求)内,所有对服务的请求共享同一个实例。
- Singleton(单例):服务在首次被请求时创建,后续所有请求都使用同一个实例。
除了标准的注册方法,还可以通过工厂模式等方式注册服务。
以下示例演示了三种生命周期的区别:
1. 定义接口
namespace MvcDemo.Interfaces
{
public interface IOperation
{
Guid Id { get; }
}
public interface IOperationTransient : IOperation { }
public interface IOperationScoped : IOperation { }
public interface IOperationSingleton : IOperation { }
}
2. 实现类
public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton
{
public Guid Id { get; } = Guid.NewGuid();
}
3. 注册服务
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
services.AddTransient<OperationService>();
services.AddMvc();
}
4. 定义测试服务类
public class OperationService
{
public IOperationTransient TransientOp { get; }
public IOperationScoped ScopedOp { get; }
public IOperationSingleton SingletonOp { get; }
public OperationService(
IOperationTransient transientOp,
IOperationScoped scopedOp,
IOperationSingleton singletonOp)
{
TransientOp = transientOp;
ScopedOp = scopedOp;
SingletonOp = singletonOp;
}
}
5. 在 Controller 中注入
public class HomeController : Controller
{
private readonly IOperationTransient _transient;
private readonly IOperationScoped _scoped;
private readonly IOperationSingleton _singleton;
private readonly OperationService _service;
public HomeController(
IOperationTransient transient,
IOperationScoped scoped,
IOperationSingleton singleton,
OperationService service)
{
_transient = transient;
_scoped = scoped;
_singleton = singleton;
_service = service;
}
public IActionResult Index()
{
ViewBag.Transient = _transient.Id;
ViewBag.Scoped = _scoped.Id;
ViewBag.Singleton = _singleton.Id;
ViewBag.ServiceTransient = _service.TransientOp.Id;
ViewBag.ServiceScoped = _service.ScopedOp.Id;
ViewBag.ServiceSingleton = _service.SingletonOp.Id;
return View();
}
}
6. 视图展示
<h2>Controller 中的服务实例 ID</h2>
<p>Transient: @ViewBag.Transient</p>
<p>Scoped: @ViewBag.Scoped</p>
<p>Singleton: @ViewBag.Singleton</p>
<h2>OperationService 中的服务实例 ID</h2>
<p>Transient: @ViewBag.ServiceTransient</p>
<p>Scoped: @ViewBag.ServiceScoped</p>
<p>Singleton: @ViewBag.ServiceSingleton</p>
7. 运行结果分析
刷新页面时,Singleton ID 始终不变;Scoped ID 在同一请求内的 Controller 和 Service 中一致,但不同请求间变化;Transient ID 则在每次注入时都不同。
5. 请求服务与上下文
在 ASP.NET Core 请求管道中,通过 HttpContext.RequestServices 可以访问当前请求范围内的服务集合。这使得框架能够在处理请求时,正确地从配置的服务容器中解析所需依赖。
6. 设计符合 DI 原则的服务
在开发自定义服务时,应避免静态方法和直接 new 出依赖的行为。依赖应通过构造函数注入方式请求。如果一个构造函数需要注入过多的依赖,通常意味着该类承担了过多的职责(违反单一职责原则),应考虑拆分或重构。
Controller 应专注于 UI 逻辑,而业务逻辑、数据访问等应委托给独立的服务类处理。
7. 替换为 Autofac 容器
当内置容器无法满足高级需求时,可以将其替换为 Autofac 等第三方 IoC 容器,以获得更丰富的功能支持。