Scenario
My scenario was to expose a legacy SOAP service in a restful way. As the customer had already API Management in place, including VNET integration, it was obvious for me to implement it via API Management policies. Please remark that the message payloads below are rather simplified, but they touch upon the challenges of the real API, I faced.
The SOAP request looks like this:
<GetOrderDetails xmlns="tvh.blogs">
<OrderId>001</OrderId>
</GetOrderDetails>
The SOAP response looks like this:
<GetOrderDetailsResponse xmlns="tvh.blogs">
<OrderId>001</OrderId>
<OrderDate>2018-08-13</OrderDate>
<OrderLines>
<OrderLine>
<Product>Pizza Margherita</Product>
<Amount>5</Amount>
</OrderLine>
<OrderLine>
<Product>Pizza Calzone</Product>
<Amount>2</Amount>
</OrderLine>
<OrderLine>
<Product>Pizza Funghi</Product>
<Amount>1</Amount>
</OrderLine>
</OrderLines>
</GetOrderDetailsResponse>
Transforming the request
The restful request must be a pure GET operation: https://***.azure-api.net/pizza/order/{orderId} Via the following inbound API Management policies, using a Liquid template, it gets converted into a valid SOAP request.
<inbound>
<base />
<!--Change for GET to POST-->
<set-method>POST</set-method>
<!--Set mandatory SOAPAction HTTP header-->
<set-header name="SOAPAction" exists-action="override">
<value>GetOrderDetails</value>
</set-header>
<!--Create SOAP Request via liquid template-->
<set-body template="liquid">
<GetOrderDetails xmlns="tvh.blogs">
<OrderId>{{context.Request.MatchedParameters["orderId"]}}</OrderId>
</GetOrderDetails>
</set-body>
<!--Overwrite backend url for easier testing-->
<set-backend-service base-url="https://webhook.site/***" />
</inbound>
Transforming the response
The XML response gets transformed into a JSON response via the following outbound policy. The xml-to-json policy is not sufficient for this scenario, because some fields require renaming, the order needs to be controlled and dates need to be formatted. That’s why the Liquid functionality is used.
<outbound>
<base />
<!--Create JSON Response via liquid template-->
<set-body template="liquid">
{
"orderId" : "{{body.GetOrderDetailsResponse.OrderId}}",
"orderDate" : "{{body.GetOrderDetailsResponse.OrderDate | Date: "dd/MM/yyyy"}}",
"orderLines" : [
{% JSONArrayFor orderline in body.GetOrderDetailsResponse.OrderLines %}
{
"product" : "{{orderline.Product}}",
"amount" : "{{orderline.Amount}}"
}
{% endJSONArrayFor %}
]
}
</set-body>
<!--Set correct content type-->
<set-header name="Content-Type" exists-action="override">
<value>application/json</value>
</set-header>
</outbound>
This results in the following JSON response:
{
"orderId": "001",
"orderDate": "13/08/2018",
"orderLines": [
{
"product": "Pizza Margherita",
"quantity": "5"
},
{
"product": "Pizza Calzone",
"quantity": "2"
},
{
"product": "Pizza Funghi",
"quantity": "1"
}
]
}
Caveats
Filters
When consulting the default Liquid template documentation, you see that all examples are shown with lower case filters. However, this does not work in Azure API Management. All filters need to start with a capital, in order to take effect within Azure API Management, otherwise they just get ignored. The same behaviour was also identified for the Liquid implementation within Logic Apps.
Accessing the context variable
The context variable is also accessible from within Liquid templates. There’s a similar object model, compared to C# expressions, however the syntax is different. As an example, reading a query string parameter in C# works like this: @(context.Request.Url.Query.GetValueOrDefault(“orderId”, “”)). Inside the Liquid template, you need to apply the following notation: {{context.Request.Url.Query[“orderId”]}}.
Reading JSON vs XML input collections
Applying a for-loop on an XML node, will iterate through all child elements. For JSON inputs, a for-loop will only execute when you’re dealing with an explicit array[]. Be aware of this, for sure if you are using the xml-to-json policy. This policy converts XML repeating nodes into a JSON array, but in case there’s only 1 repeating node, this won’t happen because APIM has no way to figure out that it’s repeating element.
Writing JSON collections
When creating a JSON collection, it quite tricky to prevent a trailing comma after the last item in the collection. I found a way to deal with this, by using the forloop.lastexpression:
"orderLines" : [
{% for orderline in body.GetOrderDetailsResponse.OrderLines %}
{
"product" : "{{orderline.Product}}",
"quantity" : "{{orderline.Amount}}"
}{% if forloop.last == false %},{% endif %}
{% endfor %}
]
Later on, I figured out the APIM has created a custom implementation for this. When using the JSONArrayFor syntax, this trailing comma logic is handled automatically, as shown in the previously mentioned outbound policy.
XML indexes not working
Indexes on XML input are not working as expected. I didn’t find the time yet to figure out whether it’s a bug or expected behaviour. When using the Liquid expression {{body.GetOrderDetailsResponse.OrderLines.OrderLine[1].Product}}, I get the following exception: Liquid error: Unable to cast object of type ‘System.Int32’ to type ‘System.String’.
Whitespace
The Liquid syntax introduces whitespace in the resulting payload. Therefore, it’s advised to put the Liquid expressions inline, to avoid unwanted whitespace. Unfortunately, this does not improve the readability of your APIM policies.
Conclusion
Liquid templates provide an easy way to transform requests and responses. It’s very powerful and intuitive to use. The documentation of API Management could be improved a bit, because many things were figured out via try-and-error or available on a somewhat hidden blog.
Cheers!
Toon
S'abonner au flux RSS