WCF - Handling Generic Messages
I had a requirement for a client where we were upgrading a web site that had a business web service exposed, and we had to create a new service that was identical in operation to the old (from a consumer perspective).
The situation we had though, was somewhat of a reversal on what I normally do (and I suspect what most people do). Typically, I would create a service, and the consumer would query its interface, and generate the appropriate client code to consume it. In this scenario, we had a service and consumer, and we had to mimic the old service so that the consumer could continue "consuming" as if nothing had changed. Furthermore, because the consumer was using a much older technology set, the messages being sent by that consumer were not quite compliant with the latest standards. So I had to create a service that essentially allowed a non compliant consumer to operate unchanged.
Initially, I tried generating interfaces using the SVCUTIL tool in WCF from the existing WSDL and then exposed those interfaces using a basic http binding. This sort of worked. Firstly, the generated interface had an incorrect soap action attribute (as I detailed in a previous post here). This was relatively easy to overcome with a little bit of code.
Once this was done however, calls from the consumer would get to the service ok, but the object being passed in by the consumer would always come through as NULL. It seemed that the serialised data from the consumer, was not getting deserialised correctly on the service end.
The original service definition looked something like:
public void SubmitOrder(Order orderRequest);
The orderRequest object was always coming through as NULL.
As with a lot of projects, time was short and the pressure was on.
I didn't have to do much processing of the orderRequest, simply get its contents as one big XML blob and pass it downstream to a legacy processing component so I didn't really have to do much with the data. Get the object, serialise it as XML, send it on. It was quite frustrating, and attempting to find out what exactly the differences were in terms of serialisation seemed not only a painful task, but a rather time consuming one.
So, I decided to implement a generic WCF service that simply accepted whatever data was sent to an endpoint, and pass it on to the legacy component. The more I thought about it, the better an idea it seemed because:
- Deserialising the incoming data into an object, and the serialising it again to pass it downstream was an excessive waste of time and cycles.
- The business service exposed was only one method end the expectations was this is how it would remain for sometime.
- The level of validation on the data being passed downstream was quite high so even though it could accept almost anything, only the correctly structured and formatted data would be accepted and processed.
So with that in mind, I created a generic service that accepted any service calls to a particular endpoint, extracted out the body of the SOAP message, and passed it on downstream.
The code looked like this:
Interface:
[MatchAllEndpoints]
[ServiceContract(SessionMode = SessionMode.Allowed)]
public interface ICatchAll
{
[OperationContract(IsOneWay = false, Action = "*", ReplyAction = "*")]
Message ProcessMessage(Message message);
}
Notice the Action parameter of the OperationContract attribute specifies '*' to indicate that any SoapAction is acceptible for this method.
Secondly, notice the [MatchAllEndpoints] attribute. Lets look at its code:
class MatchAllEndpoints : Attribute, IContractBehavior
{
public void AddBindingParameters(ContractDescription contractDescription, ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{ }
public void ApplyClientBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{ }
public void ApplyDispatchBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, DispatchRuntime dispatchRuntime)
{
dispatchRuntime.EndpointDispatcher.AddressFilter = new System.ServiceModel.Dispatcher.MatchAllMessageFilter();
}
public void Validate(ContractDescription contractDescription, ServiceEndpoint endpoint)
{ }
}
The only method we really implement is the ApplyDispatchBehavior. In this we set the AddressFilter to a MatchAllMessageFilter which determines the criteria upon which we want to deal with messages and ensures we get all the messages directed at a specific endpoint. This means it is irrelevant what service is actually called. If its directed at the endpoint exposed by this service, then we will get it.
Next comes the service itself:
public class CatchAllService : ICatchAll
{
public Message ProcessMessage(Message message)
{
// Create a buffered copy of the message in memory so we can read it AND take a another copy
MessageBuffer buffer = message.CreateBufferedCopy(8192);
// Get a copy of the original message. This will be used to read and extract the body.
Message msgCopy = buffer.CreateMessage();
// Take another copy of the same message. This will be used to return to the service. Returning an identical message forms part of
// the acknowledgement in this case.
Message returnMsg = buffer.CreateMessage();
// Use the msgCopy to get an XML Dictionary reader to extract the body contents. Once this message has been read and consumed,
// it can no longer be consumed again (this is why we took a second copy above
System.Xml.XmlDictionaryReader xrdr = msgCopy.GetReaderAtBodyContents();
string bodyData = xrdr.ReadOuterXml();
// Send the body of the message, which is the order, to be processed.
DomainObject.ProcessOrder(bodyData);
// Return the second copy of the message we took previously.
return returnMsg;
}
}
This little puppy took me a while to get right. All I wanted to do was get the body of the SOAP message as a big string. There are a myriad of methods on the Message object which seem to suggest you can use them to do this (such as Message.GetBody<T>() ) but it took sometime to experiment and get the right one.
Firstly, I grab a copy of the message in a MessageBuffer. If I dont do this, once I have read the message, I can do anything with it again. That is, once consumed, its a done deal. I needed the message again to return to the client as they compare what they sent with what is returned to act as an acknowledgement (I didn't come up with this method, I just had to make it work the same...)
So from the buffered message, I create a new message which I can read and consume. The second one is what we return to the consumer/client.
Next I construct an XmlDictionaryReader from the message itself by calling GetReaderAtBodyContents on the message object. From this I can read the OuterXml to get the body as a string and pass it on.
Lastly I return a copy of the message.
Finally, the configuration file to expose this service is pretty basic:
<services>
<service name="GlavsStuff.CatchAllService">
<endpoint binding="basicHttpBinding" name="GlavsStuff.CatchAllServiceEndpoint" contract="GlavsStuff.ICatchAll" />
</service>
</services>
Nothing special here.
And thats it. A generic message handler that can accept pretty much any service call at that endpoint.
One more thing. This service was hosted within Internet Information Server so there was an accompanying .SVC file. If your still reading this, then I figure listing the contents of that file is probably unecessary.
Note: Parts of this were taken from the Generic router example on the netfx3 site. I'd put a direct link here but things have moved around a bit and i cannot directly find it again.
This is currently working very well. Now I know about the inability to expose a decent set of metadata from this endpoint, but the client was not concerned about this. As long as the service call worked. In the end, I took a copy of the original WSDL and XSD documents, and placed them within the site so that they could query against that (with some minor modifications).
I hope this has been helpful.