Skip to main content

Dependency injection

Dependency injection is a design pattern that allows you to decouple your code from its dependencies. It allow to write more flexible and more easily testable code.

In Primno, you declare your classes dependencies without taking care of how they are instantiated. The framework will do it for you.

tip

Dependency injection of Primno is similar to Angular's dependency injection.

The following example shows how to inject a NotifyService into a component:

notify.service.ts
import { Injectable } from '@primno/core';

@Injectable() // Mark the service as injectable
export class NotifyService {
constructor() {}

notify(message: string) {
Xrm.Navigation.openAlertDialog({ text: message });
}
}
dependency-injection.component.ts
import { MnComponent } from '@primno/core';
import { NotifyService } from './notify.service';

@MnComponent({
/* ... */
providers: [NotifyService] // Register the service as a provider
})
export class DependencyInjectionComponent {
constructor(
// Inject the service `NotifyService` into the component
private notifyService: NotifyService
) {}

notify() {
this.notifyService.notify('Hello world!');
}
}

Dependency injection system is split into 2 parts:

Providing a dependency

Providers are classes or values that can be injected into consumers. A provider can be a service, factory, repository, helper, etc.

Providers are registered in the dependency injection system using the providers array of a component or a module. Primno have a resolving priority when it comes to finding a provider. The resolving priority is explained in the resolving priority section.

Register a provider is done with a token. Its a unique identifier for the provider that can be a class, string, or symbol.

The following example shows how to register a provider in a module with a string token. The dependency MyService will be injected into consumers that use the MyToken token.

di.module.ts
@MnModule({
/* ... */
providers: {
provide: "MyToken", // Token, can be a class, string, or symbol
useClass: MyService // Provided class. Here `MyService` class.
}
})
export class DIModule {}

There are three types of providers:

  • ClassProvider: Creates an instance of a class. It is the most common provider.
  • ValueProvider: Provides a value, such as a string, number, or function.
  • FactoryProvider: Provides a value, or instance of a class, using a factory function.

Class provider

To mark a class as providable, you must use the @Injectable() decorator.

my.service.ts
@Injectable()
export class MyService {}

Next, register the provider in the dependency injection system. You can do it by adding the provider to the providers array of a component or module.

If the class identifier and the provided class is the same, you can just add the class to the providers array.

my.component.ts
@MnComponent({
/* ... */
providers: [MyService]
})
export class MyComponent {}

If the class identifier and the provided class is different, you must use the useClass property.

my.component.ts
@MnComponent({
/* ... */
providers: [
{
provide: MyService, // Class identifier
useClass: RealService // Provided class
}
]
})
export class MyComponent {}

You can also mark a class as a dependency and at the same time provide it in the root injector with the Injectable() decorator. The class will be automatically injected in the root injector and will be available for all components. Because this method enable Tree-shaking, it is the recommended way to provide a dependency when the provided class is also the class identifier. The Tree-shaking is a process that removes unused code from the final bundle.

my.service.ts
@Injectable({
providedIn: "root" // Provide the class in the root injector.
})
export class MyService {}

@MnComponent({
/* ... */
// No need to add the provider in the `providers` array of this component or in the associated module
})
export class MyComponent {
constructor(myService: MyService) {
// The class will be injected automatically
}
}

Value provider

Provides a value, such as a string, number, or function by using the useValue property.

my.component.ts
@MnComponent(
/* ... */
providers: [
{
provide: "MyToken"
useValue: "Hello world!" // Value to provide
}
]
)
export class MyComponent {}

Factory provider

A factory provider is a function that returns a value, or instance of a class by using the useFactory property.

my.module.ts
@MnModule(
/* ... */
providers: [
{
provide: "randomNumber",
useFactory: () => Math.random()
}
]
)
export class MyModule {}

Injecting a dependency

Dependency injection can be done via a dependency declared in the constructor or in a property of a class.

If the dependency is a class and its token is the class, you only need to add it to the constructor, the framework will inject it automatically.

di.component.ts
@MnComponent({
/* ... */
providers: [MyService]
})
class DiComponent {
constructor(
// Will be injected automatically with the token `MyService`
private myService: MyService
) {}
}

If the dependency uses a specific token or declared in a property, you must use the @Inject() decorator.

Constructor injection with @Inject()
@MnComponent(
/* ... */
providers: [
{
provide: "MyToken",
useClass: MyService
}
]
)
class DiComponent {
constructor(
@Inject("MyToken")
private myService: MyService
)
}
Property injection
@MnComponent(
/* ... */
providers: [
{
provide: MyService,
useClass: MyService
}
]
)
class DiComponent {
@Inject(MyService)
private myService: MyService;
}

Dependency tree

Injection can be done for a component or a service as long as it has been created by the injector.

In the following example, the component DiComponent requires the service AService that itself requires the service BService.

di.component.ts
@MnComponent(
/* ... */
providers: [AService, BService]
)
class DiComponent {
constructor(
private aService: AService
) {}
}
a.service.ts
@Injectable()
class AService {
constructor(
private bService: BService
) {}
}
b.service.ts
@Injectable()
class BService {}

Optional dependency

To inject a dependency that is not required, you can use the @Optional() decorator. If the dependency is not found, the value will be undefined.

Optional dependency
@MnComponent(
/* ... */
)
class DiComponent {
constructor(
@Optional()
private myService?: MyService
)
}

Resolving priority

Register a provider in a module, makes it available in entire application.

When you register a provider in a component, it will be available only in that component, its children and services.

Primno searches in priority in component providers by going up the component tree, then it searches in module providers.

Component vs module

In the following example, the component DiComponent requires the service MyService that is provided in itself and the module DIModule. The injected service will be the one registered in the component: DiComponentService.

di.component.ts
@MnComponent(
/* ... */
providers: [
{
provide: MyService,
// Injected in priority
useClass: DiComponentService
}
]
)
class DiComponent {
constructor(
private myService: MyService
) {}
}
di.module.ts
@MnModule({
declarations: [DiComponent],
providers: [
{
provide: MyService,
// Not injected in `DiComponent` because component providers have priority
useClass: DIModuleService
}
]
})
export class DIModule {}

Component tree

The following example illustrates the search priority in the component tree. The component DiChildComponent requires the service MyService that is registered in itself and in the parent component DiComponent.

di.component.ts
@MnComponent(
/* ... */
providers: [
{
provide: MyService,
useClass: DiComponentService
}
]
)
class DiComponent {
constructor(
// `DiComponentService` because `DIChildComponentService` is provided in its child.
private myService: MyService
) {}
}
di-child.component.ts
@MnComponent(
/* ... */
providers: [
{
provide: MyService,
useClass: DiChildComponentService
}
]
)
class DiChildComponent {
constructor(
// `DiChildComponentService` because `DiChildComponent` providers
// are resolved before `DiComponent` providers.
private myService: MyService
) {}
}

If you remove the MyService provider in the child component, the DIComponentService will be injected in DiChildComponent from the parent component.