Consuming a SOAP service using HttpClient

Home / Consuming a SOAP service using HttpClient

Some of Microsoft’s built-in code generation/tooling is really janky. One such example is the code generator that will produce service references and proxy classes from a SOAP WSDL definition. I’ve never liked this particular feature of Visual Studio. The service classes themselves don’t play nicely with injection, behave strangely with instantiation, scoping, singleton patterns, and are generally so .NET 1.1…


How does one call a SOAP service with the HttpClient in newer versions of .NET, then?

Typically, a SOAP POST request posts “text/xml” with Envelope and Body nodes. Namespace attributes are also specified. It’s actually quite easy to build up the Xml with LinqToXml. In my case, I’m calling a specific endpoint to get a purchase order. The method takes some credentials and a few other identifiers. We can create an XDocument directly with something like this:

XNamespace ns = "http://schemas.xmlsoap.org/soap/envelope/";
XNamespace myns = "http://mynamespace.com";

XNamespace xsi = "http://www.w3.org/2001/XMLSchema-instance";
XNamespace xsd = "http://www.w3.org/2001/XMLSchema";

XDocument soapRequest = new XDocument(
    new XDeclaration("1.0", "UTF-8", "no"),
    new XElement(ns + "Envelope",
        new XAttribute(XNamespace.Xmlns + "xsi", xsi),
        new XAttribute(XNamespace.Xmlns + "xsd", xsd),
        new XAttribute(XNamespace.Xmlns + "soap", ns),
        new XElement(ns + "Body",
            new XElement(myns + "GetStuff",
                new XElement(myns + "client",
                    new XElement(myns + "Username", _apiUsername),
                    new XElement(myns + "Password", _apiPassword)),
                new XElement(myns + "ReferenceNumber", referenceNumber)
            )
        )
    ));

The only other thing specific to a SOAP service is that the SOAP endpoint generally expects a “SOAPAction” header indicating the action that you’re calling. Wrapping the request content with HttpClient request looks like this:

try
{
    using (var client = new HttpClient(new HttpClientHandler() { AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip }) { Timeout = _timeout })
    {
        var request = new HttpRequestMessage()
        {
            RequestUri = new Uri(_apiUrl),
            Method = HttpMethod.Post
        };

        request.Content = new StringContent(soapRequest.ToString(), Encoding.UTF8, "text/xml");

        request.Headers.Clear();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("text/xml"));
        request.Content.Headers.ContentType = new MediaTypeHeaderValue("text/xml");
        request.Headers.Add("SOAPAction", "http://mynamespace.com/GetStuff");

        HttpResponseMessage response = client.SendAsync(request).Result;

        if (!response.IsSuccessStatusCode)
        {
            throw new Exception();
        }

        Task<Stream> streamTask = response.Content.ReadAsStreamAsync();
        Stream stream = streamTask.Result;
        var sr = new StreamReader(stream);
        var soapResponse = XDocument.Load(sr);
        Console.WriteLine(soapResponse);

        var xml = soapResponse.Descendants(myns + "GetStuffResult").FirstOrDefault().ToString();
        var purchaseOrderResult = StaticMethods.Deserialize<GetStuffResult>(xml);
    }
}
catch (AggregateException ex)
{
    if (ex.InnerException is TaskCanceledException)
    {
        throw ex.InnerException;
    }
    else
    {
        throw ex;
    }
}
catch (Exception ex)
{
    throw ex;
}

You’ll notice that we use Link to Xml to grab the element “GetStuff” that is within the soap body. This allows us to pass the Xml from that specific element to the static method that deserializes the response to my own object. This method uses the XmlSerializer:

public static T Deserialize<T>(string xmlStr)
{
    var serializer = new XmlSerializer(typeof(T));
    T result;
    using (TextReader reader = new StringReader(xmlStr))
    {
        result = (T)serializer.Deserialize(reader);
    }
    return result;
}

For the XmlSerializer to function properly, we only need to then create objects using Xml attributes indicating how elements or attributes are mapped to properties. My particular response looks something like this:

<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
	<soap:Body>
	   <GetStuffResult xmlns="http://mynamespace.com">
			<PurchaseOrderData>
				...
			</PurchaseOrderData>
			<ProductsOrdered>
				<ProductInformation />
				<ProductInformation />
			</ProductsOrdered>
	   </GetStuffResult>
	 </soap:Body>
</soap:Envelope>

What we really want to do is parse the “GetStuffResult” element into an object. That object will have a PurchaseOrder member and a member that is a list of ProductInformation. Our object then, to properly deserialize only needs to be properly decorated:

[XmlRoot(ElementName = "GetStuffResult", Namespace = "http://mynamespace.com")]
public class GetPurchaseOrderResult
{
    [XmlElement("PurchaseOrderData")]
    public PurchaseOrder PurchaseOrder { get; set; }

    [XmlArray("ProductsOrdered")]
    [XmlArrayItem("ProductInformation")]
    public List<ProductInformation> Products { get; set; }
}

The PurchaseOrder and ProductInformation classes will also have Xml attribute decorations to properly map element/attribute values to properties. I won’t bore you with that, though. I like this approach because now I have full control over the Http requests/flow and none of the rigidity and strange behavior I have witnessed in the past with Visual Studio’s service reference proxy generator. I’m also not dealing with a set of proxy classes. All of the models are my own. Another consideration is that the older service proxy generators may not even work with .NET Core. This approach allows me to follow the other patterns I have mentioned for consuming other API’s.

5 thoughts on “Consuming a SOAP service using HttpClient”

  1. Hi, I just wanted to share that this came in very handy Today for me as I was in need of exactly this solution which I came to re-use with no issues. I just wanted to thank you and let you know that even if there is no people leaving feedback, surely there is people like me benefiting from you kindly sharing your posts. Keep it up and have a great day!

  2. Thank you very much for this tutorial, helped me a lot. we had a third party webservice that behaved very strangely with the normal service reference from visual studio. However you approach helped lots. Your tutorial needs more recognition

  3. Thank you for the post. I also greatly dislike the jankiness of auto-generated classes, and follow a similar method when calling web services. However, some SOAP endpoints need security headers. How do you generate these? Or have you yet needed to?

    1. If the headers are part of the XML, then you should be able to add them as nodes within the Xml. I have had occurrences in which I had to attach Xml w/ username and password and this was relatively straight-forward. If the headers are actual Http headers, then you can add them through the HttpClient headers collection.

Leave a Reply

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