ASP.NET Core 依赖注入

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

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

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>,而无需注册每个(泛型)构造类型

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

  • 通常是向其他对象提供服务的对象,如 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,因为框架提供日志记录

注入 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 命名空间中以封装服务注册的组。

服务生存期

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

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

  • 将服务注入中间件的 InvokeInvokeAsync 方法。 使用构造函数注入会引发运行时异常,因为它强制使范围内服务的行为与单一实例类似。 生存期和注册选项部分中的示例演示了 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);
    }
}

构造函数注入行为

请参阅 .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 调用服务

使用 IServiceScopeFactory.CreateScope 创建 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 公开作用域服务提供程序。 只要请求处于活动状态,所有作用域服务都有效。

备注

与解析 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 来获取服务实例:

    不正确:

    错误代码

    正确

    public class MyClass
    {
        private readonly IOptionsMonitor<MyOptions> _optionsMonitor;
    
        public MyClass(IOptionsMonitor<MyOptions> optionsMonitor)
        {
            _optionsMonitor = optionsMonitor;
        }
    
        public void MyMethod()
        {
            var option = _optionsMonitor.CurrentValue.Option;
    
            ...
        }
    }
    
  • 要避免的另一个服务定位器变体是注入需在运行时解析依赖项的工厂。 这两种做法混合了控制反转策略。

  • 避免静态访问 HttpContext(例如,IHttpContextAccessor.HttpContext)。

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

    调用 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 具有框架定义的服务(具体取决于主机配置方式)。 对于基于 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 单例

其他资源

作者:Steve SmithScott AddieBrandon Dahler

ASP.NET Core 支持依赖关系注入 (DI) 软件设计模式,这是一种在类及其依赖关系之间实现控制反转 (IoC) 的技术。

有关 MVC 控制器中依赖关系注入的详细信息,请参阅 在 ASP.NET Core 中将依赖项注入到控制器

查看或下载示例代码如何下载

依赖关系注入概述

依赖项是指另一个对象所需的任何对象。 查看以下具有 WriteMessage 方法的 MyDependency 类,此类为应用中其他类所依赖:

public class MyDependency
{
    public MyDependency()
    {
    }

    public Task WriteMessage(string message)
    {
        Console.WriteLine(
            $"MyDependency.WriteMessage called. Message: {message}");

        return Task.FromResult(0);
    }
}

可以创建 MyDependency 类的实例以使 WriteMessage 方法可用于其他类。 MyDependency 类是 IndexModel 类的依赖项:

public class IndexModel : PageModel
{
    MyDependency _dependency = new MyDependency();

    public async Task OnGetAsync()
    {
        await _dependency.WriteMessage(
            "IndexModel.OnGetAsync created this message.");
    }
}

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

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

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

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

示例应用中,IMyDependency 接口定义了服务为应用提供的方法:

public interface IMyDependency
{
    Task WriteMessage(string message);
}

此接口由具体类型 MyDependency 实现:

public class MyDependency : IMyDependency
{
    private readonly ILogger<MyDependency> _logger;

    public MyDependency(ILogger<MyDependency> logger)
    {
        _logger = logger;
    }

    public Task WriteMessage(string message)
    {
        _logger.LogInformation(
            "MyDependency.WriteMessage called. Message: {Message}", 
            message);

        return Task.FromResult(0);
    }
}

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

必须在服务容器中注册 IMyDependencyILogger<TCategoryName>IMyDependencyStartup.ConfigureServices 中注册。 ILogger<TCategoryName> 由日志记录抽象基础结构注册,因此它是框架默认注册的框架提供的服务

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

services.AddSingleton(typeof(ILogger<>), typeof(Logger<>));

在示例应用中,使用具体类型 MyDependency 注册 IMyDependency 服务。 注册将服务生存期的范围限定为单个请求的生存期。 本主题后面将介绍服务生存期

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    services.AddScoped<IMyDependency, MyDependency>();
    services.AddTransient<IOperationTransient, Operation>();
    services.AddScoped<IOperationScoped, Operation>();
    services.AddSingleton<IOperationSingleton, Operation>();
    services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));

    // OperationService depends on each of the other Operation types.
    services.AddTransient<OperationService, OperationService>();
}

备注

每个 services.Add{SERVICE_NAME} 扩展方法添加并可能配置服务。 例如,services.AddControllersWithViewsservices.AddRazorPagesservices.AddControllers 会添加 ASP.NET Core 应用所需的服务。 我们建议应用遵循此约定。 将扩展方法置于 Microsoft.Extensions.DependencyInjection 命名空间中以封装服务注册的组。 还包括用于 DI 扩展方法的命名空间部分 Microsoft.Extensions.DependencyInjection

  • 允许在不添加其他 using 块的情况下在 IntelliSense 中显示它们。
  • 防止在通常从中调用这些扩展方法的 Startup 类中出现过多的 using 语句。

如果服务的构造函数需要内置类型(如 string),则可以使用配置选项模式注入该类型:

public class MyDependency : IMyDependency
{
    public MyDependency(IConfiguration config)
    {
        var myStringValue = config["MyStringKey"];

        // Use myStringValue
    }

    ...
}

可通过类的构造函数请求服务实例,在该函数中可使用服务并将其分配给私有字段。 该字段用于在整个类中根据需要访问服务。

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

public class IndexModel : PageModel
{
    private readonly IMyDependency _myDependency;

    public IndexModel(
        IMyDependency myDependency, 
        OperationService operationService,
        IOperationTransient transientOperation,
        IOperationScoped scopedOperation,
        IOperationSingleton singletonOperation,
        IOperationSingletonInstance singletonInstanceOperation)
    {
        _myDependency = myDependency;
        OperationService = operationService;
        TransientOperation = transientOperation;
        ScopedOperation = scopedOperation;
        SingletonOperation = singletonOperation;
        SingletonInstanceOperation = singletonInstanceOperation;
    }

    public OperationService OperationService { get; }
    public IOperationTransient TransientOperation { get; }
    public IOperationScoped ScopedOperation { get; }
    public IOperationSingleton SingletonOperation { get; }
    public IOperationSingletonInstance SingletonInstanceOperation { get; }

    public async Task OnGetAsync()
    {
        await _myDependency.WriteMessage(
            "IndexModel.OnGetAsync created this message.");
    }
}

注入 Startup 的服务

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

服务可以注入 Startup.Configure

public void Configure(IApplicationBuilder app, IOptions<MyOptions> options)
{
    ...
}

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

框架提供的服务

Startup.ConfigureServices 方法负责定义应用使用的服务,包括 Entity Framework Core 和 ASP.NET Core MVC 等平台功能。 最初,提供给 ConfigureServicesIServiceCollection 具有框架定义的服务(具体取决于主机配置方式)。 基于 ASP.NET Core 模板的应用程序具有框架注册的数百个服务的情况并不少见。 下表列出了框架注册的服务的一小部分。

服务类型 生存期
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory 暂时
Microsoft.AspNetCore.Hosting.IApplicationLifetime 单例
Microsoft.AspNetCore.Hosting.IHostingEnvironment 单例
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 单例

使用扩展方法注册附加服务

当服务集合扩展方法可用于注册服务(及其依赖服务,如果需要)时,约定使用单个 Add{SERVICE_NAME} 扩展方法来注册该服务所需的所有服务。 以下代码是如何使用扩展方法 AddDbContext<TContext>AddIdentityCore 向容器添加附加服务的示例:

public void ConfigureServices(IServiceCollection services)
{
    ...

    services.AddDbContext<ApplicationDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();

    ...
}

有关详细信息,请参阅 API 文档中的 ServiceCollection 类。

服务生存期

为每个注册的服务选择适当的生存期。 可以使用以下生存期配置 ASP.NET Core 服务:

暂时

暂时生存期服务 (AddTransient) 是每次从服务容器进行请求时创建的。 这种生存期适合轻量级、 无状态的服务。

在处理请求的应用中,在请求结束时会释放暂时服务。

范围内

作用域生存期服务 (AddScoped) 针对每个客户端请求(连接)创建一次。

在处理请求的应用中,在请求结束时会释放有作用域的服务。

警告

在中间件内使用有作用域的服务时,请将该服务注入至 InvokeInvokeAsync 方法。 请不要通过构造函数注入进行注入,因为它会强制服务的行为与单一实例类似。 有关详细信息,请参阅 写入自定义 ASP.NET Core 中间件

单例

单一实例生存期服务 (AddSingleton) 是在第一次请求时(或者在运行 Startup.ConfigureServices 并且使用服务注册指定实例时)创建的。 每个后续请求都使用相同的实例。 如果应用需要单一实例行为,建议允许服务容器管理服务的生存期。 不要在类中实现单一实例设计模式并提供用户代码来管理对象的生存期。

在处理请求的应用中,当应用关闭并释放 ServiceProvider 时,会释放单一实例服务。

警告

从单一实例解析有作用域的服务很危险。 当处理后续请求时,它可能会导致服务处于不正确的状态。

服务注册方法

服务注册扩展方法提供适用于特定场景的重载。

方法 自动
对象 (object)
释放
多种
实现
传递参数
Add{LIFETIME}<{SERVICE}, {IMPLEMENTATION}>()
示例:
services.AddSingleton<IMyDep, MyDep>();
Add{LIFETIME}<{SERVICE}>(sp => new {IMPLEMENTATION})
示例:
services.AddSingleton<IMyDep>(sp => new MyDep());
services.AddSingleton<IMyDep>(sp => new MyDep("A string!"));
Add{LIFETIME}<{IMPLEMENTATION}>()
示例:
services.AddSingleton<MyDep>();
AddSingleton<{SERVICE}>(new {IMPLEMENTATION})
示例:
services.AddSingleton<IMyDep>(new MyDep());
services.AddSingleton<IMyDep>(new MyDep("A string!"));
AddSingleton(new {IMPLEMENTATION})
示例:
services.AddSingleton(new MyDep());
services.AddSingleton(new MyDep("A string!"));

要详细了解释放类型,请参阅服务释放部分。 多个实现的常见场景是为了测试而模拟各个类型

仅使用实现类型注册服务等效于使用相同的实现和服务类型注册该服务。 因此,我们不能使用捕获显式服务类型的方法来注册服务的多个实现。 这些方法可以注册服务的多个实例,但它们都具有相同的实现类型 。

上述任何服务注册方法都可用于注册同一服务类型的多个服务实例。 下面的示例以 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);
    }
}

框架还提供 TryAdd{LIFETIME} 扩展方法,只有当尚未注册某个实现时,才注册该服务。

在下面的示例中,对 AddSingleton 的调用会将 MyDependency 注册为 IMyDependency的实现。 对 TryAddSingleton 的调用没有任何作用,因为 IMyDependency 已有一个已注册的实现。

services.AddSingleton<IMyDependency, MyDependency>();
// The following line has no effect:
services.TryAddSingleton<IMyDependency, DifferentDependency>();

public class MyService
{
    public MyService(IMyDependency myDependency, 
        IEnumerable<IMyDependency> myDependencies)
    {
        Trace.Assert(myDependency is MyDependency);
        Trace.Assert(myDependencies.Single() is MyDependency);
    }
}

有关详情,请参阅:

TryAddEnumerable(ServiceDescriptor) 方法仅当没有同一类型的实现时,注册该服务。 多个服务通过 IEnumerable<{SERVICE}> 解析。 注册服务时,开发人员只希望在尚未添加一个相同类型时添加实例。 通常情况下,库创建者使用此方法来避免在容器中注册一个实例的两个副本。

在以下示例中,第一行向 IMyDep1 注册 MyDep。 第二行向 IMyDep2 注册 MyDep。 第三行没有任何作用,因为 IMyDep1 已有一个 MyDep 的已注册的实现:

public interface IMyDep1 {}
public interface IMyDep2 {}

public class MyDep : IMyDep1, IMyDep2 {}

services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyDep1, MyDep>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyDep2, MyDep>());
// Two registrations of MyDep for IMyDep1 is avoided by the following line:
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMyDep1, MyDep>());

构造函数注入行为

服务可以通过两种机制来解析:

  • IServiceProvider
  • ActivatorUtilities:允许在依赖关系注入容器中创建没有服务注册的对象。 ActivatorUtilities 用于面向用户的抽象,例如标记帮助器、MVC 控制器和模型绑定器。

构造函数可以接受非依赖关系注入提供的参数,但参数必须分配默认值。

当服务由 IServiceProviderActivatorUtilities 解析时,构造函数注入需要 public 构造函数。

当服务由 ActivatorUtilities 解析时,构造函数注入要求只存在一个适用的构造函数。 支持构造函数重载,但其参数可以全部通过依赖注入来实现的重载只能存在一个。

实体框架上下文

通常将实体框架上下文添加到使用作用域生存期的服务容器中,因为 Web 应用数据库操作通常将范围设置为客户端请求。 如果在注册数据库上下文时,AddDbContext<TContext> 重载未指定生存期,则默认生存期为作用域。 给定生存期的服务不应使用生存期比服务短的数据库上下文。

生存期和注册选项

为了演示生存期和注册选项之间的差异,请考虑以下接口,将任务表示为具有唯一标识符 OperationId 的操作。 根据为以下接口配置操作服务的生存期的方式,容器在类请求时提供相同或不同的服务实例:

public interface IOperation
{
    Guid OperationId { get; }
}

public interface IOperationTransient : IOperation
{
}

public interface IOperationScoped : IOperation
{
}

public interface IOperationSingleton : IOperation
{
}

public interface IOperationSingletonInstance : IOperation
{
}

接口在 Operation 类中实现。 Operation 构造函数将生成一个 GUID(如果未提供):

public class Operation : IOperationTransient, 
    IOperationScoped, 
    IOperationSingleton, 
    IOperationSingletonInstance
{
    public Operation() : this(Guid.NewGuid())
    {
    }

    public Operation(Guid id)
    {
        OperationId = id;
    }

    public Guid OperationId { get; private set; }
}

注册 OperationService 取决于,每个其他 Operation 类型。 当通过依赖关系注入请求 OperationService 时,它将基于依赖服务的生存期接收每个服务的新实例或现有实例。

  • 如果从容器请求时创建了临时服务,则 IOperationTransient 服务的 OperationIdOperationServiceOperationId 不同。 OperationService 将接收 IOperationTransient 类的新实例。 新实例将生成一个不同的 OperationId
  • 如果按客户端请求创建有作用域的服务,则 IOperationScoped 服务的 OperationId 与客户端请求中 OperationService 的该 ID 相同。 在客户端请求中,两个服务共享不同的 OperationId 值。
  • 如果创建了一次单例服务和单例实例服务并在所有客户端请求和所有服务中使用,则 OperationId 在所有服务请求中保持不变。
public class OperationService
{
    public OperationService(
        IOperationTransient transientOperation,
        IOperationScoped scopedOperation,
        IOperationSingleton singletonOperation,
        IOperationSingletonInstance instanceOperation)
    {
        TransientOperation = transientOperation;
        ScopedOperation = scopedOperation;
        SingletonOperation = singletonOperation;
        SingletonInstanceOperation = instanceOperation;
    }

    public IOperationTransient TransientOperation { get; }
    public IOperationScoped ScopedOperation { get; }
    public IOperationSingleton SingletonOperation { get; }
    public IOperationSingletonInstance SingletonInstanceOperation { get; }
}

Startup.ConfigureServices 中,根据其指定的生存期,将每个类型添加到容器中:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    services.AddScoped<IMyDependency, MyDependency>();
    services.AddTransient<IOperationTransient, Operation>();
    services.AddScoped<IOperationScoped, Operation>();
    services.AddSingleton<IOperationSingleton, Operation>();
    services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));

    // OperationService depends on each of the other Operation types.
    services.AddTransient<OperationService, OperationService>();
}

IOperationSingletonInstance 服务正在使用已知 ID 为 Guid.Empty 的特定实例。 此类型在使用时很明显(其 GUID 全部为零)。

示例应用演示了各个请求中和请求之间的对象生存期。 示例应用的 IndexModel 请求每种 IOperation 类型和 OperationService。 然后,页面通过属性分配显示所有页面模型类和服务的 OperationId 值:

public class IndexModel : PageModel
{
    private readonly IMyDependency _myDependency;

    public IndexModel(
        IMyDependency myDependency, 
        OperationService operationService,
        IOperationTransient transientOperation,
        IOperationScoped scopedOperation,
        IOperationSingleton singletonOperation,
        IOperationSingletonInstance singletonInstanceOperation)
    {
        _myDependency = myDependency;
        OperationService = operationService;
        TransientOperation = transientOperation;
        ScopedOperation = scopedOperation;
        SingletonOperation = singletonOperation;
        SingletonInstanceOperation = singletonInstanceOperation;
    }

    public OperationService OperationService { get; }
    public IOperationTransient TransientOperation { get; }
    public IOperationScoped ScopedOperation { get; }
    public IOperationSingleton SingletonOperation { get; }
    public IOperationSingletonInstance SingletonInstanceOperation { get; }

    public async Task OnGetAsync()
    {
        await _myDependency.WriteMessage(
            "IndexModel.OnGetAsync created this message.");
    }
}

以下两个输出显示了两个请求的结果:

第一个请求:

控制器操作:

暂时性:d233e165-f417-469b-a866-1cf1935d2518
作用域:5d997e2d-55f5-4a64-8388-51c4e3a1ad19
单一实例:01271bc1-9e31-48e7-8f7c-7261b040ded9
实例:00000000-0000-0000-0000-000000000000

OperationService 操作:

暂时性:c6b049eb-1318-4e31-90f1-eb2dd849ff64
作用域:5d997e2d-55f5-4a64-8388-51c4e3a1ad19
单一实例:01271bc1-9e31-48e7-8f7c-7261b040ded9
实例:00000000-0000-0000-0000-000000000000

第二个请求:

控制器操作:

暂时性:b63bd538-0a37-4ff1-90ba-081c5138dda0
作用域:31e820c5-4834-4d22-83fc-a60118acb9f4
单一实例:01271bc1-9e31-48e7-8f7c-7261b040ded9
实例:00000000-0000-0000-0000-000000000000

OperationService 操作:

暂时性:c4cbacb8-36a2-436d-81c8-8c1b78808aaf
作用域:31e820c5-4834-4d22-83fc-a60118acb9f4
单一实例:01271bc1-9e31-48e7-8f7c-7261b040ded9
实例:00000000-0000-0000-0000-000000000000

观察哪个 OperationId 值会在一个请求之内和不同请求之间变化:

  • 暂时性对象始终不同。 第一个和第二个客户端请求的暂时性 OperationId 值对于 OperationService 操作和在客户端请求内都是不同的。 为每个服务请求和客户端请求提供了一个新实例。
  • 作用域对象在一个客户端请求中是相同的,但在多个客户端请求中是不同的。
  • 单一实例对象对每个对象和每个请求都是相同的(不管 Startup.ConfigureServices 中是否提供 Operation 实例)。

从 main 调用服务

使用 IServiceScopeFactory.CreateScope 创建 IServiceScope 以解析应用范围内的作用域服务。 此方法可用于在启动时使用正确的服务生命周期访问范围内服务,以运行初始化任务。 以下示例演示如何在 Program.Main 中获取 MyScopedService 的上下文:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

public class Program
{
    public static async Task Main(string[] args)
    {
        var host = CreateWebHostBuilder(args).Build();

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

            try
            {
                var serviceContext = services.GetRequiredService<MyScopedService>();
                // Use the context here
            }
            catch (Exception ex)
            {
                var logger = services.GetRequiredService<ILogger<Program>>();
                logger.LogError(ex, "An error occurred.");
            }
        }

        await host.RunAsync();
    }

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>();
}

不需要为暂时性服务创建范围,其中包括前面示例中的 ILogger(请参阅:.NET Core 和 ASP.NET Core 中的日志记录)。 当从根解析时,暂时性服务并不会像限定范围的服务那样无意地解析为单一实例。 暂时性服务是在请求时创建的。 如果暂时性服务碰巧是可处置的,则在处置之前它都将以容器为根。 有关示例,请参阅 在 ASP.NET Core 中使用 IHttpClientFactory 发出 HTTP 请求

作用域验证

如果在开发环境中运行应用,默认的服务提供程序会执行检查,从而确认以下内容:

  • 没有从根服务提供程序直接或间接解析到有作用域的服务。
  • 未将有作用域的服务直接或间接注入到单一实例。

调用 BuildServiceProvider 时创建根服务提供程序。 在启动提供程序和应用时,根服务提供程序的生存期对应于应用/服务的生存期,并在关闭应用时释放。

有作用域的服务由创建它们的容器释放。 如果作用域服务创建于根容器,则该服务的生存期会实际上提升至单一实例,因为根容器只会在应用/服务关闭时将其释放。 验证服务作用域,将在调用 BuildServiceProvider 时收集这类情况。

有关详细信息,请参阅 ASP.NET Core Web 主机

请求服务

来自 HttpContext 的 ASP.NET Core 请求中可用的服务通过 HttpContext.RequestServices 集合公开。

请求服务表示作为应用的一部分配置和请求的服务。 当对象指定依赖关系时,RequestServices(而不是 ApplicationServices)中的类型将满足这些要求。

通常,应用不应直接使用这些属性。 相反,通过类构造函数请求类所需的类型,并允许框架注入依赖关系。 这样生成的类更易于测试。

备注

与访问 RequestServices 集合相比,以构造函数参数的形式请求依赖项是更优先的选择。

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

最佳做法是:

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

如果一个类似乎有过多的依赖关系注入,这通常表明该类拥有过多的责任并且违反了单一责任原则 (SRP)。 尝试通过将某些职责移动到一个新类来重构类。 请记住,Razor Pages 页面模型类和 MVC 控制器类应关注用户界面问题。 业务规则和数据访问实现细节应保留在适用于这些分离的关注点的类中。

服务释放

容器为其创建的 IDisposable 类型调用 Dispose。 如果实例是通过用户代码添加到容器中,则不会自动释放该实例。

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

public class Service1 : IDisposable {}
public class Service2 : IDisposable {}

public interface IService3 {}
public class Service3 : IService3, IDisposable {}

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<Service1>();
    services.AddSingleton<Service2>();
    services.AddSingleton<IService3>(sp => new Service3());
}

如下示例中:

  • 服务实例不是由服务容器创建的。
  • 框架不知道预期的服务生存期。
  • 框架不会自动释放服务。
  • 服务如果未通过开发者代码显式释放,则会一直保留,直到应用关闭。
public class Service1 : IDisposable {}
public class Service2 : IDisposable {}

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

暂时和共享实例的 IDisposable 指南

暂时、有限的生存期

方案

应用需要一个 IDisposable 实例,该实例在以下任一情况下具有暂时性生存期:

  • 在根作用域内解析实例。
  • 应在作用域结束之前释放实例。

解决方案

使用工厂模式在父作用域外创建实例。 在这种情况下,应用通常会使用一个 Create 方法,该方法直接调用最终类型的构造函数。 如果最终类型具有其他依赖项,则工厂可以:

共享实例,有限的生存期

方案

应用需要跨多个服务的共享 IDisposable 实例,但 IDisposable 应具有有限的生存期。

解决方案

为实例注册作用域生存期。 使用 IServiceScopeFactory.CreateScope 启动并创建新的 IServiceScope。 使用作用域的 IServiceProvider 获取所需的服务。 在生存期应结束时释放作用域。

通用准则

  • 不要为IDisposable 实例注册暂时作用域。 请改用工厂模式。
  • 不要在根作用域内解析暂时或作用域 IDisposable 实例。 唯一的一般性例外是应用创建/重建和释放 IServiceProvider 的情况,这不是理想的模式。
  • 通过 DI 接收 IDisposable 依赖项不要求接收方自行实现 IDisposableIDisposable 依赖项的接收方不能对该依赖项调用 Dispose
  • 作用域应该用于控制服务的生存期。 作用域不区分层次,并且在各作用域之间没有特定联系。

默认服务容器替换

内置的服务容器旨在满足框架和大多数消费者应用的需求。 我们建议使用内置容器,除非你需要的特定功能不受内置容器支持,例如:

  • 属性注入
  • 基于名称的注入
  • 子容器
  • 自定义生存期管理
  • 对迟缓初始化的 Func<T> 支持
  • 基于约定的注册

以下第三方容器可用于 ASP.NET Core 应用:

线程安全

创建线程安全的单一实例服务。 如果单例服务依赖于一个暂时服务,那么暂时服务可能也需要线程安全,具体取决于单例使用它的方式。

单个服务的工厂方法(例如 AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>) 的第二个参数)不必是线程安全的。 像类型 (static) 构造函数一样,它保证单个线程只调用一次。

建议

  • 不支持基于 async/awaitTask 的服务解析。 C# 不支持异步构造函数;因此建议模式是在同步解析服务后使用异步方法。

  • 避免在服务容器中直接存储数据和配置。 例如,用户的购物车通常不应添加到服务容器中。 配置应使用 选项模型。 同样,避免"数据持有者"对象,也就是仅仅为实现对某些其他对象的访问而存在的对象。 最好通过 DI 请求实际项。

  • 避免静态访问服务。 例如,避免静态键入 IApplicationBuilder.ApplicationServices 以便在其他地方使用。

  • 避免使用服务定位器模式,该模式混合了控制反转策略。

    • 可以使用 DI 代替时,不要调用 GetService 来获取服务实例:

      不正确:

      public class MyClass()
      
        public void MyMethod()
        {
            var optionsMonitor = 
                _services.GetService<IOptionsMonitor<MyOptions>>();
            var option = optionsMonitor.CurrentValue.Option;
      
            ...
        }
      

      正确

      public class MyClass
      {
          private readonly IOptionsMonitor<MyOptions> _optionsMonitor;
      
          public MyClass(IOptionsMonitor<MyOptions> optionsMonitor)
          {
              _optionsMonitor = optionsMonitor;
          }
      
          public void MyMethod()
          {
              var option = _optionsMonitor.CurrentValue.Option;
      
              ...
          }
      }
      
  • 避免注入一个使用 GetService 在运行时解析依赖项的中心。

  • 避免静态访问 HttpContext(例如,IHttpContextAccessor.HttpContext)。

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

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

其他资源