Using Dependency Injection

If you recall from the ServiceRegistration page, you can have

  • One to one relation: one ServiceType to one ImplementationType
  • Many to one: one ServiceType to many ImplementationTypes

In this page we're gonna learn how to be able to request them back! To request a ServiceType you can either use class constructor or Service Locator Pattern

Any class that is utilzing the DI should have the @Injectable() so it can be part of the play!.

Constructor parameters#

// Abstract Service
@Injectable()
abstract class Logger {}

// Concrete Service
@Injectable()
class InformativeLogger extends Logger {}

// Client
@Injectable()
class Application {
	constructor(private logger: Logger) {}
}

Injector.AddSingleton(Logger, InformativeLogger);
Injector.AddSingleton(Application);

In the previous sample the Application class declared the abstract Logger as dependence. the Logger class have InformativeLogger as implementation type which implies that the InformativeLogger will be injected in the Application class. that's due the registration of InformativeLogger against Logger

Same case with requesting multiple ImplementationTypes from the same ServiceType

abstract class Logger {}

@Injectable()
class InformativeLogger extends Logger {}

@Injectable()
class WarningLogger extends Logger {}

@Injectable()
class Application {
	constructor(@Inject(Logger) private loggers: Logger[]) {}
}

Injector.AppendSingleton(Logger, InformativeLogger);
Injector.AppendSingleton(Logger, WarningLogger);
Injector.AddSingleton(Application);

In the previous sample the Application class declared the abstract Logger as dependence but with array type instead of the abstraction and with @Inject decorator.

This is required to know that your intention is to have array of implementations.

The Logger class have InformativeLogger and WarningLogger as implementations which implies that the both InformativeLogger and WarningLogger will be injected in the Application class as array type. that's due the registration of InformativeLogger and WarningLogger against Logger

The @Inject decorator is required to get array of implementations for a service type but also you have to make sure that the type of parameter is Array as Logger[] or Array<Logger>

@Injectable()
class Application {
	constructor(@Inject(Logger) private loggers: Logger) {}
}

Important: If the array type omitted the last registered implementation will be returned.

Service Locator Pattern#

Singleton and Scoped lifetimes are bound to a context.

For Singleton there's a context that is used internally, but for Scoped service you have to create them yourself.

Singleton#

@Injectable()
class SingletonService {}
Injector.AddSingleton(SingletonService);

const service = Injector.GetRequiredService(SingletonService);

Scoped#

Scoped is more like Singleton but for an explict context, so you need to create a context and provide it.

@Injectable()
class ScopedService {}
Injector.AddScoped(ScopedService);

const context = Injector.Create();
const scopedService1 = Injector.GetRequiredService(ScopedService, context);
const scopedService2 = Injector.GetRequiredService(ScopedService, context); // return the same instance as above, scopedService1 === scopedService2

const secondContext = Injector.Create();
const scopedService3 = Injector.GetRequiredService(
	ScopedService,
	secondContext
); // will return new instance

Don't forgot to destroy the context when you done using it.

Injector.Destroy(context);

Transient#

@Injectable({
	lifetime: ServiceLifetime.Transient,
})
class TransientService {
	constructor() {}
}

const transientService = Injector.GetRequiredService(TransientService);

In case you did inject a Scoped service into a Transient service, you'll have to provide context, otherwise LifestyleMismatchException will be thrown.

this happens due to the fact that Scoped service created once per context and Transient service are created each time is requested, hence without context you'll have different instance for the Scoped service within the Transient service so providing a context will make sure that the same scoped service is returned regardless of how many times the Transient service created.

@Injectable({
	lifetime: ServiceLifetime.Scoped,
})
class ScopedService {}

@Injectable({
	lifetime: ServiceLifetime.Transient,
})
class TransientService {
	constructor(scopedService: ScopedService) {}
}

// Will throw an exception
const transientService = Injector.GetRequiredService(TransientService);

// works fine.
const context = Injector.Create();
const logger1 = Injector.GetRequiredService(TransientService, context);
const logger2 = Injector.GetRequiredService(TransientService, context);

/**
 * Now, logger1 and logger2 are different instances but,
 * logger1.scopedService and logger2.scopedService are the same
 */

It is always preferred to pass context with Transient services to avoid an handled exceptions.

Locating Array of the same service type#

abstract class Logger {}

@Injectable()
class InformativeLogger extends Logger {}

@Injectable()
class WarningLogger extends Logger {}

Injector.AppendSingleton(Logger, InformativeLogger);
Injector.AppendSingleton(Logger, WarningLogger);

const loggers = Injector.GetServices(Logger);

Lifetime and registration options#

abstract class Operation {
	abstract operationId: string;
}

@Injectable({
	lifetime: ServiceLifetime.Transient,
})
class OperationTransient extends Operation {
	operationId = Math.random().toString(36);
}

@Injectable({
	lifetime: ServiceLifetime.Scoped,
})
class OperationScoped extends Operation {
	operationId = Math.random().toString(36);
}

@Injectable({
	lifetime: ServiceLifetime.Singleton,
})
class OperationSingleton extends Operation {
	operationId = Math.random().toString(36);
}

@Injectable()
class Application {
	constructor(
		private transientOperation1: OperationTransient,
		private transientOperation2: OperationTransient,
		private scopedOperation1: OperationScoped,
		private scopedOperation2: OperationScoped,
		private singletonOperation: OperationSingleton
	) {}
}

const context = Injector.Create();
const app = Injector.GetRequiredService(Application, context);

console.log(
	app.transientOperation1.operationId === app.transientOperation2.operationId
); // false

console.log(
	app.scopedOperation1.operationId === app.scopedOperation2.operationId
); // true
  • Transient objects are always different. transientOperation2 is not the same as transientOperation1.

  • Scoped objects are the same for each context but different across each context.

  • Singleton objects are the same for every context.

Summary#

  • @Injectable is required on top of each service even if you don't use it to register a service.

  • Once you add service you cannot override it and an error will be throw if you tried to do.

  • If you want to override a service you have to use Injector.Replace{Lifetime} instead.

  • A service will only be created when requested with respect to a service lifetime.

  • You would use Append{Lifetime} to have multiple implementation for the same service type.

  • Use @Inject to resolve multiple implementations with array type, @Inject(Logger) private loggers: Logger[].

  • @Inject is optional to resolve single implementation.

@Injectable()
class Application {
	constructor(
		private logger: Logger,
		// Shorthand for
		@Inject(Logger) private logger: Logger
	) {}
}
  • Last implementation will always returned if you don't use array type.

  • Inject Context to resolve deferred dependencies.