A first look at Angel 2's revamped DI functionality.

Along with the many new features coming in Angel 2, the dependency injection has been rethought from the ground up, saying goodbye to the mirrors-powered package:container, and instead rolling a new package:angel_container, which is type-safe, and aimed for Dart 2, as well as Flutter.

As one can imagine, new foundations yield new patterns. With the new DI system, writing Controller classes, sharing data, and observing the DRY principle are all easier than ever before.

Structure and Usage

package:angel_container revolves around two classes: Container and Reflector. Whereas Reflector ultimately just boils down to an abstract API around functionality usually found in dart:mirrors, the Container class exists in a hierarchy, with support for singletons, factories, and named singletons.

// If you're ok with simply falling back upon dart:mirrors:
var container = Container(MirrorsReflector());

// If you're in a situation without reflection (Flutter):
var container = Container(EmptyReflector());

// Register a singleton:
container.registerSingleton(Engine(40));

// Register a singleton, *by type*.
// Necessary when using abstract classes.
container.registerSingleton<Engine>(Engine(40));

// Create an instance of Engine. In this case, the container
// will find our singleton.
//
// This call returns an Engine, both at runtime, AND statically,
// so you still get all your code completions and type-checking!
container.make<Engine>().drive();

// Register a factory that creates a truck,
// using a subclass as the actual implementation.
container.registerFactory<Truck>((container) {
    return _TruckImpl(container.make<Engine>());
});

// Again, create an instance.
// The container first searches for singletons, and then factories.
container.make<Truck>.revEngine();

// Register a named singleton.
container.registerNamedSingleton('the_truck', truck);

// Should print: 'Vroom! I have 40 horsepower in my engine.'
truck.drive();

// Should print the same.
container.findByName<Truck>('the_truck').drive();

In addition to singletons, Container can inject dependencies into constructors:

void pseudoCode() {
    // Register a singleton.
    container.registerSingleton(Bar(baz: 'hey!'));
    
    // The Container uses our reflector to find and inject
    // params into constructors:
    container.make<Foo>.go();
    
    // The above should print:
    // foo bar hey!
}

class Foo {
    final Bar bar;
    void go() => print('foo bar ${bar.baz}');
}

class Bar {
    final String baz;
    Bar({this.baz});
}

Hierarchy

Maybe the most important feature of Container, within a server-side framework, is that Container instances create a hierarchy. One Container may have up to one parent, and an infinite number of children. Container.make<T> first searches for singletons and factories within its own scope; if resolution fails, it then searches within the parent.

In Angel, every RequestContext has its own container property, so services (i.e. database connections, sensitive resources) can be shared across requests, as well as allowing handlers to pass around user-specific data (i.e. a JWT token or session).

Inversion of Control

A flexible container makes inversion of control (IoC) pretty straightforward to implement. Within Angel (or virtually any other HTTP framework), functions and handlers are invoked via the router, rather than the developer explicitly calling them. In addition, the same process used to inject dependencies into constructors can be used to inject parameters into handlers.

In Angel 2, the router takes advantage of Dart 2's strong typing, and so every handler must be a FutureOr Function(RequestContext, ResponseContext). To enable IoC for a specific handler, wrap it in a call to ioc (which also opens the door to other features, like pattern matching):

// Pattern matching - only call this handler if the query value of `name` equals 'emoji'.
app.get(
  '/greet',
  ioc((@Query('name', match: 'emoji') String name) => '😇🔥🔥🔥'),
);

// Handle any other query value of `name`.
app.get(
  '/greet',
  ioc((@Query('name') String name) => 'Hello, $name!'),
);

This is for a reason - Angel 2 gets rid of Angel 1's mirrors-by-default, so IoC became an opt-in feature. In addition, knowing the type of every handler beforehand makes framework maintenance much more error-proof, and doesn't even require Angel 1's strategy, which was memoizing reflection information from each handler.

Controllers

In addition to services and plain-old function route handlers, Angel also has first-class support for controllers. Within Angel, controllers "compile" to plain-old function handlers, and piggyback on top of the DI system.

You can focus on your application logic, rather than sorting out bugs or manually parsing out request parameters. The parameters to an exposed method can be virtually anything, so long as the Container can find it at runtime:

// Mounting a controller is extremely simple.
// All dependencies will be injected for you.

Future configureServer(Angel app) async {
    await app.mountController<FooController>();
}

@Expose('/api/foo')
class FooController extends Controller {
    final Db db;
    final Truck truck;
    
    // The container will find and inject these at runtime.
    FooController(Db db, Truck truck);
    
    @Expose('/find_it/:hexString')
    Future<Map<String, dynamic>> findIt(String hexString) async {
      var id = ObjectId.fromHexString(hexString);
      return await db.collection('foo').findOne(where.id(id));
    }
    
    @Expose('/bar')
    String bar() => 'truck: ${truck.engineSize}';
}

In addition, every controller injects itself into the application as a singleton, so you could call container.make<FooController>(), or even provide FooController as a function, method, or constructor parameter anywhere. When you also consider the fact that a Controller is just a regular Dart class, you can reuse application logic from anywhere you can get an instance of FooController.

Use in Flutter/without Mirrors

Part of the motivation of removing mirrors-by-default in Angel was to make it possible to run an Angel server within a Flutter application, or in another situation where dart:mirrors is unavailable (i.e. the Web).

In such a situation, rather than using MirrorsReflector(), you can use package:angel_container_generator to generate an application-specific Reflector that only contains information about the classes you instruct it to. This way, you can still have dependencies injected into constructors and functions, no matter which platform you run on.

Without package:angel_container_generator, you can still always use singletons and factories - they require no reflection and are simply table-based, so you can even use them with EmptyReflector().

Conclusion

Change is a good thing, especially when said change makes code that depends on it more maintainable, and easier to reason about. package:angel_container is just one part of an Angel framework that has been largely reworked for better performance, sustainability, and an overall better developer experience.

As always, feel free to interact with Angel: