0
# Context & Dependency Injection
1
2
LoopBack Core re-exports the complete `@loopback/context` API, providing a comprehensive IoC (Inversion of Control) container with hierarchical contexts, type-safe dependency injection, binding management, and interceptor support. This system forms the foundation for all dependency management in LoopBack applications.
3
4
## Capabilities
5
6
### Context Container
7
8
The main IoC container that manages bindings and provides dependency injection capabilities.
9
10
```typescript { .api }
11
/**
12
* Context provides an implementation of Inversion of Control (IoC) container
13
*/
14
class Context {
15
/** Create a binding with the given key */
16
bind<ValueType = BoundValue>(key: BindingAddress<ValueType>): Binding<ValueType>;
17
18
/** Add a binding to the context */
19
add<ValueType = BoundValue>(binding: Binding<ValueType>): Context;
20
21
/** Get a value from the context by key */
22
get<ValueType>(keyWithPath: BindingAddress<ValueType>): Promise<ValueType>;
23
24
/** Get a value synchronously from the context by key */
25
getSync<ValueType>(keyWithPath: BindingAddress<ValueType>): ValueType;
26
27
/** Check if a binding exists for the given key */
28
contains<ValueType>(key: BindingAddress<ValueType>): boolean;
29
30
/** Find bindings matching a filter */
31
find<ValueType = BoundValue>(filter?: BindingFilter): Readonly<Binding<ValueType>>[];
32
33
/** Create a child context */
34
createChild(name?: string): Context;
35
}
36
```
37
38
**Usage Examples:**
39
40
```typescript
41
import { Context, BindingKey } from "@loopback/core";
42
43
// Create context
44
const context = new Context('my-app');
45
46
// Create typed binding key
47
const DB_CONFIG = BindingKey.create<DatabaseConfig>('config.database');
48
49
// Bind values
50
context.bind(DB_CONFIG).to({
51
host: 'localhost',
52
port: 5432,
53
database: 'myapp'
54
});
55
56
// Bind classes
57
context.bind('services.logger').toClass(LoggerService);
58
59
// Bind providers
60
context.bind('services.config').toProvider(ConfigProvider);
61
62
// Get values
63
const dbConfig = await context.get(DB_CONFIG);
64
const logger = await context.get<LoggerService>('services.logger');
65
66
// Synchronous get
67
const syncLogger = context.getSync<LoggerService>('services.logger');
68
69
// Check existence
70
if (context.contains('services.cache')) {
71
const cache = await context.get('services.cache');
72
}
73
74
// Child contexts
75
const childContext = context.createChild('request-scope');
76
childContext.bind('request.id').to('req-123');
77
```
78
79
### Binding System
80
81
Core binding system that associates keys with values, classes, or providers.
82
83
```typescript { .api }
84
/**
85
* A binding represents a mapping from a key to a value
86
*/
87
class Binding<T = BoundValue> {
88
/** The binding key */
89
readonly key: string;
90
91
/** The binding scope */
92
scope: BindingScope;
93
94
/** Bind to a constant value */
95
to(value: T): Binding<T>;
96
97
/** Bind to a class constructor */
98
toClass<C>(ctor: Constructor<C>): Binding<C>;
99
100
/** Bind to a provider */
101
toProvider<P>(providerClass: Constructor<Provider<P>>): Binding<P>;
102
103
/** Create an alias binding */
104
toAlias(keyWithPath: BindingAddress<T>): Binding<T>;
105
106
/** Set the binding scope */
107
inScope(scope: BindingScope): Binding<T>;
108
109
/** Add tags to the binding */
110
tag(...tags: (string | object)[]): Binding<T>;
111
112
/** Apply a binding template */
113
apply(templateFn: BindingTemplate<T>): Binding<T>;
114
}
115
116
enum BindingScope {
117
TRANSIENT = 'Transient',
118
CONTEXT = 'Context',
119
SINGLETON = 'Singleton',
120
REQUEST = 'Request',
121
SESSION = 'Session',
122
APPLICATION = 'Application'
123
}
124
```
125
126
**Usage Examples:**
127
128
```typescript
129
import { Context, Binding, BindingScope, BindingKey } from "@loopback/core";
130
131
const context = new Context();
132
133
// Constant binding
134
context.bind('config.port').to(3000);
135
136
// Class binding with scope
137
context.bind('services.logger')
138
.toClass(LoggerService)
139
.inScope(BindingScope.SINGLETON);
140
141
// Provider binding
142
context.bind('services.database')
143
.toProvider(DatabaseProvider)
144
.inScope(BindingScope.SINGLETON);
145
146
// Alias binding
147
context.bind('logger').toAlias('services.logger');
148
149
// Tagged binding
150
context.bind('repositories.user')
151
.toClass(UserRepository)
152
.tag('repository', {entity: 'User'});
153
154
// Chained configuration
155
const binding = context.bind('services.email')
156
.toClass(EmailService)
157
.inScope(BindingScope.SINGLETON)
158
.tag('service', 'communication')
159
.apply(binding => {
160
// Custom configuration
161
binding.tag('priority', 'high');
162
});
163
```
164
165
### Dependency Injection
166
167
Core injection decorators and utilities for constructor, property, and method injection.
168
169
```typescript { .api }
170
/**
171
* A decorator to annotate method arguments for automatic injection by LoopBack's IoC container
172
*/
173
function inject(
174
bindingSelector?: BindingSelector<unknown>,
175
metadata?: InjectionMetadata
176
): PropertyDecorator & ParameterDecorator;
177
178
namespace inject {
179
/** Inject a binding object */
180
function binding(
181
selector?: BindingSelector<unknown>,
182
metadata?: InjectionMetadata
183
): PropertyDecorator & ParameterDecorator;
184
185
/** Inject the current context */
186
function context(): PropertyDecorator & ParameterDecorator;
187
188
/** Inject a getter function */
189
function getter(
190
bindingSelector: BindingSelector<unknown>,
191
metadata?: InjectionMetadata
192
): PropertyDecorator & ParameterDecorator;
193
194
/** Inject a setter function */
195
function setter(
196
bindingSelector: BindingSelector<unknown>
197
): PropertyDecorator & ParameterDecorator;
198
199
/** Inject values by tag */
200
function tag(
201
tagName: string | RegExp,
202
metadata?: InjectionMetadata
203
): PropertyDecorator & ParameterDecorator;
204
205
/** Inject a context view */
206
function view(
207
filter: BindingFilter,
208
metadata?: InjectionMetadata
209
): PropertyDecorator & ParameterDecorator;
210
}
211
```
212
213
**Usage Examples:**
214
215
```typescript
216
import { inject, Context, BindingKey } from "@loopback/core";
217
218
// Service class with constructor injection
219
class OrderService {
220
constructor(
221
@inject('repositories.order') private orderRepo: OrderRepository,
222
@inject('services.payment') private paymentService: PaymentService,
223
@inject.context() private context: Context
224
) {}
225
226
@inject('services.logger')
227
private logger: LoggerService;
228
229
async createOrder(orderData: OrderData): Promise<Order> {
230
this.logger.log('Creating order');
231
232
const order = await this.orderRepo.create(orderData);
233
await this.paymentService.processPayment(order.total);
234
235
return order;
236
}
237
238
// Getter injection for lazy loading
239
@inject.getter('services.email')
240
private getEmailService: Getter<EmailService>;
241
242
async sendOrderConfirmation(orderId: string): Promise<void> {
243
const emailService = await this.getEmailService();
244
await emailService.sendOrderConfirmation(orderId);
245
}
246
}
247
248
// Controller with various injection types
249
class UserController {
250
constructor(
251
@inject('repositories.user') private userRepo: UserRepository,
252
@inject.tag('validator') private validators: Validator[]
253
) {}
254
255
// Context view injection
256
@inject.view(filterByTag('middleware'))
257
private middlewareView: ContextView<Middleware>;
258
259
async createUser(userData: UserData): Promise<User> {
260
// Use injected dependencies
261
for (const validator of this.validators) {
262
await validator.validate(userData);
263
}
264
265
return this.userRepo.create(userData);
266
}
267
}
268
```
269
270
### Configuration Injection
271
272
Specialized injection for configuration values with support for nested properties.
273
274
```typescript { .api }
275
/**
276
* Decorator for configuration injection
277
*/
278
function config(
279
propertyPath?: ConfigurationPropertyPath,
280
metadata?: ConfigInjectionMetadata
281
): PropertyDecorator & ParameterDecorator;
282
283
namespace config {
284
/** Inject configuration as a context view */
285
function view(
286
propertyPath?: ConfigurationPropertyPath,
287
metadata?: ConfigInjectionMetadata
288
): PropertyDecorator & ParameterDecorator;
289
}
290
```
291
292
**Usage Examples:**
293
294
```typescript
295
import { config, Application } from "@loopback/core";
296
297
interface DatabaseConfig {
298
host: string;
299
port: number;
300
database: string;
301
ssl: boolean;
302
}
303
304
interface AppConfig {
305
database: DatabaseConfig;
306
redis: {
307
url: string;
308
ttl: number;
309
};
310
}
311
312
class DatabaseService {
313
constructor(
314
@config('database') private dbConfig: DatabaseConfig,
315
@config('database.host') private host: string,
316
@config() private fullConfig: AppConfig // Inject entire config
317
) {}
318
319
async connect(): Promise<void> {
320
console.log(`Connecting to ${this.host}:${this.dbConfig.port}`);
321
// Connection logic
322
}
323
}
324
325
// Application setup
326
const app = new Application();
327
328
app.bind('config').to({
329
database: {
330
host: 'localhost',
331
port: 5432,
332
database: 'myapp',
333
ssl: false
334
},
335
redis: {
336
url: 'redis://localhost:6379',
337
ttl: 3600
338
}
339
});
340
341
app.service(DatabaseService);
342
```
343
344
### Binding Keys
345
346
Type-safe binding keys with deep property access support.
347
348
```typescript { .api }
349
/**
350
* Binding key is used to uniquely identify a binding in the context
351
*/
352
class BindingKey<ValueType = unknown> {
353
/** Create a new binding key */
354
static create<T>(key: string): BindingKey<T>;
355
356
/** Get a property path from the binding key */
357
propertyPath<P>(path: string): BindingKey<P>;
358
359
/** The key string */
360
readonly key: string;
361
}
362
363
type BindingAddress<T = unknown> = string | BindingKey<T>;
364
```
365
366
**Usage Examples:**
367
368
```typescript
369
import { BindingKey, Context } from "@loopback/core";
370
371
// Type-safe binding keys
372
const DATABASE_CONFIG = BindingKey.create<DatabaseConfig>('config.database');
373
const LOGGER_SERVICE = BindingKey.create<LoggerService>('services.logger');
374
const USER_REPOSITORY = BindingKey.create<UserRepository>('repositories.user');
375
376
// Property path access
377
const DB_HOST = DATABASE_CONFIG.propertyPath<string>('host');
378
const DB_PORT = DATABASE_CONFIG.propertyPath<number>('port');
379
380
const context = new Context();
381
382
// Bind with typed keys
383
context.bind(DATABASE_CONFIG).to({
384
host: 'localhost',
385
port: 5432,
386
database: 'myapp',
387
ssl: false
388
});
389
390
context.bind(LOGGER_SERVICE).toClass(LoggerService);
391
392
// Type-safe access
393
const dbConfig: DatabaseConfig = await context.get(DATABASE_CONFIG);
394
const logger: LoggerService = await context.get(LOGGER_SERVICE);
395
396
// Property path access
397
const host: string = await context.get(DB_HOST); // 'localhost'
398
const port: number = await context.get(DB_PORT); // 5432
399
400
// Use in injection
401
class MyService {
402
constructor(
403
@inject(DATABASE_CONFIG) private dbConfig: DatabaseConfig,
404
@inject(DB_HOST) private host: string
405
) {}
406
}
407
```
408
409
### Context Views
410
411
Dynamic collections of bindings that update automatically as the context changes.
412
413
```typescript { .api }
414
/**
415
* ContextView provides a view for a given context chain to maintain a live list
416
* of matching bindings
417
*/
418
class ContextView<T = unknown> {
419
constructor(
420
context: Context,
421
filter: BindingFilter,
422
comparator?: BindingComparator
423
);
424
425
/** Get all matching values */
426
values(): Promise<T[]>;
427
428
/** Get all matching bindings */
429
bindings: Readonly<Binding<T>>[];
430
431
/** Listen for binding events */
432
on(eventName: string, listener: ContextViewEventListener): void;
433
434
/** Create a getter function for the view */
435
asGetter(): Getter<T[]>;
436
}
437
438
type ContextViewEventListener<T = unknown> = (event: ContextViewEvent<T>) => void;
439
```
440
441
**Usage Examples:**
442
443
```typescript
444
import {
445
Context,
446
ContextView,
447
filterByTag,
448
ContextViewEvent
449
} from "@loopback/core";
450
451
interface Plugin {
452
name: string;
453
initialize(): Promise<void>;
454
}
455
456
class PluginManager {
457
private pluginView: ContextView<Plugin>;
458
459
constructor(context: Context) {
460
// Create view for all plugins
461
this.pluginView = new ContextView(
462
context,
463
filterByTag('plugin')
464
);
465
466
// Listen for plugin additions/removals
467
this.pluginView.on('bind', this.onPluginAdded.bind(this));
468
this.pluginView.on('unbind', this.onPluginRemoved.bind(this));
469
}
470
471
private async onPluginAdded(event: ContextViewEvent<Plugin>): Promise<void> {
472
const plugin = await event.binding.getValue(event.context);
473
console.log(`Plugin added: ${plugin.name}`);
474
await plugin.initialize();
475
}
476
477
private onPluginRemoved(event: ContextViewEvent<Plugin>): void {
478
console.log(`Plugin removed: ${event.binding.key}`);
479
}
480
481
async getAllPlugins(): Promise<Plugin[]> {
482
return this.pluginView.values();
483
}
484
485
getPluginCount(): number {
486
return this.pluginView.bindings.length;
487
}
488
}
489
490
// Usage
491
const context = new Context();
492
const pluginManager = new PluginManager(context);
493
494
// Add plugins
495
context.bind('plugins.auth')
496
.toClass(AuthPlugin)
497
.tag('plugin');
498
499
context.bind('plugins.logging')
500
.toClass(LoggingPlugin)
501
.tag('plugin');
502
```
503
504
### Interceptors
505
506
Aspect-oriented programming support with method interceptors and cross-cutting concerns.
507
508
```typescript { .api }
509
/**
510
* Interceptor function to intercept method invocations
511
*/
512
type Interceptor = (
513
context: InvocationContext,
514
next: () => ValueOrPromise<InvocationResult>
515
) => ValueOrPromise<InvocationResult>;
516
517
/**
518
* Decorator to apply interceptors to methods or classes
519
*/
520
function intercept(...interceptorOrKeys: InterceptorOrKey[]): MethodDecorator & ClassDecorator;
521
522
/**
523
* Decorator to register global interceptors
524
*/
525
function globalInterceptor(group?: string, ...specs: BindingSpec[]): ClassDecorator;
526
527
/**
528
* Invoke a method with interceptors
529
*/
530
function invokeMethodWithInterceptors(
531
context: Context,
532
target: object,
533
methodName: string,
534
args: InvocationArgs,
535
options?: InvocationOptions
536
): Promise<InvocationResult>;
537
```
538
539
**Usage Examples:**
540
541
```typescript
542
import {
543
intercept,
544
globalInterceptor,
545
Interceptor,
546
InvocationContext
547
} from "@loopback/core";
548
549
// Logging interceptor
550
const loggingInterceptor: Interceptor = async (context, next) => {
551
console.log(`Calling ${context.targetName}.${context.methodName}`);
552
const startTime = Date.now();
553
554
try {
555
const result = await next();
556
const duration = Date.now() - startTime;
557
console.log(`Completed ${context.methodName} in ${duration}ms`);
558
return result;
559
} catch (error) {
560
console.error(`Error in ${context.methodName}:`, error.message);
561
throw error;
562
}
563
};
564
565
// Authentication interceptor
566
const authInterceptor: Interceptor = async (context, next) => {
567
const request = context.args[0];
568
if (!request.headers.authorization) {
569
throw new Error('Authentication required');
570
}
571
572
// Validate token
573
const token = request.headers.authorization.replace('Bearer ', '');
574
const user = await validateToken(token);
575
576
// Add user to context
577
context.source = { ...context.source, currentUser: user };
578
579
return next();
580
};
581
582
// Apply interceptors to methods
583
class UserService {
584
@intercept(loggingInterceptor, authInterceptor)
585
async createUser(userData: UserData): Promise<User> {
586
// Method implementation
587
return new User(userData);
588
}
589
590
@intercept(loggingInterceptor)
591
async getUser(id: string): Promise<User | null> {
592
// Method implementation
593
return null;
594
}
595
}
596
597
// Apply interceptors to entire class
598
@intercept(loggingInterceptor)
599
class OrderService {
600
async createOrder(orderData: OrderData): Promise<Order> {
601
// All methods will be intercepted
602
return new Order(orderData);
603
}
604
605
async updateOrder(id: string, updates: Partial<OrderData>): Promise<Order> {
606
// This method is also intercepted
607
return new Order({});
608
}
609
}
610
611
// Global interceptor
612
@globalInterceptor('audit')
613
class AuditInterceptor implements Provider<Interceptor> {
614
value(): Interceptor {
615
return async (context, next) => {
616
// Log all method calls globally
617
console.log(`Audit: ${context.targetName}.${context.methodName} called`);
618
return next();
619
};
620
}
621
}
622
```
623
624
### Provider Pattern
625
626
Interface for dynamic value providers with lazy evaluation.
627
628
```typescript { .api }
629
/**
630
* A provider is a class that defines a value function to return a value
631
*/
632
interface Provider<T> {
633
value(): T | Promise<T>;
634
}
635
636
/**
637
* Check if a class is a provider
638
*/
639
function isProviderClass<T>(providerClass: Constructor<unknown>): boolean;
640
```
641
642
**Usage Examples:**
643
644
```typescript
645
import { Provider, Context, inject } from "@loopback/core";
646
647
// Configuration provider
648
class DatabaseConfigProvider implements Provider<DatabaseConfig> {
649
value(): DatabaseConfig {
650
return {
651
host: process.env.DB_HOST || 'localhost',
652
port: parseInt(process.env.DB_PORT || '5432'),
653
database: process.env.DB_NAME || 'myapp',
654
ssl: process.env.NODE_ENV === 'production'
655
};
656
}
657
}
658
659
// Async provider
660
class ExternalServiceProvider implements Provider<ExternalService> {
661
async value(): Promise<ExternalService> {
662
const config = await loadRemoteConfig();
663
return new ExternalService(config);
664
}
665
}
666
667
// Provider with dependencies
668
class EmailServiceProvider implements Provider<EmailService> {
669
constructor(
670
@inject('config.email') private emailConfig: EmailConfig,
671
@inject('services.logger') private logger: LoggerService
672
) {}
673
674
value(): EmailService {
675
this.logger.log('Creating email service');
676
return new EmailService(this.emailConfig);
677
}
678
}
679
680
// Context setup
681
const context = new Context();
682
683
context.bind('config.database').toProvider(DatabaseConfigProvider);
684
context.bind('services.external').toProvider(ExternalServiceProvider);
685
context.bind('services.email').toProvider(EmailServiceProvider);
686
687
// Usage
688
const dbConfig = await context.get<DatabaseConfig>('config.database');
689
const emailService = await context.get<EmailService>('services.email');
690
```
691
692
## Types
693
694
```typescript { .api }
695
type BoundValue = any;
696
697
type BindingAddress<T = unknown> = string | BindingKey<T>;
698
699
type BindingSelector<T = unknown> = BindingAddress<T> | BindingFilter;
700
701
type BindingFilter = (binding: Readonly<Binding<unknown>>) => boolean;
702
703
type BindingTemplate<T = unknown> = (binding: Binding<T>) => void;
704
705
type BindingComparator = (
706
a: Readonly<Binding<unknown>>,
707
b: Readonly<Binding<unknown>>
708
) => number;
709
710
type Constructor<T = {}> = new (...args: any[]) => T;
711
712
type ValueOrPromise<T> = T | Promise<T>;
713
714
type Getter<T> = () => Promise<T>;
715
716
interface InjectionMetadata {
717
optional?: boolean;
718
asProxyWithInterceptors?: boolean;
719
bindingComparator?: BindingComparator;
720
}
721
722
interface ConfigInjectionMetadata extends InjectionMetadata {
723
fromBinding?: BindingAddress<unknown>;
724
}
725
726
type ConfigurationPropertyPath = string;
727
728
type InvocationArgs = any[];
729
type InvocationResult = any;
730
731
interface InvocationContext {
732
target: object;
733
methodName: string;
734
args: InvocationArgs;
735
source?: object;
736
targetName?: string;
737
}
738
739
interface InvocationOptions {
740
skipInterceptors?: boolean;
741
source?: object;
742
}
743
```