Open xenoterracide opened 4 years ago
I'm also interested in a per request scope. Could It be indirectly supported by a middleware helper (something similar to this)?
Sorry if I misunderstood what a "request scope" means for you, but IMO you can already do this with Lifecycle.ContainerScoped
and child containers.
In this example, the DatabaseConnection
is bound to the express request object, and an new instance will be created for every request:
import {randomBytes} from "crypto";
import express from "express";
import {container as globalContainer, DependencyContainer, scoped} from "tsyringe";
declare module "express-serve-static-core" {
interface Request {
container: DependencyContainer;
}
}
@scoped(Lifecycle.ContainerScoped)
class DatabaseConnection {
public connectionID: string = randomBytes(10).toString("hex");
public executeQuery(): void {
// ...
}
}
const app = express();
app.use((req, _res, next) => {
req.container = globalContainer.createChildContainer();
next();
});
app.use((req, _res, next) => {
// DatabaseConnection is first resolved here, and will be cached
// for the remaining of the request
const db = req.container.resolve(DatabaseConnection);
console.log(db.connectionID);
next();
});
app.get("/test", (req, res) => {
// Same DatabaseConnection instance from the above middleware
const db = req.container.resolve(DatabaseConnection);
db.executeQuery();
res.send(db.connectionID);
});
app.listen(3000);
Thanks @skiptirengu I think now I understand how it works. Only one question, if I extend your example:
@scoped(Lifecycle.ContainerScoped)
class Dataloader {
// ...
}
@scoped(Lifecycle.ContainerScoped)
class DatabaseConnection {
public connectionID: string = randomBytes(10).toString("hex");
public constructor(dataloader: Dataloader) {
// ...
}
public executeQuery(): void {
// ...
}
}
// ...
Is the DataLoader
that I get in DatabaseConnection
from same child container or the global container?
@localnet, the child container will always search for a dependency on it's internal registry first. If no token is found, it will fallback to the parent containers' registry.
Ex:
import {container as globalContainer, scoped, inject, injectable, Lifecycle} from 'tsyringe';
@injectable()
class GlobalClass {}
@scoped(Lifecycle.ContainerScoped)
class ClassOne {}
@scoped(Lifecycle.ContainerScoped)
class ClassTwo {
public constructor(
@inject(ClassOne) public myClass: ClassOne,
@inject(GlobalClass) public globalClass: GlobalClass) {
// ...
}
}
const childContainer = globalContainer.createChildContainer();
// false - not the same instance
console.log(childContainer.resolve(ClassTwo).myClass === globalContainer.resolve(ClassOne));
// true - same instance
console.log(childContainer.resolve(ClassTwo).myClass === childContainer.resolve(ClassOne));
// true - registered on the parent container
console.log(childContainer.resolve(ClassTwo).globalClass instanceof GlobalClass);
so, doing one scope with a container is easy, but I need to do the following
a request scope a transaction scope a session scope a security scope (different from above, imagine working with code that needs a user, but is not in http)
and then I need to be able to inject a security scoped object, into a request scoped object, or a session scoped one.
and after all that I need to be able to create mock/stub objects to replace any of these in a test context.
I also, do not personally believe that child containers should be used to manage scope.
we'd like to have a request scope, but we understand why tsyringe can't directly support this, however it could support adding our own custom scopes which could then be integrated into our own framework.
this is probably a decent article on how to implement one in spring... it's been a while since I've done it, since spring+ (meaning not just spring-core) generally already supports the scopes I need, since it includes a web framework, and a security framework. https://www.baeldung.com/spring-custom-scope