Dependency Injection (DI) is a design pattern that allows for creating objects dependent on other objects (their dependencies) without creating those dependencies themselves. It's a way of achieving loose coupling between classes and their dependencies. Aurelia provides a powerful and flexible DI system that can greatly simplify the process of wiring up the various parts of your application.
This document aims to provide comprehensive guidance on using DI in Aurelia, complete with explanations and code examples to illustrate its use in real-world scenarios.
As a system increases in complexity, it becomes increasingly important to break complex code down into groups of smaller, collaborating functions or objects. However, once we’ve broken down a problem/solution into smaller pieces, we have introduced a new problem: how do we put the pieces together?
One approach is to have the controlling function or object directly instantiate all its dependencies. This is tedious but also introduces the bigger problem of tight coupling and muddies the controller's primary responsibility by forcing upon it a secondary concern of locating and creating all dependencies. Inversion of Control (IoC) can be employed to address these issues.
Simply put, the responsibility for locating and/or instantiating collaborators is removed from the controlling function/object and delegated to a 3rd party (the control is inverted).
Typically, this means that all dependencies become parameters of the function or object constructor, making every function/object implemented this way not only decoupled but open for extension by providing different implementations of the dependencies. Providing these dependencies to the controller is called Dependency Injection (DI).
Once again, we’re back at our original problem: how do we put all these pieces together? With the control merely inverted and open for injection, we are now stuck having to manually instantiate or locate all dependencies and supply them before calling the function or creating the object…and we must do this at every function call site or every place that the object is instanced. It seems this may be a bigger maintenance problem than we started with!
Fortunately, there is a battle-tested solution to this problem. We can use a Dependency Injection Container. With a DI container, a class can declare its dependencies and allow the container to locate and provide them to the class. Because the container can locate and provide dependencies, it can also manage the lifetime of objects, enabling singleton, transient and object pooling patterns without consumers needing to be aware of this complexity.
Constructor injection is the most common form of DI. It involves providing the dependencies of a class through its constructor.
In Aurelia, there are several ways to declare dependencies for injection into plain classes:
You can specify the dependencies by adding a static inject
property to your class, which is an array of the dependencies:
The order of dependencies in the inject
array must match the order of the parameters in the constructor.
With the @inject
decorator, you can declare dependencies in a more declarative way:
If you use TypeScript and have enabled metadata emission, you can leverage the TypeScript compiler to deduce the types to inject:
Any decorator on a class will trigger TypeScript to emit type metadata, which Aurelia's DI can use.
An Aurelia application typically has a single root-level DI container. To create one:
In Aurelia, services can be registered with the container using the register
API:
The register
method allows you to associate a key with a value, which can be a singleton, transient, instance, callback, or alias.
Services are usually resolved automatically via constructor injection. However, you can also resolve them manually:
For multiple implementations, use getAll
:
Since TypeScript interfaces do not exist at runtime, you can use a symbol to represent the interface:
Using DI.createInterface()
, you can create an interface token that also strongly types the return value of get
:
DI.createInterface()
can take a callback to provide a default implementation:
When inheritance is involved, constructor injection may not suffice. Property injection using the resolve
function can be used in such cases:
resolve
Usagesresolve
can also be used in factory functions or other setup logic:
Remember, resolve
must be used within an active DI container context.
For those migrating from Aurelia 1, most concepts remain the same, but it is recommended to use DI.createInterface
to create injection tokens for better forward compatibility and consumer friendliness.
You can inject an interface using either the decorator or the token directly:
You can explicitly create resolvers and decorators to control how dependencies are registered:
Decorators can also be used to register classes in the root or requesting container:
You can customize how dependencies are injected using additional decorators:
For injecting objects like Window
with additional properties:
By following these guidelines and utilizing the powerful features of Aurelia's DI system, you can build a well-architected application with cleanly separated concerns and easily manageable dependencies.
Resolvers in Aurelia 2 are integral to the Dependency Injection (DI) system, providing various strategies for resolving dependencies. This guide will cover each resolver type, its usage, and when to use it, with detailed code examples for both the @inject
decorator and static inject
property methods. Additionally, we will discuss how to create custom resolvers.
Aurelia 2 offers several built-in resolvers to address different dependency resolution needs. Here's how to use them with both the @inject
decorator and static inject
property.
lazy
ResolverUse the lazy
resolver when you want to defer the creation of a service until it's needed. This is particularly useful for expensive resources.
@inject
Decoratorinject
Propertyall
ResolverThe all
resolver is used to inject an array of all instances registered under a particular key. This is useful when working with multiple implementations of an interface.
@inject
Decoratorinject
Propertyoptional
ResolverThe optional
resolver allows a service to be injected if available, or undefined
if not. This can prevent runtime errors when a dependency is not critical.
@inject
Decoratorinject
Propertyfactory
ResolverThe factory
resolver provides a function to create instances of a service, allowing for more control over the instantiation process.
@inject
Decoratorinject
PropertynewInstanceForScope
ResolverUse newInstanceForScope
when you need a unique instance of a service within a particular scope, such as a component or sub-container.
@inject
Decoratorinject
PropertynewInstanceOf
ResolverThe newInstanceOf
resolver ensures that a fresh instance of a service is created each time, regardless of other registrations.
@inject
Decoratorinject
PropertyYou can create custom resolvers by implementing the IResolver
interface. Custom resolvers give you the flexibility to implement complex resolution logic that may not be covered by the built-in resolvers.
In the example above, MyCustomResolver
is a custom resolver that creates a new instance of MyService
. You can further customize the resolve
method to suit your specific requirements.
By understanding and utilizing these resolvers, you can achieve a high degree of flexibility and control over the dependency injection process in your Aurelia 2 applications. The examples provided illustrate how to apply each resolver using both the @inject
decorator and static inject
property, giving you the tools to manage dependencies effectively in any situation.
Creating injectable services is crucial for building maintainable applications in Aurelia. Services encapsulate shared functionalities such as business logic or data access, and can be easily injected where needed. This guide will demonstrate various methods for creating injectable services, including the use of DI.createInterface()
and directly exporting classes.
DI.createInterface()
to Create Injectable ServicesDI.createInterface()
allows you to create an injection token that can be used with a default implementation:
Here, ILoggerService
is both an injection token and a type representing the LoggerService
class. This simplifies the process as you won't need to manually define an interface with all the methods of the LoggerService
.
You can also create an interface token without a default implementation for more flexibility:
In this scenario, you must register the implementation with the DI container:
For services that don't require an interface, simply exporting a class is sufficient:
This service can be auto-registered or manually registered:
Aurelia supports decorators for controlling service registration:
The @singleton()
decorator ensures a single instance of AuthService
within the DI container.
Sometimes, you may want to export a type equal to the class, which can serve as an interface. This approach reduces redundancy and keeps the service definition concise:
You can then use this type for injection and for defining the shape of your service without having to declare all methods explicitly:
Aurelia provides multiple approaches to creating and registering injectable services, including DI.createInterface()
and directly exporting classes or types. By choosing the method that best fits your needs, you can achieve a clean and flexible architecture with easily injectable services, promoting code reuse and separation of concerns across your application.