Using Web API for SOAP web services

The usual way to implement a SOAP based web service is to use WCF. When you are in charge of creating the contract, it is rather straightforward: Create some datacontracts, a servicecontract and some operations and then create a class that implements the service. WCF will give you all the options you need to host the service in IIS, to support different transport protocols and secure the service. So, why would you use Web API to implement a SOAP web service?

Actually, i made an implementation just out of curiousity, but is was triggered by a real issue with WCF services. Note that creating a service is easy when you are in charge of the contract. Things change, when you have to comply to a standard that is created by another party or committee. In many cases individual suppliers only implement a small part of large standardized service contracts and those contracts can contain constructions that are nog processed very well by SvcUtil and other tools to create c# classes from the WSDL and XSD-files. As an example: the ZKN0310-standard of KING (an organization related to the Dutch government) results in a generated file of more than 100.000 lines of code and when creating objects with the generated code and serialize them to XML, the generated XML will not validate against the original XSD.

In this situation, it seems to be more effective to manipulate the XML directly by my own code. In the past, i had build some REST-like services that used Linq-to-XML to process XML-based input. As the SOAP envelope is just an additional XML-layer around the message, creating some basic SOAP support is not that difficult.

The SOAP mediatypeFormatter

Implementing a web service should not require you to manipulate the SOAP envelope in the actual methods. In order to accomplish this, a custom mediatypeFormatter can take care of stripping the SOAP envelope from the request and adding it to the response. A custom mediaTypeFormatter is just a class that inherits from MediaTypeFormatter of BufferedMediaTypeformatter:

public class SoapFormatter : MediaTypeFormatter
{
        private XmlMediaTypeFormatter wrappedXmlFormatter
            = new XmlMediaTypeFormatter();

        public SoapFormatter()
        {
            // Add the supported media type.
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/xml"));
        }
}

At the moment, the formatter only supports SOAP 1.1 messages. Support for SOAP 1.2 would at least require support for the “application/soap+xml” content-type. The custom formatter is just a wrapper around the stands XmlMediaTypeFormatter to minimize to work needed.

The formatter must inform the Web API framework wat type of objects it can serialize and deserialize by overriding the CanWriteType and CanReadType methods:

public override bool CanWriteType(System.Type type)
{
       if (type == typeof(XElement))
       {
            return true;
        }
        return false;
}

public override bool CanReadType(Type type)
{
        if (type == typeof(XElement))
        {
            return true;
        }
        return false;
}

The last step in implementing the formatter is to supply the methods for the actual manipulation of the SOAP envelope.

public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)
{
       XNamespace soapenv = "http://schemas.xmlsoap.org/soap/envelope/";

       XElement antwoord = new XElement(soapenv + "Envelope",
                new XElement(soapenv + "Body", value));

       return wrappedXmlFormatter.WriteToStreamAsync(type, antwoord, writeStream, content, null);
}

public async override Task<object> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
{
       XNamespace soapenv = "http://schemas.xmlsoap.org/soap/envelope/";

       object input = await wrappedXmlFormatter.ReadFromStreamAsync(type, readStream, content, formatterLogger);
       XElement inputElement = input as XElement;

       XElement vraagBericht = inputElement.Element(soapenv + "Body").Elements().FirstOrDefault();
       return vraagBericht;
}

Routing on SOAP action

As a SOAP web service can provide more than one method on the same resource, a customization of the routing is required. I found out this is surprisingly easy to do using a custom ApiControllerActionSelector that reads the SOAP action from the HTTP headers and set the according action in the RouteData dictionary.

public class SoapActionSelector : ApiControllerActionSelector
{
	public override HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
	{
		if (controllerContext.Request.Headers.Contains("SOAPAction"))
		{
			var matchingHeaders = controllerContext.Request.Headers.GetValues("SOAPAction");
			var headerValue = (matchingHeaders == null) ? "" : (matchingHeaders.FirstOrDefault() ?? "");
			if (!string.IsNullOrEmpty(headerValue))
			{
				// Strip the namespace and double quotes from the soap action
				int nsSplit = headerValue.LastIndexOf("/");
				if (nsSplit >= 0)
					headerValue = headerValue.Substring(nsSplit + 1);
				headerValue = headerValue.Trim("\"".ToCharArray());

				// Set the new action
				controllerContext.RouteData.Values["action"] = headerValue;
			}
		}
		return base.SelectAction(controllerContext);
	}
}

The default route in Web API does not include the action. This means that a route must be aded to the WebAPiConfig:

config.Routes.MapHttpRoute(
	name: "SoapApi",
	routeTemplate: "{controller}/{action}",
	defaults: new { controller = "Services" }
);

Decorating the controller

In order to create a Web api controller that is able to handle SOAP request, one final step is required. The controller must be configured to use the new mediaTypeFormatter and the SoapActionSelector. Web Api offers a nice extension point to do this with the IControllerConfiguration. In this case, i created a ‘SoapControllerConfiguration’ class with only an ‘Initialize’ method. In this method, i replace the standard action selector and the standard media type formatters with the custom ones i created earlier.

public class SoapControllerConfiguration : Attribute, IControllerConfiguration
{
	public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor)
	{
		controllerSettings.Services.Replace(typeof(IHttpActionSelector), new SoapActionSelector());
		controllerSettings.Formatters.Clear();
		controllerSettings.Formatters.Add(new SoapFormatter());
	}
}

The only thing needed to change a controller in a SOAP serice is to apply the SoapControllerConfiguration attribute to the controller.

[SoapControllerConfiguration]
public class ProjectServiceController : ApiController
{
	[HttpPost]
	public XElement CreateProject(XElement createRequest)
	{
		XNamespace ns = "http://someHugeNamespace/project";

		string projectName = "unknown";
		var nameElement = createRequest.Element(ns + "ProjectName");
		if (nameElement != null)
			projectName = nameElement.Value;

		// ... 

		XElement response = new XElement(ns + "acknowledge",
			new XElement(ns + "Success", true),
			new XElement(ns + "Message", projectName + " created")
		);
		return response;
	}
}

Remarks

Should i use this in a production situation? No. This implementation is far too limited. The better way to implement a SOAP service without using generated proxy classes is to use WCF. A simple servicecontract with operations that take an XElement as input parameter and produces an XElement as output will do the trick as well.

For me, the good thing about this exercise was that i learned some new things about Web API that will be usable in other situations as well:

  • Custom media type formatters can help to move repeating work out of the controller methods.
  • The routing options are not limited to the items in the query string. Custom action selectors can lead to much cleaner controller methods.
Advertisements