tsyringe icon indicating copy to clipboard operation
tsyringe copied to clipboard

support custom scopes

Open xenoterracide opened this issue 5 years ago • 5 comments

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

xenoterracide avatar Feb 20 '20 19:02 xenoterracide

I'm also interested in a per request scope. Could It be indirectly supported by a middleware helper (something similar to this)?

ghost avatar Feb 22 '20 18:02 ghost

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);

sugiruu avatar Feb 22 '20 20:02 sugiruu

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?

ghost avatar Feb 23 '20 18:02 ghost

@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);

sugiruu avatar Feb 23 '20 19:02 sugiruu

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.

xenoterracide avatar Feb 26 '20 18:02 xenoterracide