除了这里所有的精彩答案之外,这个答案将解释不同的生命周期以及在何时选择它们是合适的。就像软件开发中的许多事情一样,没有绝对的规定,许多条件可以影响最佳选择,所以请将这个答案视为一般指导。
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控制器,在处理特定请求时都会获得同一个作用域服务的实例。
作用域服务的用途/特点
- 容器每个请求创建一个新的实例。
- 因为容器每个请求解析一个新的类型实例,所以通常不需要线程安全。
- 请求生命周期内的组件按顺序调用,因此共享实例不会被并发使用。
- 如果在一个请求中有多个消费者可能需要相同的依赖项,作用域服务非常有用。
a. 一个很好的例子是使用Entity Framework Core时。默认情况下,DbContext注册为作用域生命周期。因此,更改跟踪适用于整个请求。多个组件可以对共享的DbContext进行更改。
避免依赖关系的囚禁
在注册依赖项时,确保所选择的生命周期适合考虑到服务本身的任何依赖项是至关重要的。这是为了避免所谓的“俘获依赖项”,即服务可能比预期的寿命更长。
原则是,一个服务不应该依赖于比自身寿命更短的服务。例如,使用单例生命周期注册的服务不应该依赖于瞬态服务。这样做会导致瞬态服务被单例服务捕获,实例无意中被引用到应用程序的生命周期。这可能导致问题和有时难以追踪的运行时错误和行为,例如在线程之间意外共享非线程安全的服务,或者允许对象超过其预期的生命周期。
为了形象化这一点,让我们考虑哪些生命周期可以安全地依赖于使用另一个生命周期的服务。
由于这是一个短暂的服务,一个短暂的服务可以安全地依赖于具有短暂、作用域或单例生命周期的服务。
作用域服务有点棘手。如果它们依赖于一个短暂的服务,那么在整个请求的生命周期内,该短暂服务的单个实例将存在于作用域中。您可能希望或不希望这种行为发生。为了绝对安全起见,您可能选择不依赖于作用域服务的短暂服务,但是作用域服务可以安全地依赖于其他作用域或单例服务。
单例服务在其依赖方面最为严格,它不应该依赖于短暂或作用域服务,但可以依赖于其他单例服务。单例对作用域服务的捕获是其中更危险的可能性之一。因为作用域服务可能在作用域结束后被销毁,所以单例可能会在它们被销毁后尝试访问它们。这可能导致生产环境中的运行时异常,这是一个非常糟糕的情况。
![enter image description here](https://istack.dev59.com/BipTb.webp)