ASP.NET Core 依赖注入
目录
作者:Kirk Larkin、Steve Smith?和?Brandon Dahler
ASP.NET Core 支持依赖关系注入 (DI) 软件设计模式,这是一种在类及其依赖关系之间实现控制反转 (IoC)?的技术。
有关特定于 MVC 控制器中依赖关系注入的详细信息,请参阅在 ASP.NET Core 中将依赖关系注入控制器。
若要了解如何在 Web 应用以外的应用程序中使用依赖关系注入,请参阅?.NET 中的依赖关系注入。
有关选项的依赖关系注入的详细信息,请参阅?ASP.NET Core 中的选项模式。
本主题介绍 ASP.NET Core 中的依赖关系注入。 有关使用依赖关系注入的主要文档包含在?.NET 中的依赖关系注入。
依赖关系注入概述
依赖项是指另一个对象所依赖的对象。 使用其他类所依赖的?WriteMessage
?方法检查以下?MyDependency
?类:
public class MyDependency
{
public void WriteMessage(string message)
{
Console.WriteLine($"MyDependency.WriteMessage called. Message: {message}");
}
}
类可以创建?MyDependency
?类的实例,以便利用其?WriteMessage
?方法。 在以下示例中,MyDependency
?类是?IndexModel
?类的依赖项:
public class IndexModel : PageModel
{
private readonly MyDependency _dependency = new MyDependency();
public void OnGet()
{
_dependency.WriteMessage("IndexModel.OnGet");
}
}
该类创建并直接依赖于?MyDependency
?类。 代码依赖项(如前面的示例)会产生问题,应避免使用,原因如下:
- 要用不同的实现替换?
MyDependency
,必须修改?IndexModel
?类。 - 如果?
MyDependency
?具有依赖项,则必须由?IndexModel
?类对其进行配置。 在具有多个依赖于?MyDependency
?的类的大型项目中,配置代码将分散在整个应用中。 - 这种实现很难进行单元测试。
依赖关系注入通过以下方式解决了这些问题:
- 使用接口或基类将依赖关系实现抽象化。
- 在服务容器中注册依赖关系。 ASP.NET Core 提供了一个内置的服务容器?IServiceProvider。 服务通常已在应用的?
Program.cs
?文件中注册。 - 将服务注入到使用它的类的构造函数中。 框架负责创建依赖关系的实例,并在不再需要时将其释放。
在示例应用中,IMyDependency
?接口定义?WriteMessage
?方法:
public interface IMyDependency
{
void WriteMessage(string message);
}
此接口由具体类型?MyDependency
?实现:
public class MyDependency : IMyDependency
{
public void WriteMessage(string message)
{
Console.WriteLine($"MyDependency.WriteMessage Message: {message}");
}
}
示例应用使用具体类型?MyDependency
?注册?IMyDependency
?服务。?AddScoped?方法使用范围内生存期(单个请求的生存期)注册服务。 本主题后面将介绍服务生存期。
using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddScoped<IMyDependency, MyDependency>();
var app = builder.Build();
在示例应用中,请求?IMyDependency
?服务并用于调用?WriteMessage
?方法:
public class Index2Model : PageModel
{
private readonly IMyDependency _myDependency;
public Index2Model(IMyDependency myDependency)
{
_myDependency = myDependency;
}
public void OnGet()
{
_myDependency.WriteMessage("Index2Model.OnGet");
}
}
通过使用 DI 模式,控制器或 Razor 页面:
- 不使用具体类型?
MyDependency
,仅使用它实现的?IMyDependency
?接口。 这样可以轻松地更改实现,而无需修改控制器或 Razor 页面。 - 不创建?
MyDependency
?的实例,这由 DI 容器创建。
可以通过使用内置日志记录 API 来改善?IMyDependency
?接口的实现:
public class MyDependency2 : IMyDependency
{
private readonly ILogger<MyDependency2> _logger;
public MyDependency2(ILogger<MyDependency2> logger)
{
_logger = logger;
}
public void WriteMessage(string message)
{
_logger.LogInformation( $"MyDependency2.WriteMessage Message: {message}");
}
}
更新的?Program.cs
?会注册新的?IMyDependency
?实现:
using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddScoped<IMyDependency, MyDependency2>();
var app = builder.Build();
MyDependency2
?依赖于?ILogger<TCategoryName>,并在构造函数中对其进行请求。?ILogger<TCategoryName>
?是ILogger<TCategoryName>
。
以链式方式使用依赖关系注入并不罕见。 每个请求的依赖关系相应地请求其自己的依赖关系。 容器解析图中的依赖关系并返回完全解析的服务。 必须被解析的依赖关系的集合通常被称为“依赖关系树”、“依赖关系图”或“对象图”。
容器通过利用(泛型)开放类型解析?ILogger<TCategoryName>
,而无需注册每个(泛型)构造类型。
在依赖项注入术语中,服务:
- 通常是向其他对象提供服务的对象,如?
IMyDependency
?服务。 - 与 Web 服务无关,尽管服务可能使用 Web 服务。
框架提供可靠的日志记录系统。 编写上述示例中的?IMyDependency
?实现来演示基本的 DI,而不是来实现日志记录。 大多数应用都不需要编写记录器。 下面的代码演示如何使用默认日志记录,这不需要注册任何服务:
public class AboutModel : PageModel
{
private readonly ILogger _logger;
public AboutModel(ILogger<AboutModel> logger)
{
_logger = logger;
}
public string Message { get; set; } = string.Empty;
public void OnGet()
{
Message = $"About page visited at {DateTime.UtcNow.ToLongTimeString()}";
_logger.LogInformation(Message);
}
}
使用前面的代码时,无需更新?Program.cs
,因为框架提供Program.cs
。
使用扩展方法注册服务组
ASP.NET Core 框架使用一种约定来注册一组相关服务。 约定使用单个?Add{GROUP_NAME}
?扩展方法来注册该框架功能所需的所有服务。 例如,AddControllers?扩展方法会注册 MVC 控制器所需的服务。
下面的代码通过个人用户帐户由 Razor 页面模板生成,并演示如何使用扩展方法?AddDbContext?和?AddDefaultIdentity?将其他服务添加到容器中:
using DependencyInjectionSample.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.AddRazorPages();
var app = builder.Build();
考虑下面的方法,该方法可注册服务并配置选项:
using ConfigSample.Options;
using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.Configure<PositionOptions>(
builder.Configuration.GetSection(PositionOptions.Position));
builder.Services.Configure<ColorOptions>(
builder.Configuration.GetSection(ColorOptions.Color));
builder.Services.AddScoped<IMyDependency, MyDependency>();
builder.Services.AddScoped<IMyDependency2, MyDependency2>();
var app = builder.Build();
可以将相关的注册组移动到扩展方法以注册服务。 例如,配置服务会被添加到以下类中:
using ConfigSample.Options;
using Microsoft.Extensions.Configuration;
namespace Microsoft.Extensions.DependencyInjection
{
public static class MyConfigServiceCollectionExtensions
{
public static IServiceCollection AddConfig(
this IServiceCollection services, IConfiguration config)
{
services.Configure<PositionOptions>(
config.GetSection(PositionOptions.Position));
services.Configure<ColorOptions>(
config.GetSection(ColorOptions.Color));
return services;
}
public static IServiceCollection AddMyDependencyGroup(
this IServiceCollection services)
{
services.AddScoped<IMyDependency, MyDependency>();
services.AddScoped<IMyDependency2, MyDependency2>();
return services;
}
}
}
剩余的服务会在类似的类中注册。 下面的代码使用新扩展方法来注册服务:
using Microsoft.Extensions.DependencyInjection.ConfigSample.Options;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddConfig(builder.Configuration)
.AddMyDependencyGroup();
builder.Services.AddRazorPages();
var app = builder.Build();
注意:每个?services.Add{GROUP_NAME}
?扩展方法添加并可能配置服务。 例如,AddControllersWithViews?会添加带视图的 MVC 控制器所需的服务,AddRazorPages?会添加 Razor Pages 所需的服务。
服务生存期
请参阅?.NET 中的依赖关系注入中的服务生存期
要在中间件中使用范围内服务,请使用以下方法之一:
- 将服务注入中间件的?
Invoke
?或?InvokeAsync
?方法。 使用构造函数注入会引发运行时异常,因为它强制使范围内服务的行为与单一实例类似。?生存期和注册选项部分中的示例演示了?InvokeAsync
?方法。 - 使用基于工厂的中间件。 使用此方法注册的中间件按客户端请求(连接)激活,这也使范围内服务可注入中间件的构造函数。
有关详细信息,请参阅编写自定义 ASP.NET Core 中间件。
服务注册方法
请参阅?.NET 中的依赖关系注入中的服务注册方法
在为测试模拟类型时,使用多个实现很常见。
仅使用实现类型注册服务等效于使用相同的实现和服务类型注册该服务。 因此,我们不能使用捕获显式服务类型的方法来注册服务的多个实现。 这些方法可以注册服务的多个实例,但它们都具有相同的实现类型 。
上述任何服务注册方法都可用于注册同一服务类型的多个服务实例。 下面的示例以?IMyDependency
?作为服务类型调用?AddSingleton
?两次。 第二次对?AddSingleton
?的调用在解析为?IMyDependency
?时替代上一次调用,在通过?IEnumerable<IMyDependency>
?解析多个服务时添加到上一次调用。 通过?IEnumerable<{SERVICE}>
?解析服务时,服务按其注册顺序显示。
services.AddSingleton<IMyDependency, MyDependency>();
services.AddSingleton<IMyDependency, DifferentDependency>();
public class MyService
{
public MyService(IMyDependency myDependency,
IEnumerable<IMyDependency> myDependencies)
{
Trace.Assert(myDependency is DifferentDependency);
var dependencyArray = myDependencies.ToArray();
Trace.Assert(dependencyArray[0] is MyDependency);
Trace.Assert(dependencyArray[1] is DifferentDependency);
}
}
键控服务
密钥服务是指使用密钥注册和检索依赖项注入 (DI) 服务的机制。 通过调用?AddKeyedSingleton?(或?AddKeyedScoped
?或?AddKeyedTransient
)来注册服务,与密钥相关联。 使用?[FromKeyedServices]?属性指定密钥来访问已注册的服务。 以下代码演示如何使用键化服务:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");
builder.Services.AddControllers();
var app = builder.Build();
app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));
app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) =>
smallCache.Get("date"));
app.MapControllers();
app.Run();
public interface ICache
{
object Get(string key);
}
public class BigCache : ICache
{
public object Get(string key) => $"Resolving {key} from big cache.";
}
public class SmallCache : ICache
{
public object Get(string key) => $"Resolving {key} from small cache.";
}
[ApiController]
[Route("/cache")]
public class CustomServicesApiController : Controller
{
[HttpGet("big-cache")]
public ActionResult<object> GetOk([FromKeyedServices("big")] ICache cache)
{
return cache.Get("data-mvc");
}
}
public class MyHub : Hub
{
public void Method([FromKeyedServices("small")] ICache cache)
{
Console.WriteLine(cache.Get("signalr"));
}
}
构造函数注入行为
请参阅?.NET 中的依赖关系注入中的构造函数注入行为
实体框架上下文
默认情况下,使用设置了范围的生存期将实体框架上下文添加到服务容器中,因为 Web 应用数据库操作通常将范围设置为客户端请求。 要使用其他生存期,请使用?AddDbContext?重载来指定生存期。 给定生存期的服务不应使用生存期比服务生存期短的数据库上下文。
生存期和注册选项
为了演示服务生存期和注册选项之间的差异,请考虑以下接口,将任务表示为具有标识符?OperationId
?的操作。 根据为以下接口配置操作服务的生存期的方式,容器在类请求时提供相同或不同的服务实例:
public interface IOperation
{
string OperationId { get; }
}
public interface IOperationTransient : IOperation { }
public interface IOperationScoped : IOperation { }
public interface IOperationSingleton : IOperation { }
以下?Operation
?类实现了前面的所有接口。?Operation
?构造函数生成 GUID,并将最后 4 个字符存储在?OperationId
?属性中
public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton
{
public Operation()
{
OperationId = Guid.NewGuid().ToString()[^4..];
}
public string OperationId { get; }
}
以下代码根据命名生存期创建?Operation
?类的多个注册:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddTransient<IOperationTransient, Operation>();
builder.Services.AddScoped<IOperationScoped, Operation>();
builder.Services.AddSingleton<IOperationSingleton, Operation>();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseMyMiddleware();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
示例应用一并演示了请求中和请求之间的对象生存期。?IndexModel
?和中间件请求每种?IOperation
?类型,并记录各自的?OperationId
:
public class IndexModel : PageModel
{
private readonly ILogger _logger;
private readonly IOperationTransient _transientOperation;
private readonly IOperationSingleton _singletonOperation;
private readonly IOperationScoped _scopedOperation;
public IndexModel(ILogger<IndexModel> logger,
IOperationTransient transientOperation,
IOperationScoped scopedOperation,
IOperationSingleton singletonOperation)
{
_logger = logger;
_transientOperation = transientOperation;
_scopedOperation = scopedOperation;
_singletonOperation = singletonOperation;
}
public void OnGet()
{
_logger.LogInformation("Transient: " + _transientOperation.OperationId);
_logger.LogInformation("Scoped: " + _scopedOperation.OperationId);
_logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
}
}
与?IndexModel
?类似,中间件会解析相同的服务:
public class MyMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
private readonly IOperationSingleton _singletonOperation;
public MyMiddleware(RequestDelegate next, ILogger<MyMiddleware> logger,
IOperationSingleton singletonOperation)
{
_logger = logger;
_singletonOperation = singletonOperation;
_next = next;
}
public async Task InvokeAsync(HttpContext context,
IOperationTransient transientOperation, IOperationScoped scopedOperation)
{
_logger.LogInformation("Transient: " + transientOperation.OperationId);
_logger.LogInformation("Scoped: " + scopedOperation.OperationId);
_logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
await _next(context);
}
}
public static class MyMiddlewareExtensions
{
public static IApplicationBuilder UseMyMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<MyMiddleware>();
}
}
范围内服务和暂时性服务必须在?InvokeAsync
?方法中进行解析:
public async Task InvokeAsync(HttpContext context,
IOperationTransient transientOperation, IOperationScoped scopedOperation)
{
_logger.LogInformation("Transient: " + transientOperation.OperationId);
_logger.LogInformation("Scoped: " + scopedOperation.OperationId);
_logger.LogInformation("Singleton: " + _singletonOperation.OperationId);
await _next(context);
}
记录器输出显示:
- 暂时性对象始终不同。?
IndexModel
?和中间件中的临时?OperationId
?值不同。 - 范围内对象对给定请求而言是相同的,但在每个新请求之间不同。
- 单一实例对象对于每个请求是相同的。
若要减少日志记录输出,请在?appsettings.Development.json
?文件中设置“Logging:LogLevel:Microsoft:Error”:
{
"MyKey": "MyKey from appsettings.Developement.json",
"Logging": {
"LogLevel": {
"Default": "Information",
"System": "Debug",
"Microsoft": "Error"
}
}
}
在应用启动时解析服务
以下代码显示如何在应用启动时限时解析范围内服务:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IMyDependency, MyDependency>();
var app = builder.Build();
using (var serviceScope = app.Services.CreateScope())
{
var services = serviceScope.ServiceProvider;
var myDependency = services.GetRequiredService<IMyDependency>();
myDependency.WriteMessage("Call services from main");
}
app.MapGet("/", () => "Hello World!");
app.Run();
作用域验证
请参阅?.NET 中的依赖关系注入中的构造函数注入行为
有关详细信息,请参阅作用域验证。
请求服务
ASP.NET Core 请求中的服务及其依赖项是通过?HttpContext.RequestServices?公开的。
框架为每个请求创建一个范围,RequestServices
?公开限定范围的服务提供程序。 只要请求处于活动状态,所有作用域服务都有效。
设计能够进行依赖关系注入的服务
在设计能够进行依赖注入的服务时:
- 避免有状态的、静态类和成员。 通过将应用设计为改用单一实例服务,避免创建全局状态。
- 避免在服务中直接实例化依赖类。 直接实例化会将代码耦合到特定实现。
- 不在服务中包含过多内容,确保设计规范,并易于测试。
如果一个类有过多注入依赖项,这可能表明该类拥有过多的责任并且违反了单一责任原则 (SRP)。 尝试通过将某些职责移动到一个新类来重构类。 请记住,Razor Pages 页面模型类和 MVC 控制器类应关注用户界面问题。
服务释放
容器为其创建的?IDisposable?类型调用?Dispose。 从容器中解析的服务绝对不应由开发人员释放。 如果类型或工厂注册为单一实例,则容器自动释放单一实例。
在下面的示例中,服务由服务容器创建,并自动释放:dependency-injection\samples\6.x\DIsample2\DIsample2\Services\Service1.cs
public class Service1 : IDisposable
{
private bool _disposed;
public void Write(string message)
{
Console.WriteLine($"Service1: {message}");
}
public void Dispose()
{
if (_disposed)
return;
Console.WriteLine("Service1.Dispose");
_disposed = true;
}
}
public class Service2 : IDisposable
{
private bool _disposed;
public void Write(string message)
{
Console.WriteLine($"Service2: {message}");
}
public void Dispose()
{
if (_disposed)
return;
Console.WriteLine("Service2.Dispose");
_disposed = true;
}
}
public interface IService3
{
public void Write(string message);
}
public class Service3 : IService3, IDisposable
{
private bool _disposed;
public Service3(string myKey)
{
MyKey = myKey;
}
public string MyKey { get; }
public void Write(string message)
{
Console.WriteLine($"Service3: {message}, MyKey = {MyKey}");
}
public void Dispose()
{
if (_disposed)
return;
Console.WriteLine("Service3.Dispose");
_disposed = true;
}
}
using DIsample2.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddScoped<Service1>();
builder.Services.AddSingleton<Service2>();
var myKey = builder.Configuration["MyKey"];
builder.Services.AddSingleton<IService3>(sp => new Service3(myKey));
var app = builder.Build();
public class IndexModel : PageModel
{
private readonly Service1 _service1;
private readonly Service2 _service2;
private readonly IService3 _service3;
public IndexModel(Service1 service1, Service2 service2, IService3 service3)
{
_service1 = service1;
_service2 = service2;
_service3 = service3;
}
public void OnGet()
{
_service1.Write("IndexModel.OnGet");
_service2.Write("IndexModel.OnGet");
_service3.Write("IndexModel.OnGet");
}
}
每次刷新索引页后,调试控制台显示以下输出:
Service1: IndexModel.OnGet
Service2: IndexModel.OnGet
Service3: IndexModel.OnGet, MyKey = MyKey from appsettings.Developement.json
Service1.Dispose
不由服务容器创建的服务
考虑下列代码:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddSingleton(new Service1());
builder.Services.AddSingleton(new Service2());
在上述代码中:
- 服务实例不是由服务容器创建的。
- 框架不会自动释放服务。
- 开发人员负责释放服务。
暂时和共享实例的 IDisposable 指南
请参阅?.NET 中的依赖关系注入中的暂时和共享实例的 IDisposable 指南
默认服务容器替换
请参阅?.NET 中的依赖关系注入中的默认服务容器替换
建议
请参阅?.NET 中的依赖关系注入中的建议
-
避免使用服务定位器模式。 例如,可以使用 DI 代替时,不要调用?GetService?来获取服务实例:
错误:
?更正:
public class MyClass
{
private readonly IOptionsMonitor<MyOptions> _optionsMonitor;
public MyClass(IOptionsMonitor<MyOptions> optionsMonitor)
{
_optionsMonitor = optionsMonitor;
}
public void MyMethod()
{
var option = _optionsMonitor.CurrentValue.Option;
...
}
}
-
要避免的另一个服务定位器变体是注入需在运行时解析依赖项的工厂。 这两种做法混合了控制反转策略。
-
避免静态访问?
HttpContext
(例如,HttpContext
)。
DI 是静态/全局对象访问模式的替代方法。 如果将其与静态对象访问混合使用,则可能无法意识到 DI 的优点。
DI 中适用于多租户的推荐模式
Orchard Core?是用于在 ASP.NET Core 上构建模块化多租户应用程序的应用程序框架。 有关详细信息,请参阅?Orchard Core 文档。
请参阅?Orchard Core 示例,获取有关如何仅使用 Orchard Core Framework 而无需任何 CMS 特定功能来构建模块化和多租户应用的示例。
框架提供的服务
Program.cs
?注册应用使用的服务,包括 Entity Framework Core 和 ASP.NET Core MVC 等平台功能。 最初,提供给?Program.cs
?的?IServiceCollection
?具有框架定义的服务(具体取决于IServiceCollection
)。 对于基于 ASP.NET Core 模板的应用,该框架会注册 250 个以上的服务。
下表列出了框架注册的这些服务的一小部分:
其他资源
- 在 ASP.NET Core 中将依赖项注入到视图
- 在 ASP.NET Core 中将依赖项注入到控制器
- 在 ASP.NET Core 中将依赖关系注入要求处理程序
- ASP.NET Core Blazor 依赖关系注入
- 用于 DI 应用开发的 NDC 会议模式
- ASP.NET Core 中的应用启动
- ASP.NET Core 中基于工厂的中间件激活
- ASP.NET CORE 依赖项注入:什么是 ISERVICECOLLECTION?
- 在 ASP.NET Core 中释放 IDisposable 的四种方式
- 在 ASP.NET Core 中使用依赖关系注入编写干净代码 (MSDN)
- 显式依赖关系原则
- 控制反转容器和依赖关系注入模式 (Martin Fowler)
- 如何在 ASP.NET Core DI 中注册具有多个接口的服务
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!