In part one of this series we created a new method within in the commerce API to accept a simplified JSON format. In this, the second part, we will create a Azure function to call the new method.

This is a simple example, its purpose is to show how you can use the commerce engine proxy within Azure functions to meet many integration requirements. As Azure functions now support timed executions, they can even be used to replace asynchronous or “timed” integration batch jobs.

For example, you can use Azure functions to read product update messages from a message bus or SQL table, translate them and call the commerce API to update product and or variant pricing or inventory levels.

This post covers the following :-

  1. Creation of a HTTPTrigger Azure Function i.e. it will be executed from a HTTP call (from a browser in this example)
  2. How to execute and debug the function locally
  3. Referencing the commerce proxy from the Azure function project
  4. How to authenticate against the commerce engine via a x509 certificate from the Azure Function.
  5. How to format the JSON and make the call to new engine method

Step 1 – Create a project

The process for creating a new Azure function is widely documented online so not going to cover this here. You can follow this tutorial to get a Azure function project setup. Once you have this working go to step 2.

Step 2 – Rebuild engine proxy

As we have added a new method to the engine in the first post, we must rebuild the proxy so we can use it from our new Azure function

  • From within the commerce engine SDK folder open the commerce proxy project. For example, “C:\Git\Examples\Sitecore\Commerce\9.0.3\Sitecore.Commerce.Engine.SDK.2.4.43\src\Sitecore.Commerce.ServiceProxy”.
  • Under connected services, select “CommerceShops”, right click and select “Update OData connected service”
  • Should see the following screen, hit finish. Ensure you have correct URL to your commerce engine. Repeat for “CommerceOps”.

Step 2 – Create the Function

Here we will create the new function and write the code to call the commerce API and authenticate using the x509 certificate.

  • Add a reference to the proxy you just rebuilt in step 1 i.e. “Sitecore.Commerce.ServiceProxy”. You will find it in the bin of the proxy project.
  • Add a reference to Sitecore.Kernel.dll from either your site bin or the Nuget feed.
  • Right click on your Azure function project and select “Add”
  • Select “Azure Function” and “HTTP Trigger” (no parameters)
  • Give it the name “CreateProductRequest”
  • Paste the following code and replace your namespace etc
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;
using Sitecore.Commerce.ServiceProxy;
using System;
using Sitecore.Commerce.Engine;
using Microsoft.OData.Client;
using System.Globalization;
using System.Security.Cryptography.X509Certificates;
using Newtonsoft.Json.Linq;
namespace Sitecore.Services.Examples.Azure.Functions
{
public static class CreateProductRequest
{
[FunctionName("CreateProductRequest")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]HttpRequestMessage req, TraceWriter log)
{
log.Info("C# HTTP trigger function processed a request.");
// parse query parameter
var productid = req.GetQueryNameValuePairs()
.FirstOrDefault(q => string.Compare(q.Key, "productid", StringComparison.OrdinalIgnoreCase) == 0)
.Value;
var priceStr = req.GetQueryNameValuePairs()
.FirstOrDefault(q => string.Compare(q.Key, "price", StringComparison.OrdinalIgnoreCase) == 0)
.Value;
if (decimal.TryParse(priceStr, out var outPrice))
{
outPrice = System.Convert.ToDecimal(priceStr);
}
var productsArray = new JArray();
dynamic variant1 = new JObject();
variant1.variantid = productid + "-Variant1";
variant1.listprice = outPrice;
dynamic variant2 = new JObject();
variant2.variantid = productid + "-Variant2";
variant2.listprice = outPrice;
dynamic variant3 = new JObject();
variant3.variantid = productid + "-Variant3";
variant3.listprice = outPrice;
dynamic product = new JObject();
product.productid = productid;
product.listprice = outPrice;
product.name = "Product " + productid;
product.displayname = "Product " + productid;
product.description = "Product " + productid + " is the best!!!";
product.variants = new JArray(variant1, variant2, variant3);
productsArray.Add(product);
var o = new JObject
{
["products"] = productsArray
};
var products = o.ToString();
var re = GetContainer( System.Configuration.ConfigurationManager.AppSettings["defaultEnvironment"],
System.Configuration.ConfigurationManager.AppSettings["defaultShopName"],
string.Empty).CreateUpdateSellableItem(products);
var result = Proxy.GetValue(re);
return result == false
? req.CreateResponse(HttpStatusCode.BadRequest, "Failed to update or create")
: req.CreateResponse(HttpStatusCode.OK, "Created or updated " + productid );
}
public static Container GetContainer(string environment, string shopName, string userId, string customerId = "", string language = "", string currency = "", DateTime? effectiveDate = null)
{
return GetShopsContainer(environment, shopName, userId, customerId, language, currency, effectiveDate);
}
public static Container GetShopsContainer(string environment = "", string shopName = "", string userId = "", string customerId = "", string language = "", string currency = "", DateTime? effectiveDate = null)
{
var container = new Container(new Uri(System.Configuration.ConfigurationManager.AppSettings["shopsServiceUrl"]))
{
MergeOption = MergeOption.OverwriteChanges,
DisableInstanceAnnotationMaterialization = true
};
container.BuildingRequest += (s, e) => {
e.Headers.Add("ShopName", shopName);
e.Headers.Add("Environment", environment);
if (effectiveDate.HasValue)
{
DateTimeOffset value = effectiveDate.Value;
e.Headers.Add("EffectiveDate", value.ToString(CultureInfo.InvariantCulture));
}
e.Headers.Add("ShopperId", string.Empty);
e.Headers.Add("IsRegistered", string.Empty);
if (!string.IsNullOrEmpty(customerId))
{
e.Headers.Add("CustomerId", customerId);
}
if (!string.IsNullOrEmpty(language))
{
e.Headers.Add("Language", language);
}
if (!string.IsNullOrEmpty(currency))
{
e.Headers.Add("Currency", currency);
}
var certificate = GetCertificate();
if (certificate != null)
{
e.Headers.Add(System.Configuration.ConfigurationManager.AppSettings["certificateHeaderName"], certificate);
}
};
return container;
}
public static string GetCertificate()
{
var thumbPrint = System.Configuration.ConfigurationManager.AppSettings["certificateThumbprint"];
if (string.IsNullOrEmpty(thumbPrint))
{
return null;
}
//for azure change to User from LocalMachine
var x509Store = new X509Store(StoreName.My,StoreLocation.LocalMachine);
x509Store.Open(OpenFlags.ReadOnly);
var x509Certificate2Collection = x509Store.Certificates.Find(X509FindType.FindByThumbprint, thumbPrint, false);
if (x509Certificate2Collection.Count == 0)
{
return null;
}
var item = x509Certificate2Collection[0];
return item == null ? null : System.Convert.ToBase64String(item.Export(X509ContentType.Cert), Base64FormattingOptions.None);
}
}
}
  • To the local.settings.json add the following settings
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "",
"AzureWebJobsDashboard": "",
"shopsServiceUrl": "https://localhost:5000/api/",
"commerceOpsServiceUrl": "https://localhost:5000/commerceops/",
"defaultEnvironment": "HabitatAuthoring",
"defaultShopName": "storefront",
"defaultShopCurrency": "USD",
"certificateThumbprint": "77E580A6027FFD27C4445C08B666E46DC5ADF0DB",
"certificateStoreLocation": 2,
"certificateStoreName": 5,
"certificateHeaderName": "X-CommerceEngineCert"
}
}
view raw local.settings.json hosted with ❤ by GitHub
  • Change the settings to reflect your environment. Pay special attention to the thumbprint. This should be the thumbprint of your x509 certificate.

The code above extracts the certificate from the cert store converts it to a base 64 string and adds it to the header of the call. Note that the certificate is the same certificate that Sitecore CMS uses to call the engine. The name of this certificate if you are using a one box (developer) install is usually named “storefront.engine”.

As Azure does not have “LocalMachine” store location, you will need to change the code to use the personal store i.e. “CurrentUser” before publishing to Azure.

var x509Store = new X509Store(StoreName.My,StoreLocation.LocalMachine);

Step 3 – Test locally

  • Build the solution and hit start
  • The Azure function tools should open a console and start listening on a specific port
  • Using a browser or Postman execute the new function using the following URL for example (replace your port if need be)
http://localhost:7071/api/CreateProductRequest?productid=AwesomeProduct2&price=320.21

To see if the product was created successfully, use postman to execute the standard commerce postman command “SellableItems”. You should see your new SellableItem in the list.

https://localhost:5000/api/SellableItems