Consuming Azure Machine Learning in ASP.NET Core

Azure Machine Learning (ML) provides the infrastructure for building custom machine learning models. Once an Azure ML predictive model is deployed as a web service, a REST API is used to communicate with the model to evaluate predictions. Azure ML web services are REST API and JSON formatted messages that can be consumed by a wide range of devices and platforms. These web services are secured through a private API key, thus exposing these services to some clients requires a server layer to mediate between the client.

In this article we'll build an ASP.NET Core application that can consume an Azure web service. We'll begin with sample code generated from Azure ML studio and transition into strongly typed C# Objects with an ASP.NET Core application service.

We'll continue to use a predictive web service created in the previous article, Building Predictive Web Services, as an example in this process. The training experiment can be created using the article Building Predictive Web Services as a guide, or you can jump right in by creating a copy of the training experiment from the Cortana Intelligence Library and deploying the web service.

Azure ML Web Services

With Azure ML Web services, an external application communicates with a Machine Learning scoring model in real time. The API call returns prediction results from the model to an external application.

Azure Machine Learning has two types of services:

  • Request-Response Service (RRS) – A low latency, highly scalable service that provides an interface to the stateless models created and deployed from the Machine Learning Studio.
  • Batch Execution Service (BES) – An asynchronous service that scores a batch for data records.

For the scope of this article we'll only be discussing the RRS type of service.

To make an API service call, we'll need to pass the API key that is created when the web service is deployed. The API key should only be accessible to the application and not transmitted in the open as plain text.

Management

Managing web services can be done through the management dashboard in Azure ML studio. From here we will test our web service, get the web service API key and endpoint, and generate sample code. To manage the web services, select the web services tab from the left hand menu, then choose the desired web service, in this case we'll choose the Loan Granting web service deployed earlier. Once the web service is displayed choose the New Web Services Experience (preview).

Since we'll be building an application to consume this web service choose the Consume menu item from the top of the page. On this page we'll find the the relevant resources for building our application such as API keys, Request-Response and sample code for C# applications.

The sample code gives us a good starting point for integrating Azure ML with an application. The code provided is for a simple console application scenario, however it can easily be adapted for other application types. We'll use the sample code to get the application bootstrapped. Once communication with the web service is established we can refactor the sample code into something more manageable.

Creating an ASP.NET Core 1.1 app

To consume the API, we'll make use of the sample code inside of an ASP.NET Core application. ASP.NET Core is a solid choice since it gives us the opportunity to build a full scale web application or simply setup a web service to mediate between the Azure ML web service and data from other applications or systems.

Let's create a new ASP.NET Core project in Visual Studio 2017, we'll define a service for calling the Azure ML web service using the sample code, and display the results directly to a view. This basic setup will be just enough to prepare us for further development of our app.

The first step is to create a new ASP.NET Core 1.1 application. The ASP.NET Core Web Application template will give us everything we need to get started.

File > New Project > ASP.NET Core Web Application

Using Sample Code

Next we'll need to create a new service class for the application. To help keep things organized, we'll create a new path /Services/CreditService. The CreditService folder will be used for the service and objects used to consume the Azure ML web service.

In CreditServices we'll add a new service class called CreditPredictionService, which we'll build using the sample code supplied in the Azure ML management dashboard. Adding the new class CreditPredictionService.cs gives an empty class.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace CreditApproval.Services.CreditService
{
    public class CreditPredictionService
    {
        // We'll copy our sample code here
    }
}

From Azure ML Studio's management dashboard we'll need the InvokeRequestResponseService portion of the sample code responsible for making the service call. Let's copy the entire InvokeRequestResponseService method and add it to our CreditPredictionService class. Since we're using ASP.NET Core, we won't need to add Microsoft.AspNet.WebApi.Client, which is suggested in the sample. Since ASP.NET Core already includes System.Net.Http libraries with the framework, we'll just need the using statement to bring them in.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Net.Http; //add this using
using System.Net.Http.Headers;  //add this using


namespace CreditApproval.Services.CreditService
{
    public class CreditPredictionService
    {
        static async Task InvokeRequestResponseService()
        { ... }
    }
}

Integrating the Sample Code

With the code imported we'll need to make a few adjustments to make use of it in the application. There is an extension method missing from ASP.NET Core that will cause a compiler error. We'll need to setup the method to expose the values retrieved from the web service and the method could use proper naming.

Let's start by addressing the missing method PostAsJsonAsync. PostAsJsonAsync is not included in .NET Core, however it can be easily replaced with a single line of code. We'll replace PostAsJsonAsync with the more generic PostAsync and apply the JSON formatting explicitly. Since we're providing the JSON formatting, we'll need to add Newtonsoft.Json and System.Text to our list of using statements. In addition, it's a good time to add ConfigureAwait(false) to PostAsync as per the instructions provided with the code sample.

using Newtonsoft.Json;
using System.Text;
static async Task InvokeRequestResponseService() {
    ...
    //HttpResponseMessage response = await client.PostAsJsonAsync("", scoreRequest);
    HttpResponseMessage response = await client
        .PostAsync("", new StringContent(JsonConvert.SerializeObject(scoreRequest), Encoding.UTF8, "application/json"))
        .ConfigureAwait(false);
    ...
}   

The sample code was designed for a console application, meaning all of the results are written out via Console. Let's return the results as a string instead so that we can make practical use of the result in our application. While a string is not ideal, it's a simple step towards a more complete solution. We'll return the results for both a successful or unsuccessful service call.

This is the original sample code writing to console:

static async Task InvokeRequestResponseService() {
    if (response.IsSuccessStatusCode)
    {
        string result = await response.Content.ReadAsStringAsync();
        Console.WriteLine("Result: {0}", result);
    }
    else
    {
        Console.WriteLine(string.Format("The request failed with status code: {0}", response.StatusCode));
    
        // Print the headers - they include the requert ID and the timestamp,
        // which are useful for debugging the failure
        Console.WriteLine(response.Headers.ToString());
    
        string responseContent = await response.Content.ReadAsStringAsync();
        Console.WriteLine(responseContent);
    }
}

And here's returning string instead:

static async Task InvokeRequestResponseService() {
    ...
    if (response.IsSuccessStatusCode)
    {
        string result = await response.Content.ReadAsStringAsync();
        return result;
    }
    else
    {
        return string.Format("The request failed with status code: {0}", response.StatusCode);
    }
}

To complete the method, we'll change the signature to return a Task of string. We'll also use this opportunity to rename the method from InvokeRequestResponseService to PreApproveCredit, and change the method from static to public.

static async Task InvokeRequestResponseService() { ... }
public async Task<string> PreApproveCredit() { ... }

One final line of code remains to be changed before we move on. The sample provides a dummy string for the apiKey. This needs to be replaced with a real key from the Azure ML dashboard. For the scope of this example, we'll hard code the key directly to the apiKey variable, however, in a production application, best practices should be followed to manage and hide API keys and secrets. Also be careful when publishing code to public repositories like GitHub.

const string apiKey = "your-key-goes-here"; // Replace this with the API key for the web service

This completes the most basic implementation of the sample code and we can make a service call from our application.

Making a Sample Call

With the CreditPredictionService ready we can make a call to the Azure ML web service and receive a response. Let's do a simple call directly from the application's HomeController and render the results to the Index view.

We'll open the HomeController and modify the Index method. In the index we'll new up our CreditPredictionService and write the results directly to ViewData["Message"], which can be displayed on the Index page.

public IActionResult Index()
{
    ViewData["Message"] = new CreditPredictionService().PreApproveCredit().Result;
    return View();
}

Next we'll modify the Index.cshtml view to show the raw JSON data as an HTML page. We'll remove any templated content from the Index.cshtml view and replace it with @ViewData["Message"], effectively writing out any values directly to the browser.

<h1>@ViewData["Message"]</h1>

The application can now be run to validate the service is properly working. Running the app should produce something similar to the following output:

{"Results":{"ApprovalStatus":[{"IsApproved":"true","Scored Probabilities":"0.784003853797913"}]}}

This completes the minimum requirements for consuming the Azure ML web service, however there's a lot of room for improvement. Let's continue with the implementation by adding parameters to the PreApproveCredit method.

Strongly Typed Models for Azure ML

Currently our CreditPredictionService makes a hard-coded call to Azure ML using the PreApproveCredit method. The input values are built up in the PreApproveCredit method at the beginning of the sample code using a List<Dictionary<string,string>. If we externalized this type as a parameter, it would be very difficult for developers to understand what inputs are required for the API call.

Instead of manually building JSON data as in the sample code, what we really need as a strongly typed object that can be easily serialized into a JSON string.

Input Parameters

Let's start by creating a model to handle the Dictionary<string, string> field-value pairs. We can express these as POCO (Plain Old CLR Objects) and by using Json.NET annotations each property can be easily serialized into their respective field name.

// Excerpt from PreApproveCredit()
Inputs = new Dictionary<string, List<Dictionary<string, string>>>() {
    {
        "CreditProfile",
        new List<Dictionary<string, string>>(){new Dictionary<string, string>(){
                {
                    "Loan Amount", "12232"
                },
                {
                    "Term", "Short Term"
                },
                {
                    "Credit Score", "728"
                },
                {
                    "Years in Current Job", "< 1 year"
                },
                {
                    "Home Ownership", "Rent"
                },...

We'll add a new class to Services/CreditService named CreditProfile. The CreditProfile object will contain a property for each field required by the Azure ML web service. For each property, we'll specify the field name used in the web service input using the JsonProperty attribute.

public class CreditProfile
{
    [JsonProperty("Loan Amount")]
    public int LoanAmount { get; set; }

    public string Term { get; set; }

    [JsonProperty("Credit Score")]
    public int CreditScore { get; set; }

    [JsonProperty("Years in Current Job")]
    public string YearsInCurrentJob { get; set; }

    [JsonProperty("Home Ownership")]
    public string HomeOwnership { get; set; }
    ...

A few wrapper classes will help us complete the strongly typed object. A a new class named Inputs is added that contains a list of CreditProfile. The web service can accept multiple rows of data as a JSON data table, however we'll just be making a request for a single CreditProfile in our application so we'll restrict the number of CreditProfiles by limiting the constructor parameter to a single CreditProfile instance.

We'll use another class to wrap the CreditProfileData object and satisfy the remaining parameters of the request.

public class Inputs
{
    public IList<CreditProfile> CreditProfile { get; }

    public Inputs(CreditProfile creditProfile)
    {
        CreditProfile = new List<CreditProfile>() { creditProfile };
    }
}

public class CreditProfileData
{
    public CreditProfileData(CreditProfile creditProfile)
    {
        Inputs = new Inputs(creditProfile);
    }

    public Inputs Inputs { get; }

    public Dictionary<string, string> GlobalParameters => new Dictionary<string, string>(){};
}

The result of these few classes when serialized to JSON will be an exact match for the web service inputs. We'll need to repeat this process for the output parameters as well for a nice strongly typed return value.

Output Parameters

Much like the input parameters, we'll use a model to handle the JSON results as POCOs. We'll add a class ApprovalStatus to hold the results, and the Results and CreditService classes help deserialize the web service results.

We'll add the ApprovalStatus class and add properties for IsApproved and Probibility, these properties are mapped to their corresponding JSON results using the JsonProperty attribute. The Results and CreditService classes are added to simplify mapping

public class ApprovalStatus
{
    [JsonProperty("IsApproved")]
    public bool IsApproved { get; set; }

    [JsonProperty("Scored Probabilities")]
    public float Probability { get; set; }
}

public class Results
{
    [JsonProperty("ApprovalStatus")]
    public IList<ApprovalStatus> ApprovalStatus { get; set; }
}

public class CreditServiceResponse
{
    [JsonProperty("Results")]
    public Results Results { get; set; }

    public ApprovalStatus GetStatus() => Results.ApprovalStatus.First();
}

6services-folder-complete.jpg

Now that we have strongly typed models, the CreditService will need to be updated to utilize them.

Updating the Sample Code

To update the CreditService with the newly created models, we'll need to modify the PreApproveCredit method. Currently the PreApproveCredit method takes no parameters and returns a Task<string>. Let's update the method by adding a parameter for the CreditProfile type and we'll return an ApprovalStatus.

We'll need to modify the ApprovalStatus method signature. The method will return Task<ApprovalStatus> and accept the parameter CreditProfile.

//public async Task<string> PreApproveCredit()

public async Task<ApprovalStatus> PreApproveCredit(CreditProfileData creditProfileData) {...}

The sample code that builds up the scoreRequest can be removed. This simply hard-codes the input parameters we're replacing with the CreditProfile model.

//var scoreRequest = new
//{
//    Inputs = new Dictionary<string, List<Dictionary<string, string>>>() {
//        {
//            "CreditProfile",
//            new List<Dictionary<string, string>>(){new Dictionary<string, string>(){ ... }
//            }
//        },
//    },
//    GlobalParameters = new Dictionary<string, string>()
//    {
//    }
//};

The creditProfileData is passed into PostAsync in place of the scoreRequest. Since we prepared the CreditProfile object with the necessary JsonProperty attributes, it will easily convert to the appropriate JSON data schema needed by the web service.

//.PostAsync("", new StringContent(JsonConvert.SerializeObject(scoreRequest), Encoding.UTF8, "application/json"))

.PostAsync("", new StringContent(JsonConvert.SerializeObject(creditProfileData), Encoding.UTF8, "application/json"))

Finally, we'll handle the response by deserializing the JSON response to a CreditServiceResponse, which maps to the JSON properties received form the web service. If there are any errors in the service call, they will be bubbled up through an exception.

if (response.IsSuccessStatusCode)
{
    string result = await response.Content.ReadAsStringAsync();
    CreditServiceResponse csr = JsonConvert.DeserializeObject<CreditServiceResponse>(result);
    return csr.GetStatus(); 
}
else
{
    throw new HttpRequestException(string.Format("The request failed with status code: {0}", response.StatusCode));
}

The CreditService can now make requests for any CreditProfileData it receives. This allows the application integrate the web service with data from web forms, other web services, databases or a combination of the inputs.

Controller and View Updates

To show how the CreditService can interact with a view, let's update the application's controller and view to make use of the service.

We currently have a view that was using the hard coded values from the sample and writing out the response directly as a string.

public IActionResult Index()
{
    ViewData["Message"] = new CreditPredictionService().PreApproveCredit().Result;
    return View();
}

Let's modify the Index action to use the CreditService using a strongly typed model. A new CreditProfile object is created, then populated with data. For this example we'll apply the values in the Index view, however these values could come from any source, such as a model bound to a form.

A CreditProfileData is created with the CreditProfile, effectively wrapping the values in the data structure needed by the web service. Next a CreditPredictionService is created and we call PreApproveCredit invoking the web service and returning the ApprovalStatus object. The ApprovalStatus is then passed to the view to be displayed on the web page.

public IActionResult Index()
{
    CreditProfile profile = new CreditProfile
    {
        LoanAmount = 12232,
        Term = "Short Term",
        CreditScore = 755,
        YearsInCurrentJob = " < 1 year",
        HomeOwnership = "Rent",
        AnnualIncome = 46643,
        Purpose = "Debt Consolidation",
        MonthlyDebt = 777.39,
        YearsOfCreditHistory = 18,
        MonthsSinceLastDelinquent = 10,
        NumberOfOpenAccounts = 12,
        NumberOfCreditProblems = 0,
        CurrentCreditBalence = 6762,
        MaximumOpenCredit = 7946,
        Bankruptcies = 0,
        TaxLiens = 0
    };
    var data = new CreditProfileData(profile);
    var model = new CreditPredictionService().PreApproveCredit(data).Result;
    return View(model);
}

In the view (View.cshtml) we can take advantage of strongly typed model binding to display the results of the web service call. For this example we'll write the directly to the view using @Model.[property], additionally any HTML Helper or Tag Helper could be used to control how the data is rendered on the page.

@model CreditApproval.Services.CreditService.ApprovalStatus
@{
    ViewData["Title"] = "Home Page";
}

<h1>@Model.Probability</h1>
<h1>@Model.IsApproved</h1>

This completes the full request from web service though to the view.

Next Steps

Azure ML Studio uses Request-Response Service (RRS), a low-latency, highly scalable service that provides an interface to the stateless models created and deployed from the Machine Learning Studio. Throughout this example we learned we learned how to consume the RRS straight from the sample code provided by Azure ML studio's web services management control panel help kick-start development. With added code for translating the RRS input and output to strongly typed code, we can create services for ASP.NET applications that utilize the power of Azure Machine Learning.

In the next article, we'll learn how to use the Azure ML powered ASP.NET application to integrate a front-end development experience using ASP.NET MVC forms, HTML Helpers and TagHelpers.

Comments