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:

  1. Creates a shallow copy of the current context using this.ctx.extend(), merging any supplied properties into a new proxy.
  2. Instantiates a new service object using the same constructor but passing the extended context.
  3. 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 (from packages/core/src/reflect.ts): Returns a new proxy that forwards property access to the original context while overlaying supplied properties.
  • symbols.extend (from packages/core/src/utils.ts): The unique symbol used as the method name to prevent naming collisions.
  • Service base class (in packages/core/src/service.ts): Provides the static extend symbol 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:

Summary

  • Service.extend is 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →