Dependency injection (DI)
Dependency Injection (DI) is a design pattern that enables classes to receive their dependencies from an external source rather than instantiating them directly. This inversion of control simplifies wiring up your application, promotes loose coupling, and enables advanced patterns such as singleton, transient, and scoped lifetimes. In Aurelia, DI is a core feature that not only manages the creation and resolution of dependencies but also provides powerful strategies—called resolvers—to control how dependencies are delivered.
Table of Contents
Overview
Dependency Injection is a design pattern that decouples object creation from business logic. Instead of a class instantiating its own dependencies, those dependencies are provided by an external DI container. This approach:
Improves testability: You can inject mocks or stubs for unit testing.
Promotes loose coupling: Classes depend on abstractions rather than concrete implementations.
Manages lifetimes: The DI container controls the lifetime of objects (singleton, transient, scoped, etc.).
Facilitates configuration: Changing implementations or registration strategies is centralized.
In Aurelia 2, the DI container not only instantiates classes but also resolves dependencies based on metadata declared via constructor parameters, static properties, or decorators.
Constructor Injection & Declaring Dependencies
Injecting into Plain Classes
Aurelia supports several approaches for declaring dependencies:
Using a Static Property
Define dependencies by setting a static inject
property that lists the dependencies in the same order as the constructor parameters.
Warning: The order in the
inject
array must match the constructor parameters.
Using Decorators
Leverage the @inject
decorator for a more declarative style.
Creating Containers and Registering Services
Creating a DI Container
A typical Aurelia application has a single root-level DI container:
Registering Services
Register services with the container using the register
API. This associates a key with a value (or class) and controls its lifetime.
Deregistering Services
You can also remove a service from the container when needed:
Resolving Services
Although constructor injection is the norm, you can manually resolve services from the container:
Single instance:
Multiple implementations:
Using Interfaces & Injection Tokens
Since TypeScript interfaces don’t exist at runtime, use symbols or tokens for injection.
Using Symbols
Using DI.createInterface()
DI.createInterface()
Create a strongly typed injection token that can also provide a default implementation:
With a Default Implementation
Without a Default Implementation
Then register the implementation with the container:
Property Injection
For cases like inheritance where constructor injection may not suffice, use property injection via the resolve
function:
You can also use resolve
in factory functions:
Note:
resolve
must be used within an active DI container context.
Resolvers
Resolvers in Aurelia 2 provide strategies for how dependencies are resolved. They give you granular control over instance creation and lifetime management.
Built-in Resolvers Summary
The table below summarizes the built-in resolvers available in Aurelia:
Resolver
Purpose
Usage
lazy
Delays creation of a service until it is needed.
@inject(lazy(MyService))
or static inject = [lazy(MyService)]
all
Injects an array of all instances registered under a particular key.
@inject(all(MyService))
or static inject = [all(MyService)]
optional
Injects the service if available, otherwise undefined
.
@inject(optional(MyService))
or static inject = [optional(MyService)]
factory
Provides a function to create instances, offering control over instantiation.
@inject(factory(MyService))
or static inject = [factory(MyService)]
newInstanceForScope
Provides a unique instance within a particular scope (e.g., component or sub-container).
@inject(newInstanceForScope(MyService))
newInstanceOf
Always creates a fresh instance, regardless of existing registrations.
@inject(newInstanceOf(MyService))
last
Injects the most recently registered instance among multiple registrations.
@inject(last(MyService))
Each resolver can be used with both the @inject
decorator and the static inject
property. Below are detailed examples for a few of them.
Examples
Lazy Resolver
All Resolver
Optional Resolver
Factory Resolver
newInstanceForScope Resolver
newInstanceOf Resolver
Last Resolver
Last Resolver Example with Multiple Registrations
If no instances are registered, the last resolver returns undefined
:
Custom Resolvers
You can create custom resolvers by implementing the IResolver
interface to handle complex resolution logic.
Creating Injectable Services
Building maintainable applications in Aurelia often involves creating services that encapsulate shared functionality (business logic, data access, etc.). There are several approaches to define injectable services.
Using DI.createInterface()
to Create Injectable Services
DI.createInterface()
to Create Injectable ServicesThis method creates an injection token that doubles as a type and, optionally, provides a default implementation.
With a Default Implementation
Without a Default Implementation
Then register the service with the container:
Exporting Classes Directly for Injectable Services
For services that do not require an abstraction, simply export the class.
Simple Class Export
Register if needed:
Decorator-based Registration
Use decorators like @singleton()
for auto-registration.
Exporting a Type Equal to the Class
This approach reduces redundancy by using the class as its own interface.
Then use it for injection:
Registration Types & Customizing Injection
Aurelia’s DI system offers various registration types to control how services are instantiated:
Registration Type
Description
Example
singleton
One instance per container.
Registration.singleton(MyService, MyService)
transient
A new instance is created every time.
Registration.transient(MyService, MyService)
instance
Registers a pre-created instance.
Registration.instance(MyService, myServiceInstance)
Decorators can also be used to register classes:
You can further customize injection by using additional decorators and helper functions:
For extending built-in objects, you might define interfaces that augment native types:
Migrating from v1 to v2
Many DI concepts remain consistent between Aurelia 1 and Aurelia 2. However, it is recommended to use DI.createInterface()
to create injection tokens for better forward compatibility and improved type safety. When injecting interfaces, you can use decorators or resolve functions directly:
By following these guidelines and examples, you can leverage Aurelia 2’s powerful Dependency Injection system to create well-architected, maintainable applications with clear separation of concerns and flexible service management.
Last updated
Was this helpful?