Introduction

This sample demonstrates how to integrate Windows Azure Mobile Service with a line of business application running on-premises via Service Bus Relayed Messaging and REST protocol. The Access Control Service is used to authenticate Mobile Services against the underlying application. You can download the code from MSDN Code Gallery.

Scenario

A mobile service receives CRUD operations from an HTML5/JavaScript and Windows Store app, but instead of accessing data from a SQL Azure database, it invokes a LOB application running in a corporate data center. The LOB system uses WCF service layer to expose its functionality via SOAP to external applications. In particular, the WCF service uses a WebHttpRelayBinding endpoint to expose its functionality via a REST Service Bus Relay Service.  The endpoint is configured to authenticate incoming calls using a relay access token issued ACS. The WCF service accesses data from the ProductDb database hosted by a local instance of SQL Server 2012. In particular, the WCF services uses the new asynchronous programming feature provided by ADO.NET 4.5 to access data from the underlying database.
NOTE: Look at How to integrate a Mobile Service with a SOAP Service Bus Relay Service to see a custom API can be used to invoke a WCF service that uses a BasicHttpRelayBinding endpoint to expose its functionality via a SOAP Service Bus Relay Service.

Architecture

The following diagram shows the architecture of the solution.
ArchitectureRest
Message Flow
  1. The HTML5/JavaScript site or Windows Store app sends a request to a user-defined custom API of a Windows Azure Mobile Service via HTTPS. 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 Store app uses 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 Serviceto acquire a security token necessary to be authenticated by the underlying Service Bus Relay Service. 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 within the Access Control namespace of the Service Bus Relay Service (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 http base address of the Service Bus Relay Service (e.g. http://paolosalvatori.servicebus.windows.net/)
  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 extracts the wrap_access_token from the security token issued by ACS. 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. This method calls the GetProduct operation exposed by the underlying WCF service using the GET method.
    • getProducts: this function is invoked when the HTTP method is equal to GET and the querystring does not contain any parameter. This method calls the GetProducts operation exposed by the underlying WCF service using the GET method.
    • getProductsByCategory: this function is invoked when the HTTP method is equal to GET and the querystring contains a category parameter. This method calls the GetProductsByCategory operation exposed by the underlying WCF service using the GET method.
    • addProduct: this function is invoked when the HTTP method is equal to POST and the request body contains a new product in JSON format. This method calls the AddProduct operation exposed by the underlying WCF service using the POST method.
    • 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. This method calls the UpdateProduct operation exposed by the underlying WCF service using the PATCH method.
    • deleteProduct: this function is invoked when the HTTP method is equal to DELETE and the querystring contains a productid or id parameter. This method calls the DeleteProduct operation exposed by the underlying WCF service using the DELETE method.
  5. The Service Bus Relay Service validates and remove the security token, then forwards the request to one the listeners hosting the WCF service.
  6. The WCF service uses the new asynchronous programming feature provided by ADO.NET 4.5 to access data from the ProductDb database. For demo purpose, the WCF service runs in a console application, but the sample can easily be modified to host the service in IIS.
  7. The WCF service returns a response message to the Service Bus Relay Service.
  8. The Service Bus Relay Service forwards the message to the mobile service which in turn creates a response message in JSON object.
  9. The mobile service 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 Service Bus Relay Service using the SOAP protocol.

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.

 

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 products in the API NAME field. Select Anybody with the Application Key permission for all the HTTP methods and then click the check button. CreateCustomRestAPI
    This creates the new custom API.
  4. Click the new restproducts entry in the API table.
  5. Click the Scripts tab and replace the existing code with the following:
var namespace = '[YOUR-SERVICE-BUS-NAMESPACE]';
var issuerName = '[YOUR-ISSUER-NAME]';
var issuerSecret = '[YOUR-ISSUER-SECRET]';
var https = require('https');
var qs = require('querystring');
var uuid = require('node-uuid');
var util = require('util');
        
exports.get = function(request, response) {
    if (request.query.productid) {
        getWrapAccessToken(response, function(token) {
            getProduct(token, request.query.productid, request, response);
        });
    }
    if (request.query.id) {
        getWrapAccessToken(response, function(token) {
            getProduct(token, request.query.id, request, response);
        });
    }
    else if (request.query.category) {
        getWrapAccessToken(response, function(token) {
            getProductsByCategory(token, request.query.category, request, response);
        });
    }
    else {
        getWrapAccessToken(response, function(token) {
            getProducts(token, request, response);
        });
    }
};

exports.post = function(request, response) {
    getWrapAccessToken(response, function(token) {
            addProduct(token, request, response);
    });
}

exports.patch = function(request, response) {
    getWrapAccessToken(response, function(token) {
            updateProduct(token, request, response);
    });
}

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

exports.delete = function(request, response) {
    if (request.query.productid) {
        getWrapAccessToken(response, function(token) {
            deleteProduct(token, request.query.productid, request, response);
        });
    } else if (request.query.id) {
        getWrapAccessToken(response, function(token) {
            deleteProduct(token, request.query.id, request, response);
        });
    }
}

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

    var values = {
        wrap_name: issuerName,
        wrap_password: issuerSecret,
        wrap_scope: 'http://' + namespace + '.servicebus.windows.net/'
    };

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

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

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

function getProducts(token, request, response) {
    var headers = {
        'Authorization': token
    };
    
    var options = {
        host: namespace + '.servicebus.windows.net',
        path: '/products/webhttp/getproducts',
        headers: headers,
        method: 'GET'
    };

    console.log('[getProducts]: options: %j', options);

    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 body = data.toString('utf8');
            if (body) {
                console.log('[getProducts]: response body: ', body);
            }
            if (res.statusCode == 200) {
                response.send(200, JSON.parse(body));
            }
            else {
                response.send(400, '[getProducts]: An error occurred while invoking the downstream service.');
            }
        });
    });
    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) {
    if (category) {
        var headers = {
            'Authorization': token
        };
        
        var options = {
            host: namespace + '.servicebus.windows.net',
            path: '/products/webhttp/getproducts?category=' + category,
            headers: headers,
            method: 'GET'
        };

        console.log('[getProductsByCategory]: options: %j\r\ncategory: %s', options, category);

        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 body = data.toString('utf8');
                if (body) {
                    console.log('[getProductsByCategory]: response body: ', body);
                }
                if (res.statusCode == 200) {
                    response.send(200, JSON.parse(body));
                }
                else {
                    response.send(400, '[getProductsByCategory]: An error occurred while invoking the downstream service.');
                }
            });
        });
        req.end();

        req.on('error', function (e) {
            console.error('[getProductsByCategory]: ', e);
            response.send(400, util.format('[getProductsByCategory]: %j', e));
        });
    }
    else {
        var message = '[getProductsByCategory]: The category parameter cannot be null.';
        console.error(message);
        response.send(400, message);
    }
}

function getProduct(token, productId, request, response) {
    if (productId) {
        var headers = {
            'Authorization': token
        };
        
        var options = {
            host: namespace + '.servicebus.windows.net',
            path: '/products/webhttp/getproduct?id=' + productId,
            headers: headers,
            method: 'GET'
        };

        console.log('[getProduct]: options: %j\r\nproductId: %s', options, productId);

        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 body = data.toString('utf8');
                if (body) {
                    console.log('[getProduct]: response body: ', body);
                }
                if (res.statusCode == 200) {
                    response.send(200, JSON.parse(body));
                }
                else {
                    response.send(400, '[getProduct]: An error occurred while invoking the downstream service.');
                }
            });
        });
        req.end();

        req.on('error', function (e) {
            console.error('[getProduct]: ', e);
            response.send(400, util.format('[getProduct]: %j', e));
        });
    }
    else {
        var message = '[getProduct]: The id or productid parameter cannot be null.';
        console.error(message);
        response.send(400, message);
    }
}

function addProduct(token, request, response) {
    if (request.body.hasOwnProperty('name') &&
        request.body.hasOwnProperty('category') &&
        request.body.hasOwnProperty('price')) {
        var product = {
            productId: 0,
            name: request.body.name,
            category: request.body.category,
            price: request.body.price
        }
        
        var headers = {
            'Authorization': token,
            'Content-Type': 'application/json'
        };
        
        var options = {
            host: namespace + '.servicebus.windows.net',
            path: '/products/webhttp/addproduct',
            headers: headers,
            method: 'POST'
        };

        console.log('[addProduct]: options: %j\r\nproduct: %j', options, product);

        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 body = data.toString('utf8');
                if (body) {
                    console.log('[addProduct]: response body: ', body);
                }
                if (res.statusCode == 200 ||
                    res.statusCode == 201 ||
                    res.statusCode == 202) {
                    response.send(200, JSON.parse(body));
                }
                else {
                    response.send(400, '[addProduct]: An error occurred while invoking the downstream service.');
                }
            });
        });
        req.write(JSON.stringify(product));
        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 = {
            productId: request.body.productId,
            name: request.body.name,
            category: request.body.category,
            price: request.body.price
        }
        
        var headers = {
            'Authorization': token,
            'Content-Type': 'application/json'
        };
        
        var options = {
            host: namespace + '.servicebus.windows.net',
            path: '/products/webhttp/updateproduct',
            headers: headers,
            method: 'PATCH'
        };

        console.log('[updateProduct]: options: %j\r\nproduct: %j', options, product);

        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 body = data.toString('utf8');
                if (body) {
                    console.log('[updateProduct]: response body: ', body);
                }
                if (res.statusCode == 200 ||
                    res.statusCode == 201 ||
                    res.statusCode == 202) {
                    response.send(200, JSON.parse(body));
                }
                else {
                    response.send(400, '[updateProduct]: An error occurred while invoking the downstream service.');
                }
            });
        });
        req.write(JSON.stringify(product));
        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) {
    if (productId) {
        var headers = {
            'Authorization': token,
            'Content-Length': 0
        };
        
        var options = {
            host: namespace + '.servicebus.windows.net',
            path: '/products/webhttp/deleteproduct?id=' + productId,
            headers: headers,
            method: 'DELETE'
        };

        console.log('[deleteProduct]: options: %j\r\nproductId: %s', options, productId);

        var req = https.request(options, function (res) {
            console.log('[deleteProduct]: statusCode: %d\n\rheaders: %j', res.statusCode, res.headers);
            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.end();

        req.on('error', function (e) {
            console.error('[deleteProduct]: ', e);
            response.send(400, util.format('[deleteProduct]: %j', e));
        });
    }
    else {
        var message = '[deleteProduct]: The id or productid parameter cannot be null.';
        console.error(message);
        response.send(400, message);
    }
}
NOTE: make sure to replace the following placeholders in the upper part od the code before saving the script:
  • [YOUR-SERVICE-BUS-NAMESPACE]: specify the name of your Windows Azure Service Bus namespace.
  • [YOUR-ISSUER-NAME]: specify the name of a service identity used to access the Service Bus Relay Service (e.g. owner)
  • [YOUR-ISSUER-SECRET]: specify the password for the selected service identity.

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 restproducts.js 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.
  • Contracts: contains the service and data contracts used by the WCF service.
  • Service: contains the WCF service that represents the line of business application invoked by the mobile service. The service runs within a console application, but the project can be easily changed to host the service in IIS.
  • Client: contains a console application that you can use to test the 3 endpoints exposed by the WCF Service Bus Relay Service.
  • HTML5: contains the HTML5/JavaScript client for 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.

SQL Server Database

Run the ProductDb.sql script to create the database used by the solution and some sample data:
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
      
     

Contracts

The following table contains the code of the IProductService interface that represents the service contract implemented by the service.
#region Using Directives
using System.Collections.Generic;
using System.ServiceModel;
using System.ServiceModel.Web;
#endregion

namespace Contracts
{
    [ServiceContract(Namespace = "http://windowsazure.cat.microsoft.com/samples/servicebus")]
    public interface IProductService
    {
        [WebGet(UriTemplate = "getproduct?id={productId}", BodyStyle = WebMessageBodyStyle.Bare, 
                ResponseFormat = WebMessageFormat.Json)]
        [OperationContract(Action = "getProduct", ReplyAction = "getProductResponse")]
        [return: MessageParameter(Name = "product")]
        Product GetProduct([MessageParameter(Name = "productId")]int productId);

        [WebGet(UriTemplate = "getproducts?category={category}", BodyStyle = WebMessageBodyStyle.Bare, 
                ResponseFormat = WebMessageFormat.Json)]
        [OperationContract(Action = "getProductsByCategory", ReplyAction = "getProductsByCategoryResponse")]
        [return: MessageParameter(Name = "products")]
        List<Product> GetProductsByCategory([MessageParameter(Name = "category")]string category);

        [WebGet(UriTemplate = "getproducts", BodyStyle = WebMessageBodyStyle.Bare, 
                ResponseFormat = WebMessageFormat.Json)]
        [OperationContract(Action = "getProducts", ReplyAction = "getProductsResponse")]
        [return: MessageParameter(Name = "products")]
        List<Product> GetProducts();

        [WebInvoke(Method = "DELETE", UriTemplate = "deleteproduct?id={productId}")]
        [OperationContract(Action = "deleteProduct", ReplyAction = "deleteProductResponse")]
        void DeleteProduct([MessageParameter(Name = "productId")]int productId);

        [WebInvoke(Method = "POST", UriTemplate = "addproduct", RequestFormat = WebMessageFormat.Json, 
                   ResponseFormat = WebMessageFormat.Json)]
        [OperationContract(Action = "addProduct", ReplyAction = "addProductResponse")]
        [return: MessageParameter(Name = "product")]
        Product AddProduct([MessageParameter(Name = "product")]Product product);

        [WebInvoke(Method = "PATCH", UriTemplate = "updateproduct", RequestFormat = WebMessageFormat.Json, 
                   ResponseFormat = WebMessageFormat.Json)]
        [OperationContract(Action = "updateProduct", ReplyAction = "updateProductResponse")]
        [return: MessageParameter(Name = "product")]
        Product UpdateProduct([MessageParameter(Name="product")]Product product);
    }
}
The following table contains the code of the Product class:
#region Using Directives
using System.Runtime.Serialization;
#endregion

namespace Contracts
{
    [DataContract(Name = "product", Namespace = "http://windowsazure.cat.microsoft.com/samples/servicebus")]
    public class Product
    {
        #region Public Properties
        [DataMember(Name = "productId", Order = 1)]
        public int ProductId { get; set; }
        [DataMember(Name = "name", Order = 2)]
        public string Name { get; set; }
        [DataMember(Name = "category", Order = 3)]
        public string Category { get; set; }
        [DataMember(Name = "price", Order = 4)]
        public decimal Price { get; set; }
        #endregion
    }
}

Service

The service project contains the code of the ProductService invoked by the mobile service.
The following table contains the code of the ProductService class.
#region Using Directives
using System.Collections.Generic;
using System.ServiceModel;
using Contracts;
#endregion

namespace Service
{
    [ServiceBehavior(Namespace = "http://windowsazure.cat.microsoft.com/samples/servicebus")]
    public class ProductService : IProductService
    {
       #region Public Methods
        public  Product GetProduct(int id)
        {
            return ProductRepository.GetProduct(id).Result;
        }
        
        public List<Product> GetProductsByCategory(string category)
        {
            return ProductRepository.GetProductsByCategory(category).Result;
        }

        public List<Product> GetProducts()
        {
            return ProductRepository.GetProducts().Result;
        }

        public void DeleteProduct(int id)
        {
            ProductRepository.DeleteProduct(id).Wait();
        }

        public Product AddProduct(Product product)
        {
            return ProductRepository.AddProduct(product).Result;
        }

        public Product UpdateProduct(Product product)
        {
            return ProductRepository.UpdateProduct(product).Result;
        }
       #endregion
    }
}
The following table contains the code of the ProductRepository class. This class uses the asynchronous programming functionality provided by ADO.NET 4.5 to access data from the ProductDb.
#region Using Directives
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Threading.Tasks;
using Contracts;
#endregion

namespace Service
{
    public static class ProductRepository
    {
        #region Private Static Fields
        private static readonly string line = new string('-', 99);
        private static readonly string connectionString;
        #endregion

        #region Static Constructor
        static ProductRepository()
        {
            var setting = ConfigurationManager.ConnectionStrings["ProductDb"];
            if (setting != null)
            {
                connectionString = setting.ConnectionString;
            }
        }
        #endregion

        #region Public Methods
        public static async Task<Product> GetProduct(int id)
        {
            try
            {
                Trace.WriteLine(string.Format("GetProduct: Id=[{0}]", id));
                Trace.WriteLine(line);
                using (var connection = new SqlConnection(connectionString))
                {
                    await connection.OpenAsync();
                    using (var command = new SqlCommand("GetProduct", connection))
                    {
                        command.CommandType = CommandType.StoredProcedure;
                        command.CommandTimeout = 60;
                        command.Parameters.Add(new SqlParameter
                            {
                                ParameterName = "@ProductID",
                                Direction = ParameterDirection.Input,
                                SqlDbType = SqlDbType.Int,
                                Value = id
                            });
                        var reader = await command.ExecuteReaderAsync();
                        while (await reader.ReadAsync())
                        {
                            return new Product
                            {
                                ProductId = await reader.GetFieldValueAsync<int>(0),
                        Name = await reader.IsDBNullAsync(1) ? string.Empty : await reader.GetFieldValueAsync<string>(1),
                        Category = await reader.IsDBNullAsync(2) ? string.Empty : await reader.GetFieldValueAsync<string>(2),
                                Price = await reader.IsDBNullAsync(3) ? 0 : await reader.GetFieldValueAsync<decimal>(3)
                            };
                        }
                    }
                }
                return null;
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                throw;
            }
        }

        public static async Task<List<Product>> GetProductsByCategory(string category)
        {
            try
            {
                if (string.IsNullOrEmpty(category))
                {
                    throw new ArgumentException("The category paramater cannot be null or empty.");
                }
                Trace.WriteLine(string.Format("GetProductsByCategory: Category=[{0}]", category));
                Trace.WriteLine(line);
                using (var connection = new SqlConnection(connectionString))
                {
                    await connection.OpenAsync();
                    using (var command = new SqlCommand("GetProductsByCategory", connection))
                    {
                        command.CommandType = CommandType.StoredProcedure;
                        command.CommandTimeout = 60;
                        command.Parameters.Add(new SqlParameter
                        {
                            ParameterName = "@Category",
                            Direction = ParameterDirection.Input,
                            SqlDbType = SqlDbType.NVarChar,
                            Size = 50,
                            Value = category
                        });
                        var reader = await command.ExecuteReaderAsync();
                        var productList = new List<Product>();
                        while (await reader.ReadAsync())
                        {
                            productList.Add(new Product
                            {
                                ProductId = await reader.GetFieldValueAsync<int>(0),
                        Name = await reader.IsDBNullAsync(1) ? string.Empty : await reader.GetFieldValueAsync<string>(1),
                        Category = await reader.IsDBNullAsync(2) ? string.Empty : await reader.GetFieldValueAsync<string>(2),
                                Price = await reader.IsDBNullAsync(3) ? 0 : await reader.GetFieldValueAsync<decimal>(3)
                            });
                        }
                        return productList;
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                throw;
            }
        }

        public static async Task<List<Product>> GetProducts()
        {
            try
            {
                Trace.WriteLine("GetProducts");
                Trace.WriteLine(line);
                using (var connection = new SqlConnection(connectionString))
                {
                    await connection.OpenAsync();
                    using (var command = new SqlCommand("GetProducts", connection))
                    {
                        command.CommandType = CommandType.StoredProcedure;
                        command.CommandTimeout = 60;
                        var reader = await command.ExecuteReaderAsync();
                        var productList = new List<Product>();
                        while (await reader.ReadAsync())
                        {
                            productList.Add(new Product
                            {
                                ProductId = await reader.GetFieldValueAsync<int>(0),
                        Name = await reader.IsDBNullAsync(1) ? string.Empty : await reader.GetFieldValueAsync<string>(1),
                        Category = await reader.IsDBNullAsync(2) ? string.Empty : await reader.GetFieldValueAsync<string>(2),
                                Price = await reader.IsDBNullAsync(3) ? 0 : await reader.GetFieldValueAsync<decimal>(3)
                            });
                        }
                        return productList;
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                throw;
            }
        }

        public static async Task<int> DeleteProduct(int id)
        {
            try
            {
                Trace.WriteLine(string.Format("DeleteProduct: Id=[{0}]", id));
                Trace.WriteLine(line);
                using (var connection = new SqlConnection(connectionString))
                {
                    await connection.OpenAsync();
                    using (var command = new SqlCommand("DeleteProduct", connection))
                    {
                        command.CommandType = CommandType.StoredProcedure;
                        command.CommandTimeout = 60;
                        command.Parameters.Add(new SqlParameter
                        {
                            ParameterName = "@ProductID",
                            Direction = ParameterDirection.Input,
                            SqlDbType = SqlDbType.Int,
                            Value = id
                        });
                        return await command.ExecuteNonQueryAsync();
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                throw;
            }
        }

        public static async Task<Product> AddProduct(Product product)
        {
            try
            {
                if (product == null)
                {
                    throw new ArgumentException("The product paramater cannot be null.");
                }
                Trace.WriteLine(string.Format("AddProduct: Name=[{0}] Category=[{1}] Price=[{2}]", 
                                             product.Name,
                                             product.Category,
                                             product.Price));
                Trace.WriteLine(line);
                using (var connection = new SqlConnection(connectionString))
                {
                    await connection.OpenAsync();
                    using (var command = new SqlCommand("AddProduct", connection))
                    {
                        command.CommandType = CommandType.StoredProcedure;
                        command.CommandTimeout = 60;
                        var productIdParam = new SqlParameter
                            {
                                ParameterName = "@ProductID",
                                Direction = ParameterDirection.Output,
                                SqlDbType = SqlDbType.Int
                            };
                        command.Parameters.Add(productIdParam);
                        command.Parameters.Add(new SqlParameter
                        {
                            ParameterName = "@Name",
                            Direction = ParameterDirection.Input,
                            SqlDbType = SqlDbType.NVarChar,
                            Size = 50,
                            Value = product.Name
                        });
                        command.Parameters.Add(new SqlParameter
                        {
                            ParameterName = "@Category",
                            Direction = ParameterDirection.Input,
                            SqlDbType = SqlDbType.NVarChar,
                            Size = 50,
                            Value = product.Category
                        });
                        command.Parameters.Add(new SqlParameter
                        {
                            ParameterName = "@Price",
                            Direction = ParameterDirection.Input,
                            SqlDbType = SqlDbType.SmallMoney,
                            Value = product.Price
                        });
                        await command.ExecuteNonQueryAsync();
                        product.ProductId = (int)productIdParam.Value;
                        return product;
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                throw;
            }
        }

        public static async Task<Product> UpdateProduct(Product product)
        {
            try
            {
                if (product == null)
                {
                    throw new ArgumentException("The product paramater cannot be null.");
                }
                Trace.WriteLine(string.Format("UpdateProduct: Id=[{0}] Name=[{1}] Category=[{2}] Price=[{3}]",
                                             product.ProductId,
                                             product.Name,
                                             product.Category,
                                             product.Price));
                Trace.WriteLine(line);
                using (var connection = new SqlConnection(connectionString))
                {
                    await connection.OpenAsync();
                    using (var command = new SqlCommand("UpdateProduct", connection))
                    {
                        command.CommandType = CommandType.StoredProcedure;
                        command.CommandTimeout = 60;
                        command.Parameters.Add(new SqlParameter
                        {
                            ParameterName = "@ProductID",
                            Direction = ParameterDirection.Input,
                            SqlDbType = SqlDbType.Int,
                            Value = product.ProductId
                        });
                        command.Parameters.Add(new SqlParameter
                        {
                            ParameterName = "@Name",
                            Direction = ParameterDirection.Input,
                            SqlDbType = SqlDbType.NVarChar,
                            Size = 50,
                            Value = product.Name
                        });
                        command.Parameters.Add(new SqlParameter
                        {
                            ParameterName = "@Category",
                            Direction = ParameterDirection.Input,
                            SqlDbType = SqlDbType.NVarChar,
                            Size = 50,
                            Value = product.Category
                        });
                        command.Parameters.Add(new SqlParameter
                        {
                            ParameterName = "@Price",
                            Direction = ParameterDirection.Input,
                            SqlDbType = SqlDbType.SmallMoney,
                            Value = product.Price
                        });
                        await command.ExecuteNonQueryAsync();
                        return product;
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                throw;
            }
        }
        #endregion
    }
}
      
     
Finally, the following table contains the configuration file of the service. As you can easily notice, the service exposes three different endpoints that in turn use three different bindings:
RelayServiceEndpoints
The mobile service included in this sample invokes only the WebHttpRelayBinding.
<?xml version="1.0"?>
<configuration>
  <connectionStrings>
    <add name="ProductDb" 
connectionString="Server=(local);Database=ProductDb;Integrated Security=true;Async=true;Connection Timeout=30" />
  </connectionStrings>
  <system.diagnostics>
    <trace autoflush="false" indentsize="4">
      <listeners>
        <add name="configConsoleListener"
          type="System.Diagnostics.ConsoleTraceListener" />
      </listeners>
    </trace>
    <sources>
      <source name="System.ServiceModel.MessageLogging" switchValue="Warning, ActivityTracing">
        <listeners>
          <add type="System.Diagnostics.DefaultTraceListener" name="Default">
            <filter type=""/>
          </add>
          <add name="ServiceModelMessageLoggingListener">
            <filter type=""/>
          </add>
        </listeners>
      </source>
    </sources>
    <sharedListeners>
      <add initializeData="C:\Service.svclog" 
     type="System.Diagnostics.XmlWriterTraceListener, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" 
           name="ServiceModelMessageLoggingListener" 
           traceOutputOptions="Timestamp">
        <filter type=""/>
      </add>
    </sharedListeners>
  </system.diagnostics>
  <system.serviceModel>

    <!-- Diagnostics -->
    <diagnostics performanceCounters="All">
      <messageLogging logEntireMessage="false" 
                      logMalformedMessages="false" 
                      logMessagesAtServiceLevel="false" 
                      logMessagesAtTransportLevel="false"/>
    </diagnostics>
    <!-- Binding Configuration -->
    <bindings>
      <netTcpRelayBinding>
        <binding name="netTcpRelayBinding">
          <security mode="Transport" relayClientAuthenticationType="RelayAccessToken"/>
        </binding>
      </netTcpRelayBinding>
      <basicHttpRelayBinding>
        <binding name="basicHttpRelayBinding">
          <security mode="Transport" relayClientAuthenticationType="RelayAccessToken"/>
        </binding>
      </basicHttpRelayBinding>
      <webHttpRelayBinding>
        <binding name="webHttpRelayBinding">
          <security mode="Transport" relayClientAuthenticationType="RelayAccessToken"/>
        </binding>
      </webHttpRelayBinding>
    </bindings>
    <!-- Behavior Configuration -->
    <behaviors>
      <serviceBehaviors>
        <behavior name="serviceBehavior">
          <serviceDebug includeExceptionDetailInFaults="true"/>
        </behavior>
      </serviceBehaviors>
      <endpointBehaviors>
        <behavior name="serviceBusEndpointBehavior">
          <transportClientEndpointBehavior>
            <tokenProvider>
              <sharedSecret issuerName="owner" issuerSecret="[YOUR-ISSUER-SECRET]"/>
            </tokenProvider>
          </transportClientEndpointBehavior>
          <serviceRegistrySettings discoveryMode="Public" displayName="config"/>
        </behavior>
        <behavior name="webHttpEndpointBehavior">
          <webHttp defaultBodyStyle="Bare" defaultOutgoingResponseFormat="Json"/>
          <transportClientEndpointBehavior>
            <tokenProvider>
              <sharedSecret issuerName="owner" issuerSecret="[YOUR-ISSUER-SECRET]"/>
            </tokenProvider>
          </transportClientEndpointBehavior>
          <serviceRegistrySettings discoveryMode="Public" displayName="config"/>
        </behavior>
      </endpointBehaviors>
    </behaviors>
    <!-- Service Conbfiguration -->
    <services>
      <service name="Service.ProductService" behaviorConfiguration="serviceBehavior">
        <endpoint address="sb://[YOUR-SERVICE-BUS-NAMESPACE].servicebus.windows.net/products/nettcp" 
                  behaviorConfiguration="serviceBusEndpointBehavior" 
                  binding="netTcpRelayBinding" 
                  bindingConfiguration="netTcpRelayBinding" 
                  contract="Contracts.IProductService" 
                  name="netTcpRelayBinding"/>
        <endpoint address="https://[YOUR-SERVICE-BUS-NAMESPACE].servicebus.windows.net/products/basichttp"
                   behaviorConfiguration="serviceBusEndpointBehavior"
                   binding="basicHttpRelayBinding"
                   bindingConfiguration="basicHttpRelayBinding"
                   contract="Contracts.IProductService"
                   name="basicHttpRelayBinding"/>
        <endpoint address="https://[YOUR-SERVICE-BUS-NAMESPACE].servicebus.windows.net/products/webhttp"
                  behaviorConfiguration="webHttpEndpointBehavior"
                  binding="webHttpRelayBinding"
                  bindingConfiguration="webHttpRelayBinding"
                  contract="Contracts.IProductService"
                  name="webHttpRelayBinding"/>
      </service>
    </services>
    <extensions>
      <behaviorExtensions>
        <add name="connectionStatusBehavior" 
             type="Microsoft.ServiceBus.Configuration.ConnectionStatusElement, 
Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35"
/> <add name="transportClientEndpointBehavior" type="Microsoft.ServiceBus.Configuration.TransportClientEndpointBehaviorElement,
Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35"
/> <add name="serviceRegistrySettings" type="Microsoft.ServiceBus.Configuration.ServiceRegistrySettingsElement,
Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35"
/> </behaviorExtensions> <bindingElementExtensions> <add name="netMessagingTransport" type="Microsoft.ServiceBus.Messaging.Configuration.NetMessagingTransportExtensionElement,
Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
/> <add name="tcpRelayTransport" type="Microsoft.ServiceBus.Configuration.TcpRelayTransportElement, Microsoft.ServiceBus,
Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
/> <add name="httpRelayTransport" type="Microsoft.ServiceBus.Configuration.HttpRelayTransportElement,
Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
/> <add name="httpsRelayTransport" type="Microsoft.ServiceBus.Configuration.HttpsRelayTransportElement,
Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
/> <add name="onewayRelayTransport" type="Microsoft.ServiceBus.Configuration.RelayedOnewayTransportElement,
Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
/> </bindingElementExtensions> <bindingExtensions> <add name="basicHttpRelayBinding" type="Microsoft.ServiceBus.Configuration.BasicHttpRelayBindingCollectionElement,
Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
/> <add name="webHttpRelayBinding" type="Microsoft.ServiceBus.Configuration.WebHttpRelayBindingCollectionElement,
Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
/> <add name="ws2007HttpRelayBinding" type="Microsoft.ServiceBus.Configuration.WS2007HttpRelayBindingCollectionElement,
Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
/> <add name="netTcpRelayBinding" type="Microsoft.ServiceBus.Configuration.NetTcpRelayBindingCollectionElement,
Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
/> <add name="netOnewayRelayBinding" type="Microsoft.ServiceBus.Configuration.NetOnewayRelayBindingCollectionElement,
Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
/> <add name="netEventRelayBinding" type="Microsoft.ServiceBus.Configuration.NetEventRelayBindingCollectionElement,
Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
/> <add name="netMessagingBinding" type="Microsoft.ServiceBus.Messaging.Configuration.NetMessagingBindingCollectionElement,
Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
/> </bindingExtensions> </extensions> </system.serviceModel> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/> </startup> </configuration>

NOTE: make sure to replace the following placeholders in the app.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.

 

Client

This project contains a console application that you can use to test the three endpoints exposed by the ProductService.
The following table contains the code of the Program.cs class.
#region Using Directives
using System;
using System.Linq;
using System.Configuration;
using System.ServiceModel;
using System.ServiceModel.Configuration;
using Contracts;
#endregion

namespace Client
{
    internal class Program
    {
        static readonly string line = new string('-', 99);

        private static void Main()
        {
            try
            {
                string endpoint;
                while ((endpoint = SelectEndpoint()) != null)
                {
                    try
                    {
                        // Print status
                        Console.WriteLine(line);
                        Console.Write("Calling the service: ");
                        Console.ForegroundColor = ConsoleColor.Yellow;
                        Console.Write("Endpoint");
                        Console.ResetColor();
                        Console.WriteLine("={0} ", endpoint);
                        Console.WriteLine(line);
                        
                        var factory = new ChannelFactory<IProductService>(endpoint);
                        var channel = factory.CreateChannel();

                        // Add Product
                        Console.WriteLine("Add Product");
                        Console.WriteLine(line);
                        var product = new Product {Name="Jeero", Category = "Toys", Price = (decimal)19.99};
                        product = channel.AddProduct(product);
                        if (product == null)
                        {
                            PrintErrorMessage();
                            return;
                        }
                        PrintProduct(product);
                        Console.WriteLine(line);

                        // Update Product
                        Console.WriteLine("Uppdate Product");
                        Console.WriteLine(line);
                        product.Name = "Groody";
                        product = channel.UpdateProduct(product);
                        if (product == null)
                        {
                            PrintErrorMessage();
                            return;
                        }
                        PrintProduct(product);
                        Console.WriteLine(line);

                        // Get Product
                        Console.WriteLine("Get Product");
                        Console.WriteLine(line);
                        product = channel.GetProduct(product.ProductId);
                        if (product == null)
                        {
                            PrintErrorMessage();
                            return;
                        }
                        PrintProduct(product);
                        Console.WriteLine(line);

                        // Get List<Product> By Category
                        Console.Write("Get Products By Category: ");
                        Console.ForegroundColor = ConsoleColor.Yellow;
                        Console.Write("Category");
                        Console.ResetColor();
                        Console.WriteLine("=Toys ");
                        Console.WriteLine(line);
                        var products = channel.GetProductsByCategory("Toys");
                        foreach (var item in products)
                        {
                            PrintProduct(item);
                        }
                        Console.WriteLine(line);

                        // Delete Product
                        Console.WriteLine("Delete Product");
                        Console.WriteLine(line);
                        channel.DeleteProduct(product.ProductId);
                        PrintProductId(product);
                        Console.WriteLine(line);

                        // Get List<Product>
                        Console.WriteLine("Get Products");
                        Console.WriteLine(line);
                        products = channel.GetProducts();
                        foreach (var item in products)
                        {
                            PrintProduct(item);
                        }
                        Console.WriteLine(line);
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex.Message);
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                Console.WriteLine("Press [Enter] to exit");
                Console.ReadLine();
            }
        }

        private static string SelectEndpoint()
        {
            // Set Window Size
            Console.WindowWidth = 100;
            Console.BufferWidth = 100;
            Console.WindowHeight = 25;
            Console.BufferHeight = 200;

            // Get "system.serviceModel/client section
            Console.WriteLine("Select a client endpoint:");
            
            Console.WriteLine(line);
            var clientSection = ConfigurationManager.GetSection("system.serviceModel/client") as ClientSection;
            if (clientSection == null ||
                clientSection.Endpoints == null ||
                clientSection.Endpoints.Count == 0)
            {
                return null;
            }

            // Show client endpoints
            var endpointList = clientSection.Endpoints.
                               Cast<ChannelEndpointElement>().Where(e => e.Name.Contains("Relay")).ToList();
            if (endpointList.Count == 0)
            {
                Console.WriteLine("There are no client endpoints defined in the configuration file.");
                Console.WriteLine(line);
                Console.WriteLine("Press [Enter] to exit");
                Console.ReadLine();
                return null;
            }
            for (var i = 0; i < endpointList.Count; i++)
            {
                Console.ForegroundColor = ConsoleColor.Green;
                Console.Write("[{0}] ", i + 1);
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.Write("{0}: ", endpointList[i].Name);
                Console.ResetColor();
                Console.WriteLine(endpointList[i].Address);
            }
            // Add exit option
            Console.ForegroundColor = ConsoleColor.Green;
            Console.Write("[{0}] ", endpointList.Count + 1);
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine("Exit");
            Console.ResetColor();
            Console.WriteLine(line);

            // Select an option
            Console.Write("Select a numeric key between [1] and [{0}]: ", endpointList.Count + 1);
            var key = 'a';
            while (key < '1' || key > '9')
            {
                key = Console.ReadKey(true).KeyChar;
            }
            Console.WriteLine();
            return key - '1' == endpointList.Count ? null : endpointList[key - '1'].Name;
        }

        private static void PrintProduct(Product product)
        {
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.Write("ProductId");
            Console.ResetColor();
            Console.Write("={0} ", product.ProductId);
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.Write("Name");
            Console.ResetColor();
            Console.Write("={0} ", product.Name);
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.Write("Category");
            Console.ResetColor();
            Console.Write("={0} ", product.Category);
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.Write("Price");
            Console.ResetColor();
            Console.WriteLine("={0} ", product.Category);
        }

        private static void PrintProductId(Product product)
        {
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.Write("ProductId");
            Console.ResetColor();
            Console.WriteLine("={0} ", product.ProductId);
        }

        private static void PrintErrorMessage()
        {
            Console.WriteLine("Something went wrong.");
            Console.WriteLine("Press [Enter] to exit");
            Console.ReadLine();
        }
    }
}

The following table contains the configuration file of the client application.

<?xml version="1.0"?>
<configuration>
  <system.diagnostics>
    <sources>
      <source name="System.ServiceModel.MessageLogging" switchValue="Warning, ActivityTracing">
        <listeners>
          <add type="System.Diagnostics.DefaultTraceListener" name="Default">
            <filter type=""/>
          </add>
          <add name="ServiceModelMessageLoggingListener">
            <filter type=""/>
          </add>
        </listeners>
      </source>
    </sources>
    <sharedListeners>
      <add initializeData="C:\Demos\Client.svclog" 
           type="System.Diagnostics.XmlWriterTraceListener, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" 
           name="ServiceModelMessageLoggingListener" 
           traceOutputOptions="Timestamp">
        <filter type=""/>
      </add>
    </sharedListeners>
  </system.diagnostics>

  <system.serviceModel>

    <!-- Diagnostics -->
    <diagnostics performanceCounters="All">
      <messageLogging logEntireMessage="true" 
                      logMalformedMessages="false" 
                      logMessagesAtServiceLevel="true" 
                      logMessagesAtTransportLevel="false"/>
    </diagnostics>
    <!-- Binding Configuration -->
    <bindings>
      <netTcpRelayBinding>
        <binding name="netTcpRelayBinding">
          <security mode="Transport" relayClientAuthenticationType="RelayAccessToken"/>
        </binding>
      </netTcpRelayBinding>
      <basicHttpRelayBinding>
        <binding name="basicHttpRelayBinding">
          <security mode="Transport" relayClientAuthenticationType="RelayAccessToken"/>
        </binding>
      </basicHttpRelayBinding>
      <webHttpRelayBinding>
        <binding name="webHttpRelayBinding">
          <security mode="Transport" relayClientAuthenticationType="RelayAccessToken"/>
        </binding>
      </webHttpRelayBinding>
    </bindings>
    <!-- Behavior Configuration -->
    <behaviors>
      <endpointBehaviors>
        <behavior name="serviceBusEndpointBehavior">
          <transportClientEndpointBehavior>
            <tokenProvider>
              <sharedSecret issuerName="owner" issuerSecret="[YOUR-ISSUER-SECRET]"/>
            </tokenProvider>
          </transportClientEndpointBehavior>
          <serviceRegistrySettings discoveryMode="Public" displayName="config"/>
        </behavior>
        <behavior name="webHttpEndpointBehavior">
          <webHttp defaultBodyStyle="Bare" />
          <transportClientEndpointBehavior>
            <tokenProvider>
              <sharedSecret issuerName="owner" issuerSecret="[YOUR-ISSUER-SECRET]"/>
            </tokenProvider>
          </transportClientEndpointBehavior>
          <serviceRegistrySettings discoveryMode="Public" displayName="config"/>
        </behavior>
      </endpointBehaviors>
    </behaviors>
    <!-- Service Conbfiguration -->
    <client>
      <endpoint address="sb://[YOUR-SERVICE-BUS-NAMESPACE].servicebus.windows.net/products/nettcp"
                behaviorConfiguration="serviceBusEndpointBehavior"
                binding="netTcpRelayBinding"
                bindingConfiguration="netTcpRelayBinding"
                contract="Contracts.IProductService"
                name="netTcpRelayBinding"/>
      <endpoint address="https://[YOUR-SERVICE-BUS-NAMESPACE].servicebus.windows.net/products/basichttp"
                  behaviorConfiguration="serviceBusEndpointBehavior"
                  binding="basicHttpRelayBinding"
                  bindingConfiguration="basicHttpRelayBinding"
                  contract="Contracts.IProductService"
                  name="basicHttpRelayBinding"/>
      <endpoint address="https://[YOUR-SERVICE-BUS-NAMESPACE].servicebus.windows.net/products/webhttp"
                behaviorConfiguration="webHttpEndpointBehavior"
                binding="webHttpRelayBinding"
                bindingConfiguration="webHttpRelayBinding"
                contract="Contracts.IProductService"
                name="webHttpRelayBinding"/>
    </client>
    <extensions>
      <behaviorExtensions>
        <add name="connectionStatusBehavior"
             type="Microsoft.ServiceBus.Configuration.ConnectionStatusElement, Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
        <add name="transportClientEndpointBehavior"
             type="Microsoft.ServiceBus.Configuration.TransportClientEndpointBehaviorElement, Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
        <add name="serviceRegistrySettings"
             type="Microsoft.ServiceBus.Configuration.ServiceRegistrySettingsElement, Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
      </behaviorExtensions>
      <bindingElementExtensions>
        <add name="netMessagingTransport"
             type="Microsoft.ServiceBus.Messaging.Configuration.NetMessagingTransportExtensionElement, Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
        <add name="tcpRelayTransport"
             type="Microsoft.ServiceBus.Configuration.TcpRelayTransportElement, Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
        <add name="httpRelayTransport"
             type="Microsoft.ServiceBus.Configuration.HttpRelayTransportElement, Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
        <add name="httpsRelayTransport" type="Microsoft.ServiceBus.Configuration.HttpsRelayTransportElement, Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
        <add name="onewayRelayTransport"
             type="Microsoft.ServiceBus.Configuration.RelayedOnewayTransportElement, Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
      </bindingElementExtensions>
      <bindingExtensions>
        <add name="basicHttpRelayBinding"
             type="Microsoft.ServiceBus.Configuration.BasicHttpRelayBindingCollectionElement, Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
        <add name="webHttpRelayBinding"
             type="Microsoft.ServiceBus.Configuration.WebHttpRelayBindingCollectionElement, Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
        <add name="ws2007HttpRelayBinding"
             type="Microsoft.ServiceBus.Configuration.WS2007HttpRelayBindingCollectionElement, Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
        <add name="netTcpRelayBinding"
             type="Microsoft.ServiceBus.Configuration.NetTcpRelayBindingCollectionElement, Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
        <add name="netOnewayRelayBinding"
             type="Microsoft.ServiceBus.Configuration.NetOnewayRelayBindingCollectionElement, Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
        <add name="netEventRelayBinding"
             type="Microsoft.ServiceBus.Configuration.NetEventRelayBindingCollectionElement, Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
        <add name="netMessagingBinding"
             type="Microsoft.ServiceBus.Messaging.Configuration.NetMessagingBindingCollectionElement, Microsoft.ServiceBus, Version=2.1.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
      </bindingExtensions>
    </extensions>
  </system.serviceModel>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/>
  </startup>
</configuration>


NOTE: make sure to replace the following placeholders in the app.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]');

    // 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('restproducts', { '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" 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" value="' + product.price + '"/></td>'))
                            .append($('<td><button class="button-update">Update</button></td>'))
                            .append($('<td><button class="button-delete">Delete</button></td>')).append('</tr>');
                    });
                    $('#products > tbody').empty().append(products).toggle(products.length > 0);
                    $('#summary').html('<strong>' + products.length + '</strong> product(s) succesfully 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('restproducts', { '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> succesfully 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('restproducts', { 'method': 'PUT', 'body': product })
              .done(function (response) {
                  if (response.result) {
                      $('#summary').html('<strong>' + response.result.name + '</strong> succesfully 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('restproducts', { 'method': 'DELETE', 'parameters': {'id' : productId} })
              .done(function (response) {
                  if (response.result == true) {
                      row.remove();
                      $('#summary').html('<strong>' + name + '</strong> succesfully 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.

 

Windows Store App

The Windows Store app uses the InvokeApiAsync methods provided by the MobileServiceClient class.
The following table contains the code of the MainPage.xaml.cs class that contains the client-side logic used to invoke 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 string Price { get; set; }
    }

    public sealed partial class MainPage
    {
        private ObservableCollection<Product> products;
        private const string ApiName = "restproducts";
        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 = "product(s) succesfully retrieved."; } 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; 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) { var product = new Product {Name = ProductName.Text, Category = ProductCategory.Text, Price = ProductPrice.Text}; InsertProduct(product); } protected override void OnNavigatedTo(NavigationEventArgs e) { RefreshProducts(); } } }
            
           
NOTE: make sure to replace the following placeholders in the App.xaml.cs file:
  • [YOUR-MOBILE-SERVICE-NAME]: specify the name of your mobile service.
  • [YOUR-MOBILE-SERVICE-APPLICATION-KEY]: specify the application key of your mobile service.

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 a LOB application running on-premises via Service Bus Relayed Messaging and Access Control Service. In addition, this sample shows how to get advantage of the Git source control to manage the code of server scripts of you mobile service and how to get advantage of Git and NPM to use custom Node.js modules in your mobile service. You can download the code from MSDN Code Gallery. Look at How to integrate a Mobile Service with a SOAP Service Bus Relay Service to see a custom API can be used to invoke a WCF service that uses a BasicHttpRelayBinding endpoint to expose its functionality via a SOAP Service Bus Relay Service. See also the following articles on Windows Azure Mobile Services: