Complex data using Episerver forms

We like to dive a little deeper into a use-case where Episerver forms is used to sent more data than what is filled in by a visitor. About how to sent context related data or other complex data to an external endpoint. Some examples are; product data, or context related data.

A possible use case

You have to deliver a form where product data is sent through an external endpoint together with the phone number of a visitor. You have to create a form which you are going to display on product page with one field. "Call me back about this product". The user only has to leave his phone number. But the endpoint software wants to have more complex data about the product as well. Let’s say, the discounts which are valid for this particular user, and the price the user was seeing for this product.

The problem

Lucky we have webhook support in Episerver forms to handle the connection to the third-parties. But when using this, none formatted JSON is sent to the webhook like so;

{
  "Phone": "+31612121212",
  "Product": "{\"name\":\"Laptop X1\",\"price\":2999}"
}

Where you like to send something like

{ 
   "Phone":"+31612121212",
   "Product":{ 
      "name":"Laptop X1",
      "price":2999 
  }
}

In this blogpost you are going to read how to fix this in 2 easy steps.

Step 1; Send a valid json object to the webhook

As you can see when Json is sent as a value. A none formatted JSON string is send to the webhook.

The easiest solution is to skip the default webhook support and create your own "post submission actor". This is done by implementing a custom IPostSubmissionActor. For that you can start with PostSubmissionActorBase.

The whole forms setup is extensible / customizable on in many ways. The IFormDataRepository is used as an overall repository within Form related implementations (Like other actors) and in the default shipped implementations "TransformSubmissionDataWithFriendlyName" is responsible for the stringified json output talked about above. We have tried going down the road of implementing a custom IFormDataRepository. Because this can result in a more generic / centralized support of complex objects in the Forms framework. However i.m.o. it does come with more challenges and dependencies on other actores than you like to have. If you like to dive deeper in how forms are processed an older post written by Alf Nilsson is very helpful.

Our advice for the described use-case is to implement your own webhook submission actor and start doing all the cool stuff from there, like this;

public class CustomWebhookActor : PostSubmissionActorBase
{
    private readonly Injected _formDataRepository;

    public override object Run(object input)
    {
        // and example endpoint from https://requestbin.com/
        return this.GetWebResponseAsync("https://----.x.pipedream.net/").Result;
    }

    protected virtual async Task GetWebResponseAsync(string webhookurl)
    {
        var httpWebRequest = (HttpWebRequest)WebRequest.Create(webhookurl);
        httpWebRequest.Method = "POST";
        httpWebRequest.ContentType = "application/json" ;
        httpWebRequest.UserAgent = "-CustomWebhookActor";
        
        var headers = httpWebRequest.Headers;
        headers.Add("X-EPiServer-Forms-Event", "submitted");
        headers.Add("X-EPiServer-Forms-Signature", string.Empty);
        headers.Add("X-EPiServer-Forms-Delivery", this.SubmissionData.Id);
        
        var keyValuePairs = this._formDataRepository.Service
            .TransformSubmissionDataWithFriendlyName(this.SubmissionData.Data, 
                this.SubmissionFriendlyNameInfos, true)
            .ToDictionary(k => k.Key, k => ToValidJsonValue(k.Value));

        byte[] buffer = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(keyValuePairs, new JsonSerializerSettings()));
        
        httpWebRequest.ContentLength = buffer.Length;
        
        using (var requestStream = httpWebRequest.GetRequestStream())
            requestStream.Write(buffer, 0, buffer.Length);

        using (var reader =
            new StreamReader((await httpWebRequest.GetResponseAsync().ConfigureAwait(false)).GetResponseStream() 
                             ?? throw new InvalidOperationException("Exception while executing request to webhook.")))
        {
            return await reader.ReadToEndAsync();
        }
    }

    public static object ToValidJsonValue(object value)
    {

        if (value is string strValue &&
            (strValue.StartsWith("{") && strValue.EndsWith("}")) | //for object
            (strValue.StartsWith("[") && strValue.EndsWith("]"))) // for array
        {
            try
            {
                return JObject.Parse(strValue);
            }
            catch(JsonReaderException)
            {
                // return original value when value is not a valid json string
            }
        }

        return value; // return original when not a json value.
    }
}

Make it configurable

The above example does use a hardcoded url. In real live senarios you don’t want to have this. It’s worth checking the examples on the Episerver Forms Demo examples. Here you can see how easy it is to make this thing configurable.

Step 2; A product information form element

Besides the phone number you like to have context related data sent in a "custom" (valid json) format. Therefor you need to implement an ElementBlockBase implementing IElementCustomFormatValue. Below a mini example of how you can process product data within your form post.

[ContentType(
    DisplayName = "Product Data",
    GroupName = "Custom Elements",
    Description = "-- YOUR DESCRIPTION HERE -- ",
    GUID = "3536C557-EB9F-47A4-AAE4-7078EA9718D1")]
public class ProductDataElementBlock : InputElementBlockBase, IElementCustomFormatValue
{
    protected object GetProductData(string variantId)
    {
        // call contentrepository and productservice to get the product

        return new //product
        {
            brand = "Lenovo",
            name = "X1 Carbon", 
            price = 2999
        };
    }

    public object GetFormattedValue()
    {
        // if you have an input element rendered;
        // var rawSubmittedData = HttpContext.Current.Request.Form;
        // the variantId...
        // var submittedValue = rawSubmittedData[this.Content.GetElementName()];
        
        try
        {
            
            return JsonConvert.SerializeObject(
                GetProductData("DEMO-ID"), new JsonSerializerSettings());
        }
        catch
        {
            // do your logging here...
        }

        
        return string.Empty;
    }

}

Finally

This altogether does result in a webrequest to a webhook and you have all the control of calling this webhook. Optimize it for your live senarios by implementing an IPostSubmissionActorModel. Optimize the ProductDataElementBlock by using the context and power of your own application / integration or render a hiddenfield as the source of this block. Hopefully this post does give you all the pieces to create some great stuff with this knowledge in mind.