AddTransient、AddScoped和AddSingleton服务的区别

2032
我希望在ASP.NET Core中实现依赖注入(DI)。在将此代码添加到ConfigureServices方法后,两种方式都可以正常工作。
在ASP.NET Core中services.AddTransientservices.AddScoped方法有什么区别?
public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.

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

231
文档中说:“瞬态生命周期的服务在每次请求时都会被创建。”和“范围生命周期的服务在每个请求中只会被创建一次。”,除非我的英语掌握得比我想象的还要差,否则这两句话实际上意思是相同的。 - Neutrino
123
我知道。我只是想指出文档在这一点上并不清晰,所以向文档指引读者并不是很有帮助。 - Neutrino
14
晚了一步,更晚地读到评论,但我打印出了那篇文章,阅读并在边缘做了与@Neutrino相同的观察。这篇文章在提供分析方面完全含糊不清。幸运的是,例子没那么令人困惑。 - Wellspring
94
据我所理解:临时生命周期服务每次被请求时都会被创建。这里的“requested”是指寻求某个服务时的日常英语意义,即在这种情况下请求某个服务。而“once per request”中的“request”则指的是HTTP请求。但我理解有些混淆。 - Memet Olsen
16
Transients意味着如果“MyClass”需要“SomeDependency”的实例,它会得到一个;如果“SomeOtherClass”需要实例,它将得到“SomeDependency”的新实例。这与提供给“MyClass”的实例不同。Scoped表示(在处理HTTP请求的上下文中)给定给“MyClass”和“SomeOtherClass”的“SomeDependency”实例将是相同的(对于正在处理的同一HTTP请求)。 - Mike de Klerk
显示剩余10条评论
13个回答

3268

TL;DR

在依赖注入中,对象的生命周期分为瞬态、作用域和单例三种。

  • 瞬态对象每次被请求时都会创建新的实例。
  • 作用域对象在同一请求中是相同的,但在不同请求之间是不同的。
  • 单例对象对于每个对象和每个请求都是相同的。

例如,.NET 文档提供了一个简单接口的示例来展示这些生命周期和注册选项的区别。根据服务的生命周期配置,容器将向请求类提供相同或不同的服务实例。

为了清楚地表示所请求的生命周期,请针对每个生命周期选项创建一个类型。

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:

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,以及一个OperationService。然后Index操作显示了所有控制器和服务的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中提供实例)。


116
我理解它们各自的功能,但有人能解释一下使用其中一个而不是另一个的影响吗?如果使用不正确或选择错误,可能会引起什么问题? - pawan nepal
28
假设您正在创建一个请求上下文相关的对象(例如当前用户),其作用域为单例,则它将在所有 HTTP 请求中保持相同的实例,这是不希望的。依赖注入(IOC)是关于创建实例的,因此我们需要指定创建实例的范围。 - akardon
5
你能否解释一下我们在单例中嵌套瞬态或作用域依赖项时常见的陷阱? - Second Person Shooter
65
有道理!一般而言,如果我们将寿命较短的对象放在寿命更长的对象中,控制反转(IoC)不会再创建内部对象。例如,如果您有一个单例,其中包含一个瞬态或作用域对象,则由于单例的构造函数不会再次被调用,因此内部对象不会被重新创建。但是相反的情况是可以的。您可以将单例放入瞬态对象中,没有问题。因此,经验法则是,内部对象的寿命应与外部对象相等或更长。 - akardon
26
@akazemis 在这里做着上帝的工作......清晰简洁的解释和图形配合指南式的例子,使这个例子非常明显易懂。谢谢! - snejame
显示剩余6条评论

560

.NET的依赖注入有三种主要生存期:

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

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

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

您可以在以下链接中找到示例以查看其区别:

ASP.NET 5 MVC6 6步中的依赖项注入(由于失效链接,这是Web归档链接)

已准备好依赖项注入的ASP.NET:ASP.NET 5

以下是官方文档的链接:

ASP.NET Core中的依赖项注入


47
请问您能否解释一下为什么“瞬时作用域”是最轻量级的?我认为“瞬时作用域”很沉重,因为每次注入都需要创建一个新实例。 - Expert wanna be
35
没错,"Transient"并不是最轻量级的,我只是说它适用于轻量级的RESTful服务 :) - akardon
6
在控制器示例中,如果我们正在从数据库检索少量行数据,那么在什么情况下可以使用scoped和transient?我试图理解在这种情况下如何使用scoped和transient。 - sensei
14
这得看你期望什么样的逻辑。例如,如果只有一个数据库调用,那么使用哪个都没有任何区别。但是如果在同一请求中多次调用数据库,则可以使用作用域生命周期,因为它将保留相同的存储库对象在内存中,并在同一 Http 请求上下文中重复使用多次。而短暂的则会多次创建新的存储库对象(并消耗更多内存)。如果你解释你具体的情况,很容易判断哪个更合适。 - akardon
14
这里需要强调的一个重点是,Singleton、Scoped和Transient之间像俄罗斯套娃一样互相包含。当嵌套使用时,不能反转它们的顺序,例如,一个Scoped或Singleton不能包含在一个Transient中,因为这将延长父项的生命周期,违反了包容原则! - DL Narasimhan
显示剩余6条评论

486

使用哪一个

瞬态(Transient)

  • 由于每次使用时都会创建,因此会使用更多的内存和资源,对性能可能产生负面影响
  • 在需要轻量级服务且没有或只有少量状态时使用。

作用域(Scoped)

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

单例(Singleton)

  • 这些服务中的内存泄漏会随着时间的推移而累积。
  • 由于它们只被创建一次并在任何地方重复使用,因此也具有内存效率。

在需要维护应用程序范围状态的情况下,请使用单例。应用程序配置或参数、日志记录服务、数据缓存等都是可以使用单例的示例。

将具有不同生命周期的服务注入到另一个服务中

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

  2. 永远不要将 Transient 服务注入到 Scoped 服务中(这会将瞬态服务转换为作用域服务。)

注意:我认为可以说上述建议存在争议。许多开发人员认为,例如将瞬态服务注入到单例中是可以的。


37
这是最好的答案。我喜欢你给出例子的部分。理解它们如何工作并不那么难。想要将哪个服务放在哪里、如何以及何时清理它们的内存则更加困难。如果你能多解释一些,那就太好了。 - valentasm
17
为什么“轻量级的、没有或只有很少状态”的服务建议使用Transient,我不明白。在这种情况下,为什么不使用单例呢?既然该小型服务是无状态的,将其实例化一次并多次使用不是更好吗?即使服务实例化便宜,如果重复进行多次,则开销会增加。而且单例可以保持相同。 - mnj
5
应该补充说明的是,在使用单例模式时,必须确保它们是线程安全的,因为它们可能会被运行在不同线程上的多个并发请求使用。 - Eric Mutta
12
将瞬态服务注入到作用域服务中有什么问题?据我所知,这样做并不会使瞬态服务变成单例(如果您在其他地方注入相同的瞬态服务,它将是一个不同的对象),因此只要瞬态服务没有状态(这应该是隐含的),我就看不出有问题。 - ajbeaven
5
@S-eagle,您能举一个这样的无状态类的例子吗?如果每个请求都实例化它(瞬态),那么它将获得一些性能提升。我真的很想深入了解这个问题。 - mnj
显示剩余17条评论

168

32
以下是上述图片的原始来源。https://www.ezzylearning.net/tutorial/asp-net-core-service-lifetimes-infographic/实际上,我5天前就在我的博客上发布了这张图片 :-) - Waqas Anwar
我读了你的文章,我有很多这样的 services.AddTransient<IProductService, ProductService>();。我有一个在内存中计数为193的服务!这个服务只有无状态方法,应该将其作用域设置为scoped,这样我就可以为所有控制器创建一个服务。 - Mike Flynn
@MikeFlynn 对于每个请求的一个实例,您应该使用 AddScoped<IProductService, ProductService>();。但是对于所有请求的一个实例,请使用 AddSingleton<IProductService, ProductService>(); - Hamed Naeemaei
1
单例不会一直保留在内存中直到应用程序重启吗?我不想让太多的单例挂起来。 - Mike Flynn
是的,直到重新启动之前,它仍然保留在内存中。 - Hamed Naeemaei

75

Transient、ScopedSingleton 是 ASP.NET MVC Core 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值。

Enter image description here

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

Enter image description here

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

下面是一个简单的图示,以直观的方式解释了上述基本概念。

enter image description here

这张图片是我在孟买学习ASP.NET MVC时SBSS团队绘制的。感谢SBSS团队创作了这张图片。


25
这是我见过的关于临时服务的解释最为复杂的一篇。Transient(临时)=每次调用该服务相当于给你的变量赋值 new TService。Scoped(作用域)会将其首次初始化缓存起来,只在该“作用域”中缓存(在大多数情况下表示为 http 请求)。Singleton(单例)将仅为应用程序的生命周期缓存一个实例,就这么简单。上述图表非常复杂。 - Mardoxx
5
非常抱歉,我以为用示意图和代码快照会让问题更简单明了 :-) 不过我理解你的意思了。 - Shivprasad Koirala
1
我发现这在有多个实例注入且使用瞬态注册的情况下非常有用。谢谢。 - Stokely
控制器本身有什么范围? - variable
1
@DavidKlempfner 任何需要每个“请求”创建新实例的地方;无论请求是什么。例如,一个监听文件(意味着任何数据流)以进行处理的工作程序。您可以为每个处理任务设置作用域容器,并从中解析,而不是沿着依赖链或使所有实例成为单例。 - Mardoxx
显示剩余2条评论

70

AddSingleton()

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

AddScoped()

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

AddTransient()

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


作用域生命周期在 ASP.NET 之外是否有任何意义,例如在 .NET 6 工作器服务应用程序中? - C-F
@C-F,它确实有意义,但是你需要时自己创建作用域。请参阅 https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection#scope-scenarios。 - C-F
1
@Yasser,所有的解释中,你的解释更清晰。谢谢。 - JackFrost
干净的答案!谢谢你。 - undefined

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

通常应该通过构造函数参数进行代码请求,如下所示:

public MyConsumingClass(IDependency dependency)

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


40

在寻找这个问题的答案后,我找到了一个非常好的解释和示例,我想与您分享。

您可以观看一个展示差异的视频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;
    }
}

HomeController

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

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

AddSingleton() - 正如其名称所示,AddSingleton()方法创建一个单例服务。当首次请求该服务时,将创建一个单例服务。随后的所有请求都使用同一实例。因此,通常情况下,每个应用程序只创建一次单例服务,并且该单个实例在整个应用程序生命周期内被使用。

AddTransient() - 此方法创建一个瞬态服务。每次请求瞬态服务时,都会创建一个新实例。

AddScoped() - 此方法创建一个作用域服务。在作用域范围内仅创建一次实例。例如,在Web应用程序中,它为每个HTTP请求创建1个实例,但在同一Web请求中的其他调用中使用相同的实例。


4
给作者点个赞,加一分。 :) - Naveen Kumar V
4
我有一种感觉,似乎没有人使用我的代码,而是直接看附加的视频 :) - Offir

13

起初,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。我的回答基本上只是重复我在那里学到的内容。


11

除了这里所有的精彩答案之外,这个答案将解释不同的生命周期以及在何时选择它们是合适的。就像软件开发中的许多事情一样,没有绝对的规定,许多条件可以影响最佳选择,所以请将这个答案视为一般指导。

ASP.NET Core附带了自己内置的依赖注入容器,它在请求生命周期中使用它来解析所需的服务。所有的框架服务,例如日志记录、配置、路由等,都使用依赖注入,并在构建应用程序主机时注册到依赖注入容器中。在内部,ASP.NET Core框架在激活框架组件(如控制器和Razor页面)时提供所需的依赖项。

依赖注入容器,有时也称为控制反转或IoC容器,是一个管理对象实例化和配置的软件组件。依赖注入容器不是应用依赖注入模式的必需品,但随着应用程序的增长,使用它可以极大地简化依赖项的管理,包括它们的生命周期。服务在启动时注册到容器中,并在运行时从容器中解析出来,只要它们被需要。容器负责创建和销毁所需服务的实例,并在指定的生命周期内维护它们。

当使用Microsoft依赖注入容器时,我们主要使用两个接口进行编码。

1. IServiceCollection接口定义了一个注册和配置服务描述符集合的契约。我们通过IServiceCollection构建一个IServiceProvider。
2. IServiceProvider定义了在运行时解析服务的机制。
当向容器注册服务时,应选择一个服务生命周期。服务生命周期控制容器创建对象后,解析的对象将在其中存在多长时间。可以通过在注册服务时使用适当的扩展方法在IServiceCollection上定义生命周期。在Microsoft依赖注入容器中,有三种可用的生命周期。
1. Transient(瞬态) 2. Singleton(单例) 3. Scoped(作用域)
依赖注入容器会跟踪它创建的所有服务实例,并在它们的生命周期结束后进行处理或释放以进行垃圾回收。所选择的生命周期会影响是否可以将同一服务实例解析并注入到多个依赖的消费者中。因此,非常重要的是明智地选择服务的生命周期。
瞬态服务
当一个服务被注册为瞬态(Transient)时,容器会每次解析该服务时创建一个新的实例并返回。换句话说,每个通过容器注入瞬态服务的依赖类都会收到自己独立的实例。由于每个依赖类都有自己的实例,实例上的方法可以安全地修改内部状态,而不必担心其他消费者和线程的访问。
瞬态服务的用途/特点:
1. 瞬态服务在服务包含可变状态且不被视为线程安全时最有用。 2. 使用瞬态服务可能会带来一些性能开销,尽管对于一般负载下的应用程序来说,这个开销可能是可以忽略的。 3. 每次解析实例时,即可能是每个请求,都需要为该实例分配内存。这会给垃圾回收器增加额外的工作,以清理这些短生命周期的对象。 4. 瞬态服务最容易理解,因为实例从不共享;因此,当注册服务时不确定哪种生命周期选项最佳时,瞬态服务往往是最安全的选择。
单例服务:
一个以单例生命周期注册的应用程序服务将在依赖注入容器的生命周期内只创建一次。在ASP.NET Core中,这相当于应用程序的生命周期。当需要该服务时,容器将创建一个实例。之后,相同的实例可以被重用并注入到所有依赖的类中。该实例将在容器的生命周期内保持可访问状态,因此不需要进行处理或垃圾回收。
单例服务的用途/特点:
假设服务需要频繁使用,比如每次请求。这样可以通过避免重复分配新对象来提高应用程序性能,因为每个对象可能在短时间后需要进行垃圾回收。
此外,如果构造对象的成本很高,将其限制为单个实例可以提高应用程序性能,因为它只在服务首次使用时发生一次。
在选择使用单例生命周期注册服务时,必须考虑线程安全性。因为单例服务的同一个实例可以被多个请求同时使用。任何没有适当的锁定机制的可变状态可能会导致意外行为。
单例非常适合函数式服务,其中方法接受输入并返回输出,而不使用共享状态。
在ASP.NET Core中,内存缓存是使用单例生命周期的合理用例,因为状态必须在缓存中共享才能正常工作。
在为单例注册服务时,要考虑一个实例在应用程序的生命周期内保持分配的影响。 a. 如果实例占用大量内存,可能会导致内存泄漏。 b. 如果内存使用量在实例的生命周期内可能增长,这可能会带来特别大的问题,因为它永远不会被垃圾回收释放。 c. 如果一个服务对内存要求很高,但使用非常不频繁,那么单例生命周期可能不是最合适的选择。

作用域服务

作用域服务位于瞬态和单例之间的中间地带。作用域服务的实例存在于解析它的作用域的生命周期内。在ASP.NET Core中,每个处理的请求都会在应用程序中创建一个作用域。任何作用域服务都将在每个作用域中创建一次,因此它们的行为类似于单例服务,但在作用域的上下文中。所有的框架组件,如中间件和MVC控制器,在处理特定请求时都会获得同一个作用域服务的实例。

作用域服务的用途/特点

  1. 容器每个请求创建一个新的实例。
  2. 因为容器每个请求解析一个新的类型实例,所以通常不需要线程安全。
  3. 请求生命周期内的组件按顺序调用,因此共享实例不会被并发使用。
  4. 如果在一个请求中有多个消费者可能需要相同的依赖项,作用域服务非常有用。 a. 一个很好的例子是使用Entity Framework Core时。默认情况下,DbContext注册为作用域生命周期。因此,更改跟踪适用于整个请求。多个组件可以对共享的DbContext进行更改。

避免依赖关系的囚禁

在注册依赖项时,确保所选择的生命周期适合考虑到服务本身的任何依赖项是至关重要的。这是为了避免所谓的“俘获依赖项”,即服务可能比预期的寿命更长。
原则是,一个服务不应该依赖于比自身寿命更短的服务。例如,使用单例生命周期注册的服务不应该依赖于瞬态服务。这样做会导致瞬态服务被单例服务捕获,实例无意中被引用到应用程序的生命周期。这可能导致问题和有时难以追踪的运行时错误和行为,例如在线程之间意外共享非线程安全的服务,或者允许对象超过其预期的生命周期。 为了形象化这一点,让我们考虑哪些生命周期可以安全地依赖于使用另一个生命周期的服务。
由于这是一个短暂的服务,一个短暂的服务可以安全地依赖于具有短暂、作用域或单例生命周期的服务。
作用域服务有点棘手。如果它们依赖于一个短暂的服务,那么在整个请求的生命周期内,该短暂服务的单个实例将存在于作用域中。您可能希望或不希望这种行为发生。为了绝对安全起见,您可能选择不依赖于作用域服务的短暂服务,但是作用域服务可以安全地依赖于其他作用域或单例服务。
单例服务在其依赖方面最为严格,它不应该依赖于短暂或作用域服务,但可以依赖于其他单例服务。单例对作用域服务的捕获是其中更危险的可能性之一。因为作用域服务可能在作用域结束后被销毁,所以单例可能会在它们被销毁后尝试访问它们。这可能导致生产环境中的运行时异常,这是一个非常糟糕的情况。

enter image description here


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接