Consuming the Northgate AIS API in Visual Studio

In Local Government we use a product called AIS which is for managing Adult Social Care cases. It is owned by Northgate and is used by councils throughout Britain. AIS provides a web service interface that allows access to nearly all its functionality. Part of my job is to use this web service to integrate with our other systems so client details can be displayed to other relevant departments and so we can automatically create various assessments and review documents.

api list
List of AIS APIs

Disclaimer: I did this a year ago and am only now writing it up so I can’t remember the reasoning behind some of the decisions made. Also I am not covering the setup of AIS to enable the web service to work. You will need to set up a user for it and add an application ID to Swift.

The Problem

Java! I hate Java. I am a Microsoft coder and using .net to connect to the AIS Java web service is a real pain. In the olden days you could use the WSE3 (Web Services Enhancements) add-in for Visual Studio to generate SOAP with the correct security headers but this was depreciated post VS2005. It was possible to hack the add-in into newer Visual Studio versions  but this doesn’t work in VS2015 and to be honest it is time to move on.

Microsoft’s Solution

Microsoft depreciated WSE3 because WCF (Windows Communication Framework) can connect instead. The thing is… WCF is really complicated and fussy. The Java web service claims to be standards compliant and Microsoft are always standards compliant (of course) so how come they can’t connect to each other?

There are three problems:

  1. The web service in our installation does not use SSL. It probably should but it is internally hosted and doesn’t. Not something a lowly developer like me can fix. It uses a standard username and password header (and IP restrictions) to provide security. This works fine for what we need but WCF REFUSES to send username and password credentials over a non-secure link. No exceptions!
  2. Some of the XML it produces appears to be compliant but doesn’t work with the web service. The “mustUnderstand” tag is the one at fault here. In the XML generated by WSE this is optional (0) but in the XML generated by WSE it is mandatory (1). This breaks the AIS web service.
  3. The web service returns a few characters of garbage instead of anything meaningful. This is to do with the way the HttpWebRequest class communicates with a web service. It automatically sends an “Expect: 100-Continue” header with every request (Fiddler is your friend here). This buggers up the Java web service.

Working Solution

The great thing about WCF is that is very extensible and you can hook into just about any bit of it. The bad thing is that this is a bit of a pain in the arse and involves overriding a lot of classes.

Problem 1 has an easy solution. Fortunately some kind person has written a custom binding that allows you to send username and password without an encrypted connection. It is called ClearUsernameBinding. Go and get it from here. Go there now and grab the source code.

We are now going to create our own custom bindings based on this one and apply the tweaks we need to fix problems 2 and 3.

Step 1 – Create a plain Windows form or console project just to use as a test client and add a “Service Reference” to the AIS API that you want to use. Person is hosted at:

http://servername:8080/integration/services/trusted/i9n-modules-business-person/Person?wsdl

TIP: The reference created by this wsdl is not 100% compatible with c#.net. The definition contains some 2-dimensional arrays that it gets upset by. To fix this locate the reference.cs file in the Service References folder (you might need to show all files to see this) and go through and replace all the [][] with just [] and it will work. Remember you will need to do this whenever you update the reference. Maybe one day Northgate will fix this.

Step 2 – Create a new class project and add a reference to the dll in the release folder for ClearUsernameBinding or add the source files into your project. I put them in a separate folder for niceness. Add a project reference to the class project from your sample console app.

Step 3 – Create a new class called i9nBinding and inherit it from the ClearUsernameBinding.

You should end up with something like this:

Add in a reference to System.ServiceModel and System.Configuration and let’s get coding.

Step 4 – Code for i9nBinding

Add in a constructor that accepts a message version. Store the version in the binding and then fix problem 3 by turning off the Expect100 header.

using System.ServiceModel.Channels;
public class i9nBinding : ClearUsernameBinding
{
	private MessageVersion _version; //Message envelope/soap versions

	public i9nBinding(MessageVersion version)
	{
		SetMessageVersion(version);
		_version = version;
		//stops .net from sending an Expect Header which breaks the API
		System.Net.ServicePointManager.Expect100Continue = false;
	}

	public override BindingElementCollection CreateBindingElements()
	{
		var res = new BindingElementCollection();
		res.Add(new i9nMessageEncodingBindingElement() { MessageVersion = _version });
		TransportSecurityBindingElement securityBinding = SecurityBindingElement.CreateUserNameOverTransportBindingElement();
		securityBinding.EnableUnsecuredResponse = true;
		res.Add(securityBinding);
		AutoSecuredHttpTransportElement transportBinding = new AutoSecuredHttpTransportElement();
		transportBinding.MaxReceivedMessageSize = int.MaxValue;
		transportBinding.MaxBufferSize = int.MaxValue;
		res.Add(transportBinding);
		return res;
	}
}

Step 5 – Client code to retrieve client details from the person service

Go back to the test app and add in the code needed to call the AIS web service using our new binding object

//Create a new instance of our binding. The AIS web service uses Soap1.1 with WS Addressing.
i9nBinding binding = new i9nBinding(MessageVersion.Soap11WSAddressingAugust2004);
//ClearUsernameBinding binding = new ClearUsernameBinding();
binding.SendTimeout = new TimeSpan(0, 0, 60); //60 second timeout
//Set the url of the webservice
EndpointAddress address = new EndpointAddress("http://server:8080/integration/services/default/i9n-modules-business-person/Person");
SelectPersonDetailsResponse personDetailsResponse = null; //response from the API
SxapiAuditHeadertype applicationContext = null;
using (ChannelFactory<I9nmodulesbusinesspersonPersonPort> channelFactory = new ChannelFactory<I9nmodulesbusinesspersonPersonPort>(binding, address))
{
	channelFactory.Credentials.UserName.UserName = "username"; //integration user with correct rights
	channelFactory.Credentials.UserName.Password = "password";

	//create an instance of the webservice
	I9nmodulesbusinesspersonPersonPort client = channelFactory.CreateChannel();

	using (new OperationContextScope((IContextChannel)client))
	{
		SelectPersonDetailsRequest request = new SelectPersonDetailsRequest();
		request.SelectPersonDetails = new SelectPersonDetails();
		request.SelectPersonDetails.PersonIdentifier = "123456"; //a client id
		//Create an audit Header for the request and add it onto the SOAP headers collection
		applicationContext = new SxapiAuditHeadertype();
		applicationContext.MessageId = Guid.NewGuid().ToString();
		applicationContext.ApplicationName = "APP_NAME"; //used by Swift to allow access in
		MessageHeader<SxapiAuditHeadertype> contextHeader = new MessageHeader<SxapiAuditHeadertype>(applicationContext);
		OperationContext.Current.OutgoingMessageHeaders.Add(contextHeader.GetUntypedHeader("ApplicationContext", "http://www.aniteps.com/schemas/swift/sxapi"));
		SelectPersonDetailsResponse1 response;
		try
		{
			response = client.SelectPersonDetails(request);
			personDetailsResponse = response.SelectPersonDetailsResponse;
		}
		catch (Exception ex)
		{
			//log the error
			Console.WriteLine(ex.ToString());
		}
	}
}

This has hopefully fixed two of our problems but if you have copied the code currently you will now get

<faultcode xmlns:ns1="http://xml.apache.org/axis/">ns1:Client.NoSOAPAction</faultcode><faultstring>no SOAPAction header!</faultstring>

When connecting to the web service. This is problem 2. WCF sets the mustUnderstand attribute =”1” on some of its elements even though the actual web service doesn’t need them.

To fix this problem we need to intercept the message as it is being created and tweak it slightly. This is done in the MessageEncoder so we need to create a custom encoder and fiddle about with the output as it is written to the stream.

Go back to the class project and create another class called i9MessageEncoder and add a reference to System.Runtime.Serialization.

Inherit the encoder from MessageEncoder and copy the following code.

public class i9nMessageEncoder : MessageEncoder
{
	private MessageVersion _version;
	private XmlWriterSettings _xmlSettings;

	public i9nMessageEncoder(MessageVersion version)
	{
		_version = version;
		_xmlSettings = new XmlWriterSettings();
		_xmlSettings.Encoding = new UTF8Encoding();
	}

	public override string ContentType
	{
		get
		{
			return "text/xml";
		}
	}

	public override string MediaType
	{
		get
		{
			return ContentType;
		}
	}

	public override MessageVersion MessageVersion
	{
		get
		{
			return _version;
		}
	}

	public override Message ReadMessage(ArraySegment<byte> buffer, BufferManager bufferManager, string contentType)
	{
		byte[] msgContents = new byte[buffer.Count];
		Array.Copy(buffer.Array, buffer.Offset, msgContents, 0, msgContents.Length);
		bufferManager.ReturnBuffer(buffer.Array);

		MemoryStream stream = new MemoryStream(msgContents);
		return ReadMessage(stream, int.MaxValue);
	}

	public override Message ReadMessage(Stream stream, int maxSizeOfHeaders, string contentType)
	{
		XmlReader reader = XmlReader.Create(stream);
		return Message.CreateMessage(reader, maxSizeOfHeaders, this.MessageVersion);
	}

	public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset)
	{
		MemoryStream stream = new MemoryStream();
		StringBuilder xmlString = new StringBuilder();
		XmlWriter writer = XmlWriter.Create(xmlString, _xmlSettings);
		message.WriteMessage(writer);
		writer.Close();

		xmlString = new StringBuilder(xmlString.ToString().Replace("mustUnderstand=\"1\"", "mustUnderstand=\"0\"").Replace("encoding=\"utf-16\"", "encoding=\"utf-8\""));

		ASCIIEncoding encoding = new ASCIIEncoding();
		stream.Write(encoding.GetBytes(xmlString.ToString()), 0, xmlString.Length);

		byte[] messageBytes = stream.GetBuffer();
		int messageLength = (int)stream.Position;
		stream.Close();

		int totalLength = messageLength + messageOffset;
		byte[] totalBytes = bufferManager.TakeBuffer(totalLength);
		Array.Copy(messageBytes, 0, totalBytes, messageOffset, messageLength);

		ArraySegment<byte> byteArray = new ArraySegment<byte>(totalBytes, messageOffset, messageLength);
		return byteArray;
	}

	public override void WriteMessage(Message message, Stream stream)
	{
		XmlWriter writer = XmlWriter.Create(stream, _xmlSettings);
		message.WriteMessage(writer);
		writer.Close();
	}
}

The important method here is public override ArraySegment<byte> WriteMessage(Message message, int maxMessageSize, BufferManager bufferManager, int messageOffset) which is where the message is created.

After various attempts to do this elegantly I did a bit of a hack and used search and replace to change the xml into the format I wanted. I’m sure there are better ways to do this and if performance is an issue then I suggest you seek them out.

The code I used reads the xml stream into a StringBuilder and replaces mustUnderstand=”1” with mustUnderstand=”0”. It also changes the xml back to UTF-8.

The client needs to be changed so it uses the new encoder to create  messages rather than the default one. This requires a couple more classes to connect everything together.

i9nMessageEncoderFactory to create message encoders

public class i9nMessageEncoderFactory : MessageEncoderFactory
{
	private MessageVersion _version;

	public i9nMessageEncoderFactory(MessageVersion version)
	{
		_version = version;
	}

	public override MessageEncoder Encoder
	{
		get
		{
			return new i9nMessageEncoder(_version);
		}
	}

	public override MessageVersion MessageVersion
	{
		get
		{
			return _version;
		}
	}
}

and i9nMessageEncodingBindingElement to plug everything in

public class i9nMessageEncodingBindingElement : MessageEncodingBindingElement //BindingElement
{
	//We will use an inner binding element to store information required for the inner encoder
	//private MessageEncodingBindingElement innerBindingElement = new TextMessageEncodingBindingElement();

	//Main entry point into the encoder binding element. Called by WCF to get the factory that will create the
	//message encoder
	private MessageVersion _version;

	public i9nMessageEncodingBindingElement()
	{
	}

	public override MessageEncoderFactory CreateMessageEncoderFactory()
	{
		return new i9nMessageEncoderFactory(MessageVersion);
	}

	public override MessageVersion MessageVersion
	{
		get { return _version; }
		set { _version = value; }
	}

	public override BindingElement Clone()
	{
		return new i9nMessageEncodingBindingElement() { MessageVersion = _version };
	}

	public override T GetProperty&lt;T&gt;(BindingContext context)
	{
		return base.GetProperty&lt;T&gt;(context);
	}

	public override IChannelFactory&lt;TChannel&gt; BuildChannelFactory&lt;TChannel&gt;(BindingContext context)
	{
		if (context == null)
			throw new ArgumentNullException("context");

		context.BindingParameters.Add(this);
		return context.BuildInnerChannelFactory&lt;TChannel&gt;();
	}

	public override IChannelListener&lt;TChannel&gt; BuildChannelListener&lt;TChannel&gt;(BindingContext context)
	{
		if (context == null)
			throw new ArgumentNullException("context");

		context.BindingParameters.Add(this);
		return context.BuildInnerChannelListener&lt;TChannel&gt;();
	}

	public override bool CanBuildChannelListener&lt;TChannel&gt;(BindingContext context)
	{
		if (context == null)
			throw new ArgumentNullException("context");

		context.BindingParameters.Add(this);
		return context.CanBuildInnerChannelListener&lt;TChannel&gt;();
	}
}

Go back to the i9binding class and amend it to load the new encoder in the CreateBindingElements method

public override BindingElementCollection CreateBindingElements()
{
	var res = new BindingElementCollection();
	TransportSecurityBindingElement securityBinding = SecurityBindingElement.CreateUserNameOverTransportBindingElement();
	securityBinding.EnableUnsecuredResponse = true;
	res.Add(securityBinding);
	AutoSecuredHttpTransportElement transportBinding = new AutoSecuredHttpTransportElement();
	transportBinding.MaxReceivedMessageSize = int.MaxValue;
	transportBinding.MaxBufferSize = int.MaxValue;
	res.Add(transportBinding);
	return res;
}

Run the client with a valid SwiftID and you should receive a big chunk of XML back. Hooray.

Tips

  • If .net can’t parse one of the API WSDLs then save it to disk and add it from the disk. This allows you to edit the wsdl to remove any bits that are confusing Visual Studio. It is a bit messy but it works. The CarePlans API is particularly messy.
  • .Net can get confused with choice tags in the wsdl. These are sometimes used by the API to allow you to retrieve data using different IDs e.g. ResponsiblePersonIdentifier in Assessments can be many different types of ID. The easiest way around this to hack the WSDL to remove the choice tags and just keep the one you want to use.

Please post any questions in the comments below.

Leave a Reply

Your email address will not be published. Required fields are marked *