Controlling AJAX calls

A Breeze client makes HTTP calls by means of some 3rd party low-level AJAX component wrapped in a Breeze AJAX adapter. The default Breeze AJAX adapter wraps the jQuery.ajax method and assumes your client app is running with jQuery.

Picking an AJAX adapter

You can configure your app to use a different adapter that wraps a different ajax component. Angular applications will likely designate the 'angular' ajax adapter that wraps Angular's $http (as described below).

Breeze core offers two adapters out of the box and you can write your own. You can designate the AJAX adapter for your app with an expression like one of the following:

var adapterName;

// pick one
adapterName = 'jQuery';  // the default
adapterName = 'angular';
adapterName = 'custom';  // your custom adapter

var ajaxAdapter = 
    breeze.config.initializeAdapterInstance('ajax', adapterName, true /* use as default */); 

The "jQuery" adapter is the default AJAX adapter. You don't have to initialize this adapter as we did here for illustrative purposes.

The AngularJS $http adapter

Many Angular developers prefer to use Angular's native $http AJAX component; we do.

Breeze v.1.4.4 added an 'angular' ajax adapter that wraps $http. You could designate this as your ajax adapter as follows:

angular.module('app').run(['$http', function($http) {
    var ajax = breeze.config.initializeAdapterInstance('ajax', 'angular');
    ajax.setHttp($http); // use the $http instance that Angular injected into your app.
}]);

However, there's a much better way to prepare Breeze for Angular, a way that not only engages the Breeze 'angular' ajax adapter but also configures Breeze to us Angular $q promises and chooses the best Breeze 'modelLibrary' adapter for data binding with Angular.

It's an Angular module from our own Breeze Labs, the "Breeze Angular Service". You can download it from github or install the package as described here. After installation, you wire it up when your app boots:

angular.module('app', ['breeze.angular'])
       // merely depending on the 'breeze' service configures Breeze for Angular
       .run(['breeze', function() {/* noop - unless you want do do something */}]);

Internally, the "Breeze Angular Service" makes the same call shown above ... as well as other configurations.

Configuring an AJAX adapter

An AJAX adapter may require some setup before it can be used. It's best to pick your adapter and configure it before you make any HTTP calls through Breeze.

For example, you might want to send a fixed set of headers with every Breeze AJAX request.

 // get the current default Breeze AJAX adapter
 var ajaxAdapter = breeze.config.getAdapterInstance('ajax');

 // set fixed headers
 ajaxAdapter.defaultSettings = {
     headers: { 
        "X-Test-Header": "foo2" 
     },
};

Some low-level AJAX components have their own proprietary settings and you might want to configure them too.

For example, the jQuery ajax method takes a settings configuration object. One of the settings is a beforeSend function. You could set a default for that function too:

// get the current Breeze AJAX adapter (assume it is 'jQuery')
var ajaxAdapter = breeze.config.getAdapterInstance('ajax');

ajaxAdapter.defaultSettings = {
   beforeSend: function(xhr, settings) {
       // examine the XHR and customize the headers accordingly.
       if (isFooRequest(xhr)) {
           xhr.setRequestHeader("x-Test-Before-Send-Header", "foo");
       }
   }
};

Every AJAX component is different. This kind of configuration requires specific knowledge of the component and version deployed with your application.

Configure a specific AJAX request with requestInterceptor

The "defaultSettings" extension point is terrific if you have a fixed set of configurations for all requests.

But sometimes you need to make an adjustment for a particular request. You might be able to do that by changing "defaultSettings" before the request and restoring it after the request. That really isn't what "defaultSettings" is for; it is intended to be a one-time, "set and forget" operation.

As of v.1.4.12, the Breeze AJAX adapter offers another extension point, the requestInterceptor. This interceptor gives the developer one last look at the request before the adapter calls the wrapped AJAX component.

The interceptor takes a single parameter, the requestInfo, and returns nothing.

var requestInfo = {
        adapter: this,      // this AJAX adapter
        config: ...,        // the configuration object passed to the wrapped AJAX component
        zConfig: config,    // the config arg from the calling Breeze data service adapter
        success: successFn, // adapter's success callback
        error: errorFn      // adapter's error callback
} 

If you've set the adapter.requestInterceptor, the adapter calls it. Then it takes a last look at the requestInfo.config. If it's "truthy" (e.g., not null), the adapter calls the wrapped AJAX component.

If requestInfo.config is "falsey" (e.g., null), the adapter returns immediately without calling the wrapped AJAX component.

The logic is something like this:

if (requestInfo.config){
    ajaxComponent(requestInfo.config)
    .success(requestInfo.success)
    .error(requestInfo.error);          
}

This means the developer can

  • log details of each HTTP request

  • change any of requestInfo members just before the AJAX call.

  • setup service call timeout or provide a "cancel" option.

    The jQuery adapter adds requestInfo.jqXHR so the requestInterceptor can wire-up a "canceller" that can call jqXHR.abort(). Normally jQuery's XHR object isn't made available to developers on the grounds that it is an implementation detail. But you'll need it to cancel a jQuery AJAX request. See the DocCode:jQueryAjaxAdapterTests for an example of canceling a request.

  • make AJAX-component-specific changes for this particular request, e.g., set jQuery's 'cache' flag or setup an angular $http request cancel option.

  • skip the actual AJAX service call by setting requestInfo.config to null.

  • invoke the success or error callbacks immediately (best to skip the service call at the same time!). This is an easy way to fake an HTTP response during a test.

  • wrap or replace the success and error callbacks to change or supplement behavior. This is an opportunity to modify the raw JSON response before any downstream Breeze or application process sees it.

Here's how you could set a 5 second timeout for the adapter (works for both jQuery.ajax and Angular's $http):

var ajaxAdapter = breeze.config.getAdapterInstance('ajax');
ajaxAdapter.requestInterceptor = function (requestInfo) {
    requestInfo.config.timeout = 5000; 
}

$http's timeout configuration also accepts a promise which can be used for a cancel facility as described by Scott Allen in his blog post, "Canceling $http Requests in AngularJS".

Be careful: the requestInterceptor puts you very close to the metal. Small mistakes can have effects that show up much later and are hard to find.

Clearing the requestInterceptor

The requestInterceptor is not cleared or reset after the request completes. The adapter will run the same requestInterceptor for each subsequent request until you clear or reset it.

You can clear it yourself in your Breeze EntityManager method callbacks.

There's an easier way if you know that the interceptor should only be used for this one request: set the oneTime property on the interceptor function itself.

ajaxAdapter.requestInterceptor.oneTime = true;

Examples

The samples on github illustrate both cancel and timeout with these adapters. For users of the jQuery AJAX component there is DocCode:jQueryAjaxAdapterTests.js. For users of Angular's $http there is the Zza-Node-Mongo:ajax-adapter.async.spec.js.

It's best if you can run the samples but if you can't (perhaps because you don't use one of the technologies involved), the test files (see links above) are easy to read and you should glean the ideas that will help. At least I hope so.

OData AJAX

The "OData" and "webApiOData" OData DataService adapters do not use the AJAX adapter. Therefore, configuring the Breeze AJAX adapter is pointless when using these adapters to access OData sources.

These adapters rely on the Microsoft-sponsored Data.js library to handle many aspects of the interaction with OData sources including the AJAX calls. You adjust this library's OData component if you would configure its AJAX behavior.

Here's how you might add an authorization header to every request:

var oldClient = OData.defaultHttpClient;

var myClient = {
     request: function (request, success, error) {
         request.headers.Authorization = authorization;
         return oldClient.request(request, success, error);
     }
};

OData.defaultHttpClient = myClient;

Adjust save request data with a changeRequestInterceptor

The EntityManager.saveChanges method sends the server a "change-set" of entities-to-be-saved.

Server technologies and their Web APIs differ markedly from each other. Breeze can not anticipate all the possible variations. It doesn't try. Instead, saveChanges delegates to a DataService adapter that handles low-level details of the communication with the server ... details in the middle ground between the saveChanges method and the AJAX adapter.

Breeze ships with several DataService adapters and there are more in breeze.labs. You can also write your own.

Please note: we are talking about the DataService adapter now, not the AJAX adapter. We return to the AJAX adapter in the next section.

Writing a custom DataService adapter isn't easy because these Web APIs are complicated ... including the REST-like APIs.

Maybe one of the Breeze adapters is almost right for you. Maybe you only need a small change to the data in the body of the save request such as:

  • You want to remove data for an unmapped property.

  • You don't want to send the original value for a changed property because (a) it is big and (b) it's not needed or useful on the server.

  • You're using an OData adapter's $batch save to talk and you need to add a special authentication header to each individual request within the batch.

You don't have to write a custom DataService adapter to make these small adjustments. All of the Breeze adapters have a changeRequestInterceptor option with which you can manipulate the change requests just before they're handed off to the AJAX adapter.

Here's the basic plan:

var adapter = breeze.config.getAdapterInstance('dataService');
adapter.changeRequestInterceptor = function (saveContext, saveBundle) {
    this.getRequest = function (request, entity, index) {
        // alter the request that the adapter prepared for this entity
        // based on the cached entity, saveContext, and saveBundle
        // e.g., add a custom header or prune the entityAspect.originalValuesMap
        return request;
    };
    this.done = function (requests) {
        // alter the array of requests representing the entire change-set 
        // based on the saveContext and saveBundle
    };
}

You're setting the adapter's changeRequestInterceptor to a constructor function that creates an object with two functions: getRequest and done.

The DataService adapter will call your constructor function just as it begins to build the change-set requests array. It calls getRequest for each entity in the change-set and calls done after adding the last request to the array of requests.

The adapter gives you a lot of contextual information in the constructor via the saveContext and the saveBundle of entities-to-be-saved. The details of the saveContext are mostly the same across adapters although there may be small differences from adapter to adapter. You can dip into these objects in your getRequest and done functions as necessary.

You'll have to know something about the adapter in order to manipulate request data. A "request" in a Web API adapter will be a JSON representation of the raw entity data. The "request" in an OData API adapter will be an HTTP request. Know what you're doing and be careful. The server will complain if you send what it regards as a bad request.

Example

You'll find examples in the changeRequestInterceptorTests of the DocCode sample. Here's an example similar to one of those tests:

// Get the current Web API adapter
var dsAdapter = breeze.config.getAdapterInstance('dataService', 'webApi');

// Add the interceptor
dsAdapter.changeRequestInterceptor = function (saveContext, saveBundle) {

    // clear the original value for any "Notes" property as it could be very large ;-)
    this.getRequest = function (request, entity, index) {
        var map = request.entityAspect.originalValuesMap;
        if (map.Notes) { 
           // Null the original value but KEEP the property name.
           // The existence of this property name tells the server you want to update it
           // with the current value in the request body.              
           map.Notes = null; 
        }
        return request;
    };

    this.done = function (requests) {}; // do nothing when done
}

// ... later

employee.setProperty('Notes', someNotes);
em.saveChanges().then(inspect).fin(start);

function inspect(saveResult) {
    var empData = saveResult.entities[0];
    equal(empData.entityAspect.originalValuesMap.Notes, null, "should send null for 'Notes' in orig values");
}

When to use the changeRequestInterceptor

The AJAX adapter has a requestInterceptor and the DataService adapter has a changeRequestInterceptor. Why both?

You might get by with the AJAX adapter's requestInterceptor alone. It has the last look at the entire AJAX request just before the AJAX component turns it into an HTPP request. You can change anything about that request including the data element in the body of a save.

However, it is far more convenient to manipulate the change-set request data with the DataService changeRequestInterceptor. It is only called during a save and its methods receive detailed information about that save via the parameters passed to the constructor (saveContext and saveBundle) and the parameters of the interceptor's two methods.

In contrast, the AJAX adaptor's requestInterceptor is called for every EntityManager server-directed method (fetchMetadata, executeQuery, as well as saveChanges) and has no context to help it reason over the request. It could be much more complicated to adjust the details of a save with this interceptor alone.

You may find yourself using both interceptors: the AJAX requestInterceptor for the big picture and the DataService changeRequestInterceptor for fine-grained details of a save.

Write your own AJAX adapter

You can write your own JavaScript AJAX adapter, one that either replaces or extends one of the stock adapters.

An AJAX adapter is a constructor function. It requires a name property; the name must be unique among all AJAX adapters.

It can have additional properties and methods that make it easier for developers to configure or consume.

Here's the outline of the jQuery ajax adapter:

var ctor = function () {
    this.name = 'jQuery';
    this.defaultSettings = { };     
    this.requestInterceptor = null;
};

ctor.prototype.initialize = function () {
    // look for the jQuery lib but don't fail immediately if not found
    jQuery = core.requireLib('jQuery');
};

ctor.prototype.ajax = function (config) { ... }

It should have an initialize method like all Breeze adapters. It must have an ajax method that takes a single config parameter.

This is not the config or setting parameter of the wrapped ajax component. It isn't the argument to jQuery's $.ajax nor to Angular's $http. Rather it is in the shape of a request specification coming from a Breeze "data service adapter", the intermediary between the Breeze EntityManager and the ajax adapter.

Because of this intermediate "data service adapter" abstraction, a Breeze application can talk to a wide variety of remote services with one low-level ajax component.

Your app could talk to ASP.NET Web API, to OData, to Node, to a Rails server. Each of these targets would have its own "data service adapter", attuned to the specifics of its target service. But when it comes time to make HTTP requests, it only has to speak one language, the API of the ajax adapter.

All data service adapters make HTTP requests in terms of the ajax adapter's config object.

You have no obligation to implement the "defaultSettings" or the requestInterceptor extension points seen in the stock Breeze "jQuery" and "angular" adapters. They are "nice to have" features.

Register your adapter

You usually register your adapter with breeze in the last step of the JavaScript module that defines it. If you wrote one called myAjaxAdapter, you could register it like this:

breeze.config.registerAdapter('ajax', myAjaxAdapter); 

Then you'd make your adapter the default adapter when the application starts:

breeze.config.initializeAdapterInstance('ajax', 'myAjaxAdapter', true);

Take a look at one of the stock AJAX adapters - such as the 'jQuery' adapter - before you write your own.