Merge pull request #15504 from hajekjiri/bug/fix-undefined-injection

fix(core): fix race condition in class dependency resolution from imported modules
This commit is contained in:
Kamil Mysliwiec
2025-08-07 08:54:48 +02:00
committed by GitHub
5 changed files with 113 additions and 146 deletions

View File

@@ -49,21 +49,42 @@ class TransientProvider {}
@Injectable()
class RequestProvider {}
@Injectable()
class ForeignTransientProvider {}
@Injectable()
export class Dependant {
constructor(
private readonly transientProvider: TransientProvider,
private readonly foreignTransientProvider: ForeignTransientProvider,
@Inject(RequestProvider)
private readonly requestProvider: RequestProvider,
) {}
public checkDependencies() {
expect(this.transientProvider).to.be.instanceOf(TransientProvider);
expect(this.foreignTransientProvider).to.be.instanceOf(
ForeignTransientProvider,
);
expect(this.requestProvider).to.be.instanceOf(RequestProvider);
}
}
@Global()
@Module({
providers: [
{
provide: ForeignTransientProvider,
scope: Scope.TRANSIENT,
useClass: ForeignTransientProvider,
},
],
exports: [ForeignTransientProvider],
})
export class ModuleWithForeignTransientProvider {}
@Global()
@Module({
providers: [
@@ -98,6 +119,12 @@ export class GlobalModuleWithRequestProvider {}
@Module({
imports: [
/*
* ForeginTransientProvider will be resolved quickly because its host module is imported first.
* IMPORTANT: Do not move this module, otherwise we may not catch future regressions.
*/
ModuleWithForeignTransientProvider,
GlobalModule1,
GlobalModule2,
GlobalModule3,
@@ -115,7 +142,7 @@ export class GlobalModuleWithRequestProvider {}
export class AppModule {}
describe('Many global modules', () => {
it('should inject request-scoped useFactory provider and transient-scoped useClass provider from different modules', async () => {
it('should inject request-scoped and transient-scoped providers from different modules', async () => {
const moduleBuilder = Test.createTestingModule({
imports: [AppModule],
});

View File

@@ -2263,6 +2263,22 @@
},
"id": "-1303681274"
},
"-831049991": {
"source": "594986539",
"target": "-1721730431",
"metadata": {
"type": "class-to-class",
"sourceModuleName": "InputModule",
"sourceClassName": "InputService",
"targetClassName": "CircularService",
"sourceClassToken": "InputService",
"targetClassToken": "CircularService",
"targetModuleName": "CircularModule",
"keyOrIndex": 0,
"injectionType": "constructor"
},
"id": "-831049991"
},
"-886102564": {
"source": "208171089",
"target": "671882984",
@@ -2280,6 +2296,22 @@
},
"id": "-886102564"
},
"-2146943494": {
"source": "-234035039",
"target": "928565345",
"metadata": {
"type": "class-to-class",
"sourceModuleName": "RequestChainModule",
"sourceClassName": "RequestChainService",
"targetClassName": "HelperService",
"sourceClassToken": "RequestChainService",
"targetClassToken": "HelperService",
"targetModuleName": "HelperModule",
"keyOrIndex": 0,
"injectionType": "constructor"
},
"id": "-2146943494"
},
"-2003045613": {
"source": "-377928898",
"target": "-616397055",
@@ -2312,38 +2344,6 @@
},
"id": "-881420795"
},
"-831049991": {
"source": "594986539",
"target": "-1721730431",
"metadata": {
"type": "class-to-class",
"sourceModuleName": "InputModule",
"sourceClassName": "InputService",
"targetClassName": "CircularService",
"sourceClassToken": "InputService",
"targetClassToken": "CircularService",
"targetModuleName": "CircularModule",
"keyOrIndex": 0,
"injectionType": "constructor"
},
"id": "-831049991"
},
"-2146943494": {
"source": "-234035039",
"target": "928565345",
"metadata": {
"type": "class-to-class",
"sourceModuleName": "RequestChainModule",
"sourceClassName": "RequestChainService",
"targetClassName": "HelperService",
"sourceClassToken": "RequestChainService",
"targetClassToken": "HelperService",
"targetModuleName": "HelperModule",
"keyOrIndex": 0,
"injectionType": "constructor"
},
"id": "-2146943494"
},
"-1816180282": {
"source": "-848516688",
"target": "-1673986099",

View File

@@ -2181,22 +2181,6 @@
},
"id": "1976848738"
},
"-2105726668": {
"source": "-1803759743",
"target": "1010833816",
"metadata": {
"type": "class-to-class",
"sourceModuleName": "PropertiesModule",
"sourceClassName": "PropertiesService",
"targetClassName": "token",
"sourceClassToken": "PropertiesService",
"targetClassToken": "token",
"targetModuleName": "PropertiesModule",
"keyOrIndex": "token",
"injectionType": "property"
},
"id": "-2105726668"
},
"-21463590": {
"source": "-1378706112",
"target": "1004276345",
@@ -2213,6 +2197,22 @@
},
"id": "-21463590"
},
"-2105726668": {
"source": "-1803759743",
"target": "1010833816",
"metadata": {
"type": "class-to-class",
"sourceModuleName": "PropertiesModule",
"sourceClassName": "PropertiesService",
"targetClassName": "token",
"sourceClassToken": "PropertiesService",
"targetClassToken": "token",
"targetModuleName": "PropertiesModule",
"keyOrIndex": "token",
"injectionType": "property"
},
"id": "-2105726668"
},
"-1657371464": {
"source": "-1673986099",
"target": "1919157847",
@@ -2247,6 +2247,22 @@
},
"id": "-1303681274"
},
"-831049991": {
"source": "594986539",
"target": "-1721730431",
"metadata": {
"type": "class-to-class",
"sourceModuleName": "InputModule",
"sourceClassName": "InputService",
"targetClassName": "CircularService",
"sourceClassToken": "InputService",
"targetClassToken": "CircularService",
"targetModuleName": "CircularModule",
"keyOrIndex": 0,
"injectionType": "constructor"
},
"id": "-831049991"
},
"-886102564": {
"source": "208171089",
"target": "671882984",
@@ -2264,6 +2280,22 @@
},
"id": "-886102564"
},
"-2146943494": {
"source": "-234035039",
"target": "928565345",
"metadata": {
"type": "class-to-class",
"sourceModuleName": "RequestChainModule",
"sourceClassName": "RequestChainService",
"targetClassName": "HelperService",
"sourceClassToken": "RequestChainService",
"targetClassToken": "HelperService",
"targetModuleName": "HelperModule",
"keyOrIndex": 0,
"injectionType": "constructor"
},
"id": "-2146943494"
},
"-2003045613": {
"source": "-377928898",
"target": "-616397055",
@@ -2296,38 +2328,6 @@
},
"id": "-881420795"
},
"-831049991": {
"source": "594986539",
"target": "-1721730431",
"metadata": {
"type": "class-to-class",
"sourceModuleName": "InputModule",
"sourceClassName": "InputService",
"targetClassName": "CircularService",
"sourceClassToken": "InputService",
"targetClassToken": "CircularService",
"targetModuleName": "CircularModule",
"keyOrIndex": 0,
"injectionType": "constructor"
},
"id": "-831049991"
},
"-2146943494": {
"source": "-234035039",
"target": "928565345",
"metadata": {
"type": "class-to-class",
"sourceModuleName": "RequestChainModule",
"sourceClassName": "RequestChainService",
"targetClassName": "HelperService",
"sourceClassToken": "RequestChainService",
"targetClassToken": "HelperService",
"targetModuleName": "HelperModule",
"keyOrIndex": 0,
"injectionType": "constructor"
},
"id": "-2146943494"
},
"-1816180282": {
"source": "-848516688",
"target": "-1673986099",

View File

@@ -682,14 +682,11 @@ export class Injector {
inquirerId,
);
if (!instanceHost.isResolved && !instanceWrapperRef.forwardRef) {
wrapper.settlementSignal?.insertRef(instanceWrapperRef.id);
await this.loadProvider(
instanceWrapperRef,
relatedModule,
contextId,
wrapper,
);
/*
* Provider will be loaded shortly in resolveComponentHost() once we pass the current
* Barrier. We cannot load it here because doing so could incorrectly evaluate the
* staticity of the dependency tree and lead to undefined / null injection.
*/
break;
}
}

View File

@@ -447,63 +447,6 @@ describe('Injector', () => {
),
).to.eventually.be.eq(null);
});
it('should call "loadProvider" when component is not resolved', async () => {
const moduleFixture = {
imports: new Map([
[
'key',
{
providers: {
has: () => true,
get: () =>
new InstanceWrapper({
isResolved: false,
}),
},
exports: {
has: () => true,
},
imports: new Map(),
},
],
] as any),
};
await injector.lookupComponentInImports(
moduleFixture as any,
metatype as any,
new InstanceWrapper(),
);
expect(loadProvider.called).to.be.true;
});
it('should not call "loadProvider" when component is resolved', async () => {
const moduleFixture = {
relatedModules: new Map([
[
'key',
{
providers: {
has: () => true,
get: () => ({
isResolved: true,
}),
},
exports: {
has: () => true,
},
relatedModules: new Map(),
},
],
] as any),
};
await injector.lookupComponentInImports(
moduleFixture as any,
metatype as any,
null!,
);
expect(loadProvider.called).to.be.false;
});
});
describe('resolveParamToken', () => {