inversify / InversifyJS

A powerful and lightweight inversion of control container for JavaScript & Node.js apps powered by TypeScript.
http://inversify.io/
MIT License
11.02k stars 712 forks source link

Major bug, request scope in a singleton class is not working #1549

Open veeramarni opened 7 months ago

veeramarni commented 7 months ago

request-scoped dependency is not working in a singleton class.

Expected Behavior

Writing a test case to validate the behavior of a singleton class with a request-scoped dependency involves simulating different request contexts and verifying that the singleton class behaves according to the request-scoped dependency. Here's a basic example using a hypothetical Node.js environment with Inversify for dependency injection and a testing framework like Jest.

Current Behavior

Request scope dependency is not refreshing with new value.

Steps to Reproduce (for bugs)

Singleton Service: MySingletonService is a singleton and should be the same instance in both contexts. Request-Scoped Dependency: MyRequestScopedDependency is request-scoped, so it should have different values in different contexts.

import { Container, inject, injectable } from 'inversify';
import 'reflect-metadata';
import { Context } from 'inversify/lib/planning/context';

const TYPES = {
    MyRequestScopedDependency: Symbol.for('MyRequestScopedDependency'),
    MySingletonService: Symbol.for('MySingletonService'),
};

@injectable()
class MyRequestScopedDependency {
    public value: number;

    constructor() {
        this.value = Math.random();
    }
}

@injectable()
class MySingletonService {
    private requestScopedDependency: MyRequestScopedDependency;

    constructor(@inject(TYPES.MyRequestScopedDependency) requestScopedDependency: MyRequestScopedDependency) {
        this.requestScopedDependency = requestScopedDependency;
    }

    getRequestScopedValue() {
        return this.requestScopedDependency.value;
    }
}

const container = new Container();
container
    .bind<MyRequestScopedDependency>(TYPES.MyRequestScopedDependency)
    .to(MyRequestScopedDependency)
    .inRequestScope();
container.bind<MySingletonService>(TYPES.MySingletonService).to(MySingletonService).inSingletonScope();

describe('Singleton with Request-Scoped Dependency', () => {
    it('should have different request-scoped dependencies in different contexts', () => {
        // Simulate two different requests
        const request1Context = new Context(container);
        const request2Context = new Context(container);

        const singletonService1 = request1Context.container.get<MySingletonService>(TYPES.MySingletonService);
        const singletonService2 = request2Context.container.get<MySingletonService>(TYPES.MySingletonService);

        // Both instances should be the same (singleton)
        expect(singletonService1).toBe(singletonService2);
        console.log('VALUE__', singletonService1.getRequestScopedValue(), singletonService2.getRequestScopedValue() )
        // But their request-scoped dependencies should be different <----THIS TEST FAILS
        expect(singletonService1.getRequestScopedValue()).not.toBe(singletonService2.getRequestScopedValue());
    });
});

Your Environment

veeramarni commented 7 months ago

This might be my workaround

const TYPES = {
    RequestScopedDependencyFactory: Symbol.for('RequestScopedDependencyFactory'),
    MyRequestScopedDependency: Symbol.for('MyRequestScopedDependency'),
    MySingletonService: Symbol.for('MySingletonService'),
};

@injectable()
class MyRequestScopedDependency {
    public value: number;

    constructor() {
        this.value = Math.random();
    }
}

@injectable()
class MySingletonService {
    private getRequestScopedDependency: () => MyRequestScopedDependency;

    constructor(
        @inject(TYPES.RequestScopedDependencyFactory)
        getRequestScopedDependencyFactory: () => MyRequestScopedDependency,
    ) {
        this.getRequestScopedDependency = getRequestScopedDependencyFactory;
    }

    getRequestScopedValue() {
        return this.getRequestScopedDependency().value;
    }
}

const container = new Container();

container
    .bind<() => MyRequestScopedDependency>(TYPES.RequestScopedDependencyFactory)
    .toFactory<MyRequestScopedDependency>(
        (context: interfaces.Context) => () =>
            context.container.get<MyRequestScopedDependency>(TYPES.MyRequestScopedDependency),
    );

container
    .bind<MyRequestScopedDependency>(TYPES.MyRequestScopedDependency)
    .to(MyRequestScopedDependency)
    .inRequestScope();
container.bind<MySingletonService>(TYPES.MySingletonService).to(MySingletonService).inSingletonScope();