Better Typescript Dependency Injection
Last post I described how I was setting up a declarative API to route mapping in Typescript with Express. One of the weak points in that setup was the dependency injection. A couple of the limitations that it had was it only really worked on the controller classes and had no real concept of scope.
In this post, I’ll describe how I partially added scope and made the entire system more general purpose.
Philosophical Point
My experience with DI frameworks is limited to Autofac on .Net, so this might or might not apply very well outside of that. One frustration I had with that framework is modifying the registrations on a given child scope was not a straightforward process.
Each incoming request on a complex web application requires a number of decisions about what objects make up what is essentially the execution context of the app. Here, execution context means the transient information, both whats — like what user — and hows — like what implementation — that can only be determined at run-time per request. Conceptually, DI frameworks provide a great way to specify and control the execution context. However, in practice customizing the registrations after initialization is awkward
I’m designing this DI setup to provide registration as first-class functionality. This will hopefully make it more natural to treat the execution context as a living thing instead of something merely initialized at application startup.
To be clear, it’s not that you can’t modify the registry of the child scopes in Autofac, it’s just more awkward than it should be.
Disclaimer
While the new setup is far more functional, there are still a big DI basic missing: there is no object lifetime control. Every resolve executes the appropriate factory regardless of scope. As a result, scope merely provides scope for registrations. Object lifetime control will be added in an upcoming post.
The Interface
The DI setup is still pretty simple.
- ArgumentList describes what needs to be injected into any given constructor or function.
- CreateChildScope creates a new child scope of the current scope that lets you register and resolve new stuff
- Register lets you register new constructors using the default factory. This factory will look for metadata @InjectParameter to call the constructor with.
- RegisterFactory lets you register a new object factory to use during injection. Each factory is passed a resolve function that will resolve from the current scope. Right now if you want to register an instance you just pass in a factory like () => someInstance.
- FunctionApply will call apply on functionHandle with thisParam as this using the arguments described in ArgumentList. This is used to call the various actions on the API controllers.
Implementation
Configuring Injection
Specifying the injection information for a class or function is done with the @InjectParameter decorator and specifies what should be placed in each parameter. This example is using the new AddToMetadata and GetMetadata functions that stores metadata on the prototype and more or less work the same way as in the previous post, but have been made general purpose.
Usage is pretty straightforward: just place @InjectParameter(‘injectionKey’) in front of the parameter and it’ll work. You can produce special case versions by doing var FromBody = InjectParameter(‘BodyKey’); and then putting @FromBody in front of the parameter.
Creating Scopes
This is one point my knowledge of “how to do JS right” might be failing me, but it works and I find it fairly elegant so I’m rolling with it.
Each child scope is essentially an instance of an anonymous class with a prototype that is the parent scope. The nice thing about this is that JS provides exactly the functionality we need. New registrations will only exist at the current scope and the parent scope will still be available. New registrations will also override the parent scope registrations without destroying them.
There is one RootScope that contains the scope functionality and serves as the root of the prototype chain for all child scopes. Global registrations will be conducted on this scope.
The functions themselves are pretty straightforward. Registering a factory defines a property on this that is the factory.
- RegisterFactory uses Object.defineProperty(..) to override any properties on the parent scope when re-registering an existing name.
- Register calls RegisterFactory after getting the default factory for a constructor.
- Resolve finds the appropriate factory and then calls it, passing a bound version of Resolve for the factory to use. If the registration isn’t there then it returns undefined.
- FunctionApply will call a function on an object using the parameters described in the provided ArgumentList.
- _getChildScope() defines an anonymous class that inherits from the previous scope and then instantiate and returns the instance.
- _buidArgs() iterates through an argumentList and Resolves each item.
It is important to note that argumentList is not an array. It is entirely possible that an injected argument will be specified in argument indexes 0 and 2 but not 1. ArgumentList.length is merely the max index + 1 that @InjectParameter was used. It is not the total number of arguments specified on the function.
The Default Factory: Constructor Injection
_buildDefaultFactoryFunction() uses the @InjectParameter metadata from the constructor to build a factory with new Function() that will take the specified arguments and instantiate the provided constructor.
Constructor injection is configured the same way that injection on functions is defined, except that it just so happens the parameter decorators run with undefined as the key.
builderFunc winds up being something like:
function(c, p0, p1) { return new c(p0, p1); }The resulting factory iterates through the argumentList and resolves using the given resolve function. Then calls buildFunc with the correct arguments and returns the result.
Usage
This is how I implemented controller injection using this DI framework. Constructing the URLs and specifying the HTTP method is missing and partially described in the previous post — though the details have changed. This example just focuses on how the DI setup is used.
- _getRequestScope creates a new child scope and then registers the required context from the request. This includes the request parameters, the body of the request, the current user, and the request object itself.
- _registerController registers the controller with a unique key (I’ll probably change this to a guid at some point, right now what I have serves). You could very well use pre-registered controllers and somehow specify the registration key, but this is how I’m doing it for now.
- _makeRequestHandler builds a Express request handler function that will create a new child scope from the request, resolve a controller for it, then uses scope.FunctionAppy to call the appropriate function on the controller with the injected arguments.
- _bindToRouter essentially registers the controller, extracts all the needed metadata, and then binds each configured function to a route handler.
This setup provides a flexible way to provide an execution context for a given URL in a concise and clear manner. The URLs are generated by convention based on metadata specified using decorators on the controller classes and methods.
The end result is that controllers can state what job they do, what they need to do that job, and then all they need to be concerned with is returning the result.