Repository: nestjs/nest
Pull request #9697 (2022-05-31 09:04:55)

feat(core): add durable providers feature
## PR Checklist
Please check if your PR fulfills the following requirements:

- [x] The commit message follows our guidelines: https://github.com/nestjs/nest/blob/master/CONTRIBUTING.md
- [x] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been added / updated (for bug fixes / features)


## PR Type
What kind of change does this PR introduce?

<!-- Please check the one that applies to this PR using "x". -->
- [ ] Bugfix
- [x] Feature
- [ ] Code style update (formatting, local variables)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Build related changes
- [ ] CI related changes
- [ ] Other... Please describe:

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->

Issue Number: N/A


## What is the new behavior?


## Does this PR introduce a breaking change?
- [ ] Yes
- [x] No

<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->


## Other information


Patch:
diff --git a/integration/scopes/src/durable/durable-context-id.strategy.ts b/integration/scopes/src/durable/durable-context-id.strategy.ts
new file mode 100644
index 000000000..dffa71515
--- /dev/null
+++ b/integration/scopes/src/durable/durable-context-id.strategy.ts
@@ -0,0 +1,21 @@
+import { ChildContextIdInfo, ContextId, ContextIdStrategy } from '@nestjs/core';
+import { Request } from 'express';
+
+const tenants = new Map<string, ContextId>();
+
+export class DurableContextIdStrategy implements ContextIdStrategy {
+  attach(contextId: ContextId, request: Request) {
+    const tenantId = request.headers['x-tenant-id'] as string;
+    let tenantSubTreeId: ContextId;
+
+    if (tenants.has(tenantId)) {
+      tenantSubTreeId = tenants.get(tenantId);
+    } else {
+      tenantSubTreeId = { id: +tenantId } as ContextId;
+      tenants.set(tenantId, tenantSubTreeId);
+    }
+
+    return (info: ChildContextIdInfo) =>
+      info.isTreeDurable ? tenantSubTreeId : contextId;
+  }
+}
diff --git a/integration/scopes/src/durable/durable.controller.ts b/integration/scopes/src/durable/durable.controller.ts
new file mode 100644
index 000000000..0800381c0
--- /dev/null
+++ b/integration/scopes/src/durable/durable.controller.ts
@@ -0,0 +1,12 @@
+import { Controller, Get } from '@nestjs/common';
+import { DurableService } from './durable.service';
+
+@Controller('durable')
+export class DurableController {
+  constructor(private readonly durableService: DurableService) {}
+
+  @Get()
+  greeting(): string {
+    return this.durableService.greeting();
+  }
+}
diff --git a/integration/scopes/src/durable/durable.module.ts b/integration/scopes/src/durable/durable.module.ts
new file mode 100644
index 000000000..ed312900f
--- /dev/null
+++ b/integration/scopes/src/durable/durable.module.ts
@@ -0,0 +1,9 @@
+import { Module } from '@nestjs/common';
+import { DurableController } from './durable.controller';
+import { DurableService } from './durable.service';
+
+@Module({
+  controllers: [DurableController],
+  providers: [DurableService],
+})
+export class DurableModule {}
diff --git a/integration/scopes/src/durable/durable.service.ts b/integration/scopes/src/durable/durable.service.ts
new file mode 100644
index 000000000..84639a215
--- /dev/null
+++ b/integration/scopes/src/durable/durable.service.ts
@@ -0,0 +1,11 @@
+import { Injectable, Scope } from '@nestjs/common';
+
+@Injectable({ scope: Scope.REQUEST, durable: true })
+export class DurableService {
+  public instanceCounter = 0;
+
+  greeting() {
+    ++this.instanceCounter;
+    return `Hello world! Counter: ${this.instanceCounter}`;
+  }
+}
diff --git a/packages/common/decorators/core/controller.decorator.ts b/packages/common/decorators/core/controller.decorator.ts
index b20c4a741..ec13f5f7c 100644
--- a/packages/common/decorators/core/controller.decorator.ts
+++ b/packages/common/decorators/core/controller.decorator.ts
@@ -163,7 +163,7 @@ export function Controller(
     : [
         prefixOrOptions.path || defaultPath,
         prefixOrOptions.host,
-        { scope: prefixOrOptions.scope },
+        { scope: prefixOrOptions.scope, durable: prefixOrOptions.durable },
         Array.isArray(prefixOrOptions.version)
           ? Array.from(new Set(prefixOrOptions.version))
           : prefixOrOptions.version,
diff --git a/packages/common/interfaces/modules/provider.interface.ts b/packages/common/interfaces/modules/provider.interface.ts
index ba6d86da2..2fd7f85b6 100644
--- a/packages/common/interfaces/modules/provider.interface.ts
+++ b/packages/common/interfaces/modules/provider.interface.ts
@@ -52,6 +52,13 @@ export interface ClassProvider<T = any> {
    * @see [Use factory](https://docs.nestjs.com/fundamentals/custom-providers#factory-providers-usefactory)
    */
   inject?: never;
+  /**
+   * Flags provider as durable. This flag can be used in combination with custom context id
+   * factory strategy to construct lazy DI subtrees.
+   *
+   * This flag can be used only in conjunction with scope = Scope.REQUEST.
+   */
+  durable?: boolean;
 }
 
 /**
@@ -123,6 +130,13 @@ export interface FactoryProvider<T = any> {
    * Optional enum defining lifetime of the provider that is returned by the Factory function.
    */
   scope?: Scope;
+  /**
+   * Flags provider as durable. This flag can be used in combination with custom context id
+   * factory strategy to construct lazy DI subtrees.
+   *
+   * This flag can be used only in conjunction with scope = Scope.REQUEST.
+   */
+  durable?: boolean;
 }
 
 /**
diff --git a/packages/common/interfaces/scope-options.interface.ts b/packages/common/interfaces/scope-options.interface.ts
index f958c393b..4cca74275 100644
--- a/packages/common/interfaces/scope-options.interface.ts
+++ b/packages/common/interfaces/scope-options.interface.ts
@@ -28,4 +28,11 @@ export interface ScopeOptions {
    * Specifies the lifetime of an injected Provider or Controller.
    */
   scope?: Scope;
+  /**
+   * Flags provider as durable. This flag can be used in combination with custom context id
+   * factory strategy to construct lazy DI subtrees.
+   *
+   * This flag can be used only in conjunction with scope = Scope.REQUEST.
+   */
+  durable?: boolean;
 }
diff --git a/packages/core/helpers/context-id-factory.ts b/packages/core/helpers/context-id-factory.ts
index 0cb80d311..3b26e6eb1 100644
--- a/packages/core/helpers/context-id-factory.ts
+++ b/packages/core/helpers/context-id-factory.ts
@@ -1,4 +1,4 @@
-import { ContextId } from '../injector/instance-wrapper';
+import { ChildContextIdInfo, ContextId } from '../injector/instance-wrapper';
 import { REQUEST_CONTEXT_ID } from '../router/request/request-constants';
 
 export function createContextId(): ContextId {
@@ -13,7 +13,22 @@ export function createContextId(): ContextId {
   return { id: Math.random() };
 }
 
+export interface ContextIdStrategy<T = any> {
+  /**
+   * Allows to attach a parent context id to the existing child context id.
+   * This lets you construct durable DI sub-trees that can be shared between contexts.
+   * @param contextId auto-generated child context id
+   * @param request request object
+   */
+  attach(
+    contextId: ContextId,
+    request: T,
+  ): ((info: ChildContextIdInfo) => ContextId) | undefined;
+}
+
 export class ContextIdFactory {
+  private static strategy?: ContextIdStrategy;
+
   /**
    * Generates a context identifier based on the request object.
    */
@@ -27,16 +42,33 @@ export class ContextIdFactory {
    */
   public static getByRequest<T extends Record<any, any> = any>(
     request: T,
+    propsToInspect: string[] = ['raw'],
   ): ContextId {
     if (!request) {
-      return createContextId();
+      return ContextIdFactory.create();
     }
     if (request[REQUEST_CONTEXT_ID as any]) {
       return request[REQUEST_CONTEXT_ID as any];
     }
-    if (request.raw && request.raw[REQUEST_CONTEXT_ID]) {
-      return request.raw[REQUEST_CONTEXT_ID];
+    for (const key of propsToInspect) {
+      if (request[key]?.[REQUEST_CONTEXT_ID]) {
+        return request[key][REQUEST_CONTEXT_ID];
+      }
     }
-    return createContextId();
+    if (!this.strategy) {
+      return ContextIdFactory.create();
+    }
+    const contextId = createContextId();
+    contextId.getParent = this.strategy.attach(contextId, request);
+    return contextId;
+  }
+
+  /**
+   * Registers a custom context id strategy that lets you attach
+   * a parent context id to the existing context id object.
+   * @param strategy strategy instance
+   */
+  public static apply(strategy: ContextIdStrategy) {
+    this.strategy = strategy;
   }
 }
diff --git a/packages/core/helpers/is-durable.ts b/packages/core/helpers/is-durable.ts
new file mode 100644
index 000000000..377c84f18
--- /dev/null
+++ b/packages/core/helpers/is-durable.ts
@@ -0,0 +1,7 @@
+import { SCOPE_OPTIONS_METADATA } from '@nestjs/common/constants';
+import { Type } from '@nestjs/common/interfaces/type.interface';
+
+export function isDurable(provider: Type<unknown>): boolean | undefined {
+  const metadata = Reflect.getMetadata(SCOPE_OPTIONS_METADATA, provider);
+  return metadata && metadata.durable;
+}
diff --git a/packages/core/injector/index.ts b/packages/core/injector/index.ts
index 6eb81c89c..6d411776d 100644
--- a/packages/core/injector/index.ts
+++ b/packages/core/injector/index.ts
@@ -1,6 +1,6 @@
 export * from './container';
 export * from './inquirer';
-export { ContextId } from './instance-wrapper';
+export { ChildContextIdInfo, ContextId } from './instance-wrapper';
 export * from './lazy-module-loader';
 export * from './module-ref';
 export * from './modules-container';
diff --git a/packages/core/injector/injector.ts b/packages/core/injector/injector.ts
index d4e8c232d..4f11322e1 100644
--- a/packages/core/injector/injector.ts
+++ b/packages/core/injector/injector.ts
@@ -106,7 +106,10 @@ export class Injector {
     inquirer?: InstanceWrapper,
   ) {
     const inquirerId = this.getInquirerId(inquirer);
-    const instanceHost = wrapper.getInstanceByContextId(contextId, inquirerId);
+    const instanceHost = wrapper.getInstanceByContextId(
+      this.getContextId(contextId, wrapper),
+      inquirerId,
+    );
     if (instanceHost.isPending) {
       return instanceHost.donePromise;
     }
@@ -278,7 +281,7 @@ export class Injector {
           index,
         );
         const instanceHost = paramWrapper.getInstanceByContextId(
-          contextId,
+          this.getContextId(contextId, paramWrapper),
           inquirerId,
         );
         if (!instanceHost.isResolved && !paramWrapper.forwardRef) {
@@ -434,7 +437,7 @@ export class Injector {
   ): Promise<InstanceWrapper> {
     const inquirerId = this.getInquirerId(inquirer);
     const instanceHost = instanceWrapper.getInstanceByContextId(
-      contextId,
+      this.getContextId(contextId, instanceWrapper),
       inquirerId,
     );
     if (!instanceHost.isResolved && !instanceWrapper.forwardRef) {
@@ -463,7 +466,7 @@ export class Injector {
     }
     if (instanceWrapper.async) {
       const host = instanceWrapper.getInstanceByContextId(
-        contextId,
+        this.getContextId(contextId, instanceWrapper),
         inquirerId,
       );
       host.instance = await host.instance;
@@ -584,7 +587,7 @@ export class Injector {
 
       const inquirerId = this.getInquirerId(inquirer);
       const instanceHost = instanceWrapperRef.getInstanceByContextId(
-        contextId,
+        this.getContextId(contextId, instanceWrapperRef),
         inquirerId,
       );
       if (!instanceHost.isResolved && !instanceWrapperRef.forwardRef) {
@@ -640,7 +643,7 @@ export class Injector {
           }
           const inquirerId = this.getInquirerId(inquirer);
           const instanceHost = paramWrapper.getInstanceByContextId(
-            contextId,
+            this.getContextId(contextId, paramWrapper),
             inquirerId,
           );
           return instanceHost.instance;
@@ -692,7 +695,7 @@ export class Injector {
     const { metatype, inject } = wrapper;
     const inquirerId = this.getInquirerId(inquirer);
     const instanceHost = targetMetatype.getInstanceByContextId(
-      contextId,
+      this.getContextId(contextId, targetMetatype),
       inquirerId,
     );
     const isInContext =
@@ -732,7 +735,10 @@ export class Injector {
     await this.loadInstance(wrapper, collection, moduleRef, ctx, wrapper);
     await this.loadEnhancersPerContext(wrapper, ctx, wrapper);
 
-    const host = wrapper.getInstanceByContextId(ctx, wrapper.id);
+    const host = wrapper.getInstanceByContextId(
+      this.getContextId(ctx, wrapper),
+      wrapper.id,
+    );
     return host && (host.instance as T);
   }
 
@@ -773,7 +779,11 @@ export class Injector {
     );
     const inquirerId = this.getInquirerId(inquirer);
     return hosts.map(
-      item => item.getInstanceByContextId(contextId, inquirerId).instance,
+      item =>
+        item.getInstanceByContextId(
+          this.getContextId(contextId, item),
+          inquirerId,
+        ).instance,
     );
   }
 
@@ -797,7 +807,10 @@ export class Injector {
     return dependenciesHosts.map(({ key, host }) => ({
       key,
       name: key,
-      instance: host.getInstanceByContextId(contextId, inquirerId).instance,
+      instance: host.getInstanceByContextId(
+        this.getContextId(contextId, host),
+        inquirerId,
+      ).instance,
     }));
   }
 
@@ -899,4 +912,16 @@ export class Injector {
   private isDebugMode(): boolean {
     return !!process.env.NEST_DEBUG;
   }
+
+  private getContextId(
+    contextId: ContextId,
+    instanceWrapper: InstanceWrapper,
+  ): ContextId {
+    return contextId.getParent
+      ? contextId.getParent({
+          token: instanceWrapper.token,
+          isTreeDurable: instanceWrapper.isDependencyTreeDurable(),
+        })
+      : contextId;
+  }
 }
diff --git a/packages/core/injector/instance-wrapper.ts b/packages/core/injector/instance-wrapper.ts
index 239e89dea..17a74142a 100644
--- a/packages/core/injector/instance-wrapper.ts
+++ b/packages/core/injector/instance-wrapper.ts
@@ -1,11 +1,16 @@
-import { Provider, Scope, Type } from '@nestjs/common';
+import { Logger, LoggerService, Provider, Scope, Type } from '@nestjs/common';
 import {
   ClassProvider,
   FactoryProvider,
   ValueProvider,
 } from '@nestjs/common/interfaces';
+import { clc } from '@nestjs/common/utils/cli-colors.util';
 import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util';
-import { isNil, isUndefined } from '@nestjs/common/utils/shared.utils';
+import {
+  isNil,
+  isString,
+  isUndefined,
+} from '@nestjs/common/utils/shared.utils';
 import { iterate } from 'iterare';
 import { STATIC_CONTEXT } from './constants';
 import { InstanceToken, Module } from './module';
@@ -13,8 +18,20 @@ import { InstanceToken, Module } from './module';
 export const INSTANCE_METADATA_SYMBOL = Symbol.for('instance_metadata:cache');
 export const INSTANCE_ID_SYMBOL = Symbol.for('instance_metadata:id');
 
+export interface ChildContextIdInfo {
+  /**
+   * Injection token (or class reference)
+   */
+  token: InstanceToken;
+  /**
+   * Flag that indicates whether DI subtree is durable
+   */
+  isTreeDurable: boolean;
+}
+
 export interface ContextId {
   readonly id: number;
+  getParent?(info: ChildContextIdInfo): ContextId;
 }
 
 export interface InstancePerContext<T> {
@@ -45,6 +62,9 @@ export class InstanceWrapper<T = any> {
   public metatype: Type<T> | Function;
   public inject?: FactoryProvider['inject'];
   public forwardRef?: boolean;
+  public durable?: boolean;
+
+  private static logger: LoggerService = new Logger(InstanceWrapper.name);
 
   private readonly values = new WeakMap<ContextId, InstancePerContext<T>>();
   private readonly [INSTANCE_METADATA_SYMBOL]: InstanceMetadataStore = {};
@@ -53,6 +73,7 @@ export class InstanceWrapper<T = any> {
     | Map<string, WeakMap<ContextId, InstancePerContext<T>>>
     | undefined;
   private isTreeStatic: boolean | undefined;
+  private isTreeDurable: boolean | undefined;
 
   constructor(
     metadata: Partial<InstanceWrapper<T>> & Partial<InstancePerContext<T>> = {},
@@ -171,14 +192,40 @@ export class InstanceWrapper<T = any> {
     return this[INSTANCE_METADATA_SYMBOL].enhancers;
   }
 
-  public isDependencyTreeStatic(lookupRegistry: string[] = []): boolean {
-    if (!isUndefined(this.isTreeStatic)) {
-      return this.isTreeStatic;
+  public isDependencyTreeDurable(lookupRegistry: string[] = []): boolean {
+    if (!isUndefined(this.isTreeDurable)) {
+      return this.isTreeDurable;
     }
-    if (this.scope === Scope.REQUEST) {
-      this.isTreeStatic = false;
-      return this.isTreeStatic;
+    if (this.durable === true) {
+      this.isTreeDurable = true;
+      this.printIntrospectedAsDurable();
+      return this.isTreeDurable;
+    }
+    const isStatic = this.isDependencyTreeStatic();
+    if (isStatic) {
+      return false;
     }
+    const isTreeNonDurable = this.introspectDepsAttribute(
+      (collection, registry) =>
+        collection.every(
+          (item: InstanceWrapper) => !item.isDependencyTreeDurable(registry),
+        ),
+      lookupRegistry,
+    );
+    this.isTreeDurable = !isTreeNonDurable && this.durable !== false;
+    if (this.isTreeDurable) {
+      this.printIntrospectedAsDurable();
+    }
+    return this.isTreeDurable;
+  }
+
+  public introspectDepsAttribute(
+    callback: (
+      collection: InstanceWrapper[],
+      lookupRegistry: string[],
+    ) => boolean,
+    lookupRegistry: string[] = [],
+  ): boolean {
     if (lookupRegistry.includes(this[INSTANCE_ID_SYMBOL])) {
       return true;
     }
@@ -186,23 +233,41 @@ export class InstanceWrapper<T = any> {
 
     const { dependencies, properties, enhancers } =
       this[INSTANCE_METADATA_SYMBOL];
-    let isStatic =
-      (dependencies &&
-        this.isWrapperListStatic(dependencies, lookupRegistry)) ||
-      !dependencies;
 
-    if (!isStatic || !(properties || enhancers)) {
-      this.isTreeStatic = isStatic;
-      return this.isTreeStatic;
+    let introspectionResult =
+      (dependencies && callback(dependencies, lookupRegistry)) || !dependencies;
+
+    if (!introspectionResult || !(properties || enhancers)) {
+      return introspectionResult;
     }
     const propertiesHosts = (properties || []).map(item => item.wrapper);
-    isStatic =
-      isStatic && this.isWrapperListStatic(propertiesHosts, lookupRegistry);
-    if (!isStatic || !enhancers) {
-      this.isTreeStatic = isStatic;
+    introspectionResult =
+      introspectionResult && callback(propertiesHosts, lookupRegistry);
+    if (!introspectionResult || !enhancers) {
+      return introspectionResult;
+    }
+    return callback(enhancers, lookupRegistry);
+  }
+
+  public isDependencyTreeStatic(lookupRegistry: string[] = []): boolean {
+    if (!isUndefined(this.isTreeStatic)) {
       return this.isTreeStatic;
     }
-    this.isTreeStatic = this.isWrapperListStatic(enhancers, lookupRegistry);
+    if (this.scope === Scope.REQUEST) {
+      this.isTreeStatic = false;
+      this.printIntrospectedAsRequestScoped();
+      return this.isTreeStatic;
+    }
+    this.isTreeStatic = this.introspectDepsAttribute(
+      (collection, registry) =>
+        collection.every((item: InstanceWrapper) =>
+          item.isDependencyTreeStatic(registry),
+        ),
+      lookupRegistry,
+    );
+    if (!this.isTreeStatic) {
+      this.printIntrospectedAsRequestScoped();
+    }
     return this.isTreeStatic;
   }
 
@@ -341,15 +406,6 @@ export class InstanceWrapper<T = any> {
     return isNil(this.inject) && this.metatype && this.metatype.prototype;
   }
 
-  private isWrapperListStatic(
-    tree: InstanceWrapper[],
-    lookupRegistry: string[],
-  ): boolean {
-    return tree.every((item: InstanceWrapper) =>
-      item.isDependencyTreeStatic(lookupRegistry),
-    );
-  }
-
   private initialize(
     metadata: Partial<InstanceWrapper<T>> & Partial<InstancePerContext<T>>,
   ) {
@@ -362,4 +418,34 @@ export class InstanceWrapper<T = any> {
     });
     this.scope === Scope.TRANSIENT && (this.transientMap = new Map());
   }
+
+  private printIntrospectedAsRequestScoped() {
+    if (!this.isDebugMode() || this.name === 'REQUEST') {
+      return;
+    }
+    if (isString(this.name)) {
+      InstanceWrapper.logger.log(
+        `${clc.cyanBright(this.name)}${clc.green(
+          ' introspected as ',
+        )}${clc.magentaBright('request-scoped')}`,
+      );
+    }
+  }
+
+  private printIntrospectedAsDurable() {
+    if (!this.isDebugMode()) {
+      return;
+    }
+    if (isString(this.name)) {
+      InstanceWrapper.logger.log(
+        `${clc.cyanBright(this.name)}${clc.green(
+          ' introspected as ',
+        )}${clc.magentaBright('durable')}`,
+      );
+    }
+  }
+
+  private isDebugMode(): boolean {
+    return !!process.env.NEST_DEBUG;
+  }
 }
diff --git a/packages/core/injector/module-ref.ts b/packages/core/injector/module-ref.ts
index 22fe0f383..7051a7b34 100644
--- a/packages/core/injector/module-ref.ts
+++ b/packages/core/injector/module-ref.ts
@@ -2,6 +2,7 @@ import { IntrospectionResult, Scope, Type } from '@nestjs/common';
 import { InvalidClassScopeException } from '../errors/exceptions/invalid-class-scope.exception';
 import { UnknownElementException } from '../errors/exceptions/unknown-element.exception';
 import { getClassScope } from '../helpers/get-class-scope';
+import { isDurable } from '../helpers/is-durable';
 import { NestContainer } from './container';
 import { Injector } from './injector';
 import { InstanceLinksHost } from './instance-links-host';
@@ -107,6 +108,7 @@ export abstract class ModuleRef {
       metatype: type,
       isResolved: false,
       scope: getClassScope(type),
+      durable: isDurable(type),
       host: moduleRef,
     });
     return new Promise<T>(async (resolve, reject) => {
diff --git a/packages/core/injector/module.ts b/packages/core/injector/module.ts
index 461fd0899..15b025d4a 100644
--- a/packages/core/injector/module.ts
+++ b/packages/core/injector/module.ts
@@ -26,6 +26,7 @@ import { RuntimeException } from '../errors/exceptions/runtime.exception';
 import { UnknownExportException } from '../errors/exceptions/unknown-export.exception';
 import { createContextId } from '../helpers/context-id-factory';
 import { getClassScope } from '../helpers/get-class-scope';
+import { isDurable } from '../helpers/is-durable';
 import { CONTROLLER_ID_KEY } from './constants';
 import { NestContainer } from './container';
 import { InstanceWrapper } from './instance-wrapper';
@@ -209,6 +210,7 @@ export class Module {
         instance: null,
         isResolved: false,
         scope: getClassScope(injectable),
+        durable: isDurable(injectable),
         host: this,
       });
       this._injectables.set(injectable, instanceWrapper);
@@ -233,6 +235,7 @@ export class Module {
         instance: null,
         isResolved: false,
         scope: getClassScope(provider),
+        durable: isDurable(provider),
         host: this,
       }),
     );
@@ -301,12 +304,15 @@ export class Module {
     provider: ClassProvider,
     collection: Map<InstanceToken, InstanceWrapper>,
   ) {
-    let { scope } = provider;
+    let { scope, durable } = provider;
 
     const { useClass } = provider;
     if (isUndefined(scope)) {
       scope = getClassScope(useClass);
     }
+    if (isUndefined(durable)) {
+      durable = isDurable(useClass);
+    }
     collection.set(
       provider.provide,
       new InstanceWrapper({
@@ -316,6 +322,7 @@ export class Module {
         instance: null,
         isResolved: false,
         scope,
+        durable,
         host: this,
       }),
     );
@@ -348,6 +355,7 @@ export class Module {
       useFactory: factory,
       inject,
       scope,
+      durable,
       provide: providerToken,
     } = provider;
 
@@ -361,6 +369,7 @@ export class Module {
         isResolved: false,
         inject: inject || [],
         scope,
+        durable,
         host: this,
       }),
     );
@@ -445,6 +454,7 @@ export class Module {
         instance: null,
         isResolved: false,
         scope: getClassScope(controller),
+        durable: isDurable(controller),
         host: this,
       }),
     );
diff --git a/packages/core/middleware/container.ts b/packages/core/middleware/container.ts
index 330914a19..de8dc214b 100644
--- a/packages/core/middleware/container.ts
+++ b/packages/core/middleware/container.ts
@@ -1,9 +1,10 @@
 import { Type } from '@nestjs/common';
 import { MiddlewareConfiguration } from '@nestjs/common/interfaces/middleware/middleware-configuration.interface';
+import { getClassScope } from '../helpers/get-class-scope';
+import { isDurable } from '../helpers/is-durable';
 import { NestContainer } from '../injector/container';
 import { InstanceWrapper } from '../injector/instance-wrapper';
 import { InstanceToken } from '../injector/module';
-import { getClassScope } from '../helpers/get-class-scope';
 
 export class MiddlewareContainer {
   private readonly middleware = new Map<
@@ -45,6 +46,7 @@ export class MiddlewareContainer {
         token,
         new InstanceWrapper({
           scope: getClassScope(metatype),
+          durable: isDurable(metatype),
           name: token,
           metatype,
           token,


Test patch:
diff --git a/integration/scopes/e2e/durable-providers.spec.ts b/integration/scopes/e2e/durable-providers.spec.ts
new file mode 100644
index 000000000..306d63aec
--- /dev/null
+++ b/integration/scopes/e2e/durable-providers.spec.ts
@@ -0,0 +1,76 @@
+import { INestApplication } from '@nestjs/common';
+import { ContextIdFactory } from '@nestjs/core';
+import { Test } from '@nestjs/testing';
+import { expect } from 'chai';
+import * as request from 'supertest';
+import { DurableContextIdStrategy } from '../src/durable/durable-context-id.strategy';
+import { DurableModule } from '../src/durable/durable.module';
+
+describe('Durable providers', () => {
+  let server: any;
+  let app: INestApplication;
+
+  before(async () => {
+    const moduleRef = await Test.createTestingModule({
+      imports: [DurableModule],
+    }).compile();
+
+    app = moduleRef.createNestApplication();
+    server = app.getHttpServer();
+    await app.init();
+
+    ContextIdFactory.apply(new DurableContextIdStrategy());
+  });
+
+  describe('when service is durable', () => {
+    const performHttpCall = (tenantId: number, end: (err?: any) => void) =>
+      request(server)
+        .get('/durable')
+        .set({ ['x-tenant-id']: tenantId })
+        .end((err, res) => {
+          if (err) return end(err);
+          end(res);
+        });
+
+    it(`should share durable providers per tenant`, async () => {
+      let result: request.Response;
+      result = await new Promise<request.Response>(resolve =>
+        performHttpCall(1, resolve),
+      );
+      expect(result.text).equal('Hello world! Counter: 1');
+
+      result = await new Promise<request.Response>(resolve =>
+        performHttpCall(1, resolve),
+      );
+      expect(result.text).equal('Hello world! Counter: 2');
+
+      result = await new Promise<request.Response>(resolve =>
+        performHttpCall(1, resolve),
+      );
+      expect(result.text).equal('Hello world! Counter: 3');
+    });
+
+    it(`should create per-tenant DI sub-tree`, async () => {
+      let result: request.Response;
+      result = await new Promise<request.Response>(resolve =>
+        performHttpCall(4, resolve),
+      );
+      expect(result.text).equal('Hello world! Counter: 1');
+
+      result = await new Promise<request.Response>(resolve =>
+        performHttpCall(5, resolve),
+      );
+      expect(result.text).equal('Hello world! Counter: 1');
+
+      result = await new Promise<request.Response>(resolve =>
+        performHttpCall(6, resolve),
+      );
+      expect(result.text).equal('Hello world! Counter: 1');
+    });
+  });
+
+  after(async () => {
+    ContextIdFactory['strategy'] = undefined;
+    await app.close();
+  });
+});
diff --git a/packages/core/test/injector/instance-wrapper.spec.ts b/packages/core/test/injector/instance-wrapper.spec.ts
index b94cf4e1d..6b0e1734c 100644
--- a/packages/core/test/injector/instance-wrapper.spec.ts
+++ b/packages/core/test/injector/instance-wrapper.spec.ts
@@ -106,6 +106,118 @@ describe('InstanceWrapper', () => {
     });
   });
 
+  describe('isDependencyTreeDurable', () => {
+    describe('when request scoped and durable', () => {
+      it('should return true', () => {
+        const wrapper = new InstanceWrapper({
+          scope: Scope.REQUEST,
+          durable: true,
+        });
+        expect(wrapper.isDependencyTreeDurable()).to.be.true;
+      });
+    });
+    describe('when statically scoped', () => {
+      describe('dependencies', () => {
+        describe('when each is static', () => {
+          it('should return false', () => {
+            const wrapper = new InstanceWrapper();
+            wrapper.addCtorMetadata(0, new InstanceWrapper());
+            expect(wrapper.isDependencyTreeDurable()).to.be.false;
+          });
+        });
+        describe('when one is not static and non-durable', () => {
+          it('should return false', () => {
+            const wrapper = new InstanceWrapper();
+            wrapper.addCtorMetadata(0, new InstanceWrapper());
+            wrapper.addCtorMetadata(
+              1,
+              new InstanceWrapper({
+                scope: Scope.REQUEST,
+              }),
+            );
+            expect(wrapper.isDependencyTreeDurable()).to.be.false;
+          });
+        });
+        describe('when one is not static and durable', () => {
+          it('should return true', () => {
+            const wrapper = new InstanceWrapper();
+            wrapper.addCtorMetadata(0, new InstanceWrapper());
+            wrapper.addCtorMetadata(
+              1,
+              new InstanceWrapper({
+                scope: Scope.REQUEST,
+                durable: true,
+              }),
+            );
+            expect(wrapper.isDependencyTreeDurable()).to.be.true;
+          });
+        });
+      });
+      describe('properties', () => {
+        describe('when each is static', () => {
+          it('should return false', () => {
+            const wrapper = new InstanceWrapper();
+            wrapper.addPropertiesMetadata('key1', new InstanceWrapper());
+            wrapper.addPropertiesMetadata('key2', new InstanceWrapper());
+            expect(wrapper.isDependencyTreeDurable()).to.be.false;
+          });
+        });
+        describe('when one is not static and non-durable', () => {
+          it('should return false', () => {
+            const wrapper = new InstanceWrapper();
+            wrapper.addPropertiesMetadata(
+              'key1',
+              new InstanceWrapper({ scope: Scope.REQUEST }),
+            );
+            wrapper.addPropertiesMetadata('key2', new InstanceWrapper());
+            expect(wrapper.isDependencyTreeDurable()).to.be.false;
+          });
+        });
+        describe('when one is not static and durable', () => {
+          it('should return true', () => {
+            const wrapper = new InstanceWrapper();
+            wrapper.addPropertiesMetadata(
+              'key1',
+              new InstanceWrapper({ scope: Scope.REQUEST, durable: true }),
+            );
+            wrapper.addPropertiesMetadata('key2', new InstanceWrapper());
+            expect(wrapper.isDependencyTreeDurable()).to.be.true;
+          });
+        });
+      });
+      describe('enhancers', () => {
+        describe('when each is static', () => {
+          it('should return false', () => {
+            const wrapper = new InstanceWrapper();
+            wrapper.addEnhancerMetadata(new InstanceWrapper());
+            wrapper.addEnhancerMetadata(new InstanceWrapper());
+            expect(wrapper.isDependencyTreeDurable()).to.be.false;
+          });
+        });
+        describe('when one is not static and non-durable', () => {
+          it('should return false', () => {
+            const wrapper = new InstanceWrapper();
+            wrapper.addEnhancerMetadata(
+              new InstanceWrapper({ scope: Scope.REQUEST }),
+            );
+            wrapper.addEnhancerMetadata(new InstanceWrapper());
+            expect(wrapper.isDependencyTreeDurable()).to.be.false;
+          });
+        });
+        describe('when one is not static and durable', () => {
+          it('should return true', () => {
+            const wrapper = new InstanceWrapper();
+            wrapper.addEnhancerMetadata(
+              new InstanceWrapper({ scope: Scope.REQUEST, durable: true }),
+            );
+            wrapper.addEnhancerMetadata(new InstanceWrapper());
+            expect(wrapper.isDependencyTreeDurable()).to.be.true;
+          });
+        });
+      });
+    });
+  });
+
   describe('isNotMetatype', () => {
     describe('when metatype is nil', () => {
       it('should return true', () => {
diff --git a/packages/core/test/injector/module.spec.ts b/packages/core/test/injector/module.spec.ts
index 04581b726..9bddc5aa1 100644
--- a/packages/core/test/injector/module.spec.ts
+++ b/packages/core/test/injector/module.spec.ts
@@ -30,7 +30,7 @@ describe('Module', () => {
     const setSpy = sinon.spy(collection, 'set');
     (module as any)._controllers = collection;
 
-    @Controller({ scope: Scope.REQUEST })
+    @Controller({ scope: Scope.REQUEST, durable: true })
     class Test {}
 
     module.addController(Test);
@@ -43,6 +43,7 @@ describe('Module', () => {
           name: 'Test',
           scope: Scope.REQUEST,
           metatype: Test,
+          durable: true,
           instance: null,
           isResolved: false,
         }),
@@ -66,6 +67,7 @@ describe('Module', () => {
           scope: undefined,
           metatype: TestProvider,
           instance: null,
+          durable: undefined,
           isResolved: false,
         }),
       ),
@@ -96,6 +98,7 @@ describe('Module', () => {
           token: TestProvider,
           scope: undefined,
           metatype: TestProvider,
+          durable: undefined,
           instance: null,
           isResolved: false,
         }),
@@ -155,7 +158,7 @@ describe('Module', () => {
 
   describe('addCustomClass', () => {
     const type = { name: 'TypeTest' };
-    const provider = { provide: type, useClass: type };
+    const provider = { provide: type, useClass: type, durable: true };
     let setSpy;
 
     beforeEach(() => {
@@ -174,6 +177,7 @@ describe('Module', () => {
             name: provider.provide.name,
             scope: undefined,
             metatype: type as any,
+            durable: true,
             instance: null,
             isResolved: false,
           }),
@@ -216,7 +220,7 @@ describe('Module', () => {
   describe('addCustomFactory', () => {
     const type = { name: 'TypeTest' };
     const inject = [1, 2, 3];
-    const provider = { provide: type, useFactory: type, inject };
+    const provider = { provide: type, useFactory: type, inject, durable: true };
 
     let setSpy;
     beforeEach(() => {
@@ -236,6 +240,7 @@ describe('Module', () => {
             name: provider.provide.name,
             scope: undefined,
             metatype: type as any,
+            durable: true,
             instance: null,
             isResolved: false,
             inject: inject as any,