Open nicomouss opened 5 years ago
I cannot update the docs, but maybe I can clarify it a little for you. A 'Request scope' is not bound to a 'http request', but to a 'container request', which is a container.get or container.resolve call.
Suppose you have dependencies like this: A -> B -> R -> C -> R
So A has a dependency on B and C, and B and C both have a dependency on R.
Using only transient scope, if you get A from the container, B and C will both receive a different instance of R. If you bind R to RequestScope, B and C will both receive a reference to the same instance, as in a.b.r === a.c.r
If you resolve A again in the container, this will create a new instance of R, just like it will create new instances of A, B and C.
@allardmuis That makes sense now, thanks for clarification. This explanation should definitely go into the documentation.
I cannot update the docs
I guess one possibility is to submit a PR with the updated docs
@allardmuis
import 'reflect-metadata';
import { Container, injectable, inject } from 'inversify';
const container = new Container({
defaultScope: 'Request'
});
@injectable()
class A {}
@injectable()
class B {
@inject(A) a1!: A;
@inject(A) a2!: A;
a3: A = container.get(A);
a4: A = container.get(A);
}
container.bind(A).toSelf();
container.bind(B).toSelf();
const b: B = container.get(B);
console.log(b.a1 === b.a2); // true
console.log(b.a3 === b.a4); // false
thanks for clarification, but why b.a3
not equal b.a4
?
so I presume request scope creates instances that lasts a specific scope (an Http request for example)
It took me a while to figure out how to achieve this with inversify and the solution was surprisingly simple and performant:
where your "request scope" starts (e.g. the first middleware in express after auth or something of that sort): 1) clone you container like so:
const requestContainer = new Container();
(requestContainer as any)._bindingDictionary._map = new Map(
(container as any)._bindingDictionary._map
);
(requestContainer as any)._isRequestContainer = true; // just in case :-)
// requestContainer.rebind - bind/rebind whatevers needed
return requestContainer;
2) in the cloned container rebind services that depend on the scope's context, e.g. user context
3) pass it down the line (e.g. assign to req.container)
4) now you container and all the following resolutions (via container.get, @inject
, etc) are context-aware
@iamdanthedev we create a child container for our http request scope and bind any request scoped dependencies to that child container (and make it available via the request). That works well for us to have app and request scoped dependencies.
so I presume request scope creates instances that lasts a specific scope (an Http request for example)
It took me a while to figure out how to achieve this with inversify and the solution was surprisingly simple and performant:
where your "request scope" starts (e.g. the first middleware in express after auth or something of that sort):
- clone you container like so:
const requestContainer = new Container(); (requestContainer as any)._bindingDictionary._map = new Map( (container as any)._bindingDictionary._map ); (requestContainer as any)._isRequestContainer = true; // just in case :-) // requestContainer.rebind - bind/rebind whatevers needed return requestContainer;
- in the cloned container rebind services that depend on the scope's context, e.g. user context
- pass it down the line (e.g. assign to req.container)
- now you container and all the following resolutions (via container.get,
@inject
, etc) are context-aware
Thanks for the solution, @iamdanthedev. Just to add some resilience for case the underlying lookup data structure is changed, you can use Lookup.clone.
import { Container } from 'inversify';
const createReqContainerHack = (container: Container): Container => {
const reqContainer = new Container();
(reqContainer as any)._bindingDictionary = (container as any)._bindingDictionary.clone();
return reqContainer;
};
There is createChild()
method on the container which supports this type of scenario.
There is
createChild()
method on the container which supports this type of scenario.
createChild()
sets the container's parent
property, which may have unintended side effects. It also does not copy the _bindingDictionary
, so you cannot unbind services on the child container.
It seems to me that this should be built-in functionality then?
It seems to me that this should be built-in functionality then?
@pigulla sure it should be like typedi new Container.of(requestId)
because I don't like using hacks into the basic service container initialization.
createChild() sets the container's parent property, which may have unintended side effects. It also does not copy the _bindingDictionary, so you cannot unbind services on the child container.
Not sure, if it's correct but if you create a separate container with createChild()
for every request you can unbind services on the container. The parent is only a pointer to a fallback container when the identifier can't be resolved. Sure, you shouldn't overwrite bindings on the parent in a request context. This isn't safe.
I ended up creating the ScopedContainer
class, as seen below, to make it more obvious to configure and easier to manage.
// register-global-dependencies.ts
ScopedContainer.globalContainer = (() => {
const container = new Container();
container
.bind<SomeSingletonDep>(TOKENS.SomeSingletonDep)
.to(SomeSingletonDep)
.inSingletonScope();
return container;
})();
// register-scoped-dependencies.ts
import "register-global-dependencies";
ScopedContainer.postConfigure((container) => {
container
.bind<RequestSpecificDep>(TOKENS.RequestSpecificDep)
.to(RequestSpecificDep)
.inSingletonScope();
});
// lambda-handler.ts
import "register-scoped-dependencies";
handler = (event, context) => {
const requestId = event.requestContext.requestId;
const container = ScopedContainer.for(requestId);
try {
// This will be the same for every request
const singletonDep = container.get(TOKENS.SomeSingletonDep);
// And this will be a new instance for every request
const requestSpecificDep = container.get(TOKENS.RequestSpecificDep);
}
finally {
ScopedContainer.remove(requestId);
}
}
This is the ScopedContainer
class:
import { Container, interfaces } from "inversify";
const DEFAULT_SCOPE_ID = "__default__";
export type PostConfigureAction = (container: Container) => void;
export type ScopedContainerCache = {
[id: string]: Container;
};
export class ScopedContainer {
private static _postConfigureActions: PostConfigureAction[] = [];
private static readonly _instances: ScopedContainerCache = {};
/**
* Options object to use when creating a new container for a
* scope ID.
*/
static containerOptions: interfaces.ContainerOptions;
/**
* A global container instance, which enables truly
* singleton instances when using a scoped container. All scoped
* containers reference the global container as parent.
*
* @example
* ScopedContainer.globalContainer = (() => {
* const container = new Container();
* container
* .bind<ClassInSingletonScope>(TOKENS.ClassInSingletonScope)
* .to(ClassInSingletonScope)
* .inSingletonScope();
* return container;
* })();
*/
static globalContainer: Container;
/**
* Optional function to configure the global container.
* An alternative to directly setting the @see globalContainer property.
*/
static configureGlobalContainer: (container: Container) => void;
/**
* Returns a @see Container that is unique to the specified scope.
* If this is the first time getting the container for the scope, then a
* new container will be created using the provided factory. Any post configure
* actions will also be applied to the new container instance.
* @param scopeId Any string to identify the scope (e.g. current request ID).
* @returns A @see Container that is unique to the specified scope.
*/
static for(scopeId = DEFAULT_SCOPE_ID): Container {
let container = this._instances[scopeId];
if (!container) {
container = this.makeNewContainer();
this._instances[scopeId] = container;
}
return container;
}
/**
* Unbinds the @see Container (i.e. container.unbindAll()) and removes
* it from the cache.
* @param scopeId
*/
static remove(scopeId = DEFAULT_SCOPE_ID): void {
let container = this._instances[scopeId];
if (!container) return;
container.unbindAll();
delete this._instances[scopeId];
}
/**
* Runs the @method remove method on all instances.
*/
static removeAll(): void {
Object.keys(this._instances).forEach((key) => this.remove(key));
}
/**
* Adds a post configure action.
* @remarks A registration with .inSingletonScope() will work as .inRequestScope() should,
* that is, a new instance will be created for each instance of ScopedContainer.
* @see https://stackoverflow.com/a/71180025
* @param fn A function that will be run everytime a new @see Container is created.
* @returns The @see ScopedContainer itself, to allow chaining.
*/
static postConfigure(fn: PostConfigureAction): ScopedContainer {
this._postConfigureActions.push(fn);
return this;
}
/**
* Removes any post configure actions.
*/
static resetPostConfigureActions(): void {
this._postConfigureActions = [];
}
private static makeNewContainer(): Container {
const container = this.ensureGlobalContainer().createChild(this.containerOptions);
this._postConfigureActions.forEach((action) => action(container));
return container;
}
private static ensureGlobalContainer(): Container {
if (!this.globalContainer) {
const container = new Container(this.containerOptions);
this.configureGlobalContainer?.(container);
this.globalContainer = container;
}
return this.globalContainer;
}
}
looks nice! I ended up switching to nest.js for new projects which supports all of this out of the box
@iamdanthedev sorry in advance for the dumb question. I am a newbie to the NodeJS world. I have a happi.js server and I don't understand what to do with the returned container from your solution. You've mentioned that it can be used then with @inject and so on, but does the cloned container register automatically? Should I swap it somehow? In my case, the container is registered on app bootstrap.
import { Container } from 'inversify';
const createReqContainerHack = (container: Container): Container => { const reqContainer = new Container(); (reqContainer as any)._bindingDictionary = (container as any)._bindingDictionary.clone(); return reqContainer; };
could and httpScope
be added to satisfy this requirement?
I ended up creating the
ScopedContainer
class, as seen below, to make it more obvious to configure and easier to manage.// register-global-dependencies.ts ScopedContainer.globalContainer = (() => { const container = new Container(); container .bind<SomeSingletonDep>(TOKENS.SomeSingletonDep) .to(SomeSingletonDep) .inSingletonScope(); return container; })();
// register-scoped-dependencies.ts import "register-global-dependencies"; ScopedContainer.postConfigure((container) => { container .bind<RequestSpecificDep>(TOKENS.RequestSpecificDep) .to(RequestSpecificDep) .inSingletonScope(); });
// lambda-handler.ts import "register-scoped-dependencies"; handler = (event, context) => { const requestId = event.requestContext.requestId; const container = ScopedContainer.for(requestId); try { // This will be the same for every request const singletonDep = container.get(TOKENS.SomeSingletonDep); // And this will be a new instance for every request const requestSpecificDep = container.get(TOKENS.RequestSpecificDep); } finally { ScopedContainer.remove(requestId); } }
This is the
ScopedContainer
class:import { Container, interfaces } from "inversify"; const DEFAULT_SCOPE_ID = "__default__"; export type PostConfigureAction = (container: Container) => void; export type ScopedContainerCache = { [id: string]: Container; }; export class ScopedContainer { private static _postConfigureActions: PostConfigureAction[] = []; private static readonly _instances: ScopedContainerCache = {}; /** * Options object to use when creating a new container for a * scope ID. */ static containerOptions: interfaces.ContainerOptions; /** * A global container instance, which enables truly * singleton instances when using a scoped container. All scoped * containers reference the global container as parent. * * @example * ScopedContainer.globalContainer = (() => { * const container = new Container(); * container * .bind<ClassInSingletonScope>(TOKENS.ClassInSingletonScope) * .to(ClassInSingletonScope) * .inSingletonScope(); * return container; * })(); */ static globalContainer: Container; /** * Optional function to configure the global container. * An alternative to directly setting the @see globalContainer property. */ static configureGlobalContainer: (container: Container) => void; /** * Returns a @see Container that is unique to the specified scope. * If this is the first time getting the container for the scope, then a * new container will be created using the provided factory. Any post configure * actions will also be applied to the new container instance. * @param scopeId Any string to identify the scope (e.g. current request ID). * @returns A @see Container that is unique to the specified scope. */ static for(scopeId = DEFAULT_SCOPE_ID): Container { let container = this._instances[scopeId]; if (!container) { container = this.makeNewContainer(); this._instances[scopeId] = container; } return container; } /** * Unbinds the @see Container (i.e. container.unbindAll()) and removes * it from the cache. * @param scopeId */ static remove(scopeId = DEFAULT_SCOPE_ID): void { let container = this._instances[scopeId]; if (!container) return; container.unbindAll(); delete this._instances[scopeId]; } /** * Runs the @method remove method on all instances. */ static removeAll(): void { Object.keys(this._instances).forEach((key) => this.remove(key)); } /** * Adds a post configure action. * @remarks A registration with .inSingletonScope() will work as .inRequestScope() should, * that is, a new instance will be created for each instance of ScopedContainer. * @see https://stackoverflow.com/a/71180025 * @param fn A function that will be run everytime a new @see Container is created. * @returns The @see ScopedContainer itself, to allow chaining. */ static postConfigure(fn: PostConfigureAction): ScopedContainer { this._postConfigureActions.push(fn); return this; } /** * Removes any post configure actions. */ static resetPostConfigureActions(): void { this._postConfigureActions = []; } private static makeNewContainer(): Container { const container = this.ensureGlobalContainer().createChild(this.containerOptions); this._postConfigureActions.forEach((action) => action(container)); return container; } private static ensureGlobalContainer(): Container { if (!this.globalContainer) { const container = new Container(this.containerOptions); this.configureGlobalContainer?.(container); this.globalContainer = container; } return this.globalContainer; } }
Hello there, how exactly would this scoped container implementation be used (if possible) in case of @inject
in another class, without accessing the scoped container via specific id?
Hi guys
For everyone still looking for scoped containers: after a lot of trial and error I can say that you don't need it in javascript.
async_hooks
's AsyncLocalStorage does the job much better, more reliable and is a de-facto standard for passing contextual data through the chain of callbacks (being express routes or inversify dependencies)
I am ditching the solution proposed in the beginning of this thread (cloning container) and have successfully used AsyncLocalStorage for accessing user context, transaction context and similar things on a few projects in the past couple of years
Example 1: If you want to inject the current state of AsyncLocalStorage you can do it with inversify:
container.bind<IUserContext>(IUserContextType).toDynamicValue(() => {
return userContextStorage.getStore() || null;
});
Would you mind providing an example using Request scope?
Documentation is not very clear:
Reading that, if I call two times Container.get then I get two different instances. Which is basically the definition of how Transient scope works (one instance each time I call Container.get).
I guess InversifyJs is aligned on other IoC containers best practices, so I presume request scope creates instances that lasts a specific scope (an Http request for example). But that is not what the documentation explains here, so it's a bit confusing. I guess some examples would clarify things.