-
Notifications
You must be signed in to change notification settings - Fork 392
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Question: one controller instance per request #174
Comments
To share date between calls, you can use |
@NoNameProvided that's exactly what I wanted to avoid, because it's harder to get type information and you still have to pass the |
I am not sure I understand what do you want then.Do you want a special param, like an injectable Context type? @Get(/item/:id)
public async function(@Context() context: Context) {
// ...
} In this case I dont see the difference between injecting a Or do you want Context Attached to the actual request controllers? @Get(/item/:id)
public async function(@Context() context: Context) {
this.context.mySpecifiedParam;
} If you want this, how would you specify it's type? How will they override each other (if speaking about inheritance) In theory it would be possible. Currently we register a single global route handler to handle requests. So we can initialize a new instance of a controller for the matched route, but this would need a rewrite of the drivers to work this way. Also initializing controllers, and all needed dependency on every request probably will have a performance impact on the response time, so this should be made optional if we implement it and I am not sure if this can be done a nice way. But this is a kind of question we should ask @pleerock about |
What I really would like is to be able to register a Controller with a factory method that then returns the controller instance to handle the request. Quick example of a use case: class MyController {
constructor(private logger: Logger, private somethingModel: SomethingModel) { }
@Get("/foo")
public async getSomething() {
this.logger.log("I'm about to get something")
this.checkSomething();
return await this.somethingModel.get();
}
private checkSomething() {
this.logger.log("I'm checking!");
}
}
const app = createExpressServer({
controllers: [req => new MyController(Logger.fromRequest(req), new SomethingModel("production"))]
}); This lets you log with contextual info from any helper method ( // test!
const testController = new MyController(consoleLogger, modelMock);
await testController.getSomething(); Just an example of a use case. |
Basically you want controllers as services as I believe. If I'm not mistaken there is not such functionality right now in routing controllers. |
No, services are singletons injected by typedi (if you use typedi), what @maghis wants is a different instance of controllers for every request. From the But I don't see much value in adding such feature. Context is easily creatable in a before middleware already. Attaching a logger to the request via
You can already test them in unit tests if you want. You can inject whatever dummy class you want for your controller. They are just simple classes after all. |
routing-controllers allows you to declare routes and functions that will be executed on user's request on that route. Using decorators and class methods. What you are asking is just complexity - don't do things this way. Simply handle your actions on controller methods level and if you have something to reusable extract functionality into services and reuse services. |
@NoNameProvided my bad, I meant to say something about "you want to have one request instance and ability to work with it like in "request - process spawn -response - process died" way. Like you work with requests in PHP. There is exactly one instance of controller per request :D The thing is, it's not so easy to get used to thinking "hey, this service/controller/class is not for user who I got from request, it's for everyone, so it can have my app parameters, but not some user's parameters." and passing params everytime and everywhere. If you are from different background, tho |
@idchlife it's actually one of the requirement of the MVC design pattern, for the controller to be stateless on a request basis. Allows you to do a lot of code reuse by inheriting from common base controllers and guarantees that there is no state "leak" between requests (state persistence across requests should be handled by models). Design of other popular MVC frameworks: Maybe I'm old school, but I'm working on a large number of microservices that need to share common tracing/logging/perf measurement infrastructure and this is currently a requirement. Thanks for the help! |
actually MVC design pattern is quite abstract does not force you to make such design decisions. But yeah, controllers should be stateless on a request basis and nothing should be stored by a user in a controller, it should be a rule for everyone. In routing-controllers controllers are stateless until you set a container. If you set a container - they are services. If you make them services. You can store in such services something request-based or not, but generally its a bad idea and everyone should avoid doing that.
You can do that using services. If you need a request object in your services simply pass them as a method parameter, e.g. |
@pleerock yep, I would like to avoid that, having to pass around I'll try using services that way and maybe give a shot at implementing a driver. Thanks! |
okay I've got your idea and request, probably something like this is implementable: @JsonController({
factory: (request, response) => {
const logger = new Logger(request);
return new UserController(logger);
}
})
export class UserController {
constructor(private logger: Logger) {
}
} so, if |
@pleerock in that case you would no longer be able to use |
right he would need to do something like this: @JsonController({
factory: (request, response) => {
const logger = new Logger(request);
const userRepository = Container.get(UserRepository);
return new UserController(logger, userRepository);
}
})
export class UserController {
constructor(private logger: Logger, private userRepository: UserRepository) {
}
} |
Wouldn't it be simplier if TypeDI (or an other configured container) could create new instance of |
lets say if we do such thing in |
I was answering to And I really don't get the idea of |
yeah, I was talking about same.
Imagine you want to log each user action and add to logs request data (browser, ip, etc.). To do so right now you have two inconvenient solutions: @JsonController()
export class UserController {
constructor(private logger: Logger) {
}
@Get("/users")
all(@Req() request: Request) {
this.logger.log("message", req);
}
@Post("/users")
save(@Req() request: Request) {
this.logger.log("message", req);
}
} or @JsonController()
export class UserController {
@Get("/users")
all(@MyLogger() logger: Logger) {
logger.log("message");
}
@Post("/users")
save(@MyLogger() logger: Logger) {
this.logger.log("message");
}
} With first approach inconvenience is that you have to pass |
This is the same problem as discussed in #147 - you have to decorate each action in each controller explicit instead of implicit inherit from base controller and in every route there is a boilerplate to fetch entity from db based on id from params, etc. And we claimed that it's better to do it explicitly rather than jumping through all the files. So with the problem with logger related to single request and one controller instance: |
Currently, all services are singletons, may be you can allow users to create services with different lifetimes at the time of creation, something similar is done in Asp.Net Core: Transient Scoped Singleton |
Sorry for the long post, but I think per request injection can lead to very succinct code. Even if the injection hierarchy is complex. @JsonController()
export class UserController {
// So finally something very simple but with complex hierarchy of dependencies
// friends -> currentUser -> request
// friends -> db
// clients -> currentUser -> request
// clients -> db
// debt -> clients
@Get("/friendsClientsAndDebts")
all(friendsService: FriendsService,
clientsService: ClientsService,
debtService: DebtService) {
return {
friends: friendsService.getFriends();
clients: clientsService.getClients();
debt : debtService.getDebt();
}
}
} Explanations of dependencies : // Db is a singleton
@Singleton()
export class Db {
users:collection;
clients:collection;
friends:collection;
}
@RequestScope()
export class CurrentUserService
{
currentUser:User = null;
constructor(
private db:Db,
private req:Request) {
}
getUser() {
if (this.currentUser == null)
this.currentUser = this.db.users.find(this.req.session.userId);
return this.currentUser;
}
}
@RequestScope()
export class FriendsService {
constructor(
private db:Db,
currentUser:CurrentUserService) {}
getFriends()
{
if (this.friends == null)
this.friends = db.friends.find(currentUser.getUser());
return this.friends;
}
}
@RequestScope()
export class ClientsService {
constructor(
private db:Db,
currentUser:CurrentUserService) {}
getClients()
{
if (this.clients == null)
db.clients.find(currentUser.getUser());
return this.clients;
}
}
@RequestScope()
export class DebtService {
constructor(
private clientsService:ClientsService) {}
debt()
{
// use clientsService
}
}
|
I think controller per request is very important to have, if you are using DI. All examples here deal with a simplified use case of logger, that is too simplistic to base design decision on, it has no dependencies and only couple of methods. Sure in that case you can pass req in the call. But what if you have a DI topography of 3 classes down, DBLocator->DBService->SearchSearvice-> and it's DBService that needs user info to check permissions and set createdBy automatically? Now do you have to put extra object on every method in DBService and SearchService to pass req through to actual one function that does it? Or wouldn't it be simpler to let DI initialize SearchService, cascading to DBService which will have Zones or Context injected in it and have it as |
The whole code can be also succinct using the functional approach: export class Db {
users: collection;
clients: collection;
friends: collection;
debts: collection;
}
export class FriendsService {
constructor(
private db:Db,
) {}
getFriends(user: User) {
if (this.friends == null) {
this.friends = this.db.friends.find(user);
}
return this.friends;
}
}
export class ClientsService {
constructor(
private db:Db,
) {}
getClients(user: User) {
if (this.clients == null) {
this.db.clients.find(user);
}
return this.clients;
}
}
export class DebtService {
constructor(
private db:Db,
) {}
getDebt(user: User) {
this.db.debts.getAccountState(user)
}
}
@JsonController()
export class UserController {
constructor(
private friendsService: FriendsService,
private clientsService: ClientsService,
private debtService: DebtService,
) {}
@Get("/friendsClientsAndDebts")
all(@CurrentUser() user: User) {
return {
friends: this.friendsService.getFriends(user),
clients: this.clientsService.getClients(user),
debt: this.debtService.getDebt(user),
}
}
} So your services are stateless and they get the data from outside - it's passed to them by action handler which extract the data from the request. It doesn't need shared state and new instances for request but only some changes in app architecture and different thinking as it's single threaded, event-loop based Node.js 😉 |
This is node.js ecosystem. We don't have 5 layers of abstraction, 10 interfaces to implements and use 3 different design patterns like in C#/Java world to make the things works nice 😆
Don't overcomplicate things because when they don't have to be. Keep it simple, stupid 😜
Node.js with single threaded and event-loop needs different way of thinking. It might be hard for senior C# developers to switch thinking but this requires it. This is not .NET, we don't need overcomplicated MVC implementations. You can't map the way you always create your apps to the node.js world because you would kill all the advantages it has thanks to its nature. If it's hard to you, go back to C# 😉 |
I think what @19majkel94 want to say (and what I say for everyone who come from java/c#) is that in node.js and javascript/typescript we must think more javascript/typescript-ish way. Most of design patterns were developed for languages like java because of limitation those language have. I don't tell they are bad, they have their own pros and cons (thats why typescript exist - it took pros of another languages and merge them into javascript), but anyway you need to think new way. Thats what I think @19majkel94 want to say and thats what I personally learned in javascript world after years of programming in java. But node.js can have 5 layers of abstraction, why not? Okay, personally I hate people who overcomplicate things and make useless abstractions (one of the biggest mistakes of "pro"-s), but this can be real. And I can admit that its not pleasure to pass data from controller to all downside levels. Its really not a big pleasure to do so. And I would like to fix this problem if there is a good fix. So what is your solution guys? Can you propose some really really good solution that fits routing-controllers, typedi and nodejs ecosystems? One thing I can propose is container-per request that may work, however Im not sure what other problems it may bring |
Because Node.js is a single threaded and event-loop, it makes it difficult to maintain the scope variables across its callbacks and promises (async/await). C# (founded the async/await syntax) uses per request containers to easily maintain the scope of the request. It will also make it easier to forward request information to downstream services: extract for example an opentracing id in a request interceptor, store it in a request scoped container and use it when making a request to a downstream service. Besides that it will prevent bugs when a programmer stores (accidentally) request variables as a property of a Service. When 2 requests are using that same variable at the 'same time' it will have ugly results. On the developer pc it will work fine, but not in production... I don't really see a down side for per request containers. Maybe a solution like I proposed here will be sufficient. |
Just wanted to chime in with a real-world item that Say you have a class LogService {
private _identity: Identity;
public log(msg) {
console.log(`${this._identity.username}: ${msg}`);
}
} With a singleton service, that won't work. Sure, you can pass around the user's You could technically have a singleton which was a factory which makes a new If you had a scoped lifetime, which is to say scoped to the context of the http call, it's much easier to construct something like the |
Multiple instances is not the solution. Continuation-Local Storage is. Right now you can use bare req/ctx object to pass request-related data between services. etc. We will think how to implement request-scoped context in the future. |
Stale issue message |
@MichalLytek cls is a good decisions. I have already use it instead of passing req object everywhere. |
Any progress on this issue? |
Is there a way to create an instance of a controller per request?
This would allow providing context about the request to all the methods of the controller (ie:
currentUser
,logger
(with request metadata),requestId
, ...), without having to pass the context around via method parameters.The text was updated successfully, but these errors were encountered: