ChatGPT解决这个技术问题 Extra ChatGPT

AddTransient、AddScoped 和 AddSingleton 服务的区别

我想在 ASP.NET Core 中实现 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>();
}
@tmg 文档说“每次请求时都会创建瞬态生命周期服务。”和“每个请求创建一次范围内的生命周期服务。”除非我对英语的掌握比我想象的要差,否则实际上意思是完全相同的。
@tmg 我知道。我只是指出文档在这一点上根本不清楚,因此将人们指向文档并不是很有帮助。
@Neutrino,这就是我问这个问题的原因。
派对迟到了,甚至更晚才阅读评论,但我打印了那篇文章,阅读了它,并在页边空白处记下了我现在看到 @Neutrino 在这里所做的相同观察。这篇文章在提供该分析时完全含糊不清。幸运的是,这个例子不那么令人困惑。
据我了解:每次请求时都会创建瞬态生命周期服务。此处要求的词是要求某物的日常英语含义,在这种情况下是一项服务。而单词 request in once per request 指的是 HTTP 请求。但我确实理解这种困惑。

D
David Pine

TL;博士

瞬态对象总是不同的;为每个控制器和每个服务提供一个新实例。作用域对象在一个请求中是相同的,但在不同的请求中是不同的。对于每个对象和每个请求,单例对象都是相同的。

为了更清楚地说明,.NET documentation 中的这个示例显示了不同之处:

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

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

我们使用单个类 Operation 来实现这些接口,该类在其构造函数中接受 GUID,如果未提供任何 GUID,则使用新的 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 的特定实例,因此在使用此类型时会很清楚。我们还注册了一个依赖于每个其他 Operation 类型的 OperationService,以便在请求中明确该服务是获取与控制器相同的实例,还是为每个操作类型获取新实例.该服务所做的只是将其依赖项公开为属性,因此它们可以显示在视图中。

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

为了演示对应用程序的单独请求之内和之间的对象生命周期,示例包括一个请求每种 IOperation 类型的 OperationsController 以及一个 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();
        }
    }
}

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

https://i.stack.imgur.com/ilUB6.png

https://i.stack.imgur.com/X2QCd.png

观察请求中和请求之间的哪个 OperationId 值不同。

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

作用域对象在一个请求中是相同的,但在不同的请求中是不同的

Singleton 对象对于每个对象和每个请求都是相同的(无论在 ConfigureServices 中是否提供了实例)


我了解它们每个的功能,但有人可以解释使用一个而不是另一个的影响。如果使用不当或选择一个而不是另一个,可能会导致什么问题。
假设您正在创建一个具有单例范围的请求上下文相关对象(如当前用户),那么它将在所有不需要的 http 请求中保持相同的实例。 IOC 就是创建实例,所以我们需要指定创建实例的范围。
你能否解释一下我们在单例中嵌套瞬态或范围依赖的常见陷阱?
有道理!一般来说,如果我们将一个生命周期较短的对象放在一个生命周期较长的对象中,IoC 就不会再次创建内部对象。所以说如果你有一个单例,其中有一个瞬态或范围对象,内部对象不会被重新创建,因为单例的构造函数不会被再次调用。但反过来也可以。您可以毫无问题地将单例置于瞬态中。所以经验法则是内部对象应该具有与外部对象相同或更长的生命周期。
@akazemis 在这里做主的工作......清晰简洁的解释和带有指导示例的图形将插图带回家。谢谢!
C
Crypth

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

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

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

每次请求时都会创建瞬态生命周期服务。这个生命周期最适合轻量级、无状态的服务。

在这里您可以找到示例以查看差异:

ASP.NET 5 MVC6 Dependency Injection in 6 Steps(由于死链接导致的网络存档链接)

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

这是官方文档的链接:

Dependency injection in ASP.NET Core


你能解释一下为什么 Transient 是最轻量级的吗?我认为 Transient 是最繁重的工作,因为它需要为每次注入创建一个实例。
你是对的。 Transient 不是最轻量级的,我只是说它适合轻量级的 RESTful 服务 :)
那么在哪种情况下我们可以在控制器中使用作用域和瞬态,例如,如果我们从数据库中检索几行?在这种情况下,我试图了解作用域与瞬态使用场景。
这真的取决于你所期望的逻辑。例如,如果它是一个单独的 db 调用,它实际上并不会影响您使用哪个数据库。但是,如果您在同一个请求中多次调用 db,那么您可以使用作用域生命周期,因为它将同一个存储库对象保留在内存中,并在同一个 Http Request 上下文中重复使用多次。而瞬态创建一个新的存储库对象多次(并消耗更多的内存)。如果您解释您的具体情况,就很容易判断哪个更适合。
这里要强调的一个重点是 Singleton、Scoped 和 Transient 就像俄罗斯的玩偶,一个在另一个中。例如,嵌套时不能颠倒它们的顺序。范围内或单例不能包含在瞬态中,因为我们将延长父级的生命周期,这违背了包含!
b
bereket gebredingle

使用哪一个

短暂的

因为每次创建它们都会使用更多的内存和资源,并且会对性能产生负面影响

将此用于具有很少或没有状态的轻量级服务。

范围

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

辛格尔顿

这些服务中的内存泄漏会随着时间的推移而增加。

内存效率也很高,因为它们一旦在任何地方重用就被创建了。

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

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

切勿将 Scoped & Transient 服务注入 Singleton 服务。 (这有效地将瞬态或作用域服务转换为单例。)永远不要将瞬态服务注入作用域服务(这会将瞬态服务转换为作用域。)


这是最好的答案。我喜欢你举例的部分。理解它们的工作原理并不难。很难考虑将哪些服务放在哪里以及如何以及何时清除内存。如果您对此进行更多解释,那就太好了。
我不明白为什么将 Transient 推荐用于“状态很少或没有状态的轻量级服务”。在这种情况下为什么不单例?因为它是无状态的,所以只实例化那个小服务并多次使用它不是更好吗?即使服务实例化很便宜,如果你做很多次,开销也会增加。使用单例,它保持不变
应该补充的是,当使用单例时,您必须确保它们是线程安全的,因为它们可以被在不同线程上运行的多个并发请求使用。
将瞬态服务注入作用域服务有什么问题?据我了解,这样做不会使瞬态服务成为单例(如果您在其他地方注入相同的瞬态服务,它将是一个不同的对象),因此如果瞬态服务没有状态(应该是隐式的),我没看到问题。
@S-eagle 你能否举一个这样的无状态类的例子,如果它是按请求实例化的(瞬态),它会获得一些性能吗?我真的很想很好地理解这一点。
H
Hamed Naeemaei

https://i.stack.imgur.com/M0le5.jpg

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


这是上图的原始来源。 ezzylearning.net/tutorial/… 实际上我 5 天前在我的博客上发布了它 :-)
我读了你的文章,我有很多这样的services.AddTransient<IProductService, ProductService>();。我有一项服务,内存中的计数为 193!该服务仅具有无状态方法,是否应将其限定为范围而不是瞬态,以便我只能为所有控制器创建一个?
@MikeFlynn 对于每个请求一个实例,您应该使用 AddScoped<IProductService, ProductService>();。但是对于所有请求的一个实例,请使用 AddSingelton<IProductService, ProductService>();
在应用程序重新启动之前,单例不会保留在内存中吗?我不希望有大量单身人士在附近闲逛。
D
Dhana

Transient, scopedsingleton 定义了 ASP.NET MVC core DI(Dependency Injection) 中需要注入多个相同类型的对象时的对象创建过程。如果您不熟悉依赖项注入,您可以看到此 DI IoC video

您可以看到下面的控制器代码,其中我在构造函数中请求了两个“IDal”实例。 Transient、Scoped 和 Singleton 定义是否将同一实例注入“_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 值的快照图像。

https://i.stack.imgur.com/Xszmo.png

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

https://i.stack.imgur.com/pSm3C.png

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

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

https://i.stack.imgur.com/2b9Lq.png

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


这是我见过的对瞬态服务的最复杂的解释。瞬态 = 任何时候解决此服务都相当于分配您的变量 new TService。 Scoped 将为该“范围”缓存它的第一次初始化(大多数情况下为 http 请求)。 Singleton 将在应用程序的整个生命周期内只缓存一个实例,就这么简单。上面的图太复杂了。
很抱歉,我想我会用图表和代码快照让它更简单:-) 但我明白你的意思。
我发现这在您注入多个实例并使用瞬态注册的独特情况下很有帮助。谢谢
控制器本身有什么作用域?
C
Community

添加单例()

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

AddScoped()

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

添加瞬态()

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


u
user1969177

单例是应用程序域生命周期的单个实例。

Scoped 是作用域请求期间的单个实例,这意味着 ASP.NET 中的每个 HTTP 请求。

Transient 是每个代码请求的单个实例。

通常代码请求应通过构造函数参数进行,如

public MyConsumingClass(IDependency dependency)

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


O
Offir

在寻找这个问题的答案后,我找到了一个精彩的解释,并附有一个我想与你分享的例子。

您可以观看演示差异的视频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>

启动.cs

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

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

AddSingleton() - 顾名思义,AddSingleton() 方法创建一个 Singleton 服务。首次请求时会创建 Singleton 服务。然后,所有后续请求都使用相同的实例。因此,一般来说,每个应用程序只创建一次 Singleton 服务,并且在整个应用程序生命周期中都会使用该单个实例。 AddTransient() - 此方法创建一个 Transient 服务。每次请求时都会创建一个 Transient 服务的新实例。 AddScoped() - 此方法创建一个 Scoped 服务。范围内的每个请求都会创建一个 Scoped 服务的新实例。例如,在 Web 应用程序中,它为每个 http 请求创建 1 个实例,但在同一 Web 请求的其他调用中使用相同的实例。


加 1 为作者提供学分。 :)
我有一种感觉没有人使用我的代码,每个人都继续观看附加的视频:)
c
codevision

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

对于 .NET Web 应用程序,使用 DI 容器的替代方法是用您自己的替换默认控制器激活器,它必须管理生命周期并手动构建依赖关系图。出于学习目的,假设您有一个控制器激活器,该控制器激活器经过硬编码以在每次有 Web 请求时返回一个特定控制器:

// 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。我的回答基本上只是重复我在那里学到的东西。


我开始读那本书。谢谢你的建议。
R
Raihan Mahmud RAM

瞬态:每次请求实例时都会提供一个新实例,无论它是在同一个http请求的范围内还是在不同的http请求之间。

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

Singleton:只有一个实例。当第一次请求服务时创建一个实例,并且该单个实例单个实例将被整个应用程序中的所有后续 http 请求使用。


C
Charles Owen

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

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

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

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

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