This post is going to cover refactoring of client side code based in Aurelia to centralize access to an ASP.NET 5 web API. This will remove the need for the fetch and configuration APIs in most classes which will be replaced by access via a contact service. This is of course based on the same contacts application I have been using in most of my previous posts. I am going to limit the examples to what is need to display a contact list, but the same concepts would apply to all aspects of contact interaction.
To begin I created a new ContactService.js file which will be used to handle all interactions with the ASP.NET 5 web API.
import {inject} from 'aurelia-framework'; import {HttpClient, json} from 'aurelia-fetch-client'; import 'fetch'; import {EventAggregator} from 'aurelia-event-aggregator'; import {Configure} from 'aurelia-configuration'; import {ContactUpdatedMessage} from 'Aurelia/ContactUpdatedMessage'; @inject(HttpClient, EventAggregator, Configure) export class ContactService { constructor(http, eventAggregator, configAurelia) { http.configure(config => { config .useStandardConfiguration() .withBaseUrl(configAurelia.get('api.baseUrl')); }); this.http = http; this.eventAggregator = eventAggregator; } GetAll() { return new Promise((resolve) => { resolve(this.http.fetch('contacts') .then(response => response.json(), err => console.log(err))); }); } }
The above class utilizes the configuration for the API base URL as explained in this post. The event aggregator is also included and would be used to notify of changes on a contacts. The event aggregator is not actually used in the shown code but example usage can be found here.
Note that the GetAll function returns a promise which is built off of the promise that the http fetch returns. Promises help with async operations are needed and are what enabled the then() function that makes reacting when an async operation is completed very simple. They are not the subject of this post but I wanted to make special note of them because they are new in the latest version of JavaScript.
Next are the changes to the list class which is the view model for the contact list. All the http related imports get removed and an import for the contactService is added. For classes don’t need notification of contact updates the EventAggregator and ContactUpdatedMessage could be removed as well.
Before: import {inject} from 'aurelia-framework'; import {HttpClient} from 'aurelia-fetch-client'; import {EventAggregator} from 'aurelia-event-aggregator'; import 'fetch'; import {ContactUpdatedMessage} from 'Aurelia/ContactUpdatedMessage'; import {Configure} from 'aurelia-configuration';
After: import {inject} from 'aurelia-framework'; import {EventAggregator} from 'aurelia-event-aggregator'; import {ContactUpdatedMessage} from 'Aurelia/ContactUpdatedMessage'; import {ContactService} from 'Aurelia/contactService';
In the inject decorator HttpClient and Configure are replaced with ContactService.
Before: @inject(HttpClient, EventAggregator, Configure)
After: @inject(EventAggregator, ContactService)
Next the constructor changed to remove http related items and to save a reference to the ContactService.
Before: constructor(http, eventAggregator, configAurelia){ http.configure(config => { config .useStandardConfiguration() .withBaseUrl(configAurelia.get('api.baseUrl')); }); this.http = http; eventAggregator.subscribe(ContactUpdatedMessage, message => { let updatedContact = this.contacts.filter(contact => contact.Id == message.contact.Id)[0]; Object.assign(updatedContact, message.contact); }); }
After: constructor(eventAggregator, contactService){ eventAggregator.subscribe(ContactUpdatedMessage, message => { let updatedContact = this.contacts.filter(contact => contact.Id == message.contact.Id)[0]; Object.assign(updatedContact, message.contact); }); this.contactService = contactService; }
Finally the direct call to the web API is replaced with a call to the ContactService.
Before: created(){ return this.http.fetch('contacts') .then(response => response.json()) .then(contacts => this.contacts = contacts); }
After: created(){ return this.contactService.GetAll() .then(contacts => this.contacts = contacts); }
Following the above example, the remaining locations where the web API is accessed directly can be replaced with the ContactService.
Update
Something hit me when reading this post today. Aurelia’s fetch client returns a promise so there is no need to create a new promise in the ContactService. Here is the GetAll function from above updated to not declare a new promise.
Before: GetAll() { return new Promise((resolve) => { resolve(this.http.fetch('contacts') .then(response => response.json(), err => console.log(err))); }); }
After: GetAll() { return this.http.fetch('contacts') .then(response => response.json()) .catch(error => console.log(error)); }
There maybe case where creating is a new promise is the correct thing to do, but in this case it was unnecessary.