Cordis Service Providers Dependency Injection with Service.extend: Implementation Guide
Cordis service providers dependency injection uses Service.extend to create derived service instances that inherit state while allowing context overrides through a shallow copy of the proxy-based Context.
The Cordis framework (cordiverse/cordis) implements a lightweight dependency injection system where every service receives a Context object containing registered services and configuration. The static Service.extend symbol enables hierarchical service derivation without duplicating underlying service instances, making it ideal for scoping configurations and test isolation.
How Service.extend Implements Dependency Injection
Service.extend is a static symbol defined as Symbol.for('cordis.extend') in packages/core/src/service.ts. It provides the entry point for creating derived service instances that share the original service's state while operating with a modified context.
When invoked, this symbol triggers the protected method [symbols.extend] implemented in the same file. This method performs three critical operations:
- Creates a shallow copy of the current context using
this.ctx.extend(), merging any supplied properties into a new proxy. - Instantiates a new service object using the same constructor but passing the extended context.
- Preserves the original prototype chain, ensuring the derived instance maintains all methods and properties of the base Service class.
The context itself is a proxy-based container defined in packages/core/src/reflect.ts. Extending it does not duplicate underlying services; instead, it adds new keys or overrides existing ones specifically for the child context.
Context Proxy Architecture
The dependency injection mechanism relies on several core utilities working together:
Context.extend(frompackages/core/src/reflect.ts): Returns a new proxy that forwards property access to the original context while overlaying supplied properties.symbols.extend(frompackages/core/src/utils.ts): The unique symbol used as the method name to prevent naming collisions.Servicebase class (inpackages/core/src/service.ts): Provides the staticextendsymbol and the protected[symbols.extend]method.
This architecture ensures that Cordis service providers dependency injection remains type-safe and runtime-efficient, avoiding expensive object cloning while supporting hierarchical configuration.
Practical Implementation Patterns
Scoping Configuration Per Request
Use Service.extend to create isolated contexts with specific configuration values without affecting the global service registry:
import { Service, Context } from 'cordis';
class Logger extends Service {
log(message: string) {
console.log('[log]', message);
}
}
// Root context with registered Logger
const root = new Context().register(new Logger());
// Derive child context with debug configuration
const child = root.extend({ level: 'debug' });
child.logger.log('child logger'); // Uses same Logger instance, but child has 'level' property
Mocking Services for Testing
Override providers in derived contexts to inject mock implementations for isolated unit testing:
class MockLogger extends Service {
log(message: string) {
// Capture messages for assertions or no-op
}
}
// Replace original logger only for test context
const testCtx = root.extend({ logger: new MockLogger() });
testCtx.logger.log('this will be mocked'); // Uses MockLogger, not original
Dynamic Service Composition
Implement scoped methods within services using the bracket notation to access the protected symbol:
class Database extends Service {
get url() { return this.ctx.dbUrl; }
// Dynamically create scoped DB client with extra options
scoped(options: Partial<{ timeout: number }>) {
return this[Service.extend]({ dbOptions: options });
}
}
const db = new Database();
const dbWithTimeout = db.scoped({ timeout: 2000 });
// dbWithTimeout shares the Database instance but includes dbOptions.timeout in its context
Key Source Files
The implementation spans several files in the @cordis/core package:
packages/core/src/service.ts: Defines theServicebase class, the staticextendsymbol, and the protected[symbols.extend]method.packages/core/src/utils.ts: Declares unique symbols includingsymbols.extendandsymbols.shadowused for internal wiring.packages/core/src/reflect.ts: ImplementsContext.extend, the proxy mechanism that merges new properties while preserving original context behavior.packages/core/tests/invoke.spec.ts: Contains test suites demonstrating typicalextendusage patterns and verification of context inheritance.
Summary
Service.extendis a static symbol (Symbol.for('cordis.extend')) that triggers derivation of service instances with modified contexts.- The mechanism creates shallow context copies via proxy-based extension in
packages/core/src/reflect.ts, avoiding duplication of underlying services. - Cordis service providers dependency injection supports hierarchical configuration scoping and test mocking by allowing context property overrides.
- Protected method
[symbols.extend]preserves the prototype chain while instantiating new service objects with extended contexts.
Frequently Asked Questions
What is the difference between Service.extend and Context.extend?
Service.extend is a static symbol exposed on the Service class that triggers the internal [symbols.extend] method, which in turn calls Context.extend. While Context.extend (implemented in packages/core/src/reflect.ts) handles the proxy creation and property merging, Service.extend provides the public API for service instances to create derived versions of themselves with new contexts.
How does Service.extend preserve the prototype chain?
When [symbols.extend] executes in packages/core/src/service.ts, it instantiates a new service object using the original constructor while passing the extended context. Because it uses the same constructor rather than Object.create or manual property copying, the new instance maintains the complete prototype chain of the original service class, including all inherited methods and accessors.
Can Service.extend be used for dependency mocking in unit tests?
Yes. By calling root.extend({ serviceName: new MockService() }), you create a derived context where the original service is replaced by a mock implementation. This approach isolates the mock to the specific test context without affecting the global service registry or other tests, as demonstrated in packages/core/tests/invoke.spec.ts.
Where is the Service.extend symbol defined in the Cordis source code?
The static Service.extend symbol is defined in packages/core/src/service.ts as Symbol.for('cordis.extend'). The corresponding method implementation using symbols.extend (the runtime symbol from packages/core/src/utils.ts) is located in the same file as a protected method on the Service base class.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →