AddTransient、AddScoped 和 AddSingleton 服务差异

AddTransient, AddScoped and AddSingleton Services Differences

我想在 ASP.NET 核心中实现 dependency injection (DI)。因此,将此代码添加到 ConfigureServices 方法后,两种方法都有效。

ASP.NET Core 中的 services.AddTransientservice.AddScoped 方法有什么区别?

public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.

    // Add application services.
    services.AddTransient<IEmailSender, AuthMessageSender>();
    services.AddScoped<IEmailSender, AuthMessageSender>();
}

在 .NET 的依赖注入中,存在三个主要生命周期:

Singleton 在整个应用程序中创建单个实例。它第一次创建实例并在所有调用中重复使用相同的对象。

Scoped 范围内的每个请求创建一次生命周期服务。相当于当前作用域中的单例。例如,在 MVC 中,它为每个 HTTP 请求创建一个实例,但它在同一 Web 请求中的其他调用中使用相同的实例。

Transient 生命周期服务在每次被请求时创建。此生命周期最适用于轻量级、无状态服务。

在这里您可以找到和示例以查看区别:

ASP.NET 5 MVC6 Dependency Injection in 6 Steps(网络存档 link 由于已死 link)

Your Dependency Injection ready ASP.NET : ASP.NET 5

这是官方文档的link:

Dependency injection in ASP.NET Core

TL;DR

Transient objects are always different; a new instance is provided to every controller and every service.

Scoped objects are the same within a request, but different across different requests.

Singleton objects are the same for every object and every request.

为了进一步说明,.NET documentation 中的这个示例显示了不同之处:

为了演示这些生命周期和注册选项之间的区别,请考虑一个简单的接口,该接口将一个或多个任务表示为具有唯一标识符的操作,OperationId。根据我们如何为此服务配置生命周期,容器将向请求 class 提供相同或不同的服务实例。为了明确请求哪个生命周期,我们将为每个生命周期选项创建一个类型:

using System;

namespace DependencyInjectionSample.Interfaces
{
    public interface IOperation
    {
        Guid OperationId { get; }
    }

    public interface IOperationTransient : IOperation
    {
    }

    public interface IOperationScoped : IOperation
    {
    }

    public interface IOperationSingleton : IOperation
    {
    }

    public interface IOperationSingletonInstance : IOperation
    {
    }
}

我们使用单个 class、Operation 实现这些接口,它在其构造函数中接受 GUID,或者如果提供 none 则使用新的 GUID:

using System;
using DependencyInjectionSample.Interfaces;
namespace DependencyInjectionSample.Classes
{
    public class Operation : IOperationTransient, IOperationScoped, IOperationSingleton, IOperationSingletonInstance
    {
        Guid _guid;
        public Operation() : this(Guid.NewGuid())
        {

        }

        public Operation(Guid guid)
        {
            _guid = guid;
        }

        public Guid OperationId => _guid;
    }
}

接下来,在ConfigureServices中,每个类型根据其命名生命周期添加到容器中:

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

请注意,IOperationSingletonInstance 服务正在使用具有已知 ID Guid.Empty 的特定实例,因此何时使用此类型将一目了然。我们还注册了一个 OperationService 依赖于其他每个 Operation 类型,这样在请求中就可以清楚地知道这个服务是否正在获得与控制器相同的实例,或者一个新的实例,对于每种操作类型。此服务所做的只是将其依赖项作为属性公开,以便它们可以显示在视图中。

using DependencyInjectionSample.Interfaces;

namespace DependencyInjectionSample.Services
{
    public class OperationService
    {
        public IOperationTransient TransientOperation { get; }
        public IOperationScoped ScopedOperation { get; }
        public IOperationSingleton SingletonOperation { get; }
        public IOperationSingletonInstance SingletonInstanceOperation { get; }

        public OperationService(IOperationTransient transientOperation,
            IOperationScoped scopedOperation,
            IOperationSingleton singletonOperation,
            IOperationSingletonInstance instanceOperation)
        {
            TransientOperation = transientOperation;
            ScopedOperation = scopedOperation;
            SingletonOperation = singletonOperation;
            SingletonInstanceOperation = instanceOperation;
        }
    }
}

为了演示对应用程序的单独单独请求内和之间的对象生命周期,示例包括一个 OperationsController 请求每种 IOperation 类型以及一个 OperationServiceIndex 操作然后显示所有控制器和服务的 OperationId 值。

using DependencyInjectionSample.Interfaces;
using DependencyInjectionSample.Services;
using Microsoft.AspNetCore.Mvc;

namespace DependencyInjectionSample.Controllers
{
    public class OperationsController : Controller
    {
        private readonly OperationService _operationService;
        private readonly IOperationTransient _transientOperation;
        private readonly IOperationScoped _scopedOperation;
        private readonly IOperationSingleton _singletonOperation;
        private readonly IOperationSingletonInstance _singletonInstanceOperation;

        public OperationsController(OperationService operationService,
            IOperationTransient transientOperation,
            IOperationScoped scopedOperation,
            IOperationSingleton singletonOperation,
            IOperationSingletonInstance singletonInstanceOperation)
        {
            _operationService = operationService;
            _transientOperation = transientOperation;
            _scopedOperation = scopedOperation;
            _singletonOperation = singletonOperation;
            _singletonInstanceOperation = singletonInstanceOperation;
        }

        public IActionResult Index()
        {
            // ViewBag contains controller-requested services
            ViewBag.Transient = _transientOperation;
            ViewBag.Scoped = _scopedOperation;
            ViewBag.Singleton = _singletonOperation;
            ViewBag.SingletonInstance = _singletonInstanceOperation;

            // Operation service has its own requested services
            ViewBag.Service = _operationService;
            return View();
        }
    }
}

现在对这个控制器操作发出了两个单独的请求:

观察哪个 OperationId 值在请求中和请求之间变化。

  • 瞬态对象总是不同的;为每个控制器和每个服务提供一个新实例。

  • 范围内的对象在一个请求中是相同的,但在不同的请求中是不同的

  • 单例对象对于每个对象和每个请求都是相同的(不管ConfigureServices中是否提供实例)

Transient, scoped and singleton定义多个对象时ASP.NETMVC核心DI(Dependency Injection)中的对象创建过程必须注入相同类型的。如果你是依赖注入的新手,你可以看到这个 DI IoC video.

您可以看到下面的控制器代码,其中我在构造函数中请求了 "IDal" 的两个实例。 Transient、ScopedSingleton 定义是否将在 "_dal""_dal1" 或不同。

public class CustomerController : Controller
{
    IDal dal = null;

    public CustomerController(IDal _dal,
                              IDal _dal1)
    {
        dal = _dal;
        // DI of MVC core
        // inversion of control
    }
}

瞬态: 在瞬态中,新的对象实例将被注入到单个请求和响应中。下面是我显示 GUID 值的快照图像。

Scoped:在scoped中,同一个对象实例将被注入到一个请求和响应中。

单例: 在单例中,将在所有请求和响应中注入相同的对象。在这种情况下,将创建对象的一个​​全局实例。

下面是一个简单的图表,直观地解释了上述基本原理。

上图是我拍摄ASP.NET MVC training in Mumbai时SBSS团队画的。非常感谢 SBSS 团队创建了上面的图像。

  • Singleton 是应用程序生命周期内的单个实例 域名。
  • Scoped 是作用域持续时间内的单个实例 请求,这意味着 ASP.NET.
  • 中的每个 HTTP 请求
  • Transient 是每个 code 请求的单个实例。

一般情况下,代码请求应该通过构造函数参数进行,如

public MyConsumingClass(IDependency dependency)

我想在@akazemis 的回答中指出,DI 上下文中的 "services" 并不意味着 RESTful 服务;服务是提供功能的依赖项的实现。

AddSingleton()

AddSingleton() 在首次请求时创建服务的单个实例,并在需要该服务的所有地方重复使用该实例。

AddScoped()

在范围服务中,对于每个 HTTP 请求,我们都会获得一个新实例。然而,在同一个 HTTP 请求中,如果在多个地方需要服务,比如在视图和控制器中,那么为该 HTTP 请求的整个范围提供同一个实例。但是每个新的 HTTP 请求都会得到一个新的服务实例。

AddTransient()

对于瞬态服务,每次请求服务实例时都会提供一个新实例,无论它是在同一 HTTP 请求的范围内还是跨不同的 HTTP 请求。

在寻找这个问题的答案后,我找到了一个很好的解释和一个例子,我想与你分享。

您可以观看演示差异的视频HERE

在这个例子中,我们有这个给定的代码:

public interface IEmployeeRepository
{
    IEnumerable<Employee> GetAllEmployees();
    Employee Add(Employee employee);
}

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class MockEmployeeRepository : IEmployeeRepository
{
    private List<Employee> _employeeList;

    public MockEmployeeRepository()
    {
        _employeeList = new List<Employee>()
    {
        new Employee() { Id = 1, Name = "Mary" },
        new Employee() { Id = 2, Name = "John" },
        new Employee() { Id = 3, Name = "Sam" },
    };
    }

    public Employee Add(Employee employee)
    {
        employee.Id = _employeeList.Max(e => e.Id) + 1;
        _employeeList.Add(employee);
        return employee;
    }

    public IEnumerable<Employee> GetAllEmployees()
    {
        return _employeeList;
    }
}

家庭控制器

public class HomeController : Controller
{
    private IEmployeeRepository _employeeRepository;

    public HomeController(IEmployeeRepository employeeRepository)
    {
        _employeeRepository = employeeRepository;
    }

    [HttpGet]
    public ViewResult Create()
    {
        return View();
    }

    [HttpPost]
    public IActionResult Create(Employee employee)
    {
        if (ModelState.IsValid)
        {
            Employee newEmployee = _employeeRepository.Add(employee);
        }

        return View();
    }
}

创建视图

@model Employee
@inject IEmployeeRepository empRepository

<form asp-controller="home" asp-action="create" method="post">
    <div>
        <label asp-for="Name"></label>
        <div>
            <input asp-for="Name">
        </div>
    </div>

    <div>
        <button type="submit">Create</button>
    </div>

    <div>
        Total Employees Count = @empRepository.GetAllEmployees().Count().ToString()
    </div>
</form>

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddSingleton<IEmployeeRepository, MockEmployeeRepository>();
}

Copy-paste 此代码并按下视图中的创建按钮并在两者之间切换 AddSingletonAddScopedAddTransient 您每次都会得到不同的结果,这可能有助于您理解这一点。

AddSingleton() - As the name implies, AddSingleton() method creates a Singleton service. A Singleton service is created when it is first requested. This same instance is then used by all the subsequent requests. So in general, a Singleton service is created only one time per application and that single instance is used throughout the application life time.

AddTransient() - This method creates a Transient service. A new instance of a Transient service is created each time it is requested.

AddScoped() - This method creates a Scoped service. A new instance of a Scoped service is created once per request within the scope. For example, in a web application it creates 1 instance per each http request but uses the same instance in the other calls within that same web request.

使用哪个

瞬态

  • 因为每次创建它们都会使用更多内存和资源,并且会对性能产生负面影响
  • 将此用于 轻量级 服务,具有很少或 无状态

作用域

  • 当您想在请求中维护状态时更好的选择。

单例

  • 这些服务中的内存泄漏会随着时间的推移而累积。
  • 内存效率高,因为它们创建后随处重复使用。

在需要维护应用程序范围状态的地方使用单例。应用程序配置或参数、日志记录服务、数据缓存是您可以使用单例的一些示例。

将不同生命周期的服务注入另一个服务

  1. 切勿将 Scoped 和 Transient 服务注入单例服务。(这有效地将瞬态或范围服务转换为单例。)

  2. 切勿将瞬态服务注入范围服务(这会将瞬态服务转换为范围服务。)

这张图片很好地说明了这个概念。 不幸的是,我找不到这个图像的来源,但是有人制作了它,他以图像的形式很好地展示了这个概念。

更新:图片参考:ASP.NET Core Service Lifetimes (Infographic) , Author: @WaqasAnwar

  • 瞬态:每次提供一个新实例 请求实例是否在同一个 http 请求的范围内 或跨越不同的 http 请求。

  • Scoped:我们在一个范围内获取同一个实例 给定 http 请求,但跨不同 http 请求的新实例。

  • Singleton:只有一个实例。实例 被创建,当第一次请求服务和那个单一的实例 单个实例将被所有后续的 http 请求使用 在整个申请过程中。

DI 容器起初可能非常神秘,尤其是在生命周期方面。毕竟,容器使用反射让一切都“正常工作”。这有助于思考容器在幕后实际为您完成的工作:组合对象图。

对于 .NET 网络应用程序,使用 DI 容器的替代方法是将默认控制器激活器替换为您自己的激活器,它必须手动管理生命周期和构建依赖关系图。出于学习目的,假设你有一个控制器激活器,每次有一个网络请求时,它是 hard-coded 到 return 一个特定的控制器:

// This class is created once per application during startup.  In DI terms, it is the
// "composition root."
public class DumbControllerActivator
{
    // Shared among all consumers from all requests
    private static readonly Singleton1 singleton1 = new Singleton1();
    private static readonly Singleton2 singleton2 = new Singleton2();

    // This method's responsibility is to construct a FooController and its dependecies.
    public FooController HandleFooRequest()
    {
        // Shared among all consumers in this request
        var scoped1 = new Scoped1();
        var scoped2 = new Scoped2(singleton1, scoped1);

        return new FooController(
            singleton1,
            scoped1,
            new Transient1(                     // Fresh instance
                singleton2,
                new Transient2(scoped2)),       // Fresh instance
            new Transient3(                     // Fresh instance
                singleton1,
                scoped1,
                new Transient1(                 // Fresh instance
                    singleton2,
                    new Transient2(scoped2)));  // Fresh instance
    }
}
  • 激活器只创建每个单例实例一次,然后在应用程序的整个生命周期中保留它。每个消费者共享单个实例(甚至来自不同请求的消费者)。
  • 对于范围内的依赖项,激活器为每个 Web 请求创建一个实例。在该请求中,每个消费者都共享该单个实例,但是从请求到请求,实例是不同的。
  • 对于瞬态依赖,每个消费者都有自己的私有实例。完全没有分享。

要更深入地了解 DI,我强烈推荐这本书 Dependency Injection Principles, Practices, and Patterns。我的回答基本上只是重复我在那里学到的东西。

生命周期的最佳例证可能是通过 DbContext 与 EntityFramework/Core 一起发挥作用。

建议将 DbContext 和与 DbContext 交互的存储库与 Scoped 生命周期连接起来,因为 DbContext 显然是一个有状态的构造。所以你不想使用单例,因为你最终会遇到各种并发问题。您不想使用 Transient,因为 DbContext 不是线程安全的。请记住,Transient 适用于处理无状态 objects/classes.

的用例

而且由于大多数存储库都是由控制器调用的,因此使用 Scoped 生命周期确实很有意义。可以想象,在作为事务的一部分的单个操作方法期间,可以多次调用 DbContext。

本文没有直接讨论这些生命周期,但很好地解释了为什么 Scoped 生命周期最适合 DbContext。

https://mehdi.me/ambient-dbcontext-in-ef6/?msclkid=00251b05d01411ec8d85d232374f26d5