Overview

This article is part 2 of a 4 part series, where we’ll cover the C# code for our custom connector for Nintex Xtensions and NWC.

In part 1, I covered the project requirements and how to setup Azure and Visual Studio 2017 with Tools for Azure Functions.  The goal of this project was to allow a workflow designer in NWC to query the GSA Per Diem API to lookup the allowed Per Diem amounts for Hotel and Meals based on the travel date and location of a trip.

Below I’ll cover the GSA Per Diem API and the C# code for a wrapper function to make this API very easy to consume from Nintex Workflow Cloud.

Development

The GSA’s Per Diem API, is a RESTful service with a very simple interface.  The API URL is: https://inventory.data.gov/api/action/datastore_search?resource_id=8ea44bc4-22ba-4386-b84c-1494ab28964b

This URL allows one pass in an additional “filter” parameter with a JSON formatted string containing the parameter values that the API shall use to return the desired results.  The API allows four types of filters and they each require two arguments, with FiscalYear as one of the arguments and one of the following as the other argument:

  • Zip
  • County
  • DestinationID
  • State

For my initial project, I used Zip.  So to call this API, you’ll need to format a REST call as follows:

https://inventory.data.gov/api/action/datastore_search?resource_id=8ea44bc4-22ba-4386-b84c-1494ab28964b&filters={“FiscalYear”:”2017″,”Zip”:”10036″}

The response from this REST API is a fairly lengthy JSON message containing a lot of data.  However, my NWC connector only needs to receive two values (MealsPerDiem & HotelPerDiem).  As noted above, the Azure Function I created was a fairly simple wrapper around this REST API, that enabled the following:

  1. Allow API to be called as an HTTP POST operation
  2. Accept an input value from Nintex Workflow Cloud of a specific date, rather than the FiscalYear.
  3. Return only the two value that we need: MealsPerDiem & HotelPerDiem

So here is the C# code that I used (Since the code is pretty well commented, I’m not providing additional explanations):

using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Threading.Tasks;
using System.Text;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;
using Newtonsoft.Json;

namespace GsaPerDiemFunction
{
    ///
<summary>
    /// This class implements behavior for GetPerDiemByZip function in Azure
    /// </summary>

    public static class GetPerDiemByZip
    {
        //Constants
        private const string GSA_REST_URL = "https://inventory.data.gov/api/action/datastore_search";
        private const string RESOURCEID = "?resource_id=8ea44bc4-22ba-4386-b84c-1494ab28964b";

        ///
<summary>
        /// The PerDiemInput class is used to deserialize JSON input data from the HTTP POST
        /// Zip - Zip code of where the travel occured
        /// TripDate - Date that the specifc travel occured on
        /// </summary>

        public class PerDiemInput
        {
            public string Zip { get; set; }
            public string TripDate { get; set; }
        }

        ///
<summary>
        /// The PerDiemOutput class is returned as serialized JSON with the requested values from the GSA Per Diem API
        /// Hotels - Per Diem amount allowed by GSA for Hotel expense on specified date and zip code
        /// Meals - Per Diem amount allowed by GSA for Meals on specified date and zip code
        /// </summary>

        public class PerDiemOutput
        {
            public int Hotel { get; set; }
            public int Meals { get; set; }
        }

        ///
<summary>
        /// This method implements the  core logic for our Azure Function
        /// </summary>

        /// <param name="req">Incoming HTTP Request data as a POST operation</param>
        /// <param name="log">Log provided by Azure functions for debug purposes.  Logged data is visible by admins in the Azure portal.</param>
        /// <returns></returns>
        [FunctionName("GetPerDiemByZip")]
        public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]HttpRequestMessage req, TraceWriter log)
        {
            try
            {
                log.Info("Received Per Diem Request.");

                //Deserialize the incoming JSON data into an instance of the PerDiemInput
                string jsonInput = await req.Content.ReadAsStringAsync();
                log.Info(jsonInput);
                var perDiemInput = JsonConvert.DeserializeObject<PerDiemInput>(jsonInput);

                //Cast incoming values and check that the inputs are valid
                string zip = perDiemInput.Zip;
                string tripDate = perDiemInput.TripDate;
                string fiscalYear;
                int monthNum;
                string month;
                DateTime travelDate;
                int zipInt;

                //If TripDate is not valid, return BadRequest response, otherwise parse the date into year and month values
                if (!DateTime.TryParse(tripDate, out travelDate))
                {
                    string tripDateInvalidMsg = string.Format("The specified TripDate ({0}) is not valid.", tripDate);
                    log.Info(tripDateInvalidMsg);
                    return req.CreateResponse(HttpStatusCode.BadRequest, tripDateInvalidMsg);
                }
                else
                {
                    fiscalYear = travelDate.Year.ToString();
                    monthNum = travelDate.Month;
                    DateTime monthDate = new DateTime(1, monthNum, 1);

                    month = monthDate.ToString("MMM");
                }

                //If Zip is not valid, return BadRequest response
                if ((zip.Length != 5) || (!int.TryParse(zip, out zipInt)))
                {
                    string zipInvalidMsg = string.Format("The specified Zip ({0}) is not valid.", zip);
                    log.Info(zipInvalidMsg);
                    return req.CreateResponse(HttpStatusCode.BadRequest, zipInvalidMsg);
                }

                log.Info(string.Format("Per Diem Requested for Zip: {0} and Fiscal Year: {1}", zip, fiscalYear));

                //Get per diem data in json format from GSA REST services, based on the specified year and zip code
                //Deserialize the per diem json into an object
                string jsonResponse = GetPerDiemJson(zip, fiscalYear, log);
                var gsaPerDiem = JsonConvert.DeserializeObject<Rootobject>(jsonResponse);
                log.Info("Deserialized jsonResonse");

                //Create the output object that we want to return.  This is a simplied version of what the GSA provides,
                //returning only the max per diem allowed for meals and hotel based on the fiscal year and zip.
                PerDiemOutput perDiemOut = new PerDiemOutput();
                perDiemOut.Meals = int.Parse(gsaPerDiem.result.records[0].Meals);
                perDiemOut.Hotel = GetHotelPerDiemByMonth(ref gsaPerDiem, month);

                return req.CreateResponse(HttpStatusCode.OK, perDiemOut);
            }
            catch (Exception e)
            {
                log.Info(string.Format("ERROR:\n\r Message: {0}\r\n Source: {1}\r\n Stack: {2}\r\n TargetSite: {3}", e.Message, e.Source, e.StackTrace, e.TargetSite));
                return req.CreateResponse(HttpStatusCode.BadRequest, e.Message);
            }
        }

        ///
<summary>
        /// Formats input filters into JSON and Calls the GSA Per Diem REST API
        /// </summary>

        /// <param name="zip">Zip code specified by the caller</param>
        /// <param name="fiscalYear">Fiscal Year specified by the caller</param>
        /// <param name="log">Log provided by Azure functions for debug purposes.  Logged data is visible by admins in the Azure portal.</param>
        /// <returns></returns>
        private static string GetPerDiemJson(string zip, string fiscalYear, TraceWriter log)
        {
            HttpWebResponse response = null;
            Stream receiveStream = null;
            StreamReader streamReader = null;
            try
            {
                //format filter parameter as JSON
                string filters = string.Format("&filters={{\"FiscalYear\":\"{0}\",\"Zip\":\"{1}\"}}", fiscalYear, zip);
                string resourceUrl = string.Format("{0}{1}{2}", GSA_REST_URL, RESOURCEID, filters);

                //Create request to GSA API
                HttpWebRequest request = (HttpWebRequest)WebRequest.Create(resourceUrl);

                ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls
                                        | SecurityProtocolType.Tls11
                                        | SecurityProtocolType.Tls12
                                        | SecurityProtocolType.Ssl3;

                log.Info(string.Format("Created Request for: {0}", resourceUrl));

                request.Method = "POST";
                request.ContentType = "application/x-www-form-urlencoded";

                log.Info("Request Host: " + request.Host);

                response = (HttpWebResponse)request.GetResponse();
                log.Info("Received Response from Request");

                receiveStream = response.GetResponseStream();
                streamReader = new StreamReader(receiveStream, Encoding.UTF8);
                string jsonResponse = streamReader.ReadToEnd();

                response.Close();
                streamReader.Close();
                return jsonResponse;
            }
            catch (Exception e)
            {
                throw;
            }
            finally
            {
                if (response != null)
                {
                    response.Close();
                }
                if (streamReader != null)
                {
                    streamReader.Close();
                }
            }
        }

        private static int GetHotelPerDiemByMonth(ref Rootobject gsaPerDiem, string month)
        {
            int perDiem = 0;

            switch(month.ToLower())
            {
                case "jan":
                    perDiem = int.Parse(gsaPerDiem.result.records[0].Jan);
                    break;
                case "feb":
                    perDiem = int.Parse(gsaPerDiem.result.records[0].Feb);
                    break;
                case "mar":
                    perDiem = int.Parse(gsaPerDiem.result.records[0].Mar);
                    break;
                case "apr":
                    perDiem = int.Parse(gsaPerDiem.result.records[0].Apr);
                    break;
                case "may":
                    perDiem = int.Parse(gsaPerDiem.result.records[0].May);
                    break;
                case "jun":
                    perDiem = int.Parse(gsaPerDiem.result.records[0].Jun);
                    break;
                case "jul":
                    perDiem = int.Parse(gsaPerDiem.result.records[0].Jul);
                    break;
                case "aug":
                    perDiem = int.Parse(gsaPerDiem.result.records[0].Aug);
                    break;
                case "sep":
                    perDiem = int.Parse(gsaPerDiem.result.records[0].Sep);
                    break;
                case "oct":
                    perDiem = int.Parse(gsaPerDiem.result.records[0].Oct);
                    break;
                case "nov":
                    perDiem = int.Parse(gsaPerDiem.result.records[0].Nov);
                    break;
                case "dec":
                    perDiem = int.Parse(gsaPerDiem.result.records[0].Dec);
                    break;
            }

            return perDiem;
        }
    }
}

Once the code is complete, you can publish again to Azure.  I should also mention that there are a few ways to test and debug your Azure Function code, both locally (without publishing to Azure) or from Azure Functions.  Many REST API developers use Postman, which is what I did as well.

Before you can consume your Azure Function as a Connector for Nintex Workflow Cloud, you’ll need to define the Swagger definition file, which I’ll cover in part 3.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s