-
-
Notifications
You must be signed in to change notification settings - Fork 8.2k
Description
Is there an existing issue that is already proposing this?
- I have searched the existing issues
Is your feature request related to a problem? Please describe it
Problem statement
I have an issue when I want provider resolution to participate in the request execution lifecycle, not only in the DI construction lifecycle.
Today, providers are value-oriented (useClass, useFactory, useValue), and execution lifecycle is handled separately (e.g. handler wrappers).
For some use cases, this separation makes dependency injection awkward.
Concrete example (TypeORM):
- I want to write business services as request-scoped services.
- I want those services to inject
@InjectTransactionalRepository(Entity). - I want repository instances to be bound to a transaction started at execution time via
entityManager.transaction(...). - I do not want each controller/handler to manually add transaction wrappers.
In short: I need a first-class way for providers to be resolved through execution lifecycle boundaries, so injected dependencies can come from execution-time context (transaction, tenant context, etc.).
Important requirement: request-scoped providers and execution-scoped providers must be able to depend on each other (mutual dependency), not only one-way dependency.
Describe the solution you'd like
Proposed solution
Introduce a new provider form that is execution-oriented.
Example API shape:
{
provide: SOME_TOKEN,
inject: [...deps],
async useExecution(
next: (instance: unknown) => Promise<unknown>,
...deps: unknown[]
) {
// build execution-time instance, then pass it to next(instance)
return next(instance);
},
}Semantics:
useExecutionruns during handler execution lifecycle (before/after handler), not during bootstrap construction.next(instance)means: continue execution and inject this concrete provider instance for the downstream execution chain.- framework composes all
useExecutionproviders into a deterministic chain and awaits the final result. - request-scoped and execution-scoped providers can depend on each other in the same dependency graph (mutual dependency support), with deterministic resolution order.
TypeORM example with entityManager.transaction:
{
provide: getRepositoryToken(User),
inject: [DataSource],
async useExecution(next, dataSource: DataSource) {
return dataSource.manager.transaction(async (em) => {
const transactionalRepo = em.getRepository(User);
return next(transactionalRepo);
});
},
}Business service:
@Injectable({ scope: Scope.REQUEST })
export class UserService {
constructor(
@InjectTransactionalRepository(User)
private readonly userRepo: Repository<User>,
) {}
}This allows transactional repository injection from execution-time transaction boundaries without manual per-handler wiring.
Expected dependency behavior:
- A request-scoped business service can depend on
@InjectTransactionalRepository(Entity). - The transactional repository provider can depend on execution-time transaction context.
- An execution-scoped provider can also depend on request-scoped dependencies.
- The framework should resolve this as one ordered graph for a single request execution.
Teachability, documentation, adoption, migration strategy
Teachability, documentation, adoption, migration strategy
How users would use it
- Register execution-scoped providers in a module, same place where providers are declared.
- Keep normal constructor injection in business services.
- Use dedicated decorators like
@InjectTransactionalRepository(Entity)to inject the token exposed by an execution-scoped provider.
Documentation outline
- New chapter: Execution-scoped providers
- API contract:
useExecution(next, ...deps)andnext(instance) - Execution ordering and composition rules
- Mutual dependency rules between scopes:
- request scope depends on execution scope
- execution scope depends on request scope
- deterministic per-request graph order
- TypeORM guide:
- Start transaction with
dataSource.manager.transaction(...) - Resolve repository with
em.getRepository(Entity) - Inject with
@InjectTransactionalRepository(Entity)
- Start transaction with
- Interop section:
- Works with existing provider styles
- Works with existing scopes and module boundaries
Adoption strategy
- Optional feature: existing apps keep current behavior with no changes.
- Teams can adopt per module or per domain.
- Typical first migration target: transaction-heavy services.
Migration example
Before:
- custom transaction wrappers around service/controller logic
- manual transaction context plumbing
After:
- transaction boundary declared once in execution-scoped provider
- business service keeps regular DI with
@InjectTransactionalRepository() - request and execution scopes can reference each other in one request execution graph
What is the motivation / use case for changing the behavior?
Motivation / use case
Primary use case: transactional dependency injection with TypeORM.
I want to model transactions as execution boundaries while still using standard constructor injection in request-scoped business services.
I also need request-scoped and execution-scoped providers to depend on each other in the same request flow.
Concrete flow:
- Execution starts for a handler.
- A TypeORM execution-scoped provider opens
entityManager.transaction(...). - Inside that boundary, it resolves and passes
em.getRepository(Entity)vianext(instance). - Request-scoped services consume
@InjectTransactionalRepository(Entity)normally. - The whole chain runs in one transaction boundary.
Scope dependency expectation:
- request-scoped providers can consume values produced by execution-scoped providers
- execution-scoped providers can read request-scoped context values
- execution composition and DI resolution remain separate but coordinated per request
Why this matters:
- keeps business code focused on domain behavior
- avoids repeating transaction setup in handlers
- enables consistent transactional behavior across service layers
- keeps DI expressive for execution-time resources
Additional use cases beyond TypeORM:
- tenant-bound repositories/connections selected at execution time
- execution-bound security/audit resources
- execution-time contextual resources that should still be injectable