ASP.NET Core 依赖注入

作者:Kirk LarkinSteve SmithBrandon 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 文件中注册。
  • 将服务注入到使用它的类的构造函数中。 框架负责创建依赖关系的实例,并在不再需要时将其释放。

示例应用中, 接口定义 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>解析 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 页面模板生成,并演示如何使用扩展方法 AddDbContextAddDefaultIdentity 将其他服务添加到容器中:

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;
        }
    }
}

剩余的服务会在类似的类中注册。 下面的代码使用新扩展方法来注册服务:

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 所需的服务。 建议应用遵循在 Microsoft.Extensions.DependencyInjection 命名空间中创建扩展方法的命名约定。 在 Microsoft.Extensions.DependencyInjection 命名空间中创建扩展方法后,可以:

  • 封装服务注册组。
  • 提供对服务的便捷 IntelliSense 访问。

服务生存期

请参阅 .NET 中的依赖关系注入中的服务生存期

要在中间件中使用范围内服务,请使用以下方法之一:

  • 将服务注入中间件的 InvokeInvokeAsync 方法。 使用构造函数注入会引发运行时异常,因为它强制使范围内服务的行为与单一实例类似。 生存期和注册选项部分中的示例演示了 方法。
  • 使用基于工厂的中间件。 使用此方法注册的中间件按客户端请求(连接)激活,这也使范围内服务可注入中间件的构造函数。

有关详细信息,请参阅编写自定义 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);
    }
}

构造函数注入行为

请参阅 .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 公开限定范围的服务提供程序。 只要请求处于活动状态,所有作用域服务都有效。

注意

与解析 RequestServices 中的服务相比,以构造函数参数的形式请求依赖项是更优先的选择。 请求依赖项作为构造函数参数会生成更易于测试的类。

设计能够进行依赖关系注入的服务

在设计能够进行依赖注入的服务时:

  • 避免有状态的、静态类和成员。 通过将应用设计为改用单一实例服务,避免创建全局状态。
  • 避免在服务中直接实例化依赖类。 直接实例化会将代码耦合到特定实现。
  • 不在服务中包含过多内容,确保设计规范,并易于测试。

如果一个类有过多注入依赖项,这可能表明该类拥有过多的责任并且违反了单一责任原则 (SRP)。 尝试通过将某些职责移动到一个新类来重构类。 请记住,Razor Pages 页面模型类和 MVC 控制器类应关注用户界面问题。

服务释放

容器为其创建的 IDisposable 类型调用 Dispose。 从容器中解析的服务绝对不应由开发人员释放。 如果类型或工厂注册为单一实例,则容器自动释放单一实例。

在下面的示例中,服务由服务容器创建,并自动释放:dependency-injection\samples\6.x\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
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 来获取服务实例:

    不正确:

    Incorrect code

    正确

    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 的优点。

Orchard Core 是用于在 ASP.NET Core 上构建模块化多租户应用程序的应用程序框架。 有关详细信息,请参阅 Orchard Core 文档

请参阅 Orchard Core 示例,获取有关如何仅使用 Orchard Core Framework 而无需任何 CMS 特定功能来构建模块化和多租户应用的示例。

框架提供的服务

Program.cs 注册应用使用的服务,包括 Entity Framework Core 和 ASP.NET Core MVC 等平台功能。 最初,提供给 Program.csIServiceCollection 具有框架定义的服务(具体取决于IServiceCollection)。 对于基于 ASP.NET Core 模板的应用,该框架会注册 250 个以上的服务。

下表列出了框架注册的这些服务的一小部分:

服务类型 生存期
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory 暂时
IHostApplicationLifetime 单例
IWebHostEnvironment 单例
Microsoft.AspNetCore.Hosting.IStartup 单例
Microsoft.AspNetCore.Hosting.IStartupFilter 暂时
Microsoft.AspNetCore.Hosting.Server.IServer 单例
Microsoft.AspNetCore.Http.IHttpContextFactory 暂时
Microsoft.Extensions.Logging.ILogger<TCategoryName> 单例
Microsoft.Extensions.Logging.ILoggerFactory 单例
Microsoft.Extensions.ObjectPool.ObjectPoolProvider 单例
Microsoft.Extensions.Options.IConfigureOptions<TOptions> 暂时
Microsoft.Extensions.Options.IOptions<TOptions> 单例
System.Diagnostics.DiagnosticSource 单例
System.Diagnostics.DiagnosticListener 单例

其他资源

作者:Kirk LarkinSteve SmithScott AddieBrandon 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 created this message.");
    }
}

该类创建并直接依赖于 MyDependency 类。 代码依赖项(如前面的示例)会产生问题,应避免使用,原因如下:

  • 要用不同的实现替换 MyDependency,必须修改 IndexModel 类。
  • 如果 MyDependency 具有依赖项,则必须由 IndexModel 类对其进行配置。 在具有多个依赖于 MyDependency 的类的大型项目中,配置代码将分散在整个应用中。
  • 这种实现很难进行单元测试。 应用需使用模拟或存根 MyDependency 类,而该类不能使用此方法。

依赖关系注入通过以下方式解决了这些问题:

  • 使用接口或基类将依赖关系实现抽象化。
  • 在服务容器中注册依赖关系。 ASP.NET Core 提供了一个内置的服务容器 IServiceProvider。 服务通常已在应用的 Startup.ConfigureServices 方法中注册。
  • 将服务注入到使用它的类的构造函数中。 框架负责创建依赖关系的实例,并在不再需要时将其释放。

示例应用中, 接口定义 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 方法使用范围内生存期(单个请求的生存期)注册服务。 本主题后面将介绍服务生存期

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IMyDependency, MyDependency>();

    services.AddRazorPages();
}

在示例应用中,请求 IMyDependency 服务并用于调用 WriteMessage 方法:

public class Index2Model : PageModel
{
    private readonly IMyDependency _myDependency;

    public Index2Model(IMyDependency myDependency)
    {
        _myDependency = myDependency;            
    }

    public void OnGet()
    {
        _myDependency.WriteMessage("Index2Model.OnGet");
    }
}

通过使用 DI 模式,表示控制器:

  • 不使用具体类型 MyDependency,仅使用它实现的 IMyDependency 接口。 这样可以轻松地更改控制器使用的实现,而无需修改控制器。
  • 不创建 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}");
    }
}

更新的 ConfigureServices 方法注册新的 IMyDependency 实现:

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IMyDependency, MyDependency2>();

    services.AddRazorPages();
}

MyDependency2 依赖于 ILogger<TCategoryName>,并在构造函数中对其进行请求。 ILogger<TCategoryName>ILogger<TCategoryName>

以链式方式使用依赖关系注入并不罕见。 每个请求的依赖关系相应地请求其自己的依赖关系。 容器解析图中的依赖关系并返回完全解析的服务。 必须被解析的依赖关系的集合通常被称为“依赖关系树”、“依赖关系图”或“对象图”。

容器通过利用ILogger<TCategoryName>解析 ILogger<TCategoryName>,而无需注册每个(泛型)构造类型

在依赖项注入术语中,服务:

  • 通常是向其他对象提供服务的对象,如 IMyDependency 服务。
  • 与 Web 服务无关,尽管服务可能使用 Web 服务。

框架提供可靠的日志记录系统。 编写上述示例中的 IMyDependency 实现来演示基本的 DI,而不是来实现日志记录。 大多数应用都不需要编写记录器。 下面的代码演示如何使用默认日志记录,这不要求在 ConfigureServices 中注册任何服务:

public class AboutModel : PageModel
{
    private readonly ILogger _logger;

    public AboutModel(ILogger<AboutModel> logger)
    {
        _logger = logger;
    }
    
    public string Message { get; set; }

    public void OnGet()
    {
        Message = $"About page visited at {DateTime.UtcNow.ToLongTimeString()}";
        _logger.LogInformation(Message);
    }
}

使用前面的代码时,无需更新 ConfigureServices,因为框架提供ConfigureServices

注入 Startup 的服务

服务可以注入 Startup 构造函数和 Startup.Configure 方法。

使用泛型主机 (IHostBuilder) 时,只能将以下服务注入 Startup 构造函数:

任何向 DI 容器注册的服务都可以注入 Startup.Configure 方法:

public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
    ...
}

有关详细信息,请参阅 ASP.NET Core 中的应用启动访问 Startup 中的配置

使用扩展方法注册服务组

ASP.NET Core 框架使用一种约定来注册一组相关服务。 约定使用单个 Add{GROUP_NAME} 扩展方法来注册该框架功能所需的所有服务。 例如,AddControllers 扩展方法会注册 MVC 控制器所需的服务。

下面的代码通过个人用户帐户由 Razor 页面模板生成,并演示如何使用扩展方法 AddDbContextAddDefaultIdentity 将其他服务添加到容器中:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(
            Configuration.GetConnectionString("DefaultConnection")));
    services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();
    services.AddRazorPages();
}

请考虑下面的 ConfigureServices 方法,该方法可注册服务并配置选项:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<PositionOptions>(
        Configuration.GetSection(PositionOptions.Position));
    services.Configure<ColorOptions>(
        Configuration.GetSection(ColorOptions.Color));

    services.AddScoped<IMyDependency, MyDependency>();
    services.AddScoped<IMyDependency2, MyDependency2>();

    services.AddRazorPages();
}

可以将相关的注册组移动到扩展方法以注册服务。 例如,配置服务会被添加到以下类中:

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;
        }
    }
}

剩余的服务会在类似的类中注册。 下面的 ConfigureServices 方法使用新扩展方法来注册服务:

public void ConfigureServices(IServiceCollection services)
{
    services.AddConfig(Configuration)
            .AddMyDependencyGroup();

    services.AddRazorPages();
}

注意:每个 services.Add{GROUP_NAME} 扩展方法添加并可能配置服务。 例如,AddControllersWithViews 会添加带视图的 MVC 控制器所需的服务,AddRazorPages 会添加 Razor Pages 所需的服务。 建议应用遵循在 Microsoft.Extensions.DependencyInjection 命名空间中创建扩展方法的命名约定。 在 Microsoft.Extensions.DependencyInjection 命名空间中创建扩展方法后,可以:

  • 封装服务注册组。
  • 提供对服务的便捷 IntelliSense 访问。

服务生存期

请参阅 .NET 中的依赖关系注入中的服务生存期

要在中间件中使用范围内服务,请使用以下方法之一:

  • 将服务注入中间件的 InvokeInvokeAsync 方法。 使用构造函数注入会引发运行时异常,因为它强制使范围内服务的行为与单一实例类似。 生存期和注册选项部分中的示例演示了 方法。
  • 使用基于工厂的中间件。 使用此方法注册的中间件按客户端请求(连接)激活,这也使范围内服务可注入中间件的 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);
    }
}

构造函数注入行为

请参阅 .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; }
}

Startup.ConfigureServices 方法根据命名生存期创建 Operation 类的多个注册:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IOperationTransient, Operation>();
    services.AddScoped<IOperationScoped, Operation>();
    services.AddSingleton<IOperationSingleton, Operation>();

    services.AddRazorPages();
}

示例应用一并演示了请求中和请求之间的对象生存期。 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 IOperationTransient _transientOperation;
    private readonly IOperationSingleton _singletonOperation;

    public MyMiddleware(RequestDelegate next, ILogger<MyMiddleware> logger,
        IOperationTransient transientOperation,
        IOperationSingleton singletonOperation)
    {
        _logger = logger;
        _transientOperation = transientOperation;
        _singletonOperation = singletonOperation;
        _next = next;
    }

    public async Task InvokeAsync(HttpContext context,
        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,
    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"
    }
  }
}

从 main 调用服务

使用 IServiceScope 创建 IServiceScope 以解析应用范围内的作用域服务。 此方法可以用于在启动时访问有作用域的服务以便运行初始化任务。

以下示例演示如何访问范围内 IMyDependency 服务并在 Program.Main 中调用其 WriteMessage 方法:

public class Program
{
    public static void Main(string[] args)
    {
        var host = CreateHostBuilder(args).Build();

        using (var serviceScope = host.Services.CreateScope())
        {
            var services = serviceScope.ServiceProvider;

            try
            {
                var myDependency = services.GetRequiredService<IMyDependency>();
                myDependency.WriteMessage("Call services from main");
            }
            catch (Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex, "An error occurred.");
            }
        }

        host.Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

作用域验证

请参阅 .NET 中的依赖关系注入中的构造函数注入行为

有关详细信息,请参阅作用域验证

请求服务

ASP.NET Core 请求中的服务及其依赖项是通过 HttpContext.RequestServices 公开的。

框架为每个请求创建一个范围,RequestServices 公开限定范围的服务提供程序。 只要请求处于活动状态,所有作用域服务都有效。

注意

与解析 RequestServices 中的服务相比,以构造函数参数的形式请求依赖项是更优先的选择。 请求依赖项作为构造函数参数会生成更易于测试的类。

设计能够进行依赖关系注入的服务

在设计能够进行依赖注入的服务时:

  • 避免有状态的、静态类和成员。 通过将应用设计为改用单一实例服务,避免创建全局状态。
  • 避免在服务中直接实例化依赖类。 直接实例化会将代码耦合到特定实现。
  • 不在服务中包含过多内容,确保设计规范,并易于测试。

如果一个类有过多注入依赖项,这可能表明该类拥有过多的责任并且违反了单一责任原则 (SRP)。 尝试通过将某些职责移动到一个新类来重构类。 请记住,Razor Pages 页面模型类和 MVC 控制器类应关注用户界面问题。

服务释放

容器为其创建的 IDisposable 类型调用 Dispose。 从容器中解析的服务绝对不应由开发人员释放。 如果类型或工厂注册为单一实例,则容器自动释放单一实例。

在下面的示例中,服务由服务容器创建,并自动释放:

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;
    }
}
public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<Service1>();
    services.AddSingleton<Service2>();
    
    var myKey = Configuration["MyKey"];
    services.AddSingleton<IService3>(sp => new Service3(myKey));

    services.AddRazorPages();
}
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
Service1.Dispose

不由服务容器创建的服务

考虑下列代码:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton(new Service1());
    services.AddSingleton(new Service2());

    services.AddRazorPages();
}

在上述代码中:

  • 服务实例不是由服务容器创建的。
  • 框架不会自动释放服务。
  • 开发人员负责释放服务。

暂时和共享实例的 IDisposable 指南

请参阅 .NET 中的依赖关系注入中的暂时和共享实例的 IDisposable 指南

默认服务容器替换

请参阅 .NET 中的依赖关系注入中的默认服务容器替换

建议

请参阅 .NET 中的依赖关系注入中的建议

  • 避免使用服务定位器模式。 例如,可以使用 DI 代替时,不要调用 GetService 来获取服务实例:

    不正确:

    Incorrect code

    正确

    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)。

  • 避免在 ConfigureServices 中调用 BuildServiceProvider。 当开发人员想要在 ConfigureServices 中解析服务时,通常会调用 BuildServiceProvider。 例如,假设 LoginPath 从配置中加载。 避免采用以下方法:

    bad code calling BuildServiceProvider

    在上图中,选择 services.BuildServiceProvider 下的绿色波浪线将显示以下 ASP0000 警告:

    ASP0000 从应用程序代码调用“BuildServiceProvider”会导致创建单一实例服务的其他副本。 考虑依赖项注入服务等替代项作为“Configure”的参数。

    调用 BuildServiceProvider 会创建第二个容器,该容器可创建残缺的单一实例并导致跨多个容器引用对象图。

    获取 LoginPath 的正确方法是使用选项模式对 DI 的内置支持:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddCookie();
    
        services.AddOptions<CookieAuthenticationOptions>(
                            CookieAuthenticationDefaults.AuthenticationScheme)
            .Configure<IMyService>((options, myService) =>
            {
                options.LoginPath = myService.GetLoginPath();
            });
    
        services.AddRazorPages();
    }
    
  • 可释放的暂时性服务由容器捕获以进行释放。 如果从顶级容器解析,这会变为内存泄漏。

  • 启用范围验证,确保应用没有捕获范围内服务的单一实例。 有关详细信息,请参阅作用域验证

像任何一组建议一样,你可能会遇到需要忽略某建议的情况。 例外情况很少见,主要是框架本身内部的特殊情况。

DI 是静态/全局对象访问模式的替代方法。 如果将其与静态对象访问混合使用,则可能无法意识到 DI 的优点。

Orchard Core 是用于在 ASP.NET Core 上构建模块化多租户应用程序的应用程序框架。 有关详细信息,请参阅 Orchard Core 文档

请参阅 Orchard Core 示例,获取有关如何仅使用 Orchard Core Framework 而无需任何 CMS 特定功能来构建模块化和多租户应用的示例。

框架提供的服务

Startup.ConfigureServices 方法注册应用使用的服务,包括 Entity Framework Core 和 ASP.NET Core MVC 等平台功能。 最初,提供给 ConfigureServicesIServiceCollection 具有框架定义的服务(具体取决于IServiceCollection)。 对于基于 ASP.NET Core 模板的应用,该框架会注册 250 个以上的服务。

下表列出了框架注册的这些服务的一小部分:

服务类型 生存期
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory 暂时
IHostApplicationLifetime 单例
IWebHostEnvironment 单例
Microsoft.AspNetCore.Hosting.IStartup 单例
Microsoft.AspNetCore.Hosting.IStartupFilter 暂时
Microsoft.AspNetCore.Hosting.Server.IServer 单例
Microsoft.AspNetCore.Http.IHttpContextFactory 暂时
Microsoft.Extensions.Logging.ILogger<TCategoryName> 单例
Microsoft.Extensions.Logging.ILoggerFactory 单例
Microsoft.Extensions.ObjectPool.ObjectPoolProvider 单例
Microsoft.Extensions.Options.IConfigureOptions<TOptions> 暂时
Microsoft.Extensions.Options.IOptions<TOptions> 单例
System.Diagnostics.DiagnosticSource 单例
System.Diagnostics.DiagnosticListener 单例

其他资源