InversifyJS icon indicating copy to clipboard operation
InversifyJS copied to clipboard

Can't implement circular dependencies

Open hyze2d opened this issue 6 years ago • 9 comments

I need to implement circular dependency. But workarounds in docs don't work for me. I used lazyServiceIdentifie LazyServiceIdentifer, lazyInject but there some errors.

For example:

A injects B, B injects A.

Expected Behavior

A injected in B. B injected in A.

Current Behavior

I got error when use lazyInject

TypeError: Cannot read property 'get' of undefined
    at resolve (webpack-internal:///./node_modules/inversify-inject-decorators/lib/decorators.js:30:34)
    at AppStore.getter [as http] (webpack-internal:///./node_modules/inversify-inject-decorators/lib/decorators.js:7:47)
	at AppStore.remoteFunction (<anonymous>:2:14)

Possible Solution

Steps to Reproduce (for bugs)

  1. Create two classes marked @injectable
  2. Create decorators with getDecorators from inversify-inject-decorators
  3. Use @lazyInject to inject them to each other

Context

I try configure us common service, but to do it i need to use some methods from another service.

Your Environment

  • Version used:
  • Environment name and version (e.g. Chrome 39, node.js 5.4):
  • Operating System and version (desktop or mobile):
  • Link to your project:

Stack trace

hyze2d avatar Apr 29 '18 08:04 hyze2d

The same case I wanted to reproduce with @lazyInject but unfortunately, it doesn't work. I have another error but still. Here is a sample of my code:

import { FooService } from "../services/FooService";
import { HelperService } from "../services/HelperService";

export const container = new Container();
export const { lazyInject } = getDecorators(container, false);
import { FooService } from "../services/FooService";
import { HelperService } from "../services/HelperService";

container.load(buildProviderModule());
container.get<FooService>(FooService);
container.get<HelperService>(HelperService);

======================================

@provideSingleton(FooService)
export class FooService {

    @lazyInject(HelperService) helperService: HelperService;

    constructor() {
        setTimeout(() => {
            console.log(this.helperService);
        }, 2000)
    }

}

@provideSingleton(HelperService)
export class HelperService {

    @lazyInject(FooService) fooService: FooService;

    constructor() {
        setTimeout(() => {
            console.log(this.fooService);
        }, 3000)
    }

}

So at runtime, I am getting an error. throw new Error(ERROR_MSGS.NULL_ARGUMENT); Error: NULL argument It is thrown becouse FooService in the HelperService is null. But interesting thing is that HelperService in the FooService is defined. So I tried to investigate what is the reason for such behavior. And the reason is an order of imports of this services into the main file like app.ts or inversify.config.ts

import { FooService } from "../services/FooService";
import { HelperService } from "../services/HelperService";

Property service is allways undefined in second imported service. I think its not correct behavior of lazyInjection.

Your Environment

    "node": "9.11.1"
    "inversify": "4.13.0",
    "inversify-binding-decorators": "4.0.0",
    "inversify-express-utils": "6.1.0",
    "inversify-inject-decorators": "^3.1.0",
    "inversify-logger-middleware": "3.1.0",

Halynsky avatar Aug 30 '18 03:08 Halynsky

LazyServiceIdentifer is not a tool for implement circular depencies. It is used to reference a injection token that has not been defined yet.

https://github.com/inversify/InversifyJS/blob/master/wiki/circular_dependencies.md

Although, using property injection with LazyServiceIdentifer do have bug(producing wrong error message), which I fixed it in https://github.com/inversify/InversifyJS/pull/1177

csr632 avatar Dec 14 '19 07:12 csr632

Any update on this? This doesn't seem to be fixed. Is there any sort of workaround?

JaceyPenny avatar Jan 30 '20 07:01 JaceyPenny

Up, Is this fixed ?

Limule avatar Jul 09 '20 15:07 Limule

still a problem

vzkhrv avatar Jul 30 '20 06:07 vzkhrv

any updates ?

mohsinamjad avatar Jan 07 '21 08:01 mohsinamjad

any update?

coder-palak avatar Jul 20 '21 09:07 coder-palak

Same issue.
Here is my workaround. Let's say there are two classes Leader and Follower. These two classes depend on each other. In the inversify.config.ts file:

//Leader is the class, ILeader is the interface and TYPES.LEADER is the symbol
container
  .bind<ILeader>(TYPES.LEADER)
  .to(Leader);

container
  .bind<IFollower>(TYPES.FOLLOWER)
  .to(Follower)
  .inSingletonScope();

Then in class Follower, manually define a lazyInject method

class Follower{
  private leader:ILeader;

  lazyInject(){
    this.leader = container.get<ILeader>(TYPES.LEADER);
  }
}

Then in your index.ts or loader.ts or any file that you believe the container is initiated, call the lazyInject.

const follower = container.get<IFollower>(TYPES.FOLLWER);
follower.lazyInject();

Combo819 avatar Oct 18 '21 03:10 Combo819

In my case the problem was in the incorrect usage of the container itself, and - of course - in the accidental circular dependency. (see: solution at the bottom of this comment)


I had the following structure:

\ services
 \ ServiceA
  | index.ts
  | ServiceA.ts
  | ServiceA.contracts.ts
  | ServiceA.composable.ts
 \ ServiceB
  | index.ts
  | ServiceB.ts
  | ServiceB.contracts.ts
  | ServiceB.composable.ts

In the above "graph":

  • services is just a directory;
  • services/Service[A|B] directory contains source files for the Service[A|B];
  • services/Service[A|B].ts file contains a declaration of the main Service[A|B] class;
  • services/Service[A|B].contracts.ts file contains the interfaces, enums, and other contracts used by the Service[A|B];
  • services/Service[A|B].composable.ts file contains the declaration of the simple composable function that's capable of resolving the Service[A|B] from the IoC container;
  • services/Service[A|B]/index.ts file exports everything from its adjacent (sibling) files.
// Example body of the `services/Service[A|B]/index.ts` file

export * from './Service[A|B].ts'
export * from './Service[A|B].composable.ts' // Memorize this export! 🧠
export * from './Service[A|B].contracts.ts'
// Example body of the `services/Service[A|B]/Service[A|B].composable.ts` file

import { container } from 'src/inversify.config.ts' // Memorize this import! 🧠

import { Service[A|B], Service[A|B]Type } from './Service[A|B]' // `Service[A|B]Type` is a `symbol`
import { IService[A|B] } from './Service[A|B].contracts' // `IService[A|B]` is an interface

/* @example - usage:
 *   const { ServiceA } = useServiceA() // Easy peasy lemon squeezy, isn't it? 😉
 */
export const useService[A|B] = (): { Service[A|B]: IService[A|B] } => {
  const Service[A|B]: IService[A|B] = container.get<IService[A|B]>(Service[A|B]Type)
  return { Service[A|B] }
}

Keep in mind all the above information and consider the following situation:

  1. Somebody wants to use the ServiceA.
  2. To do this, they import the useServiceA composable from services/ServiceA (a shorthand for services/ServiceA/index.ts).
  3. The useServiceA composable depends on the container constant imported from the src/inversify.config.ts, so every time somebody wants to use one of the composables, they implicitly import the whole container.

Now, let's examine the inversify.config.ts file:

import 'reflect-metadata'

import { Container } from 'inversify'

import { IServiceA, ServiceA, ServiceAType } from './services/ServiceA' // Memorize this import! 🧠
import { IServiceB, ServiceB, ServiceBType } from './services/ServiceB' // Memorize this import! 🧠

const container = new Container()

container.bind<IServiceA>(ServiceAType).to(ServiceA)
container.bind<IServiceB>(ServiceBType).to(ServiceB)

Read the above file thoroughly, and then consider the following:

  1. Somebody requires useServiceA composable.
  2. useServiceA composable requires container.
  3. container requires all of the services/ServiceA and services/ServiceB, including their composable functions.
  4. Exported composables of course require the container again...
  5. ...and so on 🙃

Solution?

In my case, I had to stop using the composable functions and depend directly on the container constant. It meant that I needed to refactor the convenient const { Service[A|B] } = useService[A|B]() to a slightly longer const Service[A|B]: IService[A|B] = container.get<IService[A|B]>(Service[A|B]Type). This is not only longer, but also requires 2 more imports. However, at the same time, it prevents the circular dependency 😛


I know it's a lot of text, but if you'd like to learn more, feel free to contact me via e-mail. I'll try to explain better face-to-face 🙂 I didn't want to write this piece at all, but I felt like I should contribute to the community, and to pay my share 😝

staszek998 avatar May 08 '23 12:05 staszek998