9 min read

In this article written by Geoff Webber-Cross, the author of Learning Microsoft Azure, we’ll create an on-premise production management client Windows application allowing manufacturing staff to view and update order and batch data and a web service to access data in the production SQL database and send order updates to the Service Bus topic.

(For more resources related to this topic, see here.)

The site’s main feature is an ASP.NET Web API 2 HTTP service that allows the clients to read order and batch data. The site will also host a SignalR (http://signalr.net/) hub that allows the client to update order and batch statuses and have the changes broadcast to all the on-premise clients to keep them synchronized in real time. Both the Web API and SignalR hubs will use the Azure Active Directory authentication.

We’ll cover the following topic in this article:

  • Building a client application

Building a client application

For the client application, we’ll create a WPF client application to display batches and orders and allow us to change their state. We’ll use MVVM Light again, like we did for the message simulator we created in the sales solution, to help us implement a neat MVVM pattern. We’ll create a number of data services to get data from the API using Azure AD authentication.

Preparing the WPF project

We’ll create a WPF application and install NuGet packages for MVVM Light, JSON.NET, and Azure AD authentication in the following procedure (for the Express version of Visual Studio, you’ll need Visual Studio Express for desktops):

  1. Add a WPF project to the solution called ManagementApplication.
  2. In the NuGet Package Manager Console, enter the following command to install MVVM Light:
    install-package mvvmlight
  3. Now, enter the following command to install the Microsoft.IdentityModel.Clients.ActiveDirectory package:
    install-package Microsoft.IdentityModel.Clients.ActiveDirectory
  4. Now, enter the following command to install JSON.NET:
    install-package newtonsoft.json
  5. Enter the following command to install the SignalR client package (note that this is different from the server package):
    Install-package Microsoft.AspNet.SignalR.Client
  6. Add a project reference to ProductionModel by right-clicking on the References folder and selecting Add Reference, check ProductionModel by navigating to the Solution | Projects tab, and click on OK.
  7. Add a project reference to System.Configuraton and System.Net.Http by right-clicking on the References folder and selecting Add Reference, check System.Config and System.Net.Http navigating to the Assemblies | Framework tab, and click on OK.
  8. In the project’s Settings.settings file, add a string setting called Token to store the user’s auth token.
  9. Add the following appSettings block to App.config; I’ve put comments to help you understand (and remember) what they stand for and added commented-out settings for the Azure API:
    <appSettings>
    <!-- AD Tenant -->
    <add key="ida:Tenant" value="azurebakery.onmicrosoft.com" />
      
    <!-- The target api AD application APP ID (get it from 
        config tab in portal) -->
    <!-- Local -->
    <add key="ida:Audience" 
        value="https://azurebakery.onmicrosoft.com/ManagementWebApi" />
    <!-- Azure -->
    <!-- <add key="ida:Audience" 
        value="https://azurebakery.onmicrosoft.com/
          WebApp-azurebakeryproduction.azurewebsites.net" /> -->
      
    <!-- The client id of THIS application (get it from 
        config tab in portal) -->
    <add key="ida:ClientID" value=
        "1a1867d4-9972-45bb-a9b8-486f03ad77e9" />
      
    <!-- Callback URI for OAuth workflow -->
    <add key="ida:CallbackUri" 
        value="https://azurebakery.com" />
      
    <!-- The URI of the Web API -->
    <!-- Local -->
    <add key="serviceUri" value="https://localhost:44303/" />
    <!-- Azure -->
    <!-- <add key="serviceUri" value="https://azurebakeryproduction.azurewebsites.net/" /> 
     -->
    </appSettings>
  10. Add the MVVM Light ViewModelLocator to Application.Resources in App.xaml:
    <Application.Resources>
       <vm:ViewModelLocator x_Key="Locator" 
          d_IsDataSource="True" 
          
           
           DataContext="{Binding Source={StaticResource 
              Locator}, Path=Main}"
           Title="Production Management Application" 
              Height="350" Width="525">

Creating an authentication base class

Since the Web API and SignalR hubs use Azure AD authentication, we’ll create services to interact with both and create a common base class to ensure that all requests are authenticated. This class uses the AuthenticationContext.AquireToken method to launch a built-in login dialog that handles the OAuth2 workflow and returns an authentication token on successful login:

using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System;
using System.Configuration;
using System.Diagnostics;
using System.Net;
 
namespace AzureBakery.Production.ManagementApplication.Services
{
   public abstract class AzureAdAuthBase
   {
       protected AuthenticationResult Token = null;
 
       protected readonly string ServiceUri = null;
 
       protected AzureAdAuthBase()
       {
           this.ServiceUri = 
              ConfigurationManager.AppSettings["serviceUri"];
#if DEBUG
           // This will accept temp SSL certificates
           ServicePointManager.ServerCertificateValidationCallback += (se, cert, chain, sslerror) => true;
#endif
       }
 
       protected bool Login()
       {
           // Our AD Tenant domain name
           var tenantId = 
              ConfigurationManager.AppSettings["ida:Tenant"];
 
           // Web API resource ID (The resource we want to use)
           var resourceId = 
              ConfigurationManager.AppSettings["ida:Audience"];
 
           // Client App CLIENT ID (The ID of the AD app for this 
            client application)
           var clientId = 
              ConfigurationManager.AppSettings["ida:ClientID"];
 
           // Callback URI
           var callback = new 
              Uri(ConfigurationManager.AppSettings["ida:CallbackUri"]);
 
           var authContext = new 
              AuthenticationContext(string.Format("https://login.windows.net/{0}", tenantId));
 
           if(this.Token == null)
           {
               // See if we have a cached token
              var token = Properties.Settings.Default.Token;
               if (!string.IsNullOrWhiteSpace(token))
                   this.Token = AuthenticationResult.Deserialize(token);
           }
          
           if (this.Token == null)
            {
               try
               {
                   // Acquire fresh token - this will get user to 
                    login              
                   this.Token = 
                      authContext.AcquireToken(resourceId, 
                         clientId, callback);
               }
               catch(Exception ex)
               {
                   Debug.WriteLine(ex.ToString());
 
                   return false;
               }
           }
           else if(this.Token.ExpiresOn < DateTime.UtcNow)
           {
               // Refresh existing token this will not require 
                login
               this.Token = 
                  authContext.AcquireTokenByRefreshToken(this.Token.RefreshToken, 
                   clientId);
           }        
 
           if (this.Token != null && this.Token.ExpiresOn > 
              DateTime.UtcNow)
           {
               // Store token
               Properties.Settings.Default.Token = 
                  this.Token.Serialize(); // This should be 
                    encrypted
               Properties.Settings.Default.Save();
 
               return true;
           }
 
           // Clear token
           this.Token = null;
 
           Properties.Settings.Default.Token = null;
           Properties.Settings.Default.Save();
 
           return false;
       }
   }
}

The token is stored in user settings and refreshed if necessary, so the users don’t have to log in to the application every time they use it. The Login method can be called by derived service classes every time a service is called to check whether the user is logged in and whether there is a valid token to use.

Creating a data service

We’ll create a DataService class that derives from the AzureAdAuthBase class we just created and gets data from the Web API service using AD authentication. First, we’ll create a generic helper method that calls an API GET action using the HttpClient class with the authentication token added to the Authorization header, and deserializes the returned JSON object into a .NET-typed object T:

private async Task<T> GetData<T>(string action)
{
   if (!base.Login())
       return default(T);
 
   // Call Web API
   var authHeader = this.Token.CreateAuthorizationHeader();
   var client = new HttpClient();
   var uri = string.Format("{0}{1}", this.ServiceUri, 
      string.Format("api/{0}", action));
   var request = new HttpRequestMessage(HttpMethod.Get, uri);
   request.Headers.TryAddWithoutValidation("Authorization", 
      authHeader);
 
   // Get response
   var response = await client.SendAsync(request);
   var responseString = await response.Content.ReadAsStringAsync();
 
   // Deserialize JSON
   var data = await Task.Factory.StartNew(() => 
      JsonConvert.DeserializeObject<T>(responseString));
 
   return data;
}

Once we have this, we can quickly create methods for
getting order and batch data like this:

 
public async Task<IEnumerable<Order>> GetOrders()
{
   return await this.GetData<IEnumerable<Order>>("orders");
}
 
public async Task<IEnumerable<Batch>> GetBatches()
{
   return await this.GetData<IEnumerable<Batch>>("batches");
}

This service implements an IDataService interface and is registered in the ViewModelLocator class, ready to be injected into our view models like this:

SimpleIoc.Default.Register<IDataService, DataService>();

Creating a SignalR service

We’ll create another service derived from the AzureAdAuthBase class, which is called ManagementService, and which sends updated orders to the SignalR hub and receives updates from the hub originating from other clients to keep the UI updated in real time.

First, we’ll create a Register method, which creates a hub proxy using our authorization token from the base class, registers for updates from the hub, and starts the connection:

private IHubProxy _proxy = null;
 
public event EventHandler<Order> OrderUpdated;
public event EventHandler<Batch> BatchUpdated;
 
public ManagementService()
{
 
}
 
public async Task Register()
{
   // Login using AD OAuth
   if (!this.Login())
       return;
 
   // Get header from auth token
   var authHeader = this.Token.CreateAuthorizationHeader();
 
   // Create hub proxy and add auth token
   var cnString = string.Format("{0}signalr", base.ServiceUri);
   var hubConnection = new HubConnection(cnString, useDefaultUrl: 
      false);
   this._proxy = hubConnection.CreateHubProxy("managementHub");
   hubConnection.Headers.Add("Authorization", authHeader);
 
   // Register for order updates
   this._proxy.On<Order>("updateOrder", order =>
   {
       this.OnOrderUpdated(order);
   });
 
 
   // Register for batch updates
   this._proxy.On<Batch>("updateBatch", batch =>
   {
       this.OnBatchUpdated(batch);
   });
 
 
   // Start hub connection
   await hubConnection.Start();
}

The OnOrderUpdated and OnBatchUpdated methods call events to notify about updates.

Now, add two methods that call the hub methods we created in the website using the IHubProxy.Invoke<T> method:

public async Task<bool> UpdateOrder(Order order)
{
   // Invoke updateOrder method on hub
   await this._proxy.Invoke<Order>("updateOrder", 
      order).ContinueWith(task =>
   {
       return !task.IsFaulted;
   });
 
   return false;
}
 
public async Task<bool> UpdateBatch(Batch batch)
{
   // Invoke updateBatch method on hub
   await this._proxy.Invoke<Batch>("updateBatch", 
      batch).ContinueWith(task =>
   {
       return !task.IsFaulted;
   });
 
   return false;
}

This service implements an IManagementService interface and is registered in the ViewModelLocator class, ready to be injected into our view models like this:

SimpleIoc.Default.Register<IManagementService, 
 ManagementService>();

Testing the application

To test the application locally, we need to start the Web API project and the WPF client application at the same time. So, under the Startup Project section in the Solution Properties dialog, check Multiple startup projects, select the two applications, and click on OK:

Redis Applied Design Patterns

Once running, we can easily debug both applications simultaneously.

To test the application with the service running in the cloud, we need to deploy the service to the cloud, and then change the settings in the client app.config file (remember we put the local and Azure settings in the config with the Azure settings commented-out, so swap them around). To debug the client against the Azure service, make sure that only the client application is running (select Single startup project from the Solution Properties dialog).

Summary

We learned how to use a Web API to enable the production management Windows client application to access data from our production database and a SignalR hub to handle order and batch changes, keeping all clients updated and messaging the Service Bus topic.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here