diff --git a/aio/content/guide/deprecations.md b/aio/content/guide/deprecations.md
index fcc3bf33fc96b..de199704afb2a 100644
--- a/aio/content/guide/deprecations.md
+++ b/aio/content/guide/deprecations.md
@@ -36,6 +36,7 @@ v12 - v15
v13 - v16
v14 - v17
v15 - v18
+v16 - v19
-->
### Deprecated features that can be removed in v11 or later
@@ -115,6 +116,12 @@ v15 - v18
| `@angular/router` | [Router CanLoad guards](#router-can-load) | v15.1 | v17 |
| `@angular/router` | [class and `InjectionToken` guards and resolvers](#router-class-and-injection-token-guards) | v15.2 | v17 |
+### Deprecated features that can be removed in v18 or later
+
+| Area | API or Feature | Deprecated in | May be removed in |
+|:--- |:--- |:--- |:--- |
+| `@angular/core` | `EnvironmentInjector.runInContext` | v16 | v18 |
+
### Deprecated features with no planned removal version
| Area | API or Feature | Deprecated in | May be removed in |
@@ -170,6 +177,7 @@ In the [API reference section](api) of this site, deprecated APIs are indicated
| [`CompilerOptions.useJit and CompilerOptions.missingTranslation config options`](api/core/CompilerOptions) | none | v13 | Since Ivy, those config options are unused, passing them has no effect. |
| [`providedIn`](api/core/Injectable#providedIn) with NgModule | Prefer `'root'` providers, or use NgModule `providers` if scoping to an NgModule is necessary | v15 | none |
| [`providedIn: 'any'`](api/core/Injectable#providedIn) | none | v15 | This option has confusing semantics and nearly zero usage. |
+| [`EnvironmentInjector.runInContext`](api/core/EnvironmentInjector#runInContext) | `runInInjectionContext` | v16 | `runInInjectionContext` is a more flexible operation which supports element injectors as well |
@@ -377,7 +385,7 @@ be converted to functions by instead using `inject` to get dependencies.
For testing a function `canActivate` guard, using `TestBed` and `TestBed.runInInjectionContext` is recommended.
Test mocks and stubs can be provided through DI with `{provide: X, useValue: StubX}`.
Functional guards can also be written in a way that's either testable with
-`runInContext` or by passing mock implementations of dependencies.
+`runInInjectionContext` or by passing mock implementations of dependencies.
For example:
```
diff --git a/goldens/public-api/core/index.md b/goldens/public-api/core/index.md
index 1c3c2e9f0e494..d8f92aea4972b 100644
--- a/goldens/public-api/core/index.md
+++ b/goldens/public-api/core/index.md
@@ -528,6 +528,7 @@ export abstract class EnvironmentInjector implements Injector {
abstract get(token: ProviderToken, notFoundValue?: T, flags?: InjectFlags): T;
// @deprecated (undocumented)
abstract get(token: any, notFoundValue?: any): any;
+ // @deprecated
abstract runInContext(fn: () => ReturnT): ReturnT;
}
@@ -1284,6 +1285,9 @@ export interface ResolvedReflectiveProvider {
// @public
export function resolveForwardRef(type: T): T;
+// @public
+export function runInInjectionContext(injector: Injector, fn: () => ReturnT): ReturnT;
+
// @public
export abstract class Sanitizer {
// (undocumented)
diff --git a/packages/core/src/di/contextual.ts b/packages/core/src/di/contextual.ts
new file mode 100644
index 0000000000000..da3f003b7394c
--- /dev/null
+++ b/packages/core/src/di/contextual.ts
@@ -0,0 +1,39 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import type {Injector} from './injector';
+import {setCurrentInjector} from './injector_compatibility';
+import {setInjectImplementation} from './inject_switch';
+import {R3Injector} from './r3_injector';
+
+/**
+ * Runs the given function in the context of the given `Injector`.
+ *
+ * Within the function's stack frame, `inject` can be used to inject dependencies from the given
+ * `Injector`. Note that `inject` is only usable synchronously, and cannot be used in any
+ * asynchronous callbacks or after any `await` points.
+ *
+ * @param injector the injector which will satisfy calls to `inject` while `fn` is executing
+ * @param fn the closure to be run in the context of `injector`
+ * @returns the return value of the function, if any
+ * @publicApi
+ */
+export function runInInjectionContext(injector: Injector, fn: () => ReturnT): ReturnT {
+ if (injector instanceof R3Injector) {
+ injector.assertNotDestroyed();
+ }
+
+ const prevInjector = setCurrentInjector(injector);
+ const previousInjectImplementation = setInjectImplementation(undefined);
+ try {
+ return fn();
+ } finally {
+ setCurrentInjector(prevInjector);
+ setInjectImplementation(previousInjectImplementation);
+ }
+}
diff --git a/packages/core/src/di/index.ts b/packages/core/src/di/index.ts
index 6e501ca23fa9c..68d1fd14aec65 100644
--- a/packages/core/src/di/index.ts
+++ b/packages/core/src/di/index.ts
@@ -13,6 +13,7 @@
*/
export * from './metadata';
+export {runInInjectionContext} from './contextual';
export {InjectFlags} from './interface/injector';
export {ɵɵdefineInjectable, defineInjectable, ɵɵdefineInjector, InjectableType, InjectorType} from './interface/defs';
export {forwardRef, resolveForwardRef, ForwardRefFn} from './forward_ref';
diff --git a/packages/core/src/di/r3_injector.ts b/packages/core/src/di/r3_injector.ts
index 8ceb5fe1a957f..e61bb1f5a6583 100644
--- a/packages/core/src/di/r3_injector.ts
+++ b/packages/core/src/di/r3_injector.ts
@@ -118,6 +118,7 @@ export abstract class EnvironmentInjector implements Injector {
*
* @param fn the closure to be run in the context of this injector
* @returns the return value of the function, if any
+ * @deprecated use the standalone function `runInInjectionContext` instead
*/
abstract runInContext(fn: () => ReturnT): ReturnT;
@@ -327,7 +328,7 @@ export class R3Injector extends EnvironmentInjector {
return `R3Injector[${tokens.join(', ')}]`;
}
- private assertNotDestroyed(): void {
+ assertNotDestroyed(): void {
if (this._destroyed) {
throw new RuntimeError(
RuntimeErrorCode.INJECTOR_ALREADY_DESTROYED,
diff --git a/packages/core/test/acceptance/di_spec.ts b/packages/core/test/acceptance/di_spec.ts
index 747b21615d946..f661180c36fa9 100644
--- a/packages/core/test/acceptance/di_spec.ts
+++ b/packages/core/test/acceptance/di_spec.ts
@@ -7,7 +7,7 @@
*/
import {CommonModule} from '@angular/common';
-import {Attribute, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, createEnvironmentInjector, createNgModule, Directive, ElementRef, ENVIRONMENT_INITIALIZER, EnvironmentInjector, EventEmitter, forwardRef, Host, HostBinding, ImportedNgModuleProviders, importProvidersFrom, ImportProvidersSource, inject, Inject, Injectable, InjectFlags, InjectionToken, InjectOptions, INJECTOR, Injector, Input, LOCALE_ID, makeEnvironmentProviders, ModuleWithProviders, NgModule, NgZone, Optional, Output, Pipe, PipeTransform, Provider, Self, SkipSelf, TemplateRef, Type, ViewChild, ViewContainerRef, ViewEncapsulation, ViewRef, ɵcreateInjector as createInjector, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵINJECTOR_SCOPE, ɵInternalEnvironmentProviders as InternalEnvironmentProviders} from '@angular/core';
+import {Attribute, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, createEnvironmentInjector, createNgModule, Directive, ElementRef, ENVIRONMENT_INITIALIZER, EnvironmentInjector, EventEmitter, forwardRef, Host, HostBinding, ImportedNgModuleProviders, importProvidersFrom, ImportProvidersSource, inject, Inject, Injectable, InjectFlags, InjectionToken, InjectOptions, INJECTOR, Injector, Input, LOCALE_ID, makeEnvironmentProviders, ModuleWithProviders, NgModule, NgModuleRef, NgZone, Optional, Output, Pipe, PipeTransform, Provider, runInInjectionContext, Self, SkipSelf, TemplateRef, Type, ViewChild, ViewContainerRef, ViewEncapsulation, ViewRef, ɵcreateInjector as createInjector, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵINJECTOR_SCOPE, ɵInternalEnvironmentProviders as InternalEnvironmentProviders} from '@angular/core';
import {ViewRef as ViewRefInternal} from '@angular/core/src/render3/view_ref';
import {TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
@@ -3691,6 +3691,125 @@ describe('di', () => {
});
});
+ describe('runInInjectionContext', () => {
+ it('should return the function\'s return value', () => {
+ const injector = TestBed.inject(EnvironmentInjector);
+ const returnValue = runInInjectionContext(injector, () => 3);
+ expect(returnValue).toBe(3);
+ });
+
+ it('should work with an NgModuleRef injector', () => {
+ const ref = TestBed.inject(NgModuleRef);
+ const returnValue = runInInjectionContext(ref.injector, () => 3);
+ expect(returnValue).toBe(3);
+ });
+
+ it('should return correct injector reference', () => {
+ const ngModuleRef = TestBed.inject(NgModuleRef);
+ const ref1 = runInInjectionContext(ngModuleRef.injector, () => inject(Injector));
+ const ref2 = ngModuleRef.injector.get(Injector);
+ expect(ref1).toBe(ref2);
+ });
+
+ it('should make inject() available', () => {
+ const TOKEN = new InjectionToken('TOKEN');
+ const injector = createEnvironmentInjector(
+ [{provide: TOKEN, useValue: 'from injector'}], TestBed.inject(EnvironmentInjector));
+
+ const result = runInInjectionContext(injector, () => inject(TOKEN));
+ expect(result).toEqual('from injector');
+ });
+
+ it('should properly clean up after the function returns', () => {
+ const TOKEN = new InjectionToken('TOKEN');
+ const injector = TestBed.inject(EnvironmentInjector);
+ runInInjectionContext(injector, () => {});
+ expect(() => inject(TOKEN, InjectFlags.Optional)).toThrow();
+ });
+
+ it('should properly clean up after the function throws', () => {
+ const TOKEN = new InjectionToken('TOKEN');
+ const injector = TestBed.inject(EnvironmentInjector);
+ expect(() => runInInjectionContext(injector, () => {
+ throw new Error('crashes!');
+ })).toThrow();
+ expect(() => inject(TOKEN, InjectFlags.Optional)).toThrow();
+ });
+
+ it('should set the correct inject implementation', () => {
+ const TOKEN = new InjectionToken('TOKEN', {
+ providedIn: 'root',
+ factory: () => 'from root',
+ });
+
+ @Component({
+ standalone: true,
+ template: '',
+ providers: [{provide: TOKEN, useValue: 'from component'}],
+ })
+ class TestCmp {
+ envInjector = inject(EnvironmentInjector);
+
+ tokenFromComponent = inject(TOKEN);
+ tokenFromEnvContext = runInInjectionContext(this.envInjector, () => inject(TOKEN));
+
+ // Attempt to inject ViewContainerRef within the environment injector's context. This should
+ // not be available, so the result should be `null`.
+ vcrFromEnvContext = runInInjectionContext(
+ this.envInjector, () => inject(ViewContainerRef, InjectFlags.Optional));
+ }
+
+ const instance = TestBed.createComponent(TestCmp).componentInstance;
+ expect(instance.tokenFromComponent).toEqual('from component');
+ expect(instance.tokenFromEnvContext).toEqual('from root');
+ expect(instance.vcrFromEnvContext).toBeNull();
+ });
+
+ it('should support node injectors', () => {
+ @Component({
+ standalone: true,
+ template: '',
+ })
+ class TestCmp {
+ injector = inject(Injector);
+
+ vcrFromEnvContext =
+ runInInjectionContext(this.injector, () => inject(ViewContainerRef, {optional: true}));
+ }
+
+ const instance = TestBed.createComponent(TestCmp).componentInstance;
+ expect(instance.vcrFromEnvContext).not.toBeNull();
+ });
+
+ it('should be reentrant', () => {
+ const TOKEN = new InjectionToken('TOKEN', {
+ providedIn: 'root',
+ factory: () => 'from root',
+ });
+
+ const parentInjector = TestBed.inject(EnvironmentInjector);
+ const childInjector =
+ createEnvironmentInjector([{provide: TOKEN, useValue: 'from child'}], parentInjector);
+
+ const results = runInInjectionContext(parentInjector, () => {
+ const fromParentBefore = inject(TOKEN);
+ const fromChild = runInInjectionContext(childInjector, () => inject(TOKEN));
+ const fromParentAfter = inject(TOKEN);
+ return {fromParentBefore, fromChild, fromParentAfter};
+ });
+
+ expect(results.fromParentBefore).toEqual('from root');
+ expect(results.fromChild).toEqual('from child');
+ expect(results.fromParentAfter).toEqual('from root');
+ });
+
+ it('should not function on a destroyed injector', () => {
+ const injector = createEnvironmentInjector([], TestBed.inject(EnvironmentInjector));
+ injector.destroy();
+ expect(() => runInInjectionContext(injector, () => {})).toThrow();
+ });
+ });
+
it('should be able to use Host in `useFactory` dependency config', () => {
// Scenario:
// ---------