microsoft / tsyringe

Lightweight dependency injection container for JavaScript/TypeScript
MIT License
5.03k stars 168 forks source link

@injectable resolved from a child container uses dependency from a sibling container #245

Open mkq opened 1 day ago

mkq commented 1 day ago

Describe the bug I register different basic services in different child containers of the root container. Then I resolve dependent @injectable services from each child container. I assume each of those should then be automatically registered in the corresponding child container, so it should also use the basic service from the corresponding child container.

Our use case: When we serve a request given some business entity id, we must detect the company by that id and make all requests to some other APIs to the base URL for the company. I.e. the same APIs are available for company A and B, only the base URLs differ. I thought the cleanest solution would be to put the (OpenAPI-generated axios based) API clients in two different containers and also have separate instances of each dependent service in the corresponding container. Then, I would only need to pick the right child container at the start of the request (by given business entity id), resolve the high-level services from it, and nowhere else do something like "if id matches …, use company A basic service, else company B basic service". The basic API clients require some programmatic setup, so I register them explicitely (registerInstance), while high-level services are @injectable() @singleton().

To Reproduce

import 'reflect-metadata'
import {container, inject, injectable, singleton} from 'tsyringe'
import {expect} from 'chai'

describe('tsyringe child containers', function () {
    it('puts @injectables in the child container on which I resolve and injects dependencies from that container', function () {
        const childContainerA = container.createChildContainer()
        childContainerA.registerInstance(BasicService, new BasicService('A'))

        const childContainerB = container.createChildContainer()
        childContainerB.registerInstance(BasicService, new BasicService('B'))

        const basicServiceA = childContainerA.resolve(BasicService)
        const basicServiceB = childContainerB.resolve(BasicService)
        console.log('basic services ===?', basicServiceA === basicServiceB) //false (as expected)
        const highLevelServiceA = childContainerA.resolve(HighLevelService)
        const highLevelServiceB = childContainerB.resolve(HighLevelService)
        console.log('high-level services ===?', highLevelServiceA === highLevelServiceB) //true (UNEXPECTED)

        const resultA = basicServiceA.getName()
        console.log('basicServiceA result', resultA) //"A" (as expected)
        const resultB = basicServiceB.getName()
        console.log('basicServiceB result', resultB) //"B" (as expected)
        const hiResultA = highLevelServiceA.get()
        console.log('highLevelServiceA result', hiResultA) //"Hi, A" (as expected)
        const hiResultB = highLevelServiceB.get()
        console.log('highLevelServiceB result', hiResultB) //"Hi, A" (UNEXPECTED)

        expect([resultA, resultB, hiResultA, hiResultB]).eql(['A', 'B', 'Hi, A', 'Hi, B'])
    })
})

class BasicService {
    constructor(private readonly name: string) {}

    getName(): string { return this.name }
}

@injectable() @singleton()
class HighLevelService {
    constructor(@inject(BasicService) private readonly basicService: BasicService) {}

    get(): string { return 'Hi, ' + this.basicService.getName() }
}

Expected behavior Test should pass, indicating that the HighLevelService resolved from childContainerB uses the new BasicService('B') registered in childContainerB.

Version: 4.8.0

mkq commented 1 day ago

65 looks related, but is about @autoInjectable.

sdudley commented 1 day ago

@mkq What happens if HighLevelService is registered with a lifecycle of Lifecycle.ContainerScoped rather than as a singleton?

mkq commented 15 hours ago

How dumb of me! ContainerScoped fixes it. Thanks for the quick response!