Can we Bring back the Discussion of Allowing the Injection of Dependencies into Properties with the Core Framework?


#1

There have been a few feature requests, issues and even a pull request asking for the ability to inject dependencies into properties and they were all closed back in 2016. Several reasons where quoted as to why this amazing feature was not allowed in the core framework, one of which being that we can use a plugin. I argue that this functionality is needed by the core Aurelia framework and I would like to provide an example of why and bring this topic back up for discussion.

@EisenbergEffect, you stated in issue #125 of the dependency-injection project that:

"If you have classes, with many dependencies, then that can be a sign that you need to refactor in some way. "

This is a great suggestion and for most well-written (web-based) projects a class should never need more than a handful of dependencies. In these situations the current @inject pattern that Aurelia allows works just fine. However this principle–in practice, when scaled to large enterprise web development projects; can still result in a large number of composite dependencies. When you are building a configurable application, that supports multiple market segments, with complex business logic, you can have a well written class following composite design patterns and still need 40+ optional dependencies.

The application I work on contains over a thousand distinct pages. Most of these pages we are able to build from configuration files with generic components. We have a form component that dynamically creates, interacts with and composes controls. Our form class has dependencies on 40+ composite components. Doing this with Aurelia’s current dependency injection pattern looks very messy and it is difficult to maintain. To me, this feels like an issue with how well Aurelia’s built in IoC patterns scale.

My situation is further complicated by the fact that we must inject factories for all of our components into the constructor and save them to properties for later use. This is necessary because each component is optional and takes parameters into their constructors to control initialization. The dependencies we inject are composite controls (following the composite pattern) that return many different control configurations based on how they are constructed. Here is an example of just how terrible Aurelia’s built-in @inject pattern looks when applied to an example that is 1/5th the size I actually have to implement.

//This is really terrible
import ComboBox from    './ComboBox/ComboBox';
import Checkbox from    './Checkbox/Checkbox';
import NumericBox from  './NumericBox/NumericBox';
import DatePicker from  './DatePicker/DatePicker';
import TimeMask from    './TimeMask/TimeMask';
import HyperLink from   './HyperLink/HyperLink';
import Image from       './Image/Image';
import TextBox from     './TextBox/TextBox';

import { inject, Factory } from 'aurelia-framework';

// Type for Factory Creates a function with the same return type and parameters as a constructor
type Factory<T extends new (...a: any[]) => any> =
  T extends new (...a: infer A) => infer R ? (...a: A) => R : never;

@inject(
  Factory.of(ComboBox),
  Factory.of(Checkbox),
  Factory.of(NumericBox),
  Factory.of(DatePicker),
  Factory.of(TimeMask),
  Factory.of(HyperLink),
  Factory.of(Image),
  Factory.of(TextBox)
)
class Form {
  public GetComboBox: Factory<typeof ComboBox>;
  public GetCheckbox:  Factory<typeof Checkbox>;
  public GetNumericBox:  Factory<typeof NumericBox>;
  public GetDatePicker:  Factory<typeof DatePicker>;
  public GetTimeMask:  Factory<typeof TimeMask>;
  public GetHyperLink:  Factory<typeof HyperLink>;
  public GetImage:  Factory<typeof Image>;
  public GetTextBox:  Factory<typeof TextBox>;
  constructor(
    GetComboBox: Factory<typeof ComboBox>,
    GetCheckbox:  Factory<typeof Checkbox>,
    GetNumericBox:  Factory<typeof NumericBox>,
    GetDatePicker:  Factory<typeof DatePicker>,
    GetTimeMask:  Factory<typeof TimeMask>,
    GetHyperLink:  Factory<typeof HyperLink>,
    GetImage:  Factory<typeof Image>,
    GetTextBox:  Factory<typeof TextBox>
  ) {
    this.GetComboBox = GetComboBox;
    this.GetCheckbox = GetCheckbox;
    this.GetNumericBox = GetNumericBox;
    this.GetDatePicker = GetDatePicker;
    this.GetTimeMask = GetTimeMask;
    this.GetHyperLink = GetHyperLink;
    this.GetImage = GetImage;
    this.GetTextBox = GetTextBox;
  }
}

In contrast, the property injection plugin allows us to implement the same class with much less complexity:

//This is quite elegant
import ComboBox from './ComboBox/ComboBox';
import Checkbox from './Checkbox/Checkbox';
import NumericBox from './NumericBox/NumericBox';
import DatePicker from './DatePicker/DatePicker';
import TimeMask from './TimeMask/TimeMask';
import HyperLink from './HyperLink/HyperLink';
import Image from './Image/Image';
import TextBox from './TextBox/TextBox';

import { inject, factory } from 'aurelia-property-injection';

// Type for Factory Creates a function with the same return type and parameters as a constructor
type Factory<T extends new (...a: any[]) => any> =
  T extends new (...a: infer A) => infer R ? (...a: A) => R : never;

class Form {
  static injectProperties = {};
  @factory(ComboBox) GetComboBox: Factory<typeof ComboBox>;
  @factory(Checkbox) GetCheckbox:  Factory<typeof Checkbox>;
  @factory(NumericBox) GetNumericBox:  Factory<typeof NumericBox>;
  @factory(DatePicker) GetDatePicker:  Factory<typeof DatePicker>;
  @factory(TimeMask) GetTimeMask:  Factory<typeof TimeMask>;
  @factory(HyperLink) GetHyperLink:  Factory<typeof HyperLink>;
  @factory(Image) GetImage:  Factory<typeof Image>;
  @factory(TextBox) GetTextBox:  Factory<typeof TextBox>;
  constructor() {
    //setting the `injectConstructor` flag to true gives you access to injected properties here!  
  }
}

This plugin is great and I probably would not be making this post if this plugin was still being supported. Unfortunately, the property injection plugin has not been touched in two years with issues that are over a year old.

I really believe that this should be part of the core framework so that we don’t need to use an external package that is not being supported. When this feature was proposed as a pull request back in April of 2016, I don’t feel that the Aurelia team provided a good reason as to why the pull request was rejected. I want to bring this back up for discussion. I do not belive I am alone in my desire for this feature. Since it was created, the aurelia-property-injection plugin has been downloaded from npm 5,400 times


#2

I see your point with the @inject decorator on the class getting unwieldy, but what you are trying to accomplish can be done without property injection (and is perhaps better so). Now the DI container currently does not support this (however vNext does), but take a look at this and tell me what you think:

class Form {
  constructor(
    @factory(ComboBox) public GetComboBox: Factory<ComboBox>,
    @factory(Checkbox) public GetCheckbox:  Factory<Checkbox>,
    @factory(NumericBox) public GetNumericBox:  Factory<NumericBox>,
    @factory(DatePicker) public GetDatePicker:  Factory<DatePicker>,
    @factory(TimeMask) public GetTimeMask:  Factory<TimeMask>,
    @factory(HyperLink) public GetHyperLink:  Factory<HyperLink>,
    @factory(Image) public GetImage:  Factory<Image>,
    @factory(TextBox) public GetTextBox:  Factory<TextBox>
  ) {
  }
}

This is possible with parameter decorators, so I believe it solves your problem without property injection and either the temporal coupling that it introduces or, in case of that plugin, the constructor interception that it requires otherwise.

This could definitely be a core feature. Property injection probably not, but maybe someone can convince me with a legitimate use case that does not pertain to overcoming circular dependencies.


#3

This approach would still require me to set the properties in the constructor and create definitions for them correct?


#4

This approach would still require me to set the properties in the constructor

Nope, it would work exactly the way I posted it. In fact the only difference compared to your snippet is that the properties are declared as inline constructor parameter properties instead of regular properties. LOC count is the same (well one line less actually).

That’s a feat of TypeScript - it only works in TypeScript.


#5

Which is fine considering that the aurelia-property-injection only works in typescript as well.

How does this work with class inheritance?


#6

One thing I’ll mention is that, when I see code with this many dependencies, regardless of language or framework, I’m still inclined to ask whether there is a missing abstraction or other concept here that would simplify things. Without knowing your domain, I couldn’t say for sure, but I’d bet there is. Sometimes the abstraction is the idea of a factory or builder itself. Looking at this code, if it were me, I’d probably just inject Container as a single dependency and use it to create instances on demand. This is not something you want to do in normal application code. However, doing this inside core infrastructure is a legitimate exception to that rule. Your Form class looks a lot like core infrastructure to me. You’d have to be the judge of that of course.

On adding property injection to the core, I’m hesitant due to the fact that it would potentially cause a perf regression across the board. Since the framework itself only uses ctor injection and we advise ctor injection as the best practice, I’d prefer to keep any sort of property injection capability in an optional plugin. But as @fkleuver points out, if you’re using TypeScript, you can effectively get the same streamlined code by using ctor injection with ctor property definitions.


#7

Actually taking a closer look I agree with @EisenbergEffect that this form looks like it could be done more cleanly altogether.

I’m not sure if that form component represents a single form field or if it receives a list of properties that it dynamically render a list of the appropriate editors for (I’ve built something like that before as well), but in both cases it might make sense to have some kind of FormFieldResolver component that can take a piece of model metadata (like property type/name/config) and return the appropriate custom element to render.

Then you wouldn’t inject all those component factories, but rather you would instantiate the resolver somewhere and have it expose e.g. a registerFormField api. You register your components dynamically somewhere in a configure during startup, inject that resolver into your form, and the form can ask the resolver directly for components via some convention. Then adding/removing components doesn’t require you to change the shape of your classes either, and your list of components will just be a list of registration calls, which also lends itself to loops / dynamically loading everything in a folder, that sort of thing.

Maybe this approach makes no sense to your situation at all, but my point stands: these don’t look like they should be actual dependencies of the form itself.


#8

Agreed dependency injection can be done at the top level container u
sing factory. Always good to produce generic modular designs to SOLID(https://en.wikipedia.org/wiki/SOLID) principles


#9

I echo the sentiments from @EisenbergEffect and @fkleuver.

I’d like to add one point and highlight what you’re really asking.

I really believe that this should be part of the core framework so that we don’t need to use an external package that is not being supported.

It’s already available and you’re using it and like it. Your complaint is that it isn’t being supported. You’re just asking Aurelia core team members to maintain this now instead when the original authors have decided it isn’t worth maintaining.

I’d like to recommend two alternatives to you. First, if improvements to the plugin are valuable to you, try reaching out to the maintainers of the plugin and ask if you would be able to sponsor some fixes or improvements. It’s a great way to keep your favorite libraries alive and thriving. Second, if you don’t have the resources or they aren’t willing to maintain the plugin, consider contributing your time back to the library by trying to resolve the open issues. That’s why the source is open.

Also, as Rob and Fred both mentioned, it seems that there might be some improvement that could be made to your application that would eliminate the need for this plugin completely. That means your package size shrinks, you have less code to maintain, and you can iterate faster. Don’t hesitate to reach out to the Aurelia team for code reviews. Whether its a general Aurelia issue or an issue specific to your application, our mission is for every application built on Aurelia to be a success.


#10

@davismj, thank you for your response. I would like to apologize to the Aurelia team. This post was written in a moment of frustration and I am truly grateful for your patience, advice and help which has guided me to a more elegant solution. I am embarrassed to realize that in my ignorance and frustration I made several rude, presumptuous and criticizing statements regarding an issue I clearly have much to learn about. Please forgive me.

My frustration has little to do with the Aurelia framework or this community. Most of my posts here, this one included, stem from my current project of updating a large web-based application from Durandal to Aurelia. Because of the size of the application we do not always have the bandwidth required to rewrite the application using Aurelia best practices and in many cases we have to find ways to convert our existing patterns with as little refactoring as possible. I know this is not ideal and it results in me making posts here that are looking for solutions that wouldn’t be necessary if we were building our application from scratch. Knowing that the first response that your going to get when you ask a question or make a sudgestion is that: “your doing it wrong”; is not easy to deal with, especially when this is the correct response.

I fear that I have yet to make any meaningful contributions to the Aurelia community. To rectify this I would like to take my experiences and contribute towards building an official Durandal/Knockout to Aurelia cheatsheet that would be helpful to others in situations similar to my own.