inversify / InversifyJS

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

Feature Question - Dynamic Injection #1088

Open patran opened 5 years ago

patran commented 5 years ago

We use inversify with express. We would like to get the right implementation @inject'ed based on parameters coming in from the HTTP request. For example,

The typescript interface MyFooService is implemented by MyFooServiceAbc and MyFooServiceCed

The controller MyFooController would have MyFooSevice @inject'ed. However, we would like to select, at run-time and be able to choose whether MyFooServiceAbc or MyFooServiceCed should be @inject'ed.

What would be an elegant approach?

Thank-you!

cinjoff commented 5 years ago

I'm also interested in the very same question. @patran did you find some clean solution to this in the meantime?

john-landgrave commented 4 years ago

Running into this same issue and realizing that I'm effectively using the container as a Service Locator in my persistence layer.

Basic use case is that I have a facade object that implements an IPersistenceLayer (boundary of the application) but according to SRP the logic of each query is located in a separate class, and then located, via .get<T> in my facade.

This is pretty textbook service locator, but it seems better to me than my facade directly importing each query implementation class.

I'd love a way to just inject all of my queries and then filter to the one that implements a particular interface, but that doesn't seem possible at runtime due to the lack of types at runtime, right?

julienvedebe commented 4 years ago

Same issue here i'm wandering how to implement this case the right way. I translate a C# service that produce a report each reports are some kind of plugins (addins).

So after looking after a kind of plugin system on node I found inversify witch is really elegant and fine for my need.

Reports that I have inherit a base class that have the main function to process the report, each report could overwrites some methods and must implement some abstract (could also declare delegate to have some hooks).

So current app I wrote in Typescript knows what symbol to load and as we can't do some dynamic @inject i'm using toFactory that do the container.get inside to attach correct report.

According to best practice the only time you call the container get is for loading the root element and for the rest we should let @inject decorator do the work.

Is someone could point us in the right direction or confirm that we must use the get ?

Thanks

millimoose commented 4 years ago

Based on what and where do you determine which one gets injected? I'll call the parameter that determines it the "tag" and for the sake of the explanation make it a string that's either "abc" or "def".

Depending on that, you could either inject a factory with the signature (tag: 'abc'|'def') => MyFooService. This is appropriate if you only have the tag available at the point of use.

You can also use hierarchical containers; i.e. in your main container, you leave MyFooService unbound. Then in whichever layer the "tag" is available - say your facades, you create a child container of your root container. (Tip: bind a () => Container factory in your root container that does this, and inject it into your facades.) Then you bind MyFooService to whichever variant is correct in the child container, and .get() the service you're delegating the actual work to as one does in facades.

Mind that the latter will probably only work if your services are stateless and registered as transient or request scoped. You can't resolve a short-lived service from a longer-lived one without using a factory.

millimoose commented 4 years ago

@john-landgrave - There's nothing stopping you from adding your own type identifier to the query object, and you can then use multiInject.

Possibly excessive code example:

import {Container, inject, multiInject, injectable} from 'inversify';

type QueryVariant = 'foo'|'bar';

interface IQuery<TVariant extends QueryVariant = QueryVariant> {
  variant: TVariant;
  run(): void;
}
const IQuery = Symbol('IQuery');

interface IFooQuery extends IQuery<'foo'> {

}
const IFooQuery = Symbol('IFooQuery');
@injectable()
class FooQuery implements IFooQuery {
  variant = 'foo' as const
  run() {
    console.log('foo')
  }
}

interface IBarQuery extends IQuery<'bar'> {

}
const IBarQuery = Symbol('IBarQuery');
@injectable()
class BarQuery implements IBarQuery {
  variant = 'bar' as const;
  run() {
    console.log('bar');
  }
}

@injectable()
class Service {
  constructor(@multiInject(IQuery) private _queries: IQuery[]) {  }
  run(variant: QueryVariant) {
    for (const q of this._queries.filter(({variant: v}) => v === variant)) {
      q.run();
    }
  }
}

const c = new Container();
c.bind(IFooQuery).to(FooQuery);
c.bind(IBarQuery).to(BarQuery);
c.bind(IQuery).toService(IFooQuery);
c.bind(IQuery).toService(IBarQuery);
c.bind(Service).toSelf();

c.get(Service).run('foo');
c.get(Service).run('bar');