R Web API from Dynamics 365 FinOps

Microsoft gives us a fair number of options to seamlessly connect machine learning models to our production code, and I honestly love using them all. AzureML is fantastic for many use cases, and with the Data Factory, Databricks and Data Lakes combo virtually every possible scenario can be covered really nicely.

Except of course if the model you need to use is hosted by a 3rd party which does not support any of these services. Then again, you might want to quickly test a few models first in a POC context before committing to “productizing” these into AzureML. Perhaps you just don’t want all your eggs in one vendor basket, or all your raindrops in one single cloud.

Worse, you might have a requirement to call an R API from D365 FinOps. In this blog post I’ll show you how.

First things first, let’s build a simple R model using the Prophet library from Facebook to do forecasting. This uses a data frame with two columns, y & ds to feed a time series set of values (y) based on time (ds). Prophet supports a lot of parameters for seasonality and such and I suggest reading up on it.

For our example I’ll keep things simple, and assume the R script won’t be doing any munging or wrangling as such. Clean data frame goes in, Prophet predicts, but instead of returning the y-hat values (Ŷ) we’ll make it interesting and return a set of base64 encoded PNG plots containing the forecast and seasonality trends instead.

So there are a number of challenges for us:

  • We need to host this R model as an API
  • We need to grab the resulting plot predictions created by Prophet
  • Encode the plots to base64 and return it from the API as JSON
  • Call and display this all in D365 from a form

The best way I’ve found to host R as an API is to use the Plumber library. So I’ve deployed a Linux environment in my cloud of choice and installed all the required R libraries, including Plumber, and set up NGINX to route incoming traffic on port 80 to Plumber which listens on port 8000. To call this API from D365 you’ll need to install a certificate as only HTTPS will do between D365 and our Linux box.

The R code is shown below, detailing how we grab the plots and encode it to base64. We also receive our data frame as part of the call so we need to URIDecode it. This will do for small data sets; if you want to tackle a large data set, use a different mechanism of passing a reference to the data, perhaps a POST call with the data in the body as JSON. In our case our API returns JSON containing three base64 encoded plots.

library(prophet)
library(dplyr)
library(ggplot2)
library(png)
library(plumber)
library(urltools)

encodeGraphic <- function(g) {
  png(tf1 <- tempfile(fileext = ".png"))
  print(g)
  dev.off()
  encoded <- RCurl::base64Encode(readBin(tf1, "raw", file.info(tf1)[1, "size"]), "txt")
  return(encoded)
}

#* Do a forecast
#* @param data a CSV containing ordered, orderdate
#* @get /forecast
function(data="")
{
  json = '{"forecast":"'
  tmp<-URLdecode(data)
  stats <- read.csv(text=tmp, header=TRUE, sep=',',colClasses = c('numeric','Date'))
  names(stats) <- c("y","ds")
  stats$ds <- as.Date(stats$ds) # coerce to ensure date type

  m <- prophet(stats, yearly.seasonality=TRUE)
  future <- make_future_dataframe(m, periods = 4, freq="m")
  forecast <- predict(m, future)

  g<-plot(m, forecast) +
    xlab("Date") +
    ylab("Data") +
    theme_grey() +
    theme_grey() +
    theme(panel.grid.major = element_blank(),
          panel.grid.minor = element_blank(),
          axis.line = element_line(colour = "black")) +
    ggtitle("Sales Forecast");

  encodedForecast<-encodeGraphic(g)
  json <- paste(json, encodedForecast,sep='')
  g<-prophet_plot_components(m, forecast)
  json <- paste(json, '","trend":"', sep='')
  encodedTrend <- encodeGraphic(g[1])
  json<-paste(json, encodedTrend,sep='')
  json<-paste(json,'","yearly":"', sep='')
  encodedYearly <- encodeGraphic(g[2])
  json<-paste(json, encodedYearly,sep='')
  json<-paste(json, '"}', sep='')
  return(json)
}

 

Next up we’ll create an extensible control in D365 to host our plots. I like wrapping things in extensible controls as it gives me the ability to obfuscate the JavaScript to protect any commercial IP. So I try to keep as little as possible in X++ and as much as possible in JavaScript.

Here is the code for our BuildControl, just a single CSV property is defined:

[FormDesignControlAttribute("Forecast")]
class ForecastControlBuild extends FormBuildControl
{
    str csv = "";

    [FormDesignPropertyAttribute("CSV","Forecast")]
    public str parmCSV(str _csv=csv)
    {
        if (prmIsDefault(_csv))
        {
            csv = _csv;
        }
        return csv;
    }
}

 

Followed by the code for our Control class that contains our CSV property that we will populate from our X++ form.

[FormControlAttribute('Forecast','',classstr(ForecastControlBuild))]
class ForecastControl extends FormTemplateControl
{
    FormProperty csv;

    public void new(FormBuildControl _build, FormRun _formRun)
    {
        super(_build, _formRun);
        this.setTemplateId('Forecast');
        this.setResourceBundleName('/resources/html/Forecast');
        csv = properties.addProperty(methodStr(ForecastControl, parmCSV), Types::String);
    }

    [FormPropertyAttribute(FormPropertyKind::Value, "CSV")]
    public str parmCSV(str _value = csv.parmValue())
    {
        if (!prmIsDefault(_value))
        {
            csv.setValueOrBinding(_value);
        }
        return csv.parmValue();
    }

    public void applyBuild()
    {
        super();
        ForecastControlBuild build = this.build();

        if (build)
        {
            this.parmCSV(build.parmCSV());
        }
    }
}

 

We’ll add a minimal control HTML file to host our image placeholders. Three simple DIV controls with image controls with their ID’s set to forecastImage, trendImage and yearlyImage respectively, so we can get hold of them from our JavaScript code.

Finally the JavaScript for our control containing the actual Ajax call to our R API.

(function () {
    'use strict';
    $dyn.controls.Forecast = function (data, element) {
        $dyn.ui.Control.apply(this, arguments);
        $dyn.ui.applyDefaults(this, data, $dyn.ui.defaults.Forecast);
    };

    $dyn.controls.Forecast.prototype = $dyn.ui.extendPrototype($dyn.ui.Control.prototype, {
        init: function (data, element) {
            var self = this;
            $dyn.ui.Control.prototype.init.apply(this, arguments);
            $dyn.observe(data.CSV, function (csv)
            {
                document.getElementById('forecastImage').style.display = "none";
                document.getElementById('trendImage').style.display = "none";
                document.getElementById('yearlyImage').style.display = "none";
                if (csv.length>0)
                {
                    var url = 'https://yourboxhere.australiaeast.cloudapp.azure.com/forecast?data=' + csv;
                    $.ajax({
                        crossOrigin: true,
                        url: url,
                        success: function (data) {
                            var obj = JSON.parse(data);
                            var forecast = obj.forecast;
                            var trend = obj.trend;
                            var yearly = obj.yearly;

                            document.getElementById('forecastImage').src = 'data:image/png;base64,' + forecast;
                            document.getElementById('trendImage').src = 'data:image/png;base64,' + trend;
                            document.getElementById('yearlyImage').src = 'data:image/png;base64,' + yearly;
                            document.getElementById('forecastImage').style.display = "block";
                            document.getElementById('trendImage').style.display = "block";
                            document.getElementById('yearlyImage').style.display = "block";
                        }
                    });
                }
            })
        }
    });
})();

 

So far it’s all fairly simple, and we can add a demo form in X++ to use our extensible control. We’ll grab some sales orders from D365, URI encode it manually and then send it off to our extensible control to pass to our R API sitting somewhere outside the D365 cloud.

class ForecastFormClass
{
    ///

    ///
    ///

 

    ///
    ///
    [FormControlEventHandler(formControlStr(ForecastForm, FormCommandButtonControl1), FormControlEventType::Clicked)]
    public static void FormCommandButtonControl1_OnClicked(FormControl sender, FormControlEventArgs e)
    {
        FormCommandButtonControl callerButton = sender as FormCommandButtonControl; 
        FormRun form = callerButton.formRun();
        ForecastControl forecastControl;
        forecastControl = form.control(form.controlId("ForecastControl1"));

        SalesLine   SalesLine;
        date        varStartPeriodDT    = mkdate(1, 1, 2015);
        date        varEndPeriodDT      = mkDate(1,7,2016);
        str         csv                 = "ordered%2Corderdate%0D%0A";

        while select sum(QtyOrdered), ShippingDateRequested  from SalesLine group by ShippingDateRequested
            where SalesLine.ShippingDateRequested >= varStartPeriodDT && SalesLine.ShippingDateRequested <= varEndPeriodDT &&  SalesLine.ItemId == 'T0001'
        {
            csv = csv + int2str(SalesLine.QtyOrdered) + "%2C" + date2str(SalesLine.ShippingDateRequested,321,2,3,2,3,4) + "+00%3A00%3A00%0D%0A";
        }
        forecastControl.parmCSV(csv);
    }
}

 

A second or two later and we receive our plots.

AXForecast

Pretty simple stuff. We can extend this further by passing various parameters to the R API, for example, which time-series model we would like to use, whether to return the predicted values (Ŷ) or not, seasonality parameters and anything else we need.

Advertisements

Visualize 3D Models in D365 FinOps

In this short blog post I’m going to show you how to build a 3D extensible control using the Extensible Control Framework in Dynamics 365 FinOps (AX). This can come in handy for ISV’s working in the manufacturing or additive manufacturing space (3D Printing).

Being able to fully visualize and interact with 3D models of parts within D365 brings us one step close to having a full end to end ERP > 3D printing interface, which is a side project I am working on.

Extensible controls allows us to build self-contained visual controls that we can share and allow other developers to simply drop onto a form. There are basically 3 main parts to it, the HTML, optional JavaScript, and the X++ class for the control itself, which allows us to communicate between the web browser front-end and the X++ back-end side of things.

For this post I’ll focus on the STL file format, arguably the most popular of the 3D formats, and widely used by 3D printers. We’ll add some basic properties to the control, including the URL of the STL file we want to visualize, object color and control height and width. This can be extended further of course, but we’ll keep things simple for a start.

We’ll start with the X++ class (or classes, in this case) which consists of the Control and BuildControl classes. The BuildControl class is where we define our controls public properties that once dropped on a form can be set by the X++ developer, and maintained during runtime. The source for our class is shown below.

/// <summary>
/// Build Control for 3D STL Viewer
/// </summary>
[FormDesignControlAttribute("XalSTL")]
class XalSTLControlBuild extends FormBuildControl
{
    str url = "";
    int innerHeight = 540;
    int innerWidth = 1024;
    int objectColor = 925765; //dark blue
    int objectShininess = 200;

}

[FormDesignPropertyAttribute("URL","XalSTL")]
public str parmURL(str _url=url)
{
    if (prmIsDefault(_url))
    {
        url = _url;
    }
    return url;
}

[FormDesignPropertyAttribute("InnerHeight","XalSTL")]
public int parmInnerHeight(int _innerHeight=innerHeight)
{
    if (prmIsDefault(_innerHeight))
    {
        innerHeight = _innerHeight;
    }
    return innerHeight;
}

[FormDesignPropertyAttribute("InnerWidth","XalSTL")]
public int parmInnerWidth(int _innerWidth=innerWidth)
{
    if (prmIsDefault(_innerWidth))
    {
        innerWidth = _innerWidth;
    }
    return innerWidth;
}

[FormDesignPropertyAttribute("ObjectColor","XalSTL")]
public int parmObjectColor(int _objectColor=objectColor)
{
    if (prmIsDefault(_objectColor))
    {
        objectColor = _objectColor;
    }
    return objectColor;
}

[FormDesignPropertyAttribute("ObjectShininess","XalSTL")]
public int parmObjectShininess(int _objectShininess=objectShininess)
{
    if (prmIsDefault(_objectShininess))
    {
        objectShininess = _objectShininess;
    }
    return objectShininess;
}

 

Next up is our main control class, with source below. Not much happening here, just basic framework stuff.

/// <summary>
/// Defines a 3D STL Viewer Control
/// </summary>
[FormControlAttribute('XalSTL','',classstr(XalSTLControlBuild))]
class XalSTLControl extends FormTemplateControl
{
    FormProperty url;
    FormProperty innerHeight;
    FormProperty innerWidth;
    FormProperty objectColor;
    FormProperty objectShininess;

}

protected void new(FormBuildControl _build, FormRun _formRun)
{
    super(_build, _formRun);
 
    this.setTemplateId('XalSTL');
    this.setResourceBundleName('/resources/html/XalSTL');

    url = properties.addProperty(methodStr(XalSTLControl, parmURL), Types::String);
    innerHeight = properties.addProperty(methodStr(XalSTLControl, parmInnerHeight), Types::Integer);
    innerWidth = properties.addProperty(methodStr(XalSTLControl, parmInnerWidth), Types::Integer);
    objectColor = properties.addProperty(methodStr(XalSTLControl, parmObjectColor), Types::Integer);
    objectShininess = properties.addProperty(methodStr(XalSTLControl, parmObjectShininess), Types::Integer);
}

[FormPropertyAttribute(FormPropertyKind::Value, "URL")]
public str parmURL(str _value = url.parmValue())
{
    if (!prmIsDefault(_value))
    {
        url.setValueOrBinding(_value);
    }
    return url.parmValue();
}

[FormPropertyAttribute(FormPropertyKind::Value, "InnerHeight")]
public int parmInnerHeight(int _value = innerHeight.parmValue())
{
    if (!prmIsDefault(_value))
    {
        innerHeight.setValueOrBinding(_value);
    }
    return innerHeight.parmValue();
}

[FormPropertyAttribute(FormPropertyKind::Value, "InnerWidth")]
public int parmInnerWidth(int _value = innerWidth.parmValue())
{
    if (!prmIsDefault(_value))
    {
        innerWidth.setValueOrBinding(_value);
    }
    return innerWidth.parmValue();
}

[FormPropertyAttribute(FormPropertyKind::Value, "ObjectColor")]
public int parmObjectColor(int _value = objectColor.parmValue())
{
    if (!prmIsDefault(_value))
    {
        objectColor.setValueOrBinding(_value);
    }
    return objectColor.parmValue();
}

[FormPropertyAttribute(FormPropertyKind::Value, "ObjectShininess")]
public int parmObjectShininess(int _value = objectShininess.parmValue())
{
    if (!prmIsDefault(_value))
    {
        objectShininess.setValueOrBinding(_value);
    }
    return objectShininess.parmValue();
}

public void applyBuild()
{
    super();
 
    XalSTLControlBuild build = this.build();
 
    if (build)
    {
        this.parmURL(build.parmURL());
        this.parmInnerHeight(build.parmInnerHeight());
        this.parmInnerWidth(build.parmInnerWidth());
    }
}

 

We also add the control HTML which contains little more than a DIV which we will use as a canvas for our 3D viewer. I reference four additional files containing a modified version of the THREEJS library, which I’ll share upon request.

<meta name="viewport" content="width=1024, user-scalable=no, initial-scale=0.5, minimum-scale=0.2, maximum-scale=0.5">

src="/resources/scripts/three.js">
src="/resources/scripts/STLLoader.js">
src="/resources/scripts/Detector.js">
src="/resources/scripts/OrbitControls.js">
id="XalSTL" style="max-height:400px;" data-dyn-bind="visible: $data.Visible">
/>
/>
src="/resources/scripts/XalSTL.js">

 

Finally, our control JavaScript contains the nuts and bolts that ties all this down into our control and makes this all work, fast and efficient, in D365. You’ll notice that our control has a URL parameter, and this allows us to store our (large) 3D models in Azure Blob Storage or via the dedicated storage account available within D365, via X++ code.

(function () {
    'use strict';
    $dyn.controls.XalSTL = function (data, element) {
        $dyn.ui.Control.apply(this, arguments);
        $dyn.ui.applyDefaults(this, data, $dyn.ui.defaults.XalSTL);
    };
 
    $dyn.controls.XalSTL.prototype = $dyn.ui.extendPrototype($dyn.ui.Control.prototype, {
        init: function (data, element) {
            var self = this;

            var _url = "";
            var _innerHeight = 540;
            var _innerWidth = 1024;
            var _objectColor = 0x0e2045;
            var _objectShininess = 200;

            $dyn.ui.Control.prototype.init.apply(this, arguments);
 
            if (!Detector.webgl) Detector.addGetWebGLMessage();
            var camera, scene, renderer;
            scene = new THREE.Scene();
            scene.add(new THREE.AmbientLight(0x999999));
            camera = new THREE.PerspectiveCamera(35, _innerWidth / _innerHeight, 1, 500);
            camera.up.set(0, 0, 1);
            camera.position.set(0, -9, 6);
            camera.add(new THREE.PointLight(0xffffff, 0.8));
            scene.add(camera);
            var grid = new THREE.GridHelper(25, 50, 0xffffff, 0x555555);
            grid.rotateOnAxis(new THREE.Vector3(1, 0, 0), 90 * (Math.PI / 180));
            scene.add(grid);
            renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setClearColor(0x999999);
            renderer.setPixelRatio(window.devicePixelRatio);
            renderer.setSize(_innerWidth, _innerHeight);
            $(".XalSTL").context.activeElement.appendChild(renderer.domElement)

            $dyn.observe(data.URL, function (url) {
                if (url.toString().length > 0) {
                    _url = url;
                    RefreshModel();
                }
            });

            $dyn.observe(data.InnerHeight, function (innerHeight) {
                _innerHeight = innerHeight;
                RefreshModel();
            });

            $dyn.observe(data.InnerWidth, function (innerWidth) {
                _innerWidth = innerWidth;
                RefreshModel();
            });

            $dyn.observe(data.ObjectColor, function (objectColor) {
                _objectColor = objectColor;
                RefreshModel();
            });

            $dyn.observe(data.ObjectShininess, function (objectShininess) {
                _objectShininess = objectShininess;
                RefreshModel();
            });

            function RefreshModel()
            {
                if (_url.toString().length > 0) {
                    var loader = new THREE.STLLoader();
                    var material = new THREE.MeshPhongMaterial({ color: _objectColor, specular: 0x111111, _objectShininess: 200 });
                    var controls = new THREE.OrbitControls(camera, renderer.domElement);
                    loader.load(_url, function (geometry) {
                        var mesh = new THREE.Mesh(geometry, material);
                        mesh.position.set(0, 0, 0);
                        mesh.rotation.set(0, 0, 0);
                        mesh.scale.set(.02, .02, .02);
                        mesh.castShadow = true;
                        mesh.receiveShadow = true;
                        scene.add(mesh);
                        render();
                        controls.addEventListener('change', render);
                        controls.target.set(0, 1.2, 2);
                        controls.update();
                        window.addEventListener('resize', onWindowResize, false);
                    });
                }
            }

            function onWindowResize() {
                camera.aspect = _innerWidth / _innerHeight;
                camera.updateProjectionMatrix();
                renderer.setSize(_innerWidth, _innerHeight);
                render();
            }

            function render() {
                renderer.render(scene, camera);
            }
        }
    });
})();

 

We can construct a basic demo dialog form as shown below, hit the Reload button and wait for the magic. Using the mouse, we can zoom in and out, and rotate the object in 3D.

XalSTL

Adding a Fabricate button can trigger an event to kick-start our 3D printing process. To tie this all together we can use the rest of the services in D365 for a proper end to end manufacturing pipeline containing CRM, BOM, Invoicing, Projects and everything else normally involved in manufacturing, without leaving the D365 UI.

For a demo video showing this control in action, click here

Forensic Analysis with Python & Benford’s Law

Early in my career I specialised in Computer Security and more specifically Data Security. On one particular engagement I was confronted with a system that had virtually no audit log capability and very limited access control (mainframe based), and the suspicion was that staff was being paid off to alter transactional data.

The tools I had at my disposal was Microsoft Access, a basic CSV transaction log and a copy of Borland Delphi and I focussed on analysing and detecting changes in processing volume of data operators as an indication of suspicious activity, with some good success. Looking back, I wish I knew about Benford’s Law, as that would have certainly made my life much easier. Now 20 years later I work extensively in global payroll within the Microsoft Dynamics 365 ERP market, and while the threat of fraud remains, the tools and processing capability have advanced and improved dramatically.

From Wikipedia: “Benford’s law, also called Newcomb-Benford’s law, law of anomalous numbers, and first-digit law, is an observation about the frequency distribution of leading digits in many real-life sets of numerical data. The law states that in many naturally occurring collections of numbers, the leading significant digit is likely to be small. For example, in sets that obey the law, the number 1 appears as the most significant digit about 30% of the time, while 9 appears as the most significant digit less than 5% of the time. If the digits were distributed uniformly, they would each occur about 11.1% of the time. Benford’s law also makes predictions about the distribution of second digits, third digits, digit combinations, and so on.”

Payroll data as with any ERP financial data can consist of thousands or hundreds of thousands of transactions per pay run. Consider a typical worker with 10 to 15 different payments (or allowances) across a workforce of 5000 workers. This often generates 75,000 or more transactions per pay run and auditing of this volume, which can then be run weekly, fortnightly or monthly (thus 75,000 x 4 per month) presents a significant workload problem. Spot-checking becomes unfeasible unless you could reduce your focus to transactions that may require further scrutiny.

Consider a policy requiring approval of expenses that exceed $300. As long as you submit expenses totalling no more than $290 odd you might be able to sneak this through every so often, and while this is no heist, this amount can still add up over time. Anti-Money Laundering systems often utilize hundreds of rules, one typically detects money transfers exceeding a cut-off of $10,000 before raising a flag requiring bank approval. If you travel internationally often enough, you’ll see that $10,000 amount on arrival and departure cards all the time.

Let’s take a few thousand rows of allowance data, which includes salary and miscellaneous allowances and sanitize it by removing identifying columns, leaving only the amount column.

Our test data is shown below.

DataNotFake

I’ll be using a Python library available here that implements Benford’s Law by testing our null hypothesis and displaying a graph showing the digit distribution. A screenshot of the script is shown below, running in Visual Studio Code on Ubuntu Linux.

CodeView

I’ve modified the script and ran it against our clean, non-modified data and the resulting digit distribution is shown below.

NotFake

We can see a fairly good expected distribution curve with slight elevation of digit ‘6’ and ‘5’ being a bit low, but still within a fairly normal distribution. You need to understand the data fairly well to explain any deviations such as this. Here it could be that all employees receive a single allowance fixed at $60, producing the elevation. We are experimenting here after all, don’t assume you can load a bunch of numbers from a spreadsheet and this script will become your magic fraud detection silver bullet.

Let’s manually modify our test data by replacing some allowances with random numbers. An extract is shown below and notice the numerous 4xx digit amounts now occurring (my manually modified amount).

DataFaked

Running our script again produces the plot below, clearly indicating an elevation of digit ‘4’ occurring when the natural expectation of occurrence was much less. Other figures are also off as a consequence, especially ‘7’.

Fakes

With this in hand, we can now isolate these occurrences in our data and perform a deeper inspection and validation of these transactions, the associated workers and approver of the workflow, if that was required. Spot-checking, but across a more narrow area of focus.

For further reading I recommend the work done by Mark Nigrini on the subject.

Easy Dynamics 365/AX Blockchain Integration

This post continues to explore Blockchain integration into Microsoft Dynamics 365 for Finance and Operations (AX). I’ve seen examples where developers did integration using the MetaMask Chrome extension, however I want something that looks and feels like pure AX.

For this post I will be using Xalentis Fusion which provides seamless Blockchain integration, whereby Blockchain I refer to Ethereum. I don’t see much use for Bitcoin, and apart from hundreds of other altcoins available, I see more enterprise-level movement towards Ethereum or its variants, including JP Morgans’ Quorum or Microsoft “Project Bletchley”.

Xalentis Fusion works by detecting new transactions in an Ethereum Blockchain and allows filtering to take place across those. Once a filter detects your specific requirements, it can fire off any number of associated rules, written in simple script. Fusion also includes a growing API allowing REST-based integration with the outside world, which is what we will be using.

Fusion provides a Transact API method allowing transactions to be made via a REST call. We can do this ourselves easily as well, but since we’ll be using Fusion for more than transacting (later on in this post) I figured I’ll just stick with it.

We’ll keep it very basic, and create a simple form accepting a number of parameters that we will use to perform a transaction. Our form design is shown below.

BeforeTransact

We’ve added fields for Node, Sender, Recipient, Sender’s account Password, and the amount of Wei to send. Depending on the Ethereum network you are connecting to, adjust the Node value accordingly, or simply hardcode it and remove the field. We are using the Fusion Test Net so that is shown. I’ve created two addresses in the Test Net, also shown in the form design, and loaded the first with some Ether. Perhaps customers want to trade in Dollars, so you can add code to convert Dollars entered into Wei or whatever token is in use. We’ll stick with Wei for now.

Let’s submit this transaction.

AfterTransact

As you can see, the transaction has been submitted to the Blockchain and an Infolog displayed showing success. The X++ code is shown below, including the form class and a utility class that performs our REST calls to Fusion. I’ve added a TransactionRequest class as the POST action we are performing requires JSON being passed, and wrapped the class members with the DataContract attributes to enable easy serialization. This particular POST call accepts the full JSON as part of the POST URL, wrapped as Base64, and that is done in the utility class. The body is required, so we set the content-length to 0.

[DataContractAttribute]
class TransactRequestClass
{
    str addressFrom;
    str addressTo;
    str node;
    str password;
    str wei;

    [DataMemberAttribute]
    public str AddressFrom(str _addressFrom = addressFrom)
    {
        addressFrom = _addressFrom;
        return addressFrom;
    }

    [DataMemberAttribute]
    public str AddressTo(str _addressTo = addressTo)
    {
        addressTo = _addressTo;
        return addressTo;
    }

    [DataMemberAttribute]
    public str Node(str _node = node)
    {
        node = _node;
        return node;
    }

    [DataMemberAttribute]
    public str Password(str _password = password)
    {
        password = _password;
        return password;
    }

    [DataMemberAttribute]
    public str Wei(str _wei = wei)
    {
        wei = _wei;
        return wei;
    }
}

class FusionUtilityClass
{
    public static str Transact(str addressFrom, str addressTo, str password, str node, str wei)
    {
        System.Net.WebClient webClient;
        System.Text.UTF8Encoding encoder;
        System.Text.UnicodeEncoding decoder;
        System.IO.Stream s;
        System.IO.StreamReader sr;
        System.Net.HttpWebRequest myRequest;
       
        try
        {
            TransactRequestClass request = new TransactRequestClass();
            request.AddressFrom(addressFrom);
            request.AddressTo(addressTo);
            request.Node(node);
            request.Password(password);
            request.Wei(wei);

            encoder = new System.Text.UTF8Encoding();
            str json = FormJsonSerializer::serializeClass(request);
            System.Byte[] encodedBytes = encoder.GetBytes(json);
            str encoded64 = System.Convert::ToBase64String(encodedBytes);
 
            str url = "http://fusionapi.azurewebsites.net/api/transact?bodyJson=" + encoded64;
            myRequest = System.Net.WebRequest::Create(url);
            myRequest.Method = "POST";
            myRequest.Timeout = 30000;
            myRequest.ContentLength = 0;

            System.Net.WebHeaderCollection headers = myRequest.Headers;
            headers.Add("API_KEY", "your fusion api key");

            s = myRequest.GetResponse().GetResponseStream();
            sr = new System.IO.StreamReader(s);
            str txnHash = sr.ReadToEnd();
            s.Close();
            sr.Close();
            return txnHash;
        }
        catch (Exception::Error)
        {
        }
        return "";
    }
}

class XalentisTestFormClass
{
    
    [FormControlEventHandler(formControlStr(XalentisTestForm, FormButtonControl1), FormControlEventType::Clicked)]
    public static void FormButtonControl1_OnClicked(FormControl sender, FormControlEventArgs e)
    {
        FormStringControl nodeControl = sender.formRun().control(sender.formRun().controlId("FormStringControl1"));
        FormStringControl addressSenderControl = sender.formRun().control(sender.formRun().controlId("FormStringControl2"));
        FormStringControl addressRecipientControl = sender.formRun().control(sender.formRun().controlId("FormStringControl3"));
        FormStringControl passwordControl = sender.formRun().control(sender.formRun().controlId("FormStringControl4"));
        FormStringControl weiControl = sender.formRun().control(sender.formRun().controlId("FormStringControl5"));

        str addressFrom = addressSenderControl.Text();
        str node = nodeControl.Text();
        str addressTo = addressRecipientControl.Text();
        str password = passwordControl.Text();
        str wei = weiControl.Text();

        str txnHash = FusionUtilityClass::Transact(addressFrom, addressTo, Password, node, wei);
        //todo: store txnHash for history purposes.

        info("Transaction Posted");
    }
}

That works pretty well, but users don’t understand Blockchain addresses, and it would be painful to maintain that somewhere in notepad or Excel to copy and paste each time a transaction is made. Luckily Fusion provides an Account Mapping facility, which is a customer-specific key/value table mapping Blockchain addresses to friendly names, or account numbers the rest of us can readily understand.

So instead of entering address for Sender and Recipient, let’s modify our form as shown below. We can use drop-downs to pull up a list of known accounts, or use an API call to Fusion to return a full list of mapped accounts which we can then allow users to select. I’ll keep it simple with a text field. Here we’ve entered two known friendly account names we can read and verify. These could come from your chart of accounts as well, whatever works best in your scenario. As long as the display text maps to an address in Fusion, it can be resolved.

BeforeTransact2

I’ve modified our form class and utility class to add two additional API calls to Fusion to resolve the friendly names to Ethereum addresses as shown in the code below. We simply make a GET call to Fusion passing across the friendly name, and Fusion will perform the lookup, returning the proper Ethereum address we need to use when performing the transaction. The updated code is shown below.

class XalentisTestFormClass
{
    
    [FormControlEventHandler(formControlStr(XalentisTestForm, FormButtonControl1), FormControlEventType::Clicked)]
    public static void FormButtonControl1_OnClicked(FormControl sender, FormControlEventArgs e)
    {
        FormStringControl addressSenderControl = sender.formRun().control(sender.formRun().controlId("FormStringControl2"));
        FormStringControl addressRecipientControl = sender.formRun().control(sender.formRun().controlId("FormStringControl3"));
        FormStringControl passwordControl = sender.formRun().control(sender.formRun().controlId("FormStringControl4"));
        FormStringControl weiControl = sender.formRun().control(sender.formRun().controlId("FormStringControl5"));

        str addressFrom = addressSenderControl.Text();
        str addressTo = addressRecipientControl.Text();
        str password = passwordControl.Text();
        str wei = weiControl.Text();

        str txnHash = FusionUtilityClass::Transact(addressFrom, addressTo, Password, wei);
        //todo: store txnHash for history purposes.

        info("Transaction Posted");
    }
}

class FusionUtilityClass
{
    public static str Transact(str accountFrom, str accountTo, str password, str wei)
    {
        System.Net.WebClient webClient;
        System.Text.UTF8Encoding encoder;
        System.Text.UnicodeEncoding decoder;
        System.IO.Stream s;
        System.IO.StreamReader sr;
        System.Net.HttpWebRequest myRequest;
       
        try
        {
            str addressFrom;
            str addressTo;

            str url = "http://fusionapi.azurewebsites.net/api/address/" + strReplace(accountFrom, " ","%20");
            myRequest = System.Net.WebRequest::Create(url);
            myRequest.Method = "GET";
            myRequest.Timeout = 30000;
            System.Net.WebHeaderCollection headers = myRequest.Headers;
            headers.Add("API_KEY", your fusion api key);
            s = myRequest.GetResponse().GetResponseStream();
            sr = new System.IO.StreamReader(s);
            addressFrom = sr.ReadToEnd();
            s.Close();
            sr.Close();

            url = "http://fusionapi.azurewebsites.net/api/address/" + strReplace(accountTo, " ","%20");
            myRequest = System.Net.WebRequest::Create(url);
            myRequest.Method = "GET";
            myRequest.Timeout = 30000;
            headers = myRequest.Headers;
            headers.Add("API_KEY", "your fusion api key");
            s = myRequest.GetResponse().GetResponseStream();
            sr = new System.IO.StreamReader(s);
            addressTo = sr.ReadToEnd();
            s.Close();
            sr.Close();

            TransactRequestClass request = new TransactRequestClass();
            request.AddressFrom(strReplace(addressFrom,"\"",""));
            request.AddressTo(strReplace(addressTo,"\"",""));
            request.Node("http://xaleth4kq.eastus.cloudapp.azure.com:8545"); // hardcoded now
            request.Password(password);
            request.Wei(wei);
            encoder = new System.Text.UTF8Encoding();
            str json = FormJsonSerializer::serializeClass(request);
            System.Byte[] encodedBytes = encoder.GetBytes(json);
            str encoded64 = System.Convert::ToBase64String(encodedBytes);
            url = "http://fusionapi.azurewebsites.net/api/transact?bodyJson=" + encoded64;
            myRequest = System.Net.WebRequest::Create(url);
            myRequest.Method = "POST";
            myRequest.Timeout = 30000;
            myRequest.ContentLength = 0;
            headers = myRequest.Headers;
            headers.Add("API_KEY", "your fusion api key");
            s = myRequest.GetResponse().GetResponseStream();
            sr = new System.IO.StreamReader(s);
            str txnHash = sr.ReadToEnd();
            s.Close();
            sr.Close();
            return txnHash;
        }
        catch (Exception::Error)
        {
        }
        return "";
    }
}

We’ll submit this transaction, and as shown we’ve got success.

AfterTransact2

I hope this was helpful. One final item to note is using Wei, which is a BigInteger. I’ve used strings to remove the need for dealing with BigInteger types in X++.

 

GPS, IoT, Blockchain Integration to ERP

I’ve read a number of articles discussing how Blockchain could have a significant impact on Trade & Logistics, especially item tracking. Granted, Blockchain is not a requirement for shipment tracking, but it does deliver a number of benefits through being a shared, secure ledger that, depending on the network, could provide an automatic openness almost immediately. That is a vast improvement over building a custom customer and partner portal to query legacy backend systems.

Of course, there remains the problem of now having to integrate Blockchain into your legacy ERP system, a whole different level of headache. So, in this post I’m going to do a simple POC to simulate how easy, or hard, it would be to build an item tracking service using Ethereum Blockchain, add to that GPS tracking with temperature and humidity monitoring, and get that to your ERP system, in this case Microsoft Dynamics 365. I want to achieve that without modifying the ERP system in any way, by using Microsoft Flow, a PowerApp and Microsoft Common Data Service. The idea is that end users, customers or partners can use the PowerApp to monitor shipments and climate conditions in real-time. Supply-chain visibility every step of the way, basically.

To start, I built a simple IoT monitoring device around the Adafruit Huzzah. I’ll be using WiFi here, making a wild assumption that WiFi is available wherever this device goes. In the real world, GPRS or Loran might be more suitable, but I don’t have that available in my toolkit just yet and besides, this is an experiment only. I’ve added a low-cost GPS, DHT11 temperature and humidity sensor, and an LCD screen to show me what is happening without requiring connecting to my laptop via the serial interface. The basic IoT device is shown below, with GPS and DHT-11 working and transmitting data.

Circuit

The C code for the IoT device is shown below. I do a POST to my Ethereum network of choice with hardcoded addresses, and embed the GPS coordinates and DHT11 state into the data portion of the Ethereum transaction. Addressing and data is entirely up to you; perhaps instead of hardcoding, this can all be read off an SD card.

#include <DHT.h>
#include "TinyGPS++.h"
#include <SoftwareSerial.h>
#include <Adafruit_SSD1306.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266WiFi.h>
#include <ArduinoJson.h>
#include <Wire.h>
#include "Adafruit_GFX.h"
#include "Adafruit_SSD1306.h"

#define SSID "WiFiSSID" 
#define PASS "mypassword" 
#define OLED_RESET LED_BUILTIN
#define DHTPIN 12
#define DHTTYPE DHT11

TinyGPSPlus gps;
DHT dht(DHTPIN, DHTTYPE);
Adafruit_SSD1306 display(OLED_RESET);
SoftwareSerial mySerial(13, 15);

const char *gpsStream =
  "$GPRMC,045103.000,A,3014.1984,N,09749.2872,W,0.67,161.46,030913,,,A*7C\r\n"
  "$GPGGA,045104.000,3014.1985,N,09749.2873,W,1,09,1.2,211.6,M,-22.5,M,,0000*62\r\n"
  "$GPRMC,045200.000,A,3014.3820,N,09748.9514,W,36.88,65.02,030913,,,A*77\r\n"
  "$GPGGA,045201.000,3014.3864,N,09748.9411,W,1,10,1.2,200.8,M,-22.5,M,,0000*6C\r\n"
  "$GPRMC,045251.000,A,3014.4275,N,09749.0626,W,0.51,217.94,030913,,,A*7D\r\n"
  "$GPGGA,045252.000,3014.4273,N,09749.0628,W,1,09,1.3,206.9,M,-22.5,M,,0000*6F\r\n";

void setup() {
  Serial.begin(9600);
  dht.begin();
  display.setCursor(0,0);
  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  display.clearDisplay();
  display.display();
  display.setTextSize(1);
  display.setTextColor(WHITE);
  display.println("Connecting");
  display.println("to");
  display.println("WiFi...");
  display.display();

  WiFi.begin(SSID, PASS);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  } 
  display.clearDisplay();
  display.display();
  display.setCursor(0,0);
  display.println("EtherGPS");
  display.println("www.xalentis.com");
  display.display();
  mySerial.begin(38400); // GPS
  delay(5000); // warm up GPS 
  display.clearDisplay();
  display.display();
  display.setCursor(0,0);
  display.println("Scanning...");
  display.display();
}

void loop() 
{
  while (*gpsStream)
    if (gps.encode(*gpsStream++))
      updateInfo();
}

void updateInfo()
{
  float h = dht.readHumidity();
  float t = dht.readTemperature();

  if (gps.location.isValid())
  {
    display.clearDisplay();
    display.display();
    display.setCursor(0,0);
    display.println("Temp:" + String(t));
    display.println("Hum:" + String(h));
    display.println("Lat:" + String(gps.location.lat()));
    display.println("Lon:" + String(gps.location.lng()));
    display.display();

    String data = "0x";
    String deviceSerial = "3132333435"; // 12345 in HEX
    data = data + deviceSerial + "2c"; // comma
    
    String temp = String(t);
    String hum = String(h);
    String lat = String(gps.location.lat());
    String lon = String(gps.location.lng());
    byte buffer[255]={0};
    
    //temp
    temp.getBytes(buffer, 255, 0);
    for (int i=0;i<=temp.length()-1;i++)
    {
      data = data + String((int)buffer[i], HEX);
    }
    data = data + deviceSerial + "2c"; // comma

    //hum
    hum.getBytes(buffer, 255, 0);
    for (int i=0;i<=hum.length()-1;i++)
    {
      data = data + String((int)buffer[i], HEX);
    }
    data = data + deviceSerial + "2c"; // comma

    //latitude
    lat.getBytes(buffer, 255, 0);
    for (int i=0;i<=lat.length()-1;i++)
    {
      data = data + String((int)buffer[i], HEX);
    }
    data = data + deviceSerial + "2c"; // comma

    //longitude
    lon.getBytes(buffer, 255, 0);
    for (int i=0;i<=lon.length()-1;i++)
    {
      data = data + String((int)buffer[i], HEX);
    }

    // build up our Ethereum transaction
    StaticJsonBuffer<1000> JSONbufferTwo;  
    JsonObject& uploadJSON = JSONbufferTwo.createObject(); 
    uploadJSON["jsonrpc"] = "2.0";
    uploadJSON["method"] = "personal_sendTransaction";      
    JsonArray&  uploadQueryParams = uploadJSON.createNestedArray("params");
    JsonObject& callTxParams = JSONbufferTwo.createObject();
    callTxParams["from"] = "0x27f6f763ae5c52721db57c4423c298a78de1f22a";
    callTxParams["to"] = "0xcaade3aa018d57d808fceb16824c47dfd206484c";
    callTxParams["value"] = "0x6FC23AC00"; //hex value 30 Gwei 
    callTxParams["gas"] = "0x30D40"; //hex value for 200000 -high gas limit for good measure          
    callTxParams["gasPrice"] = "0x6FC23AC00"; //hex value 30 Gwei gasprice 21gwei is typical
    callTxParams["data"] = data; // device,tem,hum,lat,long
    uploadQueryParams.add(callTxParams);
    uploadQueryParams.add("myetherpassword");
    uploadJSON["id"] = 1;
    String uploadString;
    uploadJSON.printTo(uploadString);
    callGeth(uploadString); // send for mining
  }
}

String callGeth(String inputJSON) 
{
  HTTPClient http;
  http.begin("http://13.72.73.21:8545/");
  http.addHeader("Content-Type", "application/json");
  int httpCode = http.POST(inputJSON);
  String JSONResult = http.getString(); // contains Txn
  http.end();
  return JSONResult;
}

At this point the transactions are flowing to the Blockchain network and that is great, but we need to be able to monitor the Blockchain for transactions we are interested in, so we can pull that off the Blockchain and into an ERP system, right?

The easiest way to do that is to use Xalentis Fusion. Sign up for a trial account at www.xalentis.com or grab it via Microsoft AppSource. Once signed-up, and logged-in, you’ll end up at the main dashboard as shown below.

xalgps1

Follow the Getting Started tutorial which takes about 10 minutes to create a pair of accounts and top them up with credit as required. The Ethereum network being used is a canned version of Microsoft’s Project Bletchley, so it’s not on the main or test Ethereum networks and can be used without spending any real Ether. You can deploy your own network and use that within the Fusion platform as well, by creating transaction filters pointing to your own deployed RPC node. Make sure your RPC node is visibly outside your firewall, obviously.

The following image shows us having created a transaction filter to monitor the default RPC node at http://xaleth4kq.eastus.cloudapp.azure.com, for any transactions made from the address 0x27f6f763ae5c52721db57c4423c298a78de1f22a. Filters can be created to match any transaction, from any address, or even containing a specific string value in the data portion. This is useful when the address(es) constantly change, while a specific identifier is passed within the data portion, perhaps a Device ID, Company ID or Serial Number of sorts – anything static.

xalgps2

Filters execute rules containing a simple compiled script, and this is where actions are performed on matching transactions. The script below has been added as a rule for our filter.

xalgps3

The rule simply extracts whatever is in the transaction data field, parses that and constructs a JSON packet. This packet will be passed to a Microsoft Flow we will be creating.

We’ll need a place to store our data. Using Flow, we could push directly into Dynamics 365, but since we don’t want to directly modify our ERP by adding a new table, I’ve chosen to use Microsoft Common Data Service as a temporary store instead. The image below shows the new Entity we’ve created with fields for Device, Temperature, Humidity, Longitude and Latitude.

GPS_CDS

Using Microsoft Flow, we’ll first use the Request action to accept an incoming POST request (from our Rule). Next, we’ll take the body of the POST request, parse it, and store the fields into our new CDS Entity. The Flow design is shown below.

GPS_Flow

Use the generated URL from the Flow to update the Rule – the final line calling Flow requires that URL.

Run the Flow, and then power up the IoT device to start submitting GPS and climate information into the Blockchain network. As transactions are mined into new blocks, Fusion will detect the transaction matching our Filter, and execute the associated Rule. The Rule in turn will parse the transaction data field, parse the content, construct it as JSON, and call our Flow with that body content. When the Flow executes, the JSON will be parsed and the data elements inserted as a new record into the CDS, as shown below.

GPS_DataCDS

We can use the data now stored in the CDS to create a PowerApp that displays that information on a Google Map. The PowerApp shown is fairly basic, but with enough time, patience and data this can be turned into something much more interactive, and it is real-time, a vast improvement over building a customer tracking portal from scratch, getting updates only when items are scanned with a barcode or RFID reader.

GPS_Map

Apart from our Rule script, we’ve used virtually no coding, and we’ve not modified our production ERP system in any way. As a bonus, we also have a mobile app that customers and partners can use!

RFID + IoT + Ethereum Blockchain

This is a very quick post, mostly code-only, showing how to read RFID tags on say an assembly line, process those using a WiFi IoT device (Adafruit Huzzah), extract the product serial number from the RFID tag and send that as part of a transaction to an Ethereum Blockchain.

I am using a MiFare RFID card reader, but you would want to use a long-range reader that supports low-cost sticker tags. POSTing the transaction to the blockchain takes a second or two, so don’t expect to scan 100 products/second flying past the reader and manage to submit those as transactions.

URL’s and codes are hard-baked, so consider how you want to post the transaction. I use static Ethereum addresses and stick the product ID in the data portion, you might want to read that from the RFID tags as well.

Once your product ID is in the blockchain, you might want to move that to your ERP system, or even a cool PowerApp or something. Unless you want to code X++ and mess around with your production systems, I suggest using Xalentis Fusion instead, to enable code-free integration between Ethereum and Microsoft Dynamics 365 for Finance and Operations. It also supports SMS, Email messaging, Service Bus messaging, Flow, PowerApps and Common Data Service.

Enjoy.

#include <SPI.h>
#include <MFRC522.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266WiFi.h>
#include <ArduinoJson.h>
 
#define RST_PIN         15
#define SS_PIN          2
#define SSID            "mywifiSSID" 
#define PASS            "password" 
 
MFRC522 mfrc522(SS_PIN, RST_PIN);
 
void setup() {
  Serial1.begin(115200);
  while(!Serial1){}
  Serial.begin(9600);
  SPI.begin();
  mfrc522.PCD_Init();
  WiFi.begin(SSID, PASS);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }
}
 
void loop()
{
  // Using MiFare card reader here, for production use sticker tags and high-speed reader instead
  MFRC522::MIFARE_Key key; // default to FFFFFFFFFFFF
  key.keyByte[0] = 0xFF;
  key.keyByte[1] = 0xFF;
  key.keyByte[2] = 0xFF;
  key.keyByte[3] = 0xFF;
  key.keyByte[4] = 0xFF;
  key.keyByte[5] = 0xFF;
 
  // Loop until RFID tag is presented 
  if (!mfrc522.PICC_IsNewCardPresent()) return;
  if (!mfrc522.PICC_ReadCardSerial()) return;

  byte readbuffer1[18];
  byte readbuffer2[18];
  byte block;
  MFRC522::StatusCode status;
  byte len;
  byte sizeread = sizeof(readbuffer1);
  block = 0;
  
  status = mfrc522.PCD_Authenticate(MFRC522::PICC_CMD_MF_AUTH_KEY_A, block, &key, &(mfrc522.uid));
  if (status != MFRC522::STATUS_OK) {
    return;
  }
  else
  {
    for (int i = 0; i < 18; i++)
    {
      readbuffer1[i] = 0x00;
      readbuffer2[i] = 0x00;
    }
    // read product ID from RFID tag
    status = mfrc522.MIFARE_Read(1, readbuffer1, &sizeread);
    if (status != MFRC522::STATUS_OK)
    {
      return;
    }
    mfrc522.PICC_HaltA();
    mfrc522.PCD_StopCrypto1();
  }
 
  // convert product ID from RFID tag to hex string
  String data = "0x";
  for (int j=0; j<18;j++)
  {
    if (readbuffer1[j]=='\0') break;
    data = data + String(readbuffer1[j], HEX);
  }

  // build up our Ethereum transaction
  StaticJsonBuffer<1000> JSONbufferTwo;  
  JsonObject& uploadJSON = JSONbufferTwo.createObject(); 
  uploadJSON["jsonrpc"] = "2.0";
  uploadJSON["method"] = "personal_sendTransaction";      
  JsonArray&  uploadQueryParams = uploadJSON.createNestedArray("params");
  JsonObject& callTxParams = JSONbufferTwo.createObject();
  callTxParams["from"] = "0x27f6f763ae5c52721db57c4423c298a78de1f22a";
  callTxParams["to"] = "0xcaade3aa018d57d808fceb16824c47dfd206484c";
  callTxParams["value"] = "0x6FC23AC00"; //hex value 30 Gwei 
  callTxParams["gas"] = "0x30D40"; //hex value for 200000 -high gas limit for good measure          
  callTxParams["gasPrice"] = "0x6FC23AC00"; //hex value 30 Gwei gasprice 21gwei is typical
  callTxParams["data"] = data;
  uploadQueryParams.add(callTxParams);
  uploadQueryParams.add("bigsecretaccountpassword");
  uploadJSON["id"] = 1;
  String uploadString;
  uploadJSON.printTo(uploadString);
  callGeth(uploadString); // send for mining
}

String callGeth(String inputJSON) // thanks to https://github.com/gusgorman402
{
  HTTPClient http;
  http.begin("http://your RPC address here:8545/");
  http.addHeader("Content-Type", "application/json");
  int httpCode = http.POST(inputJSON);
  String JSONResult = http.getString(); // contains Txn
  http.end();
  return JSONResult;
}

 

JSON to X++ Classes the easy way

Calling API’s that expect and return JSON from within X++ has become fairly routine and simple with the release of Dynamics 365 for Operations. Recently we moved a module written in C# that resided as external DLL’s within AX, to an Azure-based API that talks JSON via REST. Calling the API was fairly straightforward but manipulating the JSON between X++ and the API seemed like a lot of manual work requiring get/set operations for each property.

Assume you have a class in X++ containing a number of properties, similar to this one below.

class PFLPayrollResultsRequest
{
    str customerID;
    str appKey;
    str legalEntity;
    str payRunNumber;
 
    public str CustomerID(str _customerID = customerID)
    {
        customerID = _customerID;
        return customerID;
    }
 
    public str AppKey(str _appKey = appKey)
    {
        appKey = _appKey;
        return appKey;
    }
 
    public str LegalEntity(str _legalEntity = legalEntity)
    {
        legalEntity = _legalEntity;
        return legalEntity;
    }
 
    public str PayRunNumber(str _payRunNumber = payRunNumber)
    {
        payRunNumber = _payRunNumber;
        return payRunNumber;
    }
}

 

This class might be used for data retrieval and storage within X++, and let’s assume at some point we want to send the state to an external API using JSON. At first glance it seems that we would have to access each individual property and retrieve it from the class, build the JSON string up from that, and then submit that across the wire. The NewtonSoft library gives us a fair bit of methods to work with, however that is external to X++, and NewtonSoft does not understand X++ classes. When the API call returns, it might contain thousands of records which then have to be loaded into X++ classes, property by property.

The solution lies in using the FormJsonSerializer class. Simply decorate your X++ class with a number of attributes (DataContractAttribute, DataMemberAttribute) as shown below:

[DataContractAttribute]
class PFLPayrollResultsRequest
{
    str customerID;
    str appKey;
    str legalEntity;
    str payRunNumber;
 
    [DataMemberAttribute]
    public str CustomerID(str _customerID = customerID)
    {
        customerID = _customerID;
        return customerID;
    }
 
    [DataMemberAttribute]
    public str AppKey(str _appKey = appKey)
    {
        appKey = _appKey;
        return appKey;
    }
 
    [DataMemberAttribute]
    public str LegalEntity(str _legalEntity = legalEntity)
    {
        legalEntity = _legalEntity;
        return legalEntity;
    }
 
    [DataMemberAttribute]
    public str PayRunNumber(str _payRunNumber = payRunNumber)
    {
        payRunNumber = _payRunNumber;
        return payRunNumber;
    }
}

 

Now we can serialize this class to a JSON string for transfer, and also load an X++ class with the JSON result set returned. A quick example is shown below, in X++.

    public static void GetTempTrans()
    {
        System.Net.WebClient webClient;
        System.Text.UTF8Encoding encoder;
       
        try
        {
            PFLPayrollResultsRequest request = new PFLPayrollResultsRequest(); - this is the request class we convert to JSON and send to the API
            request.AppKey("232B8D90-7C3BF92DEA9F");
            request.CustomerID("27C2C06F8C2737");
            request.LegalEntity("AUP");
            request.PayRunNumber("12345");
 
            webClient = new System.Net.WebClient();
            System.Net.WebHeaderCollection headers = webClient.Headers;
            headers.Add("Content-Type", "application/json");
            encoder = new System.Text.UTF8Encoding();
            str json = FormJsonSerializer::serializeClass(request);// use this AX helper class (FormJsonSerializer) to convert to JSON here
            System.Byte[] encodedBytes = encoder.GetBytes(json);
            System.Byte[] response = webClient.UploadData("https://www.myapi.com/api/Engine/Post/TemporaryResults", encodedBytes);
            str jsonResponse = webClient.Encoding.GetString(response); 
            // deserialize result returned from API here using FormJsonSerializer::deserializeClass();
        }
        catch
        {
        } 
    }