Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhanced Dependency Injection Use #73

Closed
AStoker opened this issue Jan 27, 2016 · 21 comments
Closed

Enhanced Dependency Injection Use #73

AStoker opened this issue Jan 27, 2016 · 21 comments
Assignees

Comments

@AStoker
Copy link

AStoker commented Jan 27, 2016

Is there a way for dependency injection in Aurelia to do more than just (essentially) instantiate a class and make sure that that class is the one passed around? After talking with other developers who have used Unity in C# as DI, there seems to be so much more that DI can do than Aurelia is handling, but I could just not know as much about it in Aurelia.

So, if I had a single module that would export two different classes depending on who was asking for it, can Aurelia's DI handle that situation via some configuration?

For example, say I have an api class, and for some reason I had two different kinds of servers (server A & server B) I needed to ping depending on where it was asking, so that the only difference for this api class is the base url of the request. If I now have a class A that needs to make a request to server A, and also a class B that needs to go to server B, can I use DI here so that both of those classes (A & B) will receive the correctly configured api for servers A & B?

@EisenbergEffect EisenbergEffect self-assigned this Jan 27, 2016
@jdanyow
Copy link
Contributor

jdanyow commented Jan 27, 2016

Sounds like you're looking for the equivalent of:

// configure the container
container.RegisterInstance<Something>("Server A", new Something("http://foo.com"));
container.RegisterInstance<Something>("Server B", new Something("http://bar.com"));

// resolve by name
var somethingA = container.Resolve<Something>("Server A");
var somethingB = container.Resolve<Something>("Server B");

// constructor injection
public MyClass([Dependency("Server A")] Something a, [Dependency("Server B")] Something b)
{
   .....
}

Here's the equivalent using javascript and Aurelia's container:

// configure the container
container.registerInstance("Server A", new Something("http://foo.com"));
container.registerInstance("Server B", new Something("http://bar.com"));

// resolve by name
var somethingA = container.get("Server A");
var somethingB = container.get("Server B");

// constructor injection
@inject("Server A", "Server B")
export MyClass(a, b)
{
   .....
}

Some notes:

  • In Aurelia, registration names are more commonly referred to as "keys" and don't need to be strings- they can be objects, numbers, constructor functions, anything really.
  • Unity containers can be configured via .config file. It would be possible to create an equivalent scheme for aurelia's container using json and the ES6 module loader
  • Additional info can be found in the docs at http://aurelia.io/docs.html and here.

Hope that helps!

@AStoker
Copy link
Author

AStoker commented Jan 28, 2016

Thanks! That was helpful, however, not quite exactly what I'm looking for.

So a use case that I currently just ran up against might be a better example.

I have an api client class that pings the server for data, however when testing, I have a flag to change that uses mock data instead.
Now I have another view model that is in charge of getting items for an inbox (going to call it the inboxVM for this example). That inboxVM is in charge of getting the data from the api client, and formatting it for the view, it should not have to know about whether or not it's in a testing environment (that would truly be out of scope for a view model, and would be a pain to manage on every view model in the application).
So when the inboxVM injects the api client, it shouldn't have to ask for apiClient and apiClientTest, rather it should just ask for apiClient. THEN, Aurelia's DI should be able to determine (based off some config) whether or not to serve up the apiClientTest instance or the apiClient instance.

In your example, the registering of instances sounds absolutely like the first step. However, the injection in the example still requires both to be passed in to the class. Does that all make sense?

@jdanyow
Copy link
Contributor

jdanyow commented Jan 28, 2016

definitely makes sense, this is a common scenario. Here's how you would do something like that:

main.js

import {ApiClient} from './api-client';
import {isDebug} from './configuration';

export function configure(aurelia) {
  ...
  ...
  // configure the container
  let container = aurelia.container;
  container.registerInstance(ApiClient, new ApiClient(isDebug));
  ...
  aurelia.start().then(a => a.setRoot());
}

inbox.js

import {inject} from 'aurelia-framework';
import {ApiClient} from './api-client';

@inject(ApiClient)
export class Inbox {
  constructor(api) { // instance registered at app startup will be injected here
    ...
  }
  ...
}

If you weren't passing a "debug" flag to your api client class and instead wanted to use a mock api client class, a naive approach would be to change the main.js code to something like this:

main.js

import {ApiClient} from './api-client';
import {ApiClientMock} from './api-client-mock';
import {isDebug} from './configuration';

export function configure(aurelia) {
  ...
  ...
  // configure the container
  let container = aurelia.container;
  if (isDebug) {
    container.registerHandler(ApiClient, c => c.get(ApiClientMock));
  } 
  ...
  aurelia.start().then(a => a.setRoot());
}

This does the job, but you really don't want to mix your mock container configuration logic with your production container configuration logic. There's no need to be importing mock modules like ./api-client-mock in production scenarios. A better approach would be to create two container configuration modules- one for production and one for testing and conditionally load them using the ES6 module loader:

import {isDebug} from './configuration';

export function configure(aurelia) {
  ...
  ...
  // configure the container
  let moduleName = isDebug ? './debug-container-config' : './release-container-config';
  System.import(moduleName)
    .then(module => module.configureContainer(aurelia.container))
    .then(() => aurelia.start())
    .then(a => a.setRoot());
}

@AStoker
Copy link
Author

AStoker commented Jan 29, 2016

I think that solves our use case and issue. Thanks for the help!

@AStoker AStoker closed this as completed Jan 29, 2016
@AStoker
Copy link
Author

AStoker commented Feb 9, 2016

Another question for this. Back to the Api example with multiple servers. I need to have the same base api client, but depending on which module is using the api, the baseUrl of the fetch needs to be different. How can I have the modules just inject the api class, and DI know that depending on which class asked for the api, give it the one setup with the appropriate baseUrl?

@jdanyow
Copy link
Contributor

jdanyow commented Feb 9, 2016

hmmm- sounds like you're looking for the equivalent of unity's InjectionConstructor?

var clientA = new ApiClient("http://foo.com");
var clientB = new ApiClient("http://bar.com");
container.RegisterType<ModuleA>(new InjectionConstructor(clientA));
container.RegisterType<ModuleB>(new InjectionConstructor(clientB));

if that's what you're looking for, here's how it's done with Aurelia's container:

let clientA = new ApiClient("http://foo.com");
let clientB = new ApiClient("http://bar.com");
container.registerHandler(ModuleA, c => new ModuleA(clientA));
container.registerHandler(ModuleB, c => new ModuleB(clientB));

Another way of doing this would be to create a child containers and register the appropriate ApiClient instance in each of the child containers. In practice this would be pretty tricky because you'd need to make sure ModuleA was always resolved from the correct child container, same thing for ModuleB.

Both of these approaches are less flexible than declaring the deps on the class using @inject.

@AStoker
Copy link
Author

AStoker commented Feb 10, 2016

I think I'm going to go with the approach of the registering of two "types" of api's, thanks. For future knowledge, could you register the handler via a path? For example, any modules that exist inside of specific folder have clientA as the api?

@AStoker
Copy link
Author

AStoker commented Feb 10, 2016

However, registering those classes in the main.js configure function might be a problem. The api client makes use of the HttpClient from Aurelia, and is currently injecting it. I could new up an HttpClient in the main.js configure function, but then if I use DI anywhere else with the HttpClient, to my understanding, the two won't be the same.

@jdanyow
Copy link
Contributor

jdanyow commented Feb 10, 2016

get the HttpClient from the container:

let http = container.get(HttpClient);
... proceed to configure the container ...

@AStoker
Copy link
Author

AStoker commented Feb 10, 2016

Thanks for all the help. Still learning all the ins and outs here with containers and DI

@millicandavid
Copy link

I have tried using the register methods as @jdanyow mentioned above but I'm guessing the interface has changed. Does registration require a key even if you don't intend to resolve by key? I'm trying to register one concretion for an interface under debug config but another concretion in all other configurations and just can't figure out how to do this.

if (environment.debug) { aurelia.use.developmentLogging(); aurelia.container.registerHandler(IWebApiClient, TestApiClient); } else { aurelia.container.registerHandler(IWebApiClient, WebApiClient); }

@jdanyow
Copy link
Contributor

jdanyow commented Sep 7, 2016

@millicandavid there was a bug in the code I had shared above- I've updated it.

Needs to be:

....registerHandler(IWebApiClient, c => c.get(TestApiClient))

@millicandavid
Copy link

Thanks Jeremy. You're a life saver. Now if I can just figure out why the typescript transpiler can't find my interface even though it is clearly imported.

@jdanyow
Copy link
Contributor

jdanyow commented Sep 8, 2016

@millicandavid unfortunately you cannot use a typescript interface as a registration key. At runtime there's no interface (because interfaces don't exist in JavaScript). Try using an abstract class instead (assuming you're using the TypeScript 2.x RC).

@millicandavid
Copy link

Thanks again Jeremy. That makes complete sense. I was just used to the pattern of using an interface there and didn't even think about it. This is all working for me now.

@CasiOo
Copy link

CasiOo commented Oct 6, 2016

Great advice @jdanyow thanks a lot.
I guess mock container configuration could also be loaded through a feature, or would that be abusing features for what they weren't meant for?

if (environment.debug) {
  aurelia.use.feature('mock-api');
}

@jdanyow
Copy link
Contributor

jdanyow commented Oct 6, 2016

@CasiOo that sounds like a great use-case for .feature. Good thinking! 💯

@elitemike
Copy link

What is the proper way to do this with the CLI and typescript
System.import(moduleName) .then(module => module.configureContainer(aurelia.container)) .then(() => aurelia.start()) .then(a => a.setRoot());

@lemoustachiste
Copy link

Hi,

sorry for hijacking this, but it looks like almost what I'd like to achieve.

My code base works for 2 somewhat different contexts, which share code.

In one context I will have a specific CurrentUserStore and in the other context I'll have AnotherCurrentUserStore. They are 2 different classes serving the same purpose (maintain the current user state), so they vary in code and behaviour depending on the context.

Now some part of my code are referencing the CurrentUserStore and getting some information that would be common to both context.

Overloading the CurrentUserStore DI reference seems like an appropriate way for me to not worry about which CurrentUserStore we are using, so I went ahead and did this:

function configureCurrentUserStore (container) {
  container.registerInstance(CurrentUserStore, OtherContextCurrentUserStore);
}

This is in the bootstrap of my other context app.

However, instead of having a proper instance object of OtherContextCurrentUserStore injected, I receive a reference to its function: function OtherContextCurrentUserStore(). Which is not what I need to be able to manipulate data afterwards.

I understand that this approach could work in my case:

container.registerHandler(ModuleA, c => new ModuleA(clientA))

But I am a bit annoyed when using this approach since the OtherContextCurrentUserStore also takes some autoinject instances as part of the constructor signature.

I am afraid that if I manually instantiate each of these instances to follow the registerHandler pattern, I will a) enter a loop of hell to instantiate each class and their DI manually (not tenable really) b) and most importantly will not be dealing with the same references of each class injected for each dependencies as what is instantiated in the rest of the app.

So my question is, how could I do the registerHandler pattern, but with a reference to the OtherContextCurrentUserStore that has been instantiated and registered in the DI engine?

@EisenbergEffect
Copy link
Contributor

EisenbergEffect commented Nov 27, 2019

@lemoustachiste I think I can make this super easy for you :) In the case above, registerInstance is not the API you want. That registers an already instantiated instance. If you have a constructor you want to instance, you would use registerSingleton or registerTransient depending on the lifetime you want for the object. Then the DI container will handle instantiating it on demand and supplying its dependencies.

Bear in mind that if you register two things in the container using the same key, that won't override anything. Instead, resolving the key will return the first thing that was registered and doing a getAll will return both. To override, you must create a child container and register the override in the child container. This is one of the main uses for child containers. Often in a complex app I will have a root DI container with all globally shared services, then I might create one child container per area or per screen (in and MDI type app) and within that child container register things that have a lifetime or alternate implementation that is specific to that screen instance alone. So, when the screen is closed, the child container can be disposed and all the resources cleaned up as well.

@lemoustachiste
Copy link

Thanks @EisenbergEffect, it worked very smoothly and I like that :).
For all intent and purposes I have used registerSingleton as indeed I want this to be the same occurrence throughout the app lifetime.

Thanks again for the prompt answer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants