Introduction

This sample demonstrates how to integrate Windows Azure Mobile Service with a line of business application, running on-premises or in the cloud, via Windows Azure BizTalk Services, Service Bus Relayed Messaging, BizTalk Adapter Service and BizTalk Adapter for SQL Server. The Access Control Service is used to authenticate Mobile Services against the XML Request-Reply Bridge used by the solution to transform and route messages to the line of business applications. For more information on Windows Azure BizTalk Services and in particular BizTalk Adapter Service, please see the following resources:

The solution demonstrates how to use a Notification Hub to send a push notification to mobile applications to advice that a new item is available in the products database. For more information on Notification Hubs, see the following resources:

You can download the code from MSDN Code Gallery.

Scenario

A mobile service receives CRUD operations from a client application (Windows Phone 8 app, Windows Store app, HTML5/JS web site), but instead of accessing data stored in Windows Azure SQL Database, it invokes a LOB application running in a corporate data center. The LOB system, in this sample represented by a SQL Server database, uses the BizTalk Adapter Service to expose its functionality as a BasicHttpRelayBinding relay service to external applications and in particular to a XML Request-Reply Bridge running in a Windows Azure BizTalk Services namespace. The endpoint exposed by the BizTalk Adapter Service on Windows Azure Service Bus is configured to authenticate incoming calls using a relay access token issued ACS. The WCF service created by the BizTalk Adapter Service runs on a dedicated web site on IIS and uses the BizTalk Adapter for SQL Server to access data stored in the ProductDb database hosted by a local instance of SQL Server 2012.

The mobile service custom API acquires a security token from the Access Control Service to authenticate against the BizTalk Service that acts as a message broker and intermediary service towards the LOB system running on-premises. The server script creates the SOAP envelope for the request message, includes the security token in the HTTP Authorization request header and send the message to the runtime address of the underlying XML Request-Reply Bridge. The bridge transforms the incoming message into the canonical request format expected by the BizTalk Adapter for SQL Server and
promotes its SOAP Action header to the RequestAction property. In the message flow itinrerary, the value of this property is used to choose one of four routes (Select, Delete, Update, Create) that connect the bridge to BizTalk Adapter Service. Each route sets the SOAP Action of the outgoing SOAP request based on on the type of the CRUD operation. The BizTalk Adapter Service running on-premises as a WCF service accesses the data stored in the ProductDb database on a local instance of SQL Server using the BizTalk Adapter for SQL Server. This data is returned to Windows Azure BizTalk Services in SOAP format. The bridge applies a map to transform the incoming message into the response format expected by the caller then it sends the resulting message back to the mobile service. The mobile service changes the format of the response message from SOAP/XML to JSON format, then extracts and sends the result data back to the client application.

NOTE:

Architecture

The following diagram shows the architecture of the solution.
   
Architecture
Message Flow
  1. The client application (Windows Phone 8 appWindows Store app or HTML5/JavaScript web site) sends a request via HTTPS to the biztalkproducts custom API of a Windows Azure Mobile Service. The HTML5/JS application uses the invokeApi method exposed by the HTML5/JavaScript client for Windows Azure Mobile Services to call the mobile service. Likewise, the Windows Phone 8 and Windows Store apps use the InvokeApiAsync method exposed by the MobileServiceClient class. The custom API implements CRUD methods to create, read, update and delete data. The HTTP method used by the client application to invoke the user-defined custom API depends on the invoked operation:
    • Read: GET method
    • Add: POST method
    • Update: POST method
    • Delete: DELETE method
  2. The custom API sends a request to the Access Control Service to acquire a security token necessary to be authenticated by the Windows Azure BizTalk Servicesnamespace. The client uses the OAuth WRAP Protocol to acquire a security token from ACS. In particular, the server script sends a request to ACS using a HTTPS form POST. The request contains the following information:
    • wrap_name: the name of a service identity in ACS used to authenticate with the Windows Azure BizTalk Services namespace (e.g. owner)
    • wrap_password: the password of the service identity specified by the wrap_name parameter.
    • wrap_scope: this parameter contains the relying party application realm. In our case, it contains the public URL of the bridge. (e.g. http://babonet.biztalk.windows.net/default/ProductService)
  3. ACS issues and returns a security token. For more information on the OAuth WRAP Protocol, see How to: Request a Token from ACS via the OAuth WRAP Protocol.
  4. The mobile service user-defined custom API performs the following actions:
    • Extracts the wrap_access_token from the security token issued by ACS and uses its value to create the Authorization HTTP request header.
    • Creates a SOAP envelope to invoke the XML Request-Reply Bridge. The custom API uses a different function to serve the request depending on the HTTP method and parameters sent by the client application.
      • getProduct: this function is invoked when the HTTP method is equal to GET and the querystring contains a productid or id parameter.
      • getProducts: this function is invoked when the HTTP method is equal to GET and the querystring does not contain any parameter.
      • getProductsByCategory: this function is invoked when the HTTP method is equal to GET and the querystring contains a category parameter.
      • addProduct: this function is invoked when the HTTP method is equal to POST and the request body contains a new product in JSON format.
      • updateProduct: this function is invoked when the HTTP method is equal to PUT or PATCH and the request body contains an existing product in JSON format.
      • deleteProduct: this function is invoked when the HTTP method is equal to DELETE and the querystring contains a productid or id parameter.
    • Uses the https Node.js module to send the SOAP envelope to the XML Request-Reply Bridge.
  5. The XML Request-Reply Bridgeperforms the following actions:
  6. The The XML Request-Reply Bridge uses the Route Rules and Route Ordering Table to route the request to the BizTalk Adapter Service. The value of RequestAction promoted property is used to choose one of four routes (Select, Delete, Update, Create) that connect the bridge to BizTalk Adapter Service. Each route sets the SOAP Action of the outgoing SOAP request based on on the type of the CRUD operation:
    • TableOp/Insert/dbo/Products
    • TableOp/Update/dbo/Products
    • TableOp/Delete/dbo/Products
    • TableOp/Insert/dbo/Products
    The configuration file of the destination node in the message flow itinerary defines a BasicHttpRelayEndpoint client endpoint to communicate with the relay service exposed by the BizTalk Adapter Service. For more information, see Including a Two-Way Relay Endpoint and Routing Messages from Bridges to Destinations in the BizTalk Service Project
  7. The Two-Way Relay Endpoint sends the request message to the BizTalk Adapter Service.
  8. The BizTalk Adapter Service uses the BizTalk Adapter for SQL Server to execute the CRUD operation contained in the request message. 
  9. The BizTalk Adapter Service returns a response message to the bridge via relay service.
  10. The relay service forwards the message to the bridge.
  11. The XML Request-Reply Bridge performs the following actions:
  12. The XML Request-Reply Bridge sends the response message to the mobile service. The mobile service sends a push notification to mobile devices that registered one or more templates (e.g. a toast notification and/or a live tile) with the notification hub. In particular, the notification is sent with a tag equal to productservice. For more information, see Send cross-platform notifications to users with Notification Hubs.
  13. The custom API performs the following actions:
    • Uses the xml2js Node.js module to change the format of the response SOAP message from XML to JSON.
    • Flattens the resulting JSON object to eliminate unnecessary arrays.
    • Extracts data from the flattened representation of the SOAP envelope and creates a response message in JSON object.
    • Returns data in JSON format to the client application
NOTE: the mobile service communicates with client applications using a REST interface and messages in JSON format, while it communicates with the XML Request-Reply Bridge using SOAP messages.

 

Access Control Service

The following diagram shows the steps performed by a WCF service and client to communicate via a Service Bus Relay Service. The Service Bus uses a claims-based security model implemented using the Access Control Service (ACS). The service needs to authenticate against ACS and acquire a token with the Listen claim in order to be able to open an endpoint on the Service Bus. Likewise, when both the service and client are configured to use the RelayAccessToken authentication type, the client needs to acquire a security token from ACS containing the Send claim. When sending a request to the Service Bus Relay Service, the client needs to include the token in the RelayAccessToken element in the Header section of the request SOAP envelope. The Service Bus Relay Service validates the security token and then sends the message to the underlying WCF service. For more information on this topic, see How to: Configure a Service Bus Service Using a Configuration File.

Prerequisites

Building the Sample

Proceed as follows to set up the solution.

Configure your Notification Hub

  1. Log on to the Windows Azure Management Portal, and click NEW at the bottom of the screen.

  2. Click on App Services, then Service Bus, then Notification Hub, then Quick Create.

  3. Type a name for your notification hub, select your desired Region, and then click Create a new Notification Hub.

  4. Click the namespace you just created (usually notification hub name-ns), then click the Configure tab at the top.

  5. Select the tab Notification Hubs at the top, and then click the notification hub you just created.

  6. Select the tab Configure at the top, enter the Client secret and Package SID for your Windows Store App in the Windows notification settings, and then click Save. Repeat the same operation for Windows Phone notification settings, Apple Notification Settings, Google cloud messaging settings, if you plan to use any of these push notification services. For more information on how to configure a Notification Hub, see Getting Started with Notification Hubs.

  7. Select the tab Dashboard at the top, and then click Connection Information. Take note of the two connection strings.

Your notification hub is now configured to work with the specified push notification services and you have the connection strings to register your app and send notifications.

Create the BizTalk Service

Follow the steps described in BizTalk Services: Provisioning Using Windows Azure Management Portal to provision a new BizTalk Service on Windows Azure. After completing the provisioning process, you should see a new BizTalk Service on the Windows Azure Management Portal as shown by the following figure:

If you click the new BizTalk Service and then click Dashboard, you can access a page that provides a graph of commonly used performance metrics. You can use the Dashboard to perform a quick check of the health of your BizTalk Service, including failed messages and successful messages. Click the circle next to the metric to enable and disable the metric output in the graph.

In addition, you can click the Manage button at the bottom of the page to access the Windows Azure BizTalk Service Management Portal where you can manage your BizTalk Services. In particular, the Tracking section can be used to monitor the messages exchanged by bridges with external applications.

Create the Mobile Service

Follow the steps in the tutorial to create the mobile service.
  1. Log into the Management Portal.
  2. At the bottom of the navigation pane, click +NEW.
  3. Expand Mobile Service, then click Create.
    This displays the New Mobile Service dialog.
  4. In the Create a mobile service page, type a subdomain name for the new mobile service in the URL textbox and wait for name verification. Once name verification completes, click the right arrow button to go to the next page.
    This displays the Specify database settings page. NOTE: as part of this tutorial, you create a new SQL Database instance and server. However, the database is not used by the present solution. Hence, if you already have a database in the same region as the new mobile service, you can instead choose Use existing Database and then select that database. The use of a database in a different region is not recommended because of additional bandwidth costs and higher latencies.
  5. In Name, type the name of the new database, then type Login name, which is the administrator login name for the new SQL Database server, type and confirm the password, and click the check button to complete the process.

Define the custom API

  1. Log into the Windows Azure Management Portal, click Mobile Services, and then click your app.
  2. Click the API tab, and then click Create a custom API. This displays the Create a new custom API dialog.
  3. Enter biztalkproducts in the API NAME field. Select Anybody with the Application Key permission for all the HTTP methods and then click the check button. This creates the new custom API
     
  4. Click the new biztalkproducts entry in the API table.
  5. Click the Scripts tab and replace the existing code with the following:
var bizTalkServiceNamespace = '[YOUR-BIZTALK-SERVICE-NAMESPACE]'; 
var bizTalkServiceUrl = bizTalkServiceNamespace + '.biztalk.windows.net'; 
var bridgePath = '/default/CalculatorService'; 
var bizTalkServiceScope = 'http://' + bizTalkServiceUrl + bridgePath; 
var acsNamespace = '[YOUR-BIZTALK-SERVICE-ACS-NAMESPACE]'; 
var issuerName = '[YOUR-BIZTALK-SERVICE-ACS-ISSUER-NAME]'; 
var issuerSecret = '[YOUR-BIZTALK-SERVICE-ACS-ISSUER-SECRET]'; 
var https = require('https');
var qs = require('querystring');
var util = require('util');
var azure = require('azure');
var hubName = '[YOUR-NOTIFICATION-HUB-NAME]';
var hubConnectionString = '[YOUR-NOTIFICATION-HUB-CONNECTION-STRING]';

exports.get = function(request, response) {
    https.globalAgent.options.secureProtocol = 'SSLv3_method';
  https.globalAgent.options.rejectUnauthorized = false;

  if (request.query.productid) {
    getAcsToken(response, function(token) {
      getProduct(token, request.query.productid, request, response);
    });
  }
  if (request.query.id) {
    getAcsToken(response, function(token) {
      getProduct(token, request.query.id, request, response);
    });
  }
  else if (request.query.category) {
    getAcsToken(response, function(token) {
      getProductsByCategory(token, request.query.category, request, response);
    });
  }
  else {
    getAcsToken(response, function(token) {
      getProducts(token, request, response);
    });
  }
};

exports.post = function(request, response) {
    https.globalAgent.options.secureProtocol = 'SSLv3_method';
  https.globalAgent.options.rejectUnauthorized = false;

    getAcsToken(response, function(token) {
      addProduct(token, request, response);
  });
}

exports.patch = function(request, response) {
    https.globalAgent.options.secureProtocol = 'SSLv3_method';
  https.globalAgent.options.rejectUnauthorized = false;

    getAcsToken(response, function(token) {
      updateProduct(token, request, response);
  });
}

exports.put = function(request, response) {
    getAcsToken(response, function(token) {
      updateProduct(token, request, response);
  });
}

exports.delete = function(request, response) {
    https.globalAgent.options.secureProtocol = 'SSLv3_method';
  https.globalAgent.options.rejectUnauthorized = false;

  if (request.query.productid) {
    getAcsToken(response, function(token) {
      deleteProduct(token, request.query.productid, request, response);
    });
  } else if (request.query.id) {
    getAcsToken(response, function(token) {
      deleteProduct(token, request.query.id, request, response);
    });
  }
}

function getAcsToken(response,
           callback) {
  var options = {
    host: acsNamespace + '.accesscontrol.windows.net',
    path: '/WRAPv0.9/',
    method: 'POST'
  };

  var values = {
    wrap_name: issuerName,
    wrap_password: issuerSecret,
    wrap_scope: bizTalkServiceScope
  };

  console.log('[getAcsToken]: options: %j\n\rrequest body: %j', options, values);

  var req = https.request(options, function (res) {
    console.log("[getAcsToken]: statusCode: %d\n\rheaders: %j", res.statusCode, res.headers);
    res.on('data', function (data) {
      var body = qs.parse(data.toString('utf8'));
      if (res.statusCode == 200) {
        if (body.hasOwnProperty('wrap_access_token')) {
          var header = 'WRAP access_token=\"' + body.wrap_access_token + '\"';
          console.log('[getAcsToken]: header = %s', header);
          callback(header);
        }
        else {
          response.send(400, util.format("[getAcsToken]: ACS didn't return a valid token"));
        }
      }
      else {
        response.send(res.statusCode, util.format('[getAcsToken]:\n%s', data));
      }
    });
  });
  req.write(qs.stringify(values));
  req.end();

  req.on('error', function (e) {
    console.error('[getAcsToken]: ', e);
    response.send(400, util.format('[getAcsToken]: %j', e));
  });
}

function getProducts(token, request, response) {
  //var json = [{"productId" : "1", "name" : "Babo", "category" : "Toys", "price" : "100"}];
  //return json;
  var body = '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" xmlns:a="http://www.w3.org/2005/08/addressing">' +
        '<s:Header>' +
        '<a:Action s:mustUnderstand="1">GetProducts</a:Action>' +
        '</s:Header>' +
        '<s:Body>' +
        '<GetProducts xmlns="http://windowsazure.cat.microsoft.com/samples/mobileservices"/>' +
        '</s:Body>' +
        '</s:Envelope>';

    var headers = {
      'Accept': 'application/soap+xml',
      'Content-Type': 'application/soap+xml',
      'Content-Length': body.length,
      'Authorization': token
    };

    var agentOptions = {
            secureOptions: require('constants').SSL_OP_NO_TLSv1_2,
            ciphers: 'ECDHE-RSA-AES256-SHA:AES256-SHA:RC4-SHA:RC4:HIGH:!MD5:!aNULL:!EDH:!AESGCM',
            honorCipherOrder: true
    };

    var agent = new https.Agent(agentOptions);

    var options = {
        host: bizTalkServiceUrl,
        path: bridgePath,
        headers: headers,
        method: 'POST',
        agent: agent
    };

  console.log('[getProducts]: options: %j\n\rrequest body:\n%s', options, body);

  var req = https.request(options, function (res) {
    console.log('[getProducts]: statusCode: %d\n\rheaders: %j', res.statusCode, res.headers);
    res.on('data', function (data) {
      var soap = data.toString('utf8');
      console.log('[getProducts]: response body:\n%s', soap);
      var re = /(\w+:)/g;
      soap = soap.replace(re, '');
      console.log('[getProducts]: removed namespace prefix from xml document:\n%s', soap);
      
      if (res.statusCode == 200) {
        // Import the xml2js module
         var xml2js = require('xml2js');
          
        // Create a xml2js parser
         var parser = new xml2js.Parser();
        
        // Parse the SOAP envelope of the response from XML to JSON
        parser.parseString(soap, function (error, json) {
          if (error) {
            // Log the error
            console.log('[getProduct]: An error occurred while parsing the response: ', error);
            response.send(400, '[getProduct]: An error occurred while parsing the response.');
          }
          else {
            try {
              // flatten json object
              var flat = flatten(json);
              console.log('[getProducts]: flat json:\n%s', JSON.stringify(json));
              if (flat) {
                var products = flat["Envelope"]["Body"]["GetProductsResponse"]["products"]["product"];
                if (products) {
                  if (!Array.isArray(products)) {
                    products = [products];
                  }
                }
                else {
                  products = [];
                }
                response.send(200, products);
              }
              else {
                response.send(200, []);
              }
            }
            catch (ex) {
              // Log the error
              console.log('[getProducts]: An error occurred while parsing the response: ', ex);
              response.send(400, '[getProducts]: An error occurred while parsing the response.');
            }
          }
        });
      }
      else {
        response.send(400, '[getProducts]: An error occurred while invoking the downstream service.');
      }
    });
  });
  req.write(body);
  req.end();

  req.on('error', function (e) {
    console.error('[getProducts]: ', e);
        response.send(400, util.format('[getProducts]: %j', e));
  });
}

function getProductsByCategory(token, category, request, response) {
  var body = '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" xmlns:a="http://www.w3.org/2005/08/addressing">' +
        '<s:Header>' +
        '<a:Action s:mustUnderstand="1">GetProductsByCategory</a:Action>' +
        '</s:Header>' +
        '<s:Body>' +
        '<GetProductsByCategory xmlns="http://windowsazure.cat.microsoft.com/samples/mobileservices">' +
        '<category>' + category + '</category>' +
        '</GetProductsByCategory>' +
        '</s:Body>' +
        '</s:Envelope>';

  var headers = {
      'Accept': 'application/soap+xml',
      'Content-Type': 'application/soap+xml',
      'Content-Length': body.length,
      'Authorization': token
    };

    var agentOptions = {
            secureOptions: require('constants').SSL_OP_NO_TLSv1_2,
            ciphers: 'ECDHE-RSA-AES256-SHA:AES256-SHA:RC4-SHA:RC4:HIGH:!MD5:!aNULL:!EDH:!AESGCM',
            honorCipherOrder: true
    };

    var agent = new https.Agent(agentOptions);

    var options = {
        host: bizTalkServiceUrl,
        path: bridgePath,
        headers: headers,
        method: 'POST',
        agent: agent
    };

  console.log('[getProductsByCategory]: options: %j\n\rrequest body:\n%s', options, body);

  var req = https.request(options, function (res) {
    console.log('[getProductsByCategory]: statusCode: %d\n\rheaders: %j', res.statusCode, res.headers);
    res.on('data', function (data) {
      var soap = data.toString('utf8');
      console.log('[getProductsByCategory]: response body:\n%s', soap);
            var re = /(\w+:)/g;
      soap = soap.replace(re, '');
      console.log('[getProducts]: removed namespace prefix from xml document:\n%s', soap);
      
       if (res.statusCode == 200) {
        // Import the xml2js module
         var xml2js = require('xml2js');
          
        // Create a xml2js parser
         var parser = new xml2js.Parser();
        
        // Parse the SOAP envelope of the response from XML to JSON
        parser.parseString(soap, function (error, json) {
          if (error) {
            // Log the error
            console.log('[getProduct]: An error occurred while parsing the response: ', error);
            response.send(400, '[getProduct]: An error occurred while parsing the response.');
          }
          else {
            try {
              // flatten json object
              var flat = flatten(json);
              console.log('[getProductsByCategory]: flat json:\n%s', JSON.stringify(json));
              if (flat) {
                var products = flat["Envelope"]["Body"]["GetProductsByCategoryResponse"]["products"]["product"];
                if (products) {
                  if (!Array.isArray(products)) {
                    products = [products];
                  }
                }
                else {
                  products = [];
                }
                response.send(200, products);
              }
              else {
                response.send(200, []);
              }
            }
            catch (ex) {
              // Log the error
              console.log('[getProductsByCategory]: An error occurred while parsing the response: ', ex);
              response.send(400, '[getProductsByCategory]: An error occurred while parsing the response.');
            }
          }
        });
      }
      else {
        response.send(400, '[getProductsByCategory]: An error occurred while invoking the downstream service.');
      }
    });
  });
  req.write(body);
  req.end();

  req.on('error', function (e) {
    console.error('[getProductsByCategory]: ', e);
        response.send(400, util.format('[getProductsByCategory]: %j', e));
  });
}

function getProduct(token, productId, request, response) {
  var body = '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" xmlns:a="http://www.w3.org/2005/08/addressing">' +
        '<s:Header>' +
        '<a:Action s:mustUnderstand="1">GetProduct</a:Action>' +
        '</s:Header>' +
        '<s:Body>' +
        '<GetProduct xmlns="http://windowsazure.cat.microsoft.com/samples/mobileservices">' +
        '<productId>' + productId + '</productId>' +
        '</GetProduct>' +
        '</s:Body>' +
        '</s:Envelope>';

  var headers = {
      'Accept': 'application/soap+xml',
      'Content-Type': 'application/soap+xml',
      'Content-Length': body.length,
      'Authorization': token
    };

    var agentOptions = {
            secureOptions: require('constants').SSL_OP_NO_TLSv1_2,
            ciphers: 'ECDHE-RSA-AES256-SHA:AES256-SHA:RC4-SHA:RC4:HIGH:!MD5:!aNULL:!EDH:!AESGCM',
            honorCipherOrder: true
    };

    var agent = new https.Agent(agentOptions);

    var options = {
        host: bizTalkServiceUrl,
        path: bridgePath,
        headers: headers,
        method: 'POST',
        agent: agent
    };

  console.log('[getProduct]: options: %j\n\rrequest body:\n%s', options, body);

  var req = https.request(options, function (res) {
    console.log('[getProduct]: statusCode: %d\n\rheaders: %j', res.statusCode, res.headers);
    res.on('data', function (data) {
      var soap = data.toString('utf8');
      console.log('[getProduct]: response body:\n%s', soap);
            var re = /(\w+:)/g;
      soap = soap.replace(re, '');
      console.log('[getProducts]: removed namespace prefix from xml document:\n%s', soap);
      
      if (res.statusCode == 200) {
        // Import the xml2js module
         var xml2js = require('xml2js');
          
        // Create a xml2js parser
         var parser = new xml2js.Parser();
        
        // Parse the SOAP envelope of the response from XML to JSON
        parser.parseString(soap, function (error, json) {
          if (error) {
            // Log the error
            console.log('[getProduct]: An error occurred while parsing the response: ', error);
            response.send(400, '[getProduct]: An error occurred while parsing the response.');
          }
          else {
            try {
              // flatten json object
              var flat = flatten(json);
              console.log('[getProduct]: flat json:\n%s', JSON.stringify(json));
              if (flat) {
                var products = flat["Envelope"]["Body"]["GetProductResponse"]["products"]["product"];
                if (products) {
                  if (!Array.isArray(products)) {
                    products = [products];
                  }
                }
                else {
                  products = [];
                }
                response.send(200, products);
              }
              else {
                response.send(200, []);
              }
            }
            catch (ex) {
              // Log the error
              console.log('[getProduct]: An error occurred while parsing the response: ', ex);
              response.send(400, '[getProduct]: An error occurred while parsing the response.');
            }
          }
        });
      }
      else {
        response.send(400, '[getProduct]: An error occurred while invoking the downstream service.');
      }
    });
  });
  req.write(body);
  req.end();

  req.on('error', function (e) {
    console.error('[getProduct]: ', e);
        response.send(400, util.format('[getProduct]: %j', e));
  });
}

function addProduct(token, request, response) {
  if (request.body.hasOwnProperty('name') &&
    request.body.hasOwnProperty('category') &&
    request.body.hasOwnProperty('price')) {
    var product = '<product xmlns:i="http://www.w3.org/2001/XMLSchema-instance">' +
            '<name>' + request.body.name + '</name>' +
            '<category>' + request.body.category + '</category>' +
            '<price>' + request.body.price + '</price>' +
            '</product>'
    var body = '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" xmlns:a="http://www.w3.org/2005/08/addressing">' +
          '<s:Header>' +
                    '<a:Action s:mustUnderstand="1">AddProduct</a:Action>' +
                    '</s:Header>' +
          '<s:Body>' +
          '<AddProduct xmlns="http://windowsazure.cat.microsoft.com/samples/mobileservices">' +
          product +
          '</AddProduct>' +
          '</s:Body>' +
          '</s:Envelope>';

    var headers = {
      'Accept': 'application/soap+xml',
      'Content-Type': 'application/soap+xml',
      'Content-Length': body.length,
      'Authorization': token
    };

        var agentOptions = {
                secureOptions: require('constants').SSL_OP_NO_TLSv1_2,
                ciphers: 'ECDHE-RSA-AES256-SHA:AES256-SHA:RC4-SHA:RC4:HIGH:!MD5:!aNULL:!EDH:!AESGCM',
                honorCipherOrder: true
        };

        var agent = new https.Agent(agentOptions);

        var options = {
            host: bizTalkServiceUrl,
            path: bridgePath,
            headers: headers,
            method: 'POST',
            agent: agent
        };

    console.log('[addProduct]: options: %j\n\rrequest body:\n%s', options, body);

    var req = https.request(options, function (res) {
      console.log('[addProduct]: statusCode: %d\n\rheaders: %j', res.statusCode, res.headers);
      res.on('data', function (data) {
        var soap = data.toString('utf8');
        console.log('[addProduct]: response body:\n%s', soap);
                var re = /(\w+:)/g;
                soap = soap.replace(re, '');
                console.log('[getProducts]: removed namespace prefix from xml document:\n%s', soap);
        
        if (res.statusCode == 200 ||
          res.statusCode == 201 ||
                    res.statusCode == 202) {
          // Import the xml2js module
           var xml2js = require('xml2js');
            
          // Create a xml2js parser
           var parser = new xml2js.Parser();
          
          // Parse the SOAP envelope of the response from XML to JSON
          parser.parseString(soap, function (error, json) {
            if (error) {
              // Log the error
              console.log('[addProduct]: An error occurred while parsing the response: ', error);
              response.send(400, '[addProduct]: An error occurred while parsing the response.');
            }
            else {
              try {
                // flatten json object
                var flat = flatten(json);
                console.log('[addProduct]: flat json:\n%s', JSON.stringify(json));
                if (flat) {
                  var productId = flat["Envelope"]["Body"]["AddProductResponse"]["ProductId"];
                                    var product = {
                                        productId : productId,
                                        name : request.body.name,
                                        category : request.body.category,
                                        price : request.body.price
                                    }
                  response.send(200, product);
                  sendNotification('Products', product.name + ' added.', 'productservice');
                }
                else {
                  response.send(200, []);
                }
              }
              catch (ex) {
                // Log the error
                console.log('[addProduct]: An error occurred while processing the response: ', ex);
                response.send(400, '[addProduct]: An error occurred while processing the response.');
              }
            }
          });
        }
        else {
          response.send(400, '[addProduct]: An error occurred while invoking the downstream service.');
        }
      });
    });
    req.write(body);
    req.end();

    req.on('error', function (e) {
      console.error('[addProduct]: ', e);
            response.send(400, util.format('[addProduct]: %j', e));
    });
  }
  else {
    var message = "[addProduct]: The request body is not in " + 
            "JSON format or doesn't contain a well-formed product. request.body=" + 
            JSON.stringify(request.body);
    console.error(message);
    response.send(400, message);
  }
}

function updateProduct(token, request, response) {
  if (request.body.hasOwnProperty('productId') &&
    request.body.hasOwnProperty('name') &&
    request.body.hasOwnProperty('category') &&
    request.body.hasOwnProperty('price')) {
    var product = '<product xmlns:i="http://www.w3.org/2001/XMLSchema-instance">' +
            '<productId>' + request.body.productId + '</productId>' +
            '<name>' + request.body.name + '</name>' +
            '<category>' + request.body.category + '</category>' +
            '<price>' + request.body.price + '</price>' +
            '</product>'
    var body = '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" xmlns:a="http://www.w3.org/2005/08/addressing">' +
          '<s:Header>' +
                    '<a:Action s:mustUnderstand="1">UpdateProduct</a:Action>' +
                    '</s:Header>' +
          '<s:Body>' +
          '<UpdateProduct xmlns="http://windowsazure.cat.microsoft.com/samples/mobileservices">' +
          product +
          '</UpdateProduct>' +
          '</s:Body>' +
          '</s:Envelope>';

    var headers = {
      'Accept': 'application/soap+xml',
      'Content-Type': 'application/soap+xml',
      'Content-Length': body.length,
      'Authorization': token
    };

        var agentOptions = {
                secureOptions: require('constants').SSL_OP_NO_TLSv1_2,
                ciphers: 'ECDHE-RSA-AES256-SHA:AES256-SHA:RC4-SHA:RC4:HIGH:!MD5:!aNULL:!EDH:!AESGCM',
                honorCipherOrder: true
        };

        var agent = new https.Agent(agentOptions);

        var options = {
            host: bizTalkServiceUrl,
            path: bridgePath,
            headers: headers,
            method: 'POST',
            agent: agent
        };

    console.log('[updateProduct]: options: %j\n\rrequest body:\n%s', options, body);

    var req = https.request(options, function (res) {
      console.log('[updateProduct]: statusCode: %d\n\rheaders: %j', res.statusCode, res.headers);
      res.on('data', function (data) {
        var soap = data.toString('utf8');
        console.log('[updateProduct]: response body:\n%s', soap);
                var re = /(\w+:)/g;
                soap = soap.replace(re, '');
                console.log('[getProducts]: removed namespace prefix from xml document:\n%s', soap);
        
        if (res.statusCode == 200 ||
                    res.statusCode == 202 ||
                    res.statusCode == 204) {
                    console.log('[updateProduct]: product successfully updated.');
                    response.send(200, request.body);
                }
        else {
          response.send(400, '[updateProduct]: An error occurred while invoking the downstream service.');
        }
      });
    });
    req.write(body);
    req.end();

    req.on('error', function (e) {
      console.error('[updateProduct]: ', e);
            response.send(400, util.format('[updateProduct]: %j', e));
    });
  }
  else {
    var message = "[updateProduct]: The request body is not in " + 
            "JSON format or doesn't contain a well-formed product. request.body=" + 
            JSON.stringify(request.body);
    console.error(message);
    response.send(400, message);
  }
}

function deleteProduct(token, productId, request, response) {
  var body = '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" xmlns:a="http://www.w3.org/2005/08/addressing">' +
        '<s:Header>' +
        '<a:Action s:mustUnderstand="1">DeleteProduct</a:Action>' +
        '</s:Header>' +
        '<s:Body>' +
        '<DeleteProduct xmlns="http://windowsazure.cat.microsoft.com/samples/mobileservices">' +
        '<productId>' + productId + '</productId>' +
        '</DeleteProduct>' +
        '</s:Body>' +
        '</s:Envelope>';

  var headers = {
      'Accept': 'application/soap+xml',
      'Content-Type': 'application/soap+xml',
      'Content-Length': body.length,
      'Authorization': token
    };

    var agentOptions = {
            secureOptions: require('constants').SSL_OP_NO_TLSv1_2,
            ciphers: 'ECDHE-RSA-AES256-SHA:AES256-SHA:RC4-SHA:RC4:HIGH:!MD5:!aNULL:!EDH:!AESGCM',
            honorCipherOrder: true
    };

    var agent = new https.Agent(agentOptions);

    var options = {
        host: bizTalkServiceUrl,
        path: bridgePath,
        headers: headers,
        method: 'POST',
        agent: agent
    };

  console.log('[deleteProduct]: options: %j\n\rrequest body:\n%s', options, body);

  var req = https.request(options, function (res) {
    console.log('[deleteProduct]: statusCode: %d\n\rheaders: %j', res.statusCode, res.headers);
    res.on('data', function (data) {
      var soap = data.toString('utf8');
      console.log('[deleteProduct]: response body:\n%s', soap);
      if (res.statusCode == 200 ||
                res.statusCode == 202 ||
        res.statusCode == 204) {
                console.log('[deleteProduct]: product successfully deleted.');
                response.send(200, true);
      }
      else {
        response.send(400, '[deleteProduct]: An error occurred while invoking the downstream service.');
      }
    });
  });
  req.write(body);
  req.end();

  req.on('error', function (e) {
    console.error('[deleteProduct]: ', e);
        response.send(400, util.format('[deleteProduct]: %j', e));
  });
}

function flatten(obj, lowerCase) {
    if (Array.isArray(obj))    {
    if (obj.length == 1) {
      return flatten(obj[0]);
    }
    else {
      return obj.map(flatten); 
    }
    }
  if (typeof obj === 'string') {
    return obj;
  }
    if (obj === null || 
    obj === undefined ||
    Object.getOwnPropertyNames(obj).length === 0) {
        return ""; 
    }

  var flat = {};
    for (var k in obj) {
        if (obj.hasOwnProperty(k)) {
            var value = obj[k];
            if (null === value || undefined === value) {
                continue;
            }
      if (k !== '$') {
        flat[k] = flatten(value);
      }

        }
    }
    return flat;
}

function sendNotification(text1, text2, tag) {
  var hub = azure.createNotificationHubService(hubName, hubConnectionString);
  var payload = '{ "title" : "' + text1 + '", "msg" : "' + text2 + '"}';
  var tag;
  hub.send(tag, payload,
    function (error) {
      if (!error) {
        console.log('[sendNotification] Notification successfully sent.');
      }
      else {
        console.error(error);
      }
    });
}
 
NOTE: make sure to replace the following placeholders in the upper part of the code before saving the script:
  • [YOUR-BIZTALK-SERVICE-NAMESPACE]: specify the name of your BizTalk Service namespace.
  • [YOUR-BIZTALK-SERVICE-ACS-NAMESPACE]: specify the name of the ACS namespace thet issues the security tokens to authenticate against your BizTalk Service
  • [YOUR-BIZTALK-SERVICE-ACS-ISSUER-NAME]: specify the name of a service identity used to access your BizTalk Service (e.g. owner)
  • [YOUR-BIZTALK-SERVICE-ACS-ISSUER-SECRET]: specify the password for the selected service identity.
  • [YOUR-NOTIFICATION-HUB-NAME]: the name of the notification hub used to send push notifications.
  • [YOUR-NOTIFICATION-HUB-CONNECTION-STRING]: the DefaultFullSharedAccessSignature connection string of the notification hub used to send push notifications.  

For more information, see:

Configure Git Source Control

The source control support provides a Git repository as part your mobile service, and it includes all of your existing Mobile Service scripts and permissions. You can clone that git repository on your local machine, make changes to any of your scripts, and then easily deploy the mobile service to production using Git. This enables a really great developer workflow that works on any developer machine (Windows, Mac and Linux). To configure the Git source control proceed as follows:
  1. Navigate to the dashboard for your mobile service and select the Set up source control link:
  2. If this is your first time enabling Git within Windows Azure, you will be prompted to enter the credentials you want to use to access the repository:
  3. Once you configure this, you can switch to the CONFIGURE tab of your Mobile Service and you will see a Git URL you can use to use your repository:
You can use the GIT URL to clone the repository locally using Git from the command line:
cd C:\ 
mkdir Git 
cd Git 
git clone https://servicebus.scm.azure-mobile.net/servicebus.git
 
 
You can make changes to the code of server scripts and then upload changes to your mobile service using the following script.
git add -A .
git commit -m "Modified calculator server script"
git push

Visual Studio Solution

The Visual Studio solution includes the following projects:
  • ProductDb.sql: this script can be used to create the SQL Server database used by the WCF service to store data.
  • BizTalkService: contains the artifacts used by the BizTalk Service:
    • XML Request-Reply Bridge
    • XML Schemas
    • Maps
  • HTML5: contains the HTML5/JavaScript client for the mobile service.
  • WindowsPhone8: contains the Windows Phone 8 app that can be used to test the mobile service.
  • WindowsStoreApp: contains the Windows Store app that can be used to test the mobile service.

NOTE: the WindowsStoreApp project uses the Windows Azure Mobile Services NuGet package. To recuce the size of tha zip file, I deleted some of the asemblies from the packages folder. To repair the solution, make sure to right click the solution and select Enable NuGet Package Restore as shown in the picture below. For more information on this topic, see the following post.

 

ProductDb

Use the following script to create the ProductDb database on a local instance of SQL Server.

USE master
GO
-- Create ProductDb database
DECLARE @Count int
SELECT @Count = COUNT(*) FROM sysdatabases 
WHERE [name] = 'ProductDb'
IF (@Count = 0) 
CREATE DATABASE ProductDb
GO
USE ProductDb
GO
IF OBJECT_ID('Products') > 0 DROP TABLE [Products]
GO
-- Create Products table
CREATE TABLE [Products] (
    [ProductID] [int] IDENTITY(1,1) NOT NULL ,
    [Name] [nvarchar](50) NOT NULL ,
    [Category] [nvarchar](50) NOT NULL ,
    [Price] [smallmoney] NOT NULL
    CONSTRAINT [PK_Products] PRIMARY KEY CLUSTERED 
    (
        [ProductID]
    )
)
GO
-- Create stored procedures
IF OBJECT_ID('GetProduct') > 0 DROP PROCEDURE [GetProduct]
GO
CREATE PROCEDURE GetProduct
@ProductID int
AS
SELECT [ProductID], [Name], [Category], [Price] 
FROM [Products] 
WHERE [ProductID] = @ProductID
GO
IF OBJECT_ID('GetProducts') > 0 DROP PROCEDURE [GetProducts]
GO
CREATE PROCEDURE GetProducts
AS
SELECT [ProductID], [Name], [Category], [Price]  
FROM [Products] 
GO
IF OBJECT_ID('GetProductsByCategory') > 0 DROP PROCEDURE [GetProductsByCategory]
GO
CREATE PROCEDURE GetProductsByCategory
@Category [nvarchar](50)
AS
SELECT [ProductID], [Name], [Category], [Price]  
FROM [Products] 
WHERE [Category] = @Category
GO
IF OBJECT_ID('AddProduct') > 0 DROP PROCEDURE [AddProduct]
GO
CREATE PROCEDURE AddProduct
@ProductID int OUTPUT,
@Name [nvarchar](50),
@Category [nvarchar](50),
@Price [smallmoney]
AS
INSERT INTO Products VALUES (@Name, @Category, @Price)
SET @ProductID = @@IDENTITY
GO
IF OBJECT_ID('UpdateProduct') > 0 DROP PROCEDURE [UpdateProduct]
GO
CREATE PROCEDURE UpdateProduct
@ProductID int,
@Name [nvarchar](50),
@Category [nvarchar](50),
@Price [smallmoney]
AS
UPDATE Products 
SET [Name] = @Name,
    [Category] = @Category,
    [Price] = @Price
WHERE [ProductID] = @ProductID
GO
IF OBJECT_ID('DeleteProduct') > 0 DROP PROCEDURE [DeleteProduct]
GO
CREATE PROCEDURE DeleteProduct
@ProductID int
AS
DELETE [Products]
WHERE [ProductID] = @ProductID
GO
-- Create test data
SET NOCOUNT ON
GO
INSERT INTO Products VALUES (N'Tomato soup', N'Groceries', 1.39)
GO
INSERT INTO Products VALUES (N'Babo', N'Toys', 19.99)
GO
INSERT INTO Products VALUES (N'Hammer', N'Hardware', 16.49)
GO

BizTalkService

This project contains the artifacts used by the BizTalk Service:

  • XML Schemas:
    • MobileServiceAddProduct.xsd: defines the format of the AddProduct message sent by the mobile service to the bridge.
    • MobileServiceAddProductResponse.xsd: defines the format of the AddProductResponse message sent by the bridge to the mobile service.
    • MobileServiceDeleteProduct.xsd: defines the format of the DeleteProduct message sent by the mobile service to the bridge.
    • MobileServiceDeleteProductResponse.xsd: defines the format of the DeleteProductResponse message sent by the bridge to the mobile service.
    • MobileServiceUpdateProduct: defines the format of the UpdateProduct message sent by the mobile service to the bridge.
    • MobileServiceUpdateProductResponse.xsd: defines the format of the UpdateProductResponse message sent by the bridge to the mobile service.
    • MobileServiceGetProduct.xsd: defines the format of the GetProduct message sent by the mobile service to the bridge.
    • MobileServiceGetProductResponse.xsd: defines the format of the GetProductResponse message sent by the bridge to the mobile service.
    • MobileServiceGetProducts.xsd: defines the format of the GetProducts message sent by the mobile service to the bridge.
    • MobileServiceGetProductsResponse.xsd: defines the format of the GetProductsResponse message sent by the bridge to the mobile service.
    • MobileServiceGetProductsByCategory.xsd: defines the format of the GetProductsByCategory message sent by the mobile service to the bridge.
    • MobileServiceGetProductsByCategoryResponse.xsd: defines the format of the GetProductsByCategoryResponse message sent by the bridge to the mobile service.
    • SQLAdapterSimpleTypeArray.xsd: defines the ArrayOfLong complext type. 
    • SQLAdapterTable.dbo.xsd: defines the Products and ArrayOfProducts complext types.
    • SQLAdapterTableOperation.dbo.Products.xsd: defines the schema for the request and response messages used to communicate with the BizTalk Adapter for SQL Server.
    • NOTE: The last 3 XML Schemas were generated when creating the LOB Target for the BizTalk Adapter Service. For more information, see Step 4(c): Generate Schema for the Insert Operation of the tutorial: Using Azure BizTalk Bridges to Insert Flat File Messages into an On-premises SQL Server.
  • Maps:
    • AddProductMap: transforms a MobileServiceAddProduct message into an Insert message.
    • AddProductResponseMap: transforms an InsertResponse message into a MobileServiceAddProductResponse message.
    • DeleteProductMap: transforms a MobileServiceDeleteProduct message into a Delete message.
    • DeleteProductResponseMap: transforms a DeleteResponse message into a MobileServiceDeleteProductResponse message.
    • UpdateProductMap: transforms a MobileServiceUpdateProduct message into an Update message.
    • UpdateProductResponseMap: transforms an UpdateResponse message into a MobileServiceUpdateProductResponse message.
    • GetProductMap: transforms a MobileServiceGetProduct message into a Select message.
    • GetProductResponseMap: transforms an SelectResponse message into a MobileServiceGetProductResponse message.
    • GetProductsMap: transforms a MobileServiceGetProducts message into a Select message.
    • GetProductsResponseMap: transforms an SelectResponse message into a MobileServiceGetProductsResponse message.
    • GetProductsByCategoryMap: transforms a MobileServiceGetProduct ByCategory message into a Select message.
    • GetProductsByCategoryResponseMap: transforms an SelectResponse message into a MobileServiceGetProductByCategoryResponse message.
  • Message Flow Itineraries:
    • MessageFlowItinerary: defines the XML Request-Reply Bridge used to route messages to the BizTalk Adapter Service.

The following figure shows the structure of the MessageFlowItinerary:

As you can see, the message flow itinerary defines 4 routes between the XML Request-Reply Bridge and the BizTalk Adapter Service. Each route corresponds to one of the CRUD operations that can be invoked on the SQL Server LOB Target:

  1. Insert
  2. Update
  3. Delete
  4. Select

The following picture shows the Route Ordering Table of the message flow itinerary. As you can see, the RequestAction promoted property is used to decide which route the outgoing message has to follow.

THe following picture shows the Route Actions table for both the Select route. As you can see, the Route action sets the SOAP Action header used by the service to dispatch the request to the right operation exposed by the WCF relay service that provides access to the SQL Server LOB target.

The following table shows the SOAP Action for each of the 4 routes:

  1. Insert: TableOp/Insert/dbo/Products
  2. Update: TableOp/Update/dbo/Products
  3. Delete: TableOp/Delete/dbo/Products
  4. Select: TableOp/Select/dbo/Products

For more information on the schemas used by the BizTalk Adapter for SQL Server, see Message Schemas for Insert, Update, Delete, and Select Operations on Tables and Views.

The following picture shows the Xml Request-Reply Bridge:

The following table contains the content of the ProductDb.config configuration file for the Two-Way Relay Endpoint exposed by the BizTalk Adapter Service.

 

 

<?xml version="1.0" encoding="utf-8" ?> <configuration> <system.serviceModel> <extensions> <behaviorExtensions> <add name="transportClientEndpointBehavior" type="Microsoft.ServiceBus.Configuration.TransportClientEndpointBehaviorElement, Microsoft.ServiceBus, Version=1.8.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> </behaviorExtensions> <bindingElementExtensions> <add name="netMessagingTransport" type="Microsoft.ServiceBus.Messaging.Configuration.NetMessagingTransportExtensionElement, Microsoft.ServiceBus, Version=1.8.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> </bindingElementExtensions> <bindingExtensions> <add name="basicHttpRelayBinding" type="Microsoft.ServiceBus.Configuration.BasicHttpRelayBindingCollectionElement, Microsoft.ServiceBus, Version=1.8.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> <add name="webHttpRelayBinding" type="Microsoft.ServiceBus.Configuration.WebHttpRelayBindingCollectionElement, Microsoft.ServiceBus, Version=1.8.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> <add name="ws2007HttpRelayBinding" type="Microsoft.ServiceBus.Configuration.WS2007HttpRelayBindingCollectionElement, Microsoft.ServiceBus, Version=1.8.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> <add name="netTcpRelayBinding" type="Microsoft.ServiceBus.Configuration.NetTcpRelayBindingCollectionElement, Microsoft.ServiceBus, Version=1.8.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> <add name="netMessagingBinding" type="Microsoft.ServiceBus.Messaging.Configuration.NetMessagingBindingCollectionElement, Microsoft.ServiceBus, Version=1.8.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" /> </bindingExtensions> </extensions> <behaviors> <endpointBehaviors> <behavior name="serviceBusCredentialBehavior"> <transportClientEndpointBehavior> <tokenProvider> <sharedSecret issuerName="owner" issuerSecret="[YOUR-ISSUER-SECRET]" /> </tokenProvider> </transportClientEndpointBehavior> </behavior> </endpointBehaviors> </behaviors> <bindings> <basicHttpRelayBinding> <binding name="basicHttpRelayBinding1" receiveTimeout="00:20:00" sendTimeout="00:20:00" maxBufferPoolSize=" 1048576" maxBufferSize="67108864" maxReceivedMessageSize="67108864"> <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="67108864" maxNameTableCharCount="2147483647" /> <security mode="Transport" /> </binding> </basicHttpRelayBinding> </bindings> <client> <clear /> <endpoint address="https://[YOUR-SERVICE-BUS-NAMESPACE].servicebus.windows.net/sqllobrelay/productdb/" behaviorConfiguration="serviceBusCredentialBehavior" binding="basicHttpRelayBinding" bindingConfiguration="basicHttpRelayBinding1" contract="System.ServiceModel.Routing.IRequestReplyRouter" name="sqllobrelay_productdb" /> </client> </system.serviceModel> </configuration>

NOTE: make sure to replace the following placeholders in the ProductDb.config file:

  • [YOUR-SERVICE-BUS-NAMESPACE]: specify the name of your Service Bus namespace.
  • [YOUR-ISSUER-SECRET]: specify the issuer secret of the owner service identity for your Service Bus namespace.

  

HTML5/JavaScript Client

The HTML5/JavaScript client application calls the mobile service useing the invokeApi method exposed by the MobileServiceClient contained in the MobileServices.Web-1.0.0.min.js script. This method can be used to invoke any HTTP method. For more information, see Custom API in Azure Mobile Services – Client SDKs by Carlos Figueira.

The following table contains the code the page.js script that contains the client-side logic used by the application to invoke the mobile service.

$(function() { 
  var client = new WindowsAzure.MobileServiceClient('https://[YOUR-MOBILE-SERVICE-NAME].azure-mobile.net/',  
                                                    '[YOUR-MOBILE-SERVICE-APPLICATION-KEY]'); 
 
  // Cloud Animation 
  $('#far-clouds1').pan({ fps: 30, speed: 0.7, dir: 'left', depth: 30 }); 
  $('#near-clouds1').pan({ fps: 30, speed: 1, dir: 'left', depth: 70 }); 
  $('#far-clouds2').pan({ fps: 30, speed: 0.7, dir: 'left', depth: 100 }); 
  $('#near-clouds2').pan({ fps: 30, speed: 1, dir: 'left', depth: 130 }); 
 
  window.actions = { 
    speedyClouds: function () { 
      $('#far-clouds').spSpeed(12); 
      $('#near-clouds').spSpeed(20); 
    }, 
    runningClouds: function () { 
      $('#far-clouds').spSpeed(8); 
      $('#near-clouds').spSpeed(12); 
    }, 
    walkingClouds: function () { 
      $('#far-clouds').spSpeed(3); 
      $('#near-clouds').spSpeed(5); 
    }, 
    lazyClouds: function () { 
      $('#far-clouds').spSpeed(0.7); 
      $('#near-clouds').spSpeed(1); 
    }, 
    stop: function () { 
      $('#far-clouds, #near-clouds').spStop(); 
    }, 
    start: function () { 
      $('#far-clouds, #near-clouds').spStart(); 
    }, 
    toggle: function () { 
      $('#far-clouds, #near-clouds').spToggle(); 
    }, 
    left: function () { 
      $('#far-clouds, #near-clouds').spChangeDir('left'); 
    }, 
    right: function () { 
      $('#far-clouds, #near-clouds').spChangeDir('right'); 
    } 
  }; 
 
  // Read current data and rebuild UI. 
  // If you plan to generate complex UIs like this, consider using a JavaScript templating library. 
  function refreshProducts() { 
    $('#summary').html('Retrieving products...'); 
    client.invokeApi('biztalkproducts', { 'method': 'GET' }) 
      .done(function (response) { 
        if (Array.isArray(response.result) && response.result.length > 0) { 
          var products = $.map(response.result, function (product) { 
            return $('<tr>').append($('<td><input class="product-text" style='width: 70px' value='' +  
                                      product.productId + '"/></td>')) 
              .append($('<td><input class="product-text" value="' + product.name + '"/></td>')) 
              .append($('<td><input class="product-text" value="' + product.category + '"/></td>')) 
              .append($('<td><input class="product-text" style='width: 100px' value='' +  
                        parseFloat(product.price).toFixed(2) + '"/></td>')) 
              .append($('<td><button class="button-update" style='width: 70px' >Update</button></td>')) 
   .append($('<td><button class="button-delete" style='width: 70px' >Delete</button></td>')).append('</tr>'); 
          }); 
          $('#products > tbody').empty().append(products).toggle(products.length > 0); 
          $('#summary').html('<strong>' + products.length + '</strong> product(s) successfully retrieved.'); 
        } else { 
          $('#summary').html('No product retrieved.'); 
        } 
      }, 
        function (error) { 
          var text = error + (error.request ? ' - ' + error.request.status : ''); 
          $('#errorlog').append($('<li>').text(text)); 
        } 
      ); 
  } 
 
  // Handle insert 
  $('#add-product').submit(function(event) { 
    if ($('#product-name').val() === '') { 
      $('#summary').html('The <strong>Name</strong> field cannot be null.'); 
      event.preventDefault(); 
      return; 
    } 
    if ($('#product-category').val() === '') { 
      $('#summary').html('The <strong>Category</strong> field cannot be null.'); 
      event.preventDefault(); 
      return; 
    } 
    if ($('#product-price').val() === '') { 
      $('#summary').html('The <strong>Price</strong> field cannot be null.'); 
      event.preventDefault(); 
      return; 
    } 
    var product = { 
      'name': $('#product-name').val(), 
      'category': $('#product-category').val(), 
      'price': $('#product-price').val() 
    }; 
    $('#summary').html('Adding <strong>' + product.name + '</strong>...'); 
    client.invokeApi('biztalkproducts', { 'method': 'POST', 'body': product }) 
        .done(function(response) { 
          if (response.result) { 
            var row = $('<tr>').append($('<td><input class="product-text" value="' +  
                                          response.result.productId + '"/></td>')) 
              .append($('<td><input class="product-text" value="' + response.result.name + '"/></td>')) 
              .append($('<td><input class="product-text" value="' + response.result.category + '"/></td>')) 
              .append($('<td><input class="product-text" value="' + response.result.price + '"/></td>')) 
              .append($('<td><button class="button-update">Update</button></td>')) 
              .append($('<td><button class="button-delete">Delete</button></td>')).append('</tr>'); 
            $('#products > tbody').append(row).toggle(true); 
            $('#summary').html('<strong>' + response.result.name + '</strong> successfully added.'); 
        } 
        }, 
        function (error) { 
          var text = error + (error.request ? ' - ' + error.request.status : ''); 
          $('#errorlog').append($('<li>').text(text)); 
        } 
      ); 
    $('#product-name').val(''); 
    $('#product-category').val(''); 
    $('#product-price').val(''); 
    event.preventDefault(); 
  }); 
 
  // Handle update 
  $(document.body).on('click', '.button-update', function (event) { 
    var row = $(this).closest('tr'); 
    var productId = row.find('td:nth-child(1) input').val(); 
    if (!productId) { 
      $('#summary').html('The <strong>product id</strong> field cannot be null.'); 
      event.preventDefault(); 
      return; 
    } 
    var name = row.find('td:nth-child(2) input').val(); 
    if (!name) { 
      $('#summary').html('The <strong>name</strong> field cannot be null.'); 
      event.preventDefault(); 
      return; 
    } 
    var category = row.find('td:nth-child(3) input').val(); 
    if (!category) { 
      $('#summary').html('The <strong>category</strong> field cannot be null.'); 
      event.preventDefault(); 
      return; 
    } 
    var price = row.find('td:nth-child(4) input').val(); 
    if (!price) { 
      $('#summary').html('The <strong>price</strong> field cannot be null.'); 
      event.preventDefault(); 
      return; 
    } 
    var product = { 
      'productId': productId, 
      'name': name, 
      'category': category, 
      'price': price 
    }; 
    $('#summary').html('Updating <strong>' + product.name + '</strong>...'); 
    client.invokeApi('biztalkproducts', { 'method': 'PUT', 'body': product }) 
        .done(function (response) { 
          if (response.result) { 
            $('#summary').html('<strong>' + response.result.name +  
                               '</strong> successfully updated.'); 
          } 
        }, 
        function (error) { 
          var text = error + (error.request ? ' - ' + error.request.status : ''); 
          $('#errorlog').append($('<li>').text(text)); 
        } 
      ); 
    event.preventDefault(); 
  }); 
 
  // Handle delete 
  $(document.body).on('click', '.button-delete', function (event) { 
    var productId = $(this).closest('tr').find('td:nth-child(1) input').val(); 
    var name = $(this).closest('tr').find('td:nth-child(2) input').val(); 
    var row = $(this).parents('tr').first(); 
    if (productId) { 
      $('#summary').html('Deleting <strong>' + name + '</strong>...'); 
      client.invokeApi('biztalkproducts',  
                       { 'method': 'DELETE', 'parameters': { 'id': productId } }) 
        .done(function (response) { 
          if (response.result == true) { 
            row.remove(); 
            $('#summary').html('<strong>' + name + '</strong> successfully deleted.'); 
          } 
        }, 
        function (error) { 
          var text = error + (error.request ? ' - ' + error.request.status : ''); 
          $('#errorlog').append($('<li>').text(text)); 
        } 
      ); 
      event.preventDefault(); 
    } 
  }); 
 
  // Handle refresh 
  $(document.body).on('click', '.button-refresh', function (event) { 
    refreshProducts(); 
    event.preventDefault(); 
  }); 
   
  // On initial load, start by fetching the current data 
  refreshProducts(); 
});
NOTE: make sure to replace the following placeholders in the page.js and index.html files:
  • [YOUR-MOBILE-SERVICE-NAME]: specify the name of your mobile service.
  • [YOUR-MOBILE-SERVICE-APPLICATION-KEY]: specify the application key of your mobile service.

Create a web site in the portal

  1. Login to the Windows Azure Management Portal.
  2. Click the New icon on the bottom left of the dashboard.

    Create New
  3. Click the Web Site icon, and click Quick Create.

  4. Select a Subscription, a Region and enter a URL for the new web site, then click Create Web Site.
     
  5. Select the new web site on the Windows Azure Management Portal and click the Dashboard tab. Click the Download the publish profile link and save the .PublishSettings file on your disk. For more information, see How to deploy an ASP.NET web application to a Windows Azure Web Site using Visual Studio.
     
  6. Open your solution in Visual Studio, right click the HTML5 web site and click Publish Web Site in the context menu.

     
  7. Click the Import button in the the Publish Web dialog.
     
  8. Click the Browse button and select the .PublishSettings file obtained at step 5.
     
  9. Publishing data are automatically loaded from the .PublishSettings file. Click the Next button
     
  10. Click the Next button.
     
  11. Finally, click the Publish button.

Configure Mobile Service CORS to accept calls from the Web Site

Cross-Origin Resource Sharing (CORS) allows JavaScript code running in a browser on an external host to interact with your mobile service. Specify the host names that should be allowed to make cross-origin calls. You can use wildcards if required (for example, *.example.com). These settings affect only the use of your mobile service from web applications. Native applications can access your mobile service regardless of CORS settings. Follow the steps below to configure your mobile service to accept calls from your Windows Azure Web Site.

  1. Click the Dashboard tab on your web site and copy the Site Url.
     
  2. Click the Configure tab on your mobile service.
     
  3. Pste the URL of the web site in the Cross-origin resource sharing (cors) section and click the Save button.

Windows Store App

The following picture shows the Windows Store App that can be used to test the mobile service.
 
 
The following table contains the code of the App.xaml.cs class that contains the client-side logic used to invoke the mobile service. The App class performs the following actions:
#region Using Directives
using System;
using System.Threading.Tasks;
using Microsoft.WindowsAzure.Messaging;
using Microsoft.WindowsAzure.MobileServices;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation;
using Windows.Data.Xml.Dom;
using Windows.Networking.PushNotifications;
using Windows.UI.Notifications;
using Windows.UI.Popups;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls; 
#endregion

namespace WindowsStoreApp
{
  /// <summary>
  /// Provides application-specific behavior to supplement the default Application class.
  /// </summary>
  sealed partial class App 
  {
    // This MobileServiceClient has been configured to communicate with your Mobile Service's url
    // and application key. You're all set to start working with your Mobile Service!
    #region Public Constants
    private const string NotificationHubName = "[YOUR-NOTIFICATION-HUB-NAME]";
    private const string ConnectionString = "[YOUR-NOTIFICATION-HUB-CONNECTION-STRING]; #endregion #region Private Instance Fields // Notification Hub readonly NotificationHub notificationHub; // Registration private Registration toastRegistration; private Registration tileRegistration; #endregion #region Public Static Variables public static MobileServiceClient MobileService = new MobileServiceClient("[YOUR-MOBILE-SERVICE-URL]", "[YOUR-MOBILE-SERVICE-APPLICATION-KEY]"); #endregion #region Public Constructor /// <summary> /// Initializes the singleton application object. This is the first line of authored code /// executed, and as such is the logical equivalent of main() or WinMain(). /// </summary> public App() { notificationHub = new NotificationHub(NotificationHubName, ConnectionString); InitializeComponent(); Suspending += OnSuspending; } #endregion /// <summary> /// It takes care of refreshing any existing registrations and reacquiring and /// re-associating the WNS channel URI whenever needed. /// It also checks whether a nativeRegistration exists for a Toast notification /// and creates a new nativeRegistration if it's not found. /// </summary> /// <returns>A Task</returns> async Task InitializeNotificationsAsync() { string message = null; try { var channel = await PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync(); toastRegistration = await notificationHub.RegisterTemplateAsync(channel.Uri, BuildTextToastTemplate(), "toastRegistration", new[] { "productservice" }); tileRegistration = await notificationHub.RegisterTemplateAsync(channel.Uri, BuildTextTileTemplate(), "tileRegistration", new[] { "productservice" }); } catch (Exception ex) { message = ex.Message; } if (string.IsNullOrWhiteSpace(message)) { return; } var messageDialog = new MessageDialog(message, "Notification Hub"); await messageDialog.ShowAsync(); } /// <summary>; /// It is used when calling CreatingTemplateRegistrationForApplicationAsync on the notificationHub instance. /// It uses the Windows’ native notification template library. In the ToastText01 template, modify its text node /// by adding a Notification Hub template expression: $(msg) /// </summary> /// <returns>The xml document containing the toast template.</returns> static string BuildTextToastTemplate() { var template = ToastNotificationManager.GetTemplateContent(ToastTemplateType.ToastText02); var textNode = template.SelectSingleNode("//text[@id='1']") as XmlElement;
      if (textNode != null)
      {
        textNode.InnerText = "$(title)";
      }
      textNode = template.SelectSingleNode("//text[@id='2']") as XmlElement;
      if (textNode != null)
      {
        textNode.InnerText = "$(msg)";
      }
      textNode = template.SelectSingleNode("//toast") as XmlElement;
      if (textNode != null)
      {
        textNode.SetAttribute("launch", "$(msg)");
      }
      return template.DocumentElement.GetXml();
    }

    /// <summary>;
    /// It is used when calling CreatingTemplateRegistrationForApplicationAsync on the notificationHub instance. 
    /// It uses the Windows’ native notification template library. In the ToastText01 template, modify its text node 
    /// by adding a Notification Hub template expression: $(msg)
    /// </summary>
    /// <returns>The xml document containing the toast template.</returns>
    static string BuildTextTileTemplate()
    {
      var template = TileUpdateManager.GetTemplateContent(TileTemplateType.TileWideText09);
      var textNode = template.SelectSingleNode("//text[@id='1']") as XmlElement;
      if (textNode != null)
      {
        textNode.InnerText = "$(title)";
      }
      textNode = template.SelectSingleNode("//text[@id='2']") as XmlElement;
      if (textNode != null)
      {
        textNode.InnerText = "$(msg)";
      }
      return template.DocumentElement.GetXml();
    }

    /// <summary>
    /// Invoked when the application is launched normally by the end user. Other entry points
    /// will be used when the application is launched to open a specific file, to display
    /// search results, and so forth.
    /// </summary>
    /// <param name="args">Details about the launch request and process.</param>
    protected async override void OnLaunched(LaunchActivatedEventArgs args)
    {
      await InitializeNotificationsAsync();
      // Do not repeat app initialization when already running, just ensure that
      // the window is active
      if (args.PreviousExecutionState == ApplicationExecutionState.Running)
      {
        Window.Current.Activate();
        return;
      }

      if (args.PreviousExecutionState == ApplicationExecutionState.Terminated)
      {
      }

      // Create a Frame to act navigation context and navigate to the first page
      var rootFrame = new Frame();
      if (!rootFrame.Navigate(typeof(MainPage)))
      {
        throw new Exception("Failed to create initial page");
      }

      // Place the frame in the current Window and ensure that it is active
      Window.Current.Content = rootFrame;
      Window.Current.Activate();
    }

    /// <summary>
    /// Invoked when application execution is being suspended. Application state is saved
    /// without knowing whether the application will be terminated or resumed with the contents
    /// of memory still intact.
    /// </summary>
    /// <param name="sender">The source of the suspend request.</param>
    /// <param name="e">Details about the suspend request.</param>
    private void OnSuspending(object sender, SuspendingEventArgs e)
    {
      var deferral = e.SuspendingOperation.GetDeferral();
      deferral.Complete();
    }
  }
}
NOTE: make sure to replace the following placeholders in the App.xaml.cs file:
  • [YOUR-MOBILE-SERVICE-URL]: specify the URL of your mobile service.
  • [YOUR-MOBILE-SERVICE-APPLICATION-KEY]: specify the application key of your mobile service.
  • [YOUR-NOTIFICATION-HUB-NAME]: specify the name of the notification hub used to send push notifications.
  • [YOUR-NOTIFICATION-HUB-CONNECTION-STRING]: specify the DefaultFullSharedAccessSignature connection string of the notification hub used to send push notifications. 
The following table contains the code of the MainPage.xaml.cs class that contains the client-side logic used to invoke the mobile service, and the code of the Product class that defines entity model. The class uses the InvokeApiAsync method of the MobileServiceClient class to call the mobile service.
#region Using Directives
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Net.Http;
using Microsoft.WindowsAzure.MobileServices;
using Newtonsoft.Json;
using System;
using Windows.UI.Popups;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation; 
#endregion

namespace WindowsStoreApp
{
  public class Product
  {
    [JsonProperty(PropertyName = "productId", Order = 1)]
    public int ProductId { get; set; }
    [JsonProperty(PropertyName = "name", Order = 2)]
    public string Name { get; set; }
    [JsonProperty(PropertyName = "category", Order = 3)]
    public string Category { get; set; }
    [JsonProperty(PropertyName = "price", Order = 4)]
    public float Price { get; set; }
  }

  public sealed partial class MainPage
  {
    private ObservableCollection<Product> products;
    private const string ApiName = "biztalkproducts";
    public MainPage()
    {
      InitializeComponent();
    }

    private async void InsertProduct(Product product)
    {
      MobileServiceInvalidOperationException exception = null;
      try
      {
        Summary.Text = string.Format("Adding {0}...", product.Name);
        product = await App.MobileService.InvokeApiAsync<Product, Product>(ApiName, product);
        products.Add(product);
        Summary.Text = string.Format("{0} successfully added.", product.Name);
        ProductName.Text = string.Empty;
        ProductCategory.Text = string.Empty;
        ProductPrice.Text = string.Empty;
      }
      catch (MobileServiceInvalidOperationException ex)
      {
        Summary.Text = ex.Message;
        exception = ex;
      }

      if (exception != null)
      {
        await new MessageDialog(exception.Message, "Error loading products").ShowAsync();
      }         
    }

    private async void RefreshProducts()
    {
      MobileServiceInvalidOperationException exception = null;
      try
      {
        Summary.Text = "Retrieving products...";
        // This code refreshes the entries in the list view by querying the Products table.
        // The query excludes completed Products
        products = await App.MobileService.InvokeApiAsync<ObservableCollection<Product>>(ApiName, 
                                                                                         HttpMethod.Get, 
                                                                                         null);
        Summary.Text = products.Count == 0 ? 
                       "No product retrieved" : 
                       string.Format("{0} product(s) successfully retrieved.", products.Count);
      }
      catch (MobileServiceInvalidOperationException ex)
      {
        Summary.Text = ex.Message;
        exception = ex;
      }

      if (exception != null)
      {
        await new MessageDialog(exception.Message, "Error loading products").ShowAsync();
      }
      else
      {
        ListProducts.ItemsSource = products;
      }
    }

    private void ButtonRefresh_Click(object sender, RoutedEventArgs e)
    {
      RefreshProducts();
    }

    private async void UpdateProduct_OnClick(object sender, RoutedEventArgs e)
    {
      var stackPanel = (StackPanel)((Button)sender).Parent;
      if (stackPanel == null || !(stackPanel.DataContext is Product))
      {
        return;
      }
      var product = stackPanel.DataContext as Product;
      if (string.IsNullOrWhiteSpace(product.Name))
      {
        Summary.Text = "The name cannot be null";
        return;
      }
      if (string.IsNullOrWhiteSpace(product.Category))
      {
        Summary.Text = "The category cannot be null";
        return;
      }
      MobileServiceInvalidOperationException exception = null;
      try
      {
        Summary.Text = string.Format("Updating {0}...", product.Name);
        await App.MobileService.InvokeApiAsync<Product, Product>(ApiName, 
                                                                 product, 
                                                                 HttpMethod.Put, 
                                                                 null);
        Summary.Text = string.Format("{0} successfully updated.", product.Name);
      }
      catch (MobileServiceInvalidOperationException ex)
      {
        Summary.Text = ex.Message;
        exception = ex;
      }

      if (exception != null)
      {
        await new MessageDialog(exception.Message, "Error updating product").ShowAsync();
      }
    }

    private async void DeleteProduct_OnClick(object sender, RoutedEventArgs e)
    {
      var stackPanel = (StackPanel)((Button)sender).Parent;
      if (stackPanel == null || !(stackPanel.DataContext is Product))
      {
        return;
      }
      var product = stackPanel.DataContext as Product;
      MobileServiceInvalidOperationException exception = null;
      try
      {
        Summary.Text = string.Format("Deleting {0}...", product.Name);
        await App.MobileService.InvokeApiAsync(ApiName, 
               HttpMethod.Delete, 
               new Dictionary<string, string> { { "id", product.ProductId.ToString() } });
        Summary.Text = string.Format("{0} successfully deleted.", product.Name);
        products.Remove(product);
      }
      catch (MobileServiceInvalidOperationException ex)
      {
        Summary.Text = ex.Message;
        exception = ex;
      }

      if (exception != null)
      {
        await new MessageDialog(exception.Message, "Error deleting product").ShowAsync();
      }
    }

    private void ButtonSave_Click(object sender, RoutedEventArgs e)
    {
      float price;
      if (string.IsNullOrWhiteSpace(ProductName.Text))
      {
        Summary.Text = "The name cannot be null";
        return;
      }
      if (string.IsNullOrWhiteSpace(ProductCategory.Text))
      {
        Summary.Text = "The category cannot be null";
        return;
      }
      if (!float.TryParse(ProductPrice.Text, out price))
      {
        Summary.Text = "The price must be a number";
        return;
      }
      var product = new Product { Name = ProductName.Text, 
                                  Category = ProductCategory.Text, 
                                  Price = price};
      InsertProduct(product);
    }

    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
      RefreshProducts();
    }
  }
}

When a new item is added to the ProductDb, the mobile service sends a notification. The Windows Phone 8 app registers two templates with the notification hub, one to receive toast notification and one to receive live tiles, as shown in the picture below.

Windows Phone 8 App

The following picture shows the Windows Phone 8 App that can be used to test the mobile service.
The following table contains the code of the App.xaml.cs class that contains the client-side logic used to invoke the mobile service. The App class creates the MobileServiceClient object used by the MainPage class to communicate with the underlying mobile service.
#region Using Directives 
using System; 
using System.Diagnostics; 
using System.Windows; 
using System.Windows.Markup; 
using System.Windows.Navigation; 
using Microsoft.Phone.Controls; 
using Microsoft.Phone.Shell; 
using ProductWindowsPhoneApp.Resources; 
using Microsoft.WindowsAzure.MobileServices; 
#endregion 
 
namespace ProductWindowsPhoneApp 
{ 
  public partial class App 
  { 
    #region Public Static Properties 
    /// <summary> 
    /// Provides easy access to the root frame of the Phone Application. 
    /// </summary> 
    /// <returns>The root frame of the Phone Application.</returns> 
    public static PhoneApplicationFrame RootFrame { get; private set; } 
    public static string ApplicationUrl = "[YOUR-MOBILE-SERVICE-URL]"; 
    public static string ApplicationKey = "[YOUR-MOBILE-SERVICE-APPLICATION-KEY]"; 
    public static string CustomApi = "biztalkproducts"; 
    public static MobileServiceClient MobileService = new MobileServiceClient(ApplicationUrl, ApplicationKey);  
    #endregion 
     
    /// <summary> 
    /// Constructor for the Application object. 
    /// </summary> 
    public App() 
    { 
      // Global handler for uncaught exceptions. 
      UnhandledException += Application_UnhandledException; 
 
      // Standard XAML initialization 
      InitializeComponent(); 
 
      // Phone-specific initialization 
      InitializePhoneApplication(); 
 
      // Language display initialization 
      InitializeLanguage(); 
 
      // Show graphics profiling information while debugging. 
      if (Debugger.IsAttached) 
      { 
        // Display the current frame rate counters. 
        Application.Current.Host.Settings.EnableFrameRateCounter = true; 
 
        // Show the areas of the app that are being redrawn in each frame. 
        //Application.Current.Host.Settings.EnableRedrawRegions = true; 
 
        // Enable non-production analysis visualization mode, 
        // which shows areas of a page that are handed off to GPU with a colored overlay. 
        //Application.Current.Host.Settings.EnableCacheVisualization = true; 
 
        // Prevent the screen from turning off while under the debugger by disabling 
        // the application's idle detection. 
        // Caution:- Use this under debug mode only. Application that disables user idle detection will continue to run 
        // and consume battery power when the user is not using the phone. 
        PhoneApplicationService.Current.UserIdleDetectionMode = IdleDetectionMode.Disabled; 
      } 
 
    } 
 
    // Code to execute when the application is launching (eg, from Start) 
    // This code will not execute when the application is reactivated 
    private void Application_Launching(object sender, LaunchingEventArgs e) 
    { 
    } 
 
    // Code to execute when the application is activated (brought to foreground) 
    // This code will not execute when the application is first launched 
    private void Application_Activated(object sender, ActivatedEventArgs e) 
    { 
    } 
 
    // Code to execute when the application is deactivated (sent to background) 
    // This code will not execute when the application is closing 
    private void Application_Deactivated(object sender, DeactivatedEventArgs e) 
    { 
    } 
 
    // Code to execute when the application is closing (eg, user hit Back) 
    // This code will not execute when the application is deactivated 
    private void Application_Closing(object sender, ClosingEventArgs e) 
    { 
    } 
 
    // Code to execute if a navigation fails 
    private void RootFrame_NavigationFailed(object sender, NavigationFailedEventArgs e) 
    { 
      if (Debugger.IsAttached) 
      { 
        // A navigation has failed; break into the debugger 
        Debugger.Break(); 
      } 
    } 
 
    // Code to execute on Unhandled Exceptions 
    private void Application_UnhandledException(object sender, ApplicationUnhandledExceptionEventArgs e) 
    { 
      if (Debugger.IsAttached) 
      { 
        // An unhandled exception has occurred; break into the debugger 
        Debugger.Break(); 
      } 
    } 
 
    #region Phone application initialization 
 
    // Avoid double-initialization 
    private bool phoneApplicationInitialized = false; 
 
    // Do not add any additional code to this method 
    private void InitializePhoneApplication() 
    { 
      if (phoneApplicationInitialized) 
        return; 
 
      // Create the frame but don't set it as RootVisual yet; this allows the splash 
      // screen to remain active until the application is ready to render. 
      RootFrame = new TransitionFrame(); 
      RootFrame.Navigated += CompleteInitializePhoneApplication; 
 
      // Handle navigation failures 
      RootFrame.NavigationFailed += RootFrame_NavigationFailed; 
 
      // Handle reset requests for clearing the backstack 
      RootFrame.Navigated += CheckForResetNavigation; 
 
      // Ensure we don't initialize again 
      phoneApplicationInitialized = true; 
    } 
 
    // Do not add any additional code to this method 
    private void CompleteInitializePhoneApplication(object sender, NavigationEventArgs e) 
    { 
      // Set the root visual to allow the application to render 
      if (RootVisual != RootFrame) 
        RootVisual = RootFrame; 
 
      // Remove this handler since it is no longer needed 
      RootFrame.Navigated -= CompleteInitializePhoneApplication; 
    } 
 
    private void CheckForResetNavigation(object sender, NavigationEventArgs e) 
    { 
      // If the app has received a 'reset' navigation, then we need to check 
      // on the next navigation to see if the page stack should be reset 
      if (e.NavigationMode == NavigationMode.Reset) 
        RootFrame.Navigated += ClearBackStackAfterReset; 
    } 
 
    private void ClearBackStackAfterReset(object sender, NavigationEventArgs e) 
    { 
      // Unregister the event so it doesn't get called again 
      RootFrame.Navigated -= ClearBackStackAfterReset; 
 
      // Only clear the stack for 'new' (forward) and 'refresh' navigations 
      if (e.NavigationMode != NavigationMode.New && e.NavigationMode != NavigationMode.Refresh) 
        return; 
 
      // For UI consistency, clear the entire page stack 
      while (RootFrame.RemoveBackEntry() != null) 
      { 
        ; // do nothing 
      } 
    } 
 
    #endregion 
 
    // Initialize the app's font and flow direction as defined in its localized resource strings. 
    // 
    // To ensure that the font of your application is aligned with its supported languages and that the 
    // FlowDirection for each of those languages follows its traditional direction, ResourceLanguage 
    // and ResourceFlowDirection should be initialized in each resx file to match these values with that 
    // file's culture. For example: 
    // 
    // AppResources.es-ES.resx 
    // ResourceLanguage's value should be "es-ES" 
    // ResourceFlowDirection's value should be "LeftToRight" 
    // 
    // AppResources.ar-SA.resx 
    // ResourceLanguage's value should be "ar-SA" 
    // ResourceFlowDirection's value should be "RightToLeft" 
    // 
    // For more info on localizing Windows Phone apps see http://go.microsoft.com/fwlink/?LinkId=262072. 
    // 
    private void InitializeLanguage() 
    { 
      try 
      { 
        // Set the font to match the display language defined by the 
        // ResourceLanguage resource string for each supported language. 
        // 
        // Fall back to the font of the neutral language if the Display 
        // language of the phone is not supported. 
        // 
        // If a compiler error is hit then ResourceLanguage is missing from 
        // the resource file. 
        RootFrame.Language = XmlLanguage.GetLanguage(AppResources.ResourceLanguage); 
 
        // Set the FlowDirection of all elements under the root frame based 
        // on the ResourceFlowDirection resource string for each 
        // supported language. 
        // 
        // If a compiler error is hit then ResourceFlowDirection is missing from 
        // the resource file. 
        FlowDirection flow = (FlowDirection)Enum.Parse(typeof(FlowDirection),  
                                 AppResources.ResourceFlowDirection); 
        RootFrame.FlowDirection = flow; 
      } 
      catch 
      { 
        // If an exception is caught here it is most likely due to either 
        // ResourceLangauge not being correctly set to a supported language 
        // code or ResourceFlowDirection is set to a value other than LeftToRight 
        // or RightToLeft. 
 
        if (Debugger.IsAttached) 
        { 
          Debugger.Break(); 
        } 
 
        throw; 
      } 
    } 
  } 
}

NOTE: make sure to replace the following placeholders in the App.xaml.cs file:

  • [YOUR-MOBILE-SERVICE-URL]: specify the URL of your mobile service.
  • [YOUR-MOBILE-SERVICE-APPLICATION-KEY]: specify the application key of your mobile service.

The following table contains the code of the MainPage.xaml.cs class that contains the client-side logic used to invoke the mobile service, and the code of the Product class that defines entity model. The class uses the InvokeApiAsync method of the MobileServiceClient class to call the mobile service. The MainPage class performs the following actions:

  • Creates and open a new HttpNotificationChannel object to receive toast notifications and live tiles from the Windows Phone Push Notification Service.  
  • Creates a NotificationHub object used to register a template for toast notifications and one for live tiles with the notification hub.
#region Using Directives 
using System; 
using System.Collections.Generic; 
using System.Collections.ObjectModel; 
using System.Linq; 
using System.Net.Http; 
using System.Text; 
using System.Windows.Navigation; 
using Microsoft.Phone.Notification; 
using Microsoft.WindowsAzure.Messaging; 
using Microsoft.WindowsAzure.MobileServices; 
using System.Windows; 
#endregion 
 
namespace ProductWindowsPhoneApp 
{ 
  public partial class MainPage 
  { 
    #region Private Constants 
    // Private Constants 
    private const string NotificationHubName = "[YOUR-NOTIFICATION-HUB-NAME]"; 
    private const string ConnectionString = "[YOUR-NOTIFICATION-HUB-CONNECTION-STRING]"; 
    private const string ToastNotificationBodyTemplate = "<wp:Notification xmlns:wp=\"WPNotification\">" + 
                               "<wp:Toast>*" + 
                               "<wp:Text1>$(title)</wp:Text1>" + 
                               "<wp:Text2>$(msg)</wp:Text2>" + 
                               "<wp:Param></wp:Param>" + 
                               "</wp:Toast>" + 
                               "</wp:Notification>"; 
    private const string LiveTileBodyTemplate = "<wp:Notification xmlns:wp=\"WPNotification\" Version=\"2.0\">" + 
                          "<wp:Tile Template=\"FlipTile\">" + 
                          "<wp:Title>$(title)</wp:Title>" + 
                          "<wp:Count>1</wp:Count>" + 
                          "<wp:BackTitle>$(title)</wp:BackTitle>" + 
                          "<wp:BackContent>$(msg)</wp:BackContent>" + 
                          "<wp:BackgroundImage>Assets/MediumTile.png</wp:BackgroundImage>" + 
                          "</wp:Tile>" + 
                          "</wp:Notification>"; 
    private const string ToastNotificationTemplateName = "ToastNotification"; 
    private const string LiveTileTemplateName = "LiveTile"; 
    #endregion 
 
    #region Private Fields 
    private bool registered; 
    #endregion 
 
    #region Public Static Properties 
    public static ObservableCollection<Product> Products; 
    public static string MessageText; 
    public static string Category; 
    public static string ProductId; 
    #endregion 
 
    #region Public Constructor 
    // Constructor 
    public MainPage() 
    { 
      InitializeComponent(); 
      Loaded += MainPage_Loaded; 
    }  
    #endregion 
 
    #region Protected Methods 
    protected override void OnNavigatedTo(NavigationEventArgs e) 
    { 
      if (string.IsNullOrWhiteSpace(MessageText)) 
      { 
        return; 
      } 
      Message.Text = MessageText; 
      MessageText = null; 
    } 
    #endregion 
     
    #region Private Methods 
    private async void GetAllProducts() 
    { 
      try 
      { 
        Message.Text = "Retrieving Products..."; 
        // This code refreshes the entries in the list view by querying the Products table. 
        // The query excludes completed Products 
        Products = await App.MobileService.InvokeApiAsync<ObservableCollection<Product>>(App.CustomApi,  
                                                                                         HttpMethod.Get,  
                                                                                         null); 
        Message.Text = Products.Count == 0 ?  
                       "No product retrieved" :  
                       string.Format("{0} product(s) successfully retrieved.", Products.Count); 
      } 
      catch (MobileServiceInvalidOperationException ex) 
      { 
        Message.Text = ex.Message; 
      } 
      ProductList.ItemsSource = Products; 
    } 
 
    private async void GetProductById() 
    { 
      try 
      { 
        Message.Text = "Retrieving Products..."; 
        // This code refreshes the entries in the list view by querying the Products table. 
        // The query excludes completed Products 
        Products = await App.MobileService.InvokeApiAsync<ObservableCollection<Product>>(App.CustomApi,  
                                                                                         HttpMethod.Get,  
                                                  new Dictionary<string, string> { { "id", ProductId } }); 
        Message.Text = Products.Count == 0 ?  
                        "No product retrieved" :  
                        string.Format("{0} product(s) successfully retrieved.", Products.Count); 
      } 
      catch (MobileServiceInvalidOperationException ex) 
      { 
        Message.Text = ex.Message; 
      } 
      ProductList.ItemsSource = Products; 
    } 
 
    private async void GetProductsByCategory() 
    { 
      try 
      { 
        Message.Text = "Retrieving Products..."; 
        // This code refreshes the entries in the list view by querying the Products table. 
        // The query excludes completed Products 
        Products = await App.MobileService.InvokeApiAsync<ObservableCollection<Product>>(App.CustomApi,  
                                                                                         HttpMethod.Get,  
                                             new Dictionary<string, string> { { "category", Category } }); 
        Message.Text = Products.Count == 0 ?  
                       "No product retrieved" :  
                       string.Format("{0} product(s) successfully retrieved.", Products.Count); 
      } 
      catch (MobileServiceInvalidOperationException ex) 
      { 
        Message.Text = ex.Message; 
      } 
      ProductList.ItemsSource = Products; 
    } 
 
    void MainPage_Loaded(object sender, RoutedEventArgs e) 
    { 
      if (!registered) 
      { 
        registered = true; 
        RegisterChannel(); 
      } 
      if (!string.IsNullOrWhiteSpace(ProductId)) 
      { 
        GetProductById(); 
        ProductId = null; 
        return; 
      } 
      if (!string.IsNullOrWhiteSpace(Category)) 
      { 
        GetProductsByCategory(); 
        Category = null; 
        return; 
      } 
      if (Products == null) 
      { 
        GetAllProducts(); 
      } 
    } 
 
    private void New_Click(object sender, EventArgs e) 
    { 
      NavigationService.Navigate(new Uri("/NewProductPage.xaml", UriKind.Relative)); 
    } 
 
    private void Refresh_Click(object sender, EventArgs e) 
    { 
      GetAllProducts(); 
    } 
 
    private void ProductList_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) 
    { 
      if (ProductList.SelectedItem == null || !(ProductList.SelectedItem is Product)) 
      { 
        return; 
      } 
      var product = ProductList.SelectedItem as Product; 
      var index = Products.IndexOf(product); 
      NavigationService.Navigate(new Uri(string.Format("/ProductPage.xaml?selectedProduct={0}",  
                                                       index),  
                                         UriKind.Relative)); 
      ProductList.SelectedItem = null; 
    } 
 
    private void Search_Click(object sender, EventArgs e) 
    { 
      NavigationService.Navigate(new Uri("/SearchPage.xaml", UriKind.Relative)); 
    } 
 
    private void Settings_Click(object sender, EventArgs e) 
    { 
      NavigationService.Navigate(new Uri("/SettingsPage.xaml", UriKind.Relative)); 
    } 
 
    private void Info_Click(object sender, EventArgs e) 
    { 
      NavigationService.Navigate(new Uri("/InfoPage.xaml", UriKind.Relative)); 
    } 
 
    private void RegisterChannelWithNotificationHub(string channelUri) 
    { 
      Dispatcher.BeginInvoke(async () => 
      { 
        try 
        { 
          Message.Text = "Registering with notification hub..."; 
          var hub = new NotificationHub(NotificationHubName, ConnectionString); 
          await hub.RegisterTemplateAsync(channelUri,  
                          ToastNotificationBodyTemplate, 
                          ToastNotificationTemplateName, 
                          new []{"productservice"}); 
          await hub.RegisterTemplateAsync(channelUri, 
                          LiveTileBodyTemplate, 
                          LiveTileTemplateName, 
                          new[] { "productservice" }); 
        } 
        catch (Exception) 
        { 
          Message.Text = "Unable to receive push notifications"; 
        } 
      }); 
    } 
 
    private void PushChannel_ChannelUriUpdated(object sender, NotificationChannelUriEventArgs e) 
    { 
      var channelUri = (HttpNotificationChannel)sender; 
      channelUri.BindToShellToast(); 
      channelUri.BindToShellTile(); 
      RegisterChannelWithNotificationHub(channelUri.ChannelUri.ToString()); 
    } 
 
    private void RegisterChannel() 
    { 
      Message.Text = "Opening notification channel..."; 
      var channel = HttpNotificationChannel.Find("MyPushChannel"); 
      if (channel == null) 
      { 
        channel = new HttpNotificationChannel("MyPushChannel"); 
        channel.ChannelUriUpdated += PushChannel_ChannelUriUpdated; 
        channel.ErrorOccurred += (o, args) => Dispatcher.BeginInvoke(() => 
        { 
          Message.Text = args.Message; 
        }); 
 
        // Register for this notification only if you need to receive the notifications 
        // while your application is running. 
        channel.ShellToastNotificationReceived += (o, args) => Dispatcher.BeginInvoke(() => 
        { 
          if (!args.Collection.Any()) 
          { 
            return; 
          } 
          var builder = new StringBuilder(); 
          foreach (var item in args.Collection) 
          { 
            builder.AppendLine(item.Value); 
          } 
          Message.Text = builder.ToString(); 
        }); 
        channel.Open(); 
      } 
      else 
      { 
        RegisterChannelWithNotificationHub(channel.ChannelUri.ToString()); 
      } 
      Message.Text = "Notification channel opened"; 
    } 
    #endregion 
  } 
}
 
NOTE: make sure to replace the following placeholders in the App.xaml.cs file:
  • [YOUR-MOBILE-SERVICE-URL]: specify the URL of your mobile service.
  • [YOUR-MOBILE-SERVICE-APPLICATION-KEY]: specify the application key of your mobile service.
  • [YOUR-NOTIFICATION-HUB-NAME]: specify the name of the notification hub used to send push notifications.
  • [YOUR-NOTIFICATION-HUB-CONNECTION-STRING]: specify the DefaultFullSharedAccessSignature connection string of the notification hub used to send push notifications. 

The followin table contains the code of the ProductPage.xaml.cs file. This page can be used to read, delete, update, navigate data of products. For brevity, I omit the code of the other pages of the application.

#region Using Directivesusing System.Collections.Generic; 
using System.Globalization; 
using System.Net.Http; 
using System.Windows.Controls; 
using System.Windows.Input; 
using System.Windows.Navigation; 
using Microsoft.WindowsAzure.MobileServices; 
#endregionnamespace ProductWindowsPhoneApp 
{ 
  public partial class ProductPage 
  { 
    #region Public Constructorprivateint index; 
    #endregion 
 
    #region Public Constructorpublic ProductPage() 
    { 
      InitializeComponent(); 
    }  
    #endregion 
 
    #region Protected Methodsprotectedoverridevoid OnNavigatedTo(NavigationEventArgs e) 
    { 
      string selectedProduct; 
      if (!NavigationContext.QueryString.TryGetValue("selectedProduct", out selectedProduct)) 
      { 
        return; 
      } 
      if (!int.TryParse(selectedProduct, out index)) 
      { 
        return; 
      } 
      GetProduct(); 
    }  
    #endregion 
 
    #region Private Methodsprivate async void Save_Click(object sender, System.EventArgs e) 
    { 
      var focusObj = FocusManager.GetFocusedElement(); 
      if (focusObj is TextBox) 
      { 
        var binding = (focusObj as TextBox).GetBindingExpression(TextBox.TextProperty); 
        binding.UpdateSource(); 
      } 
      var product = MainPage.Products[index]; 
      if (product == null) 
      { 
        return; 
      } 
      if (string.IsNullOrWhiteSpace(product.Name)) 
      { 
        Message.Text = "Name cannot be null"; 
        return; 
      } 
      if (string.IsNullOrWhiteSpace(product.Category)) 
      { 
        Message.Text = "Category cannot be null"; 
        return; 
      } 
      try 
      { 
        Message.Text = string.Format("Updating {0}...", product.Name); 
        await App.MobileService.InvokeApiAsync<Product, Product>(App.CustomApi,  
                                                                 product,  
                                                                 HttpMethod.Put,  
                                                                 null); 
        Message.Text = string.Format("{0} successfully updated.", product.Name); 
      } 
      catch (MobileServiceInvalidOperationException ex) 
      { 
        Message.Text = ex.Message; 
      } 
    } 
 
    private async void Delete_Click(object sender, System.EventArgs e) 
    { 
      var product = MainPage.Products[index]; 
      if (product == null) 
      { 
        return; 
      } 
      try 
      { 
        Message.Text = string.Format("Deleting {0}...", product.Name); 
        await App.MobileService.InvokeApiAsync(App.CustomApi, HttpMethod.Delete,  
        new Dictionary<string, string> { { "id",  
                                           product.ProductId.ToString(CultureInfo.InvariantCulture) } }); 
        Message.Text = string.Format("{0} successfully deleted.", product.Name); 
        MainPage.Products.Remove(product); 
        if (MainPage.Products.Count == 0) 
        { 
          NavigationService.GoBack(); 
          return; 
        } 
        if (index >= MainPage.Products.Count) 
        { 
          index = MainPage.Products.Count - 1; 
        } 
        GetProduct(); 
      } 
      catch (MobileServiceInvalidOperationException ex) 
      { 
        Message.Text = ex.Message; 
      } 
    } 
 
    privatevoid Back_Click(object sender, System.EventArgs e) 
    { 
      index = index == 0 ? MainPage.Products.Count - 1 : index - 1; 
      GetProduct(); 
    } 
 
    privatevoid Next_Click(object sender, System.EventArgs e) 
    { 
      index = index == MainPage.Products.Count - 1 ? 0 : index + 1; 
      GetProduct(); 
    }  
 
    privatevoid GetProduct() 
    { 
      var product = MainPage.Products[index]; 
      ProductId.DataContext = product; 
      Name.DataContext = product; 
      Category.DataContext = product; 
      Price.DataContext = product; 
    } 
    #endregion 
  } 
}
NOTE: this Windows Phone 8 app can be used also with the following two mobile services as they share the same interface. It's sufficient to change the Custom API to the Settings page.

When a new item is added to the ProductDb, the mobile service sends a notification. The Windows Phone 8 app registers two templates with the notification hub, one to receive toast notification and one to receive live tiles, as shown in the picture below.

Conclusions

Mobile services can easily be extended to get advantage of the services provided by Windows Azure. In particular, this solution shows how to integrate Windows Azure Mobile Service with line of business applications, running on-premises or in the cloud, via Windows Azure BizTalk Services and Service Bus Relayed Messaging. Besides, this solution shows how to use Access Control Service to authenticate Mobile Services against an XML Request-Reply Bridge. You can download the code from MSDN Code Gallery. See also the following articles on Windows Azure Mobile Services: