REST to SOAP using Azure API Management

I have seen several use-cases where you have to expose your legacy SOAP based services to modern REST base clients. Azure API Management really helps a lot in doing the magic and transforming the SOAP services to REST and it does out of the box.

But I have also seen a reverse use-case, it may come as a surprise but we had a scenario where we had to convert the REST API to be consumed by the SOAP clients. That’s where Azure’s Liquid Templates shine, but the issue is to be able to do that, we first need to know how both protocols differs so that we can write the policies to convert between each other.

I will demonstrate how we can do that, but first let’s see what’s the main difference that we need to understand to be able to do the conversion.

Fundamental difference between REST and SOAP

SOAPREST
Payload FormatXMLJSON Preferred, can support XML
Resource IdentifiersOne root URL, Individual Operations go in header – SOAPAction
Example:
URL: https://<URL>/Students
GetAllStudent Operation: (Header) SOAPAction: “http://tempuri.org/IService1/GetAllStudents”
Each resource have individual URI
Example:
URL: https://<URL>/Students
No Header needed to identify Individual operation
Request Sample<?xml version="1.0" encoding="utf-8"?>
<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
  <Body>
    <GetStudentData xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://tempuri.org/">
      <input>
        <StudentId xmlns="http://schemas.datacontract.org/2004/07/WcfPocForApiM">1</StudentId>
        <StudentName xmlns="http://schemas.datacontract.org/2004/07/WcfPocForApiM">Tahir</StudentName>
      </input>
    </GetStudentData>
  </Body>
</Envelope>
{
   "studentId": 1,
   "studentName": "Tom"
}

Response Sample<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
   <s:Body>
      <GetStudentDataResponse xmlns="http://tempuri.org/">
         <GetStudentDataResult xmlns:a="http://schemas.datacontract.org/2004/07/WcfPocForApiM" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<a:StudentId>1</a:StudentId>            
<a:ClassName>Grade 10</a:ClassName>
            <a:CollegeName>School of Garden Design</a:CollegeName>
         </GetStudentDataResult>
      </GetStudentDataResponse>
   </s:Body>
</s:Envelope>
{
   "studentId": 1,
   "className": "Gr-1",
    "colgName": "xyz",
}

HTTP VerbsOnly POSTAll verbs supported

Case study at hand

I have already hosted a bunch of resources in the Azure as listed below and I will just put the screenshots below to show you what I have already prepared and then we will have a look at the Azure API M Policies used to do the conversion and we will test that through a Client web app as well as Soap UI.

I have hosted the below API in Azure App service as you can see in the POST request through Postman. This is the request we will be converting to SOAP.

API-M Policy

As you can see in the table above that the SOAP UI has only one URI and each individual operation goes in the Request header as the “SOAPAction”, which means we need conditional policies to send the request to appropriate backend REST API resource, it also means that we need to create the REST payload on the fly. Lets see the inbound policy below.

<inbound> 
<base /> 

<set-variable name="operationName" 
 value="@(context.Response.Headers.GetValueOrDefault("SOAPAction",""))" /> 

<choose> 
   <when condition="@(context.Variables.GetValueOrDefault<string>("operationName") 
     =="http://tempuri.org/IService1/GetStudentData")"> 
        <set-backend-service base-url="https://restwebapi120200412190526.azurewebsites.net" /> 
        <set-body template="liquid"> 
          { 
              "studentId" : "{{body.envelope.body.GetStudentData.input.StudentId}}", 
              "studentName" : "{{body.envelope.body.GetStudentData.input.StudentName}}" 
          } 
        </set-body>
   </when>
</choose>

<set-header name="Content-Type" exists-action="override"> 
<value>application/json</value> 
</set-header> 

</inbound>

Note how we have used <choose> and <when> to change the backend URL and Request REST Payload based on the header condition.

Now let’s have a look at the Outbound policy.

<?xml version="1.0" encoding="UTF-8"?>
<outbound>
   <base />
   <set-body template="liquid">
      <choose>
         <when  condition="@(context.Variables.GetValueOrDefault<string>("operationName") 
     =="http://tempuri.org/IService1/GetStudentData")">
            <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
               <s:Body>
                  <GetStudentDataResponse xmlns="http://tempuri.org/">
                     <GetStudentDataResult xmlns:a="http://schemas.datacontract.org/2004/07/WcfPocForApiM" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
                        <a:ClassName>{{body.className}}</a:ClassName>
                        <a:CollegeName>{{body.collegeName}}</a:CollegeName>
                        <a:Courses>
                           {%- for item in {{body.courses}} -%}
                           <a:Course>
                              <a:CourseId>{{item.courseId}}</a:CourseId>
                              <a:CourseName>{{item.courseName}}</a:CourseName>
                           </a:Course>
                           {%- endfor -%}
                        </a:Courses>
                        <a:Gpa>{{body.gpa}}</a:Gpa>
                        <a:StudentId>{{body.studentId}}</a:StudentId>
                        <a:StudentName>{{body.studentName}}</a:StudentName>
                     </GetStudentDataResult>
                  </GetStudentDataResponse>
               </s:Body>
            </s:Envelope>
         </when>
      </choose>
   </set-body>
   <set-header name="Content-Type" exists-action="override">
      <value>text/xml</value>
   </set-header>
   <choose>
      <when  condition="@(context.Variables.GetValueOrDefault<string>("operationName") 
     =="http://tempuri.org/IService1/GetStudentData")">
         <set-header name="ReplyAction" exists-action="override">
            <value>http://tempuri.org/IService1/GetStudentDataResponse</value>
         </set-header>
      </when>
   </choose>
</outbound>

Note how When condition is used to set the Response Body and Response Header, so that SOAP clients can interpret it accordingly.

The result

See in the SOAP UI screenshot below, where we are calling APIM request which is a SOAP endpoint and it then converts and talks to the REST API and returns the SOAP response back.

You can see exactly the same through the C# SOAP client code and it works perfectly fine.

Key Take Aways

Although this approach overall solves your problem and you can convert the REST API to be consumed by the SOAP clients but I highly discourage such conversion because of the following reasons and you should only do such conversion when there is no other way out.

  1. In this case you don’t have WSDL URLs which means any change in the formats of the request or response will be very tedious to maintain as “Update service reference” won’t work.
  2. You will have to manually change the auto-generated code of the ServiceReferences to point to the APIM URL.
  3. Since there can be only one URL and the operations are segregated by only headers, there will be only one API operation in the API Management which will become point of contention.
  4. It will become very hard to maintain as you are dealing with conversion of every single response and request in a single Policy.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.