Extending WCF to support custom data formats
Overview
WCF is a highly extensible framework. If you are familiar with WCF you would be happy to know that all of WCF programming model is built on this extensibility model. Things like Instancing, Concurrency Behaviours etc are all built by levering the WCF extensibility model.
In this article, I will show a how to extend WCF to support custom data formats. By using such techniques you can bring any data format into WCF programming model which a huge powerful thing.
So here is the data format which I want to bring in to WCF programming model. The ultimate goal is: “If I post following XML to a service endpoint, a method in service class should be called and all this information should be made available to that method in a typed fashion.”
Input xml is just a usual XML infoset with following two differences:
-
It is length prefixed (which makes it unusable in WCF)
-
Dispatch information (highlighted) is embedded somewhere in the message rather than in a SOAP Action header.
2922 <cupps xmlns="IATA-CUPPS/1.0" messageID="1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="IATA-CUPPS/1.0..\cupps-v1.0.xsd" messageName="authenticateRequest"> <authenticateRequest airline="JL" eventToken="S3M6CJ8L3J9M1C5X" platformDefinedParameter="ZBKD70DF3K:25324;asd="> <applicationList> <application applicationName="ABCMS" applicationVersion="01.02"/> <application applicationName="WOLMO" applicationVersion="02.03" applicationData="printerinterface"/> <application applicationName="JLABC" applicationVersion="01.02"/> </applicationList> </authenticateRequest> </cupps>
|
First of all let’s list down the requirement for our customized solution.
-
We need to support above format over Http transport.
-
Length information needs to be provided to WCF methods in an out of band manner.
-
Information from wrapper element (cupps in this case) MUST be provided to service method in an out of band manner.
-
We want to extract first element and pass it to the service method using method’s signature.
-
We want to do operation dispatch using the messageName attribute of the root element.
-
We want to support same contract to both proprietary clients and standard SOAP clients.
By default this input data format (length prefixed) cannot be processed by WCF. So how should we extend WCF to support this format? Do we need a custom channel for this?
Channel model customization
The guideline is, if your input data has some obvious structure on the write you usually don’t a need a channel. In my case, input is standard XML (with a well defined structure) however it is length prefixed which makes it unusable in WCF as is.
Now inside the WCF pipeline (both Service Model & Channel Stack) data flows as a Message object while on the wire, data flows as a sequence of bytes. Encoder is a WCF component which transforms a Message object into a byte stream and vice versa. Out of box WCF provides following 4 encoders:
-
Text: Uses text based (UTF-8 by default) XML encoding.
-
MTOM: An interoperable encoder which supports efficient binary data transmission.
-
Binary: A highly optimized dictionary based WCF specific encoder.
-
Web: Added in .NET Framework 3.5 and support JSON (JavaScript Object Notation) and POX (Plain-Old XML) encoding.
None of these encoders understands our length prefixed data format. But if I strip the length out of input xml, I will get a regular text encoded XML document which can easily be processed by a Text encoder. So to bring this custom format into WCF model, first thing I have to do is to a new custom encoder. This custom encoder will simply be a wrapper around an existing encoder (Text). It will extract and remove the length from the input byte stream and then simply delegates the actual reading to a wrapped encoder.
class LengthPrefixedMessageEncoder : MessageEncoder { MessageEncoder orignalEncoder; public LengthPrefixedMessageEncoder(MessageEncoder orignalEncoder){ this.orignalEncoder = orignalEncoder; } public override Message ReadMessage(ArraySegment<byte> buffer, BufferManager bufferManager, string contentType) { int i = 0; i = contentType.ToLower().IndexOf("charset"); string charset = "utf-8"; if (i > 0){ charset = contentType.Substring(i); charset = charset.Split('=')[1]; } var encoding = Encoding.GetEncoding(charset); int length = 0;
for (i = 0; i < buffer.Count; i++) if ((buffer.Array[i] == 13 && buffer.Array[i + 1] == 10) || (buffer.Array[i] == 10 && buffer.Array[i + 1] == 13)) break; if (i < buffer.Count){ var strLength = encoding.GetString(buffer.Array.Take(i).ToArray()); if (int.TryParse(strLength, out length{ buffer = new ArraySegment<byte>(buffer.Array,i + 2, buffer.Count - (i + 2));
var messsage = orignalEncoder.ReadMessage(buffer, bufferManager, contentType);
var property = new XmlRequestMessageProperty(); property.PrefixedLength = length; messsage.Properties.Add(XmlRequestMessageProperty.Name, property); return messsage; } } return orignalEncoder.ReadMessage(buffer, bufferManager,contentType); } } |
Now with our custom encoder plugged into the pipeline, this is how the server side runtime will look like.
Figure 1: Extended runtime
A byte stream will be read by transport channel (Http) and is handed over to the encoder. In this case our custom encoder will be called which simply reads the length out of the byte stream and delegates the actual reading to a wrapped encoder which creates the Message object out of the remaining byte stream.
This Message object then flow up the WCF channel stack and will reach the Dispatcher. Let’s see what’s happen in the dispatcher and what customization are required to route this message to appropriate endpoint, contract and ultimately to the correct method in our service implementation.
Dispatcher customizations
The very step done in the dispatcher is to find out if the incoming message matches to any endpoint. This is done using two filters: AddressFilter & ContractFilter.
AddressFilter determines whether the address on the incoming message matches to any of the endpoint(s) addresses.
ContactFilter determines whether this message is as per the contract exposed on the matched endpoint. By default this filter checks if the SOAP Action header of incoming message matches to any of the service operations.
Now in my case – there is no SOAP action header either at the WS-Addressing level or Http level. So the first customization I needed is a custom MessageFilter which will use some other marker inside the message (in my case messageName attribute of root element) for its decision.
class CutomAttributeMessageFilter : MessageFilter { XName attributeName; string xmlns; public CutomAttributeMessageFilter(string xmlns, string attributeName) { this.attributeName = attributeName; this.xmlns = xmlns; } public override bool Match(Message message) { XmlRequestMessageProperty property = null; var msgElement = XElement.Parse(message.ToString()) as XElement;
if ( message.Properties.Keys.Contains(XmlRequestMessageProperty.Name)) property = message.Properties[XmlRequestMessageProperty.Name] as XmlRequestMessageProperty;
if (property == null) { property = new XmlRequestMessageProperty(); message.Properties.Add(XmlRequestMessageProperty.Name, property); } msgElement.Attributes().Where(a => ((a.Name.Namespace.NamespaceName == "" && msgElement.GetDefaultNamespace().NamespaceName == xmlns) || (a.Name.Namespace.NamespaceName == xmlns)) && !a.IsNamespaceDeclaration).All(a => { property.Parameters.Add(a.Name.LocalName, a.Value); return true; } );
try { string operationName = msgElement.Attribute(attributeName).Value; var actualMsgElement = msgElement.Elements().First(); var fixedMsg = Message.CreateMessage( message.Version, message.Headers.Action, actualMsgElement.CreateReader());
fixedMsg.Properties.CopyProperties(message.Properties);
property.Message = fixedMsg; property.Operation = operationName; } catch (Exception ex) { throw new Exception("Filter mismatch...", ex); } return true; } public override bool Match(MessageBuffer buffer) { return this.Match(buffer.CreateMessage()); } }
|
Now I have done quite a lot of work here. Let me explain that. First of all I parsed the message into an XElement so that I process it using Linq to XML API.
I created an instance of custom message property so that I can flow some of the data in an out of band manner to the service operation. I then extracted some information from the message and copied it into this custom property object.
After extracting all the contextual information from root element – I simply created a new message containing the actual business information which needs to be communicated to a service method via its method signature.
If the incoming message satisfies the Address & Contract filters, message processing continues with the execution of WCF dispatch pipeline. Following are major extensibility points exposed on dispatch pipeline.
Figure 2: Dispatch pipeline
OperationSelector component is responsible for selecting a service operation based on incoming message. By default WCF use SOAP Action header for this selection however in my case this selection will be done based on a custom attribute. So I need to customize this functionality.
I have already done all the heavily lifting in the filter so here I can simply reusing that information.
class CustomAttributeOperationSelector : IDispatchOperationSelector { public string SelectOperation(ref Message message) { var prop = message.Properties[XmlRequestMessageProperty.Name] as XmlRequestMessageProperty; if (prop == null) throw new Exception("Dispatch information missing...");
message = prop.Message; return prop.Operation; } }
|
I simply returned an already computed operationName and also replaced the actual message to a rewritten one which is as per my service data contract.
After the operation selection stage, message processing continues with an operation specific dispatch pipeline and first step performed in this pipeline is the transformation of message object into parameters required by that operation. Formatter is the WCF component which does this transformation.
To enable deserialization of my custom XML into the objects required by the service method I have created a customized formatter. In my custom formatter, I am simply leveraging the Xml serializer to do the actual de-serialization. On the reply, I’m simply delegating the serialization work to the configured formatter (the default is based DataContractSerializer)
class CustomizedXmlSerializerFormatter : IDispatchMessageFormatter { IDispatchMessageFormatter orignal; OperationDescription operationDescription; public CustomizedXmlSerializerFormatter(IDispatchMessageFormatter orignal, OperationDescription operationDescription) { this.orignal = orignal; this.operationDescription = operationDescription; }
public void DeserializeRequest(Message message, object[] parameters) { if (message.Properties.Keys.Contains(XmlRequestMessageProperty.Name)) { var inputMsg = operationDescription.Messages.First(md => md.Direction == MessageDirection.Input); if (inputMsg.Body.Parts.Count > 1) throw new Exception("Configured formatter only supports a single [XmlSerializable] input parameter."); XmlSerializer serializer = new XmlSerializer(inputMsg.Body.Parts[0].Type); parameters[0] = serializer.Deserialize(message.GetReaderAtBodyContents()); return; } orignal.DeserializeRequest(message, parameters); } public Message SerializeReply(MessageVersion messageVersion, object[] parameters, object result){ return orignal.SerializeReply(messageVersion, parameters, result); } }
|
After this stage message flows through the rest of pipeline and ultimately reaches to a configured operation invoker, which invokes the operation on my configured service instance.
Now let’s see how various bits & pieces tie together.
Putting everything together
If you have a standard WCF service contract then for every operation where you need this custom framework (custom de-serialization), you will use CustomizedXmlSerializerFormat attribute. This attribute will simply replace the default formatter for the operation with my CustomizedXmlSerializerFormatter.
[ServiceContract] public interface ICustomServiceContrat { [OperationContract(Name = "authenticateRequest")] [CustomizedXmlSerializerFormat] string AuthenticateRequest(AuthenticateRequest request); }
|
Now from runtime perspective I need a custom endpoint behaviour for every endpoint on which I want to enable this custom dispatch functionality. This custom behaviour simply customizes some of the above extensibility points with my customized implementations.
class ActionLessDispatchEndpointBehavior : IEndpointBehavior { public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { endpointDispatcher.ContractFilter = new CutomAttributeMessageFilter("IATA-CUPPS/1.0", "messageName"); endpointDispatcher.DispatchRuntime.OperationSelector = new CustomAttributeOperationSelector();
foreach (var od in endpoint.Contract.Operations) { var ca = od.SyncMethod.GetCustomAttributes(typeof(CustomizedXmlSerializerFormat), false); if (ca.Length == 1) { od.Behaviors.Add(new SerializerOperationBehavior()); } } }
public void Validate(ServiceEndpoint endpoint) { if (endpoint.Binding.MessageVersion != MessageVersion.None) throw new Exception("SOAP is not supported. Please use MessageVersion.None"); } }
|
In SerializerOperationBehavior, I’m simply replacing the MessageFormatter to my custom one.
class SerializerOperationBehavior : IOperationBehavior { public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation) { dispatchOperation.Formatter = new CustomizedXmlSerializerFormatter(dispatchOperation.Formatter, operationDescription); } } |
The final customization is to change the default encoder with our customized encoder.
static void ConfigureEndpoint(ServiceEndpoint endpoint) { var be = endpoint.Binding.CreateBindingElements(); var orignal = be.Remove<MessageEncodingBindingElement>(); if (orignal != null) be.Insert(be.Count - 1, new LengthPrefixedMessageEncodingBindingElement(orignal)); endpoint.Binding = new CustomBinding(be); endpoint.Behaviors.Add(new ActionLessDispatchEndpointBehavior()); }
|
Both of these hooks (encoder & endpoint behaviour) can be easily applied using the configuration file as well.
Finally this is how a typical method implementation will look like. Contextual information is provided to the method via the incoming message properties while business information is provided via the parameter(s) of the method.
public string AuthenticateRequest(AuthenticateRequest request) { foreach(var app in request.Applications) Console.WriteLine("\tAppName={0}, AppVersion={1}, AppData={2}\n", app.Name,app.Version, app.Data);
// extract out of band information if (OperationContext.Current.IncomingMessageProperties.Keys.Contains(XmlRequestMessageProperty.Name)) { var xrmp = OperationContext.Current.IncomingMessageProperties[XmlRequestMessageProperty.Name] as XmlRequestMessageProperty;
foreach(var de in xrmp.Parameters) Console.WriteLine("\tName={0}, Value={1}", de.Key, de.Value);
} return "Done."; }
|
Wrap-up
WCF is highly extensible messaging framework supporting a rich programming model. All of web programming model introduced in .NET Framework 3.5 is build using the same extensibility model. If you are still doing communication using some proprietary data format then you can use the techniques explained in this article to bring your proprietary format into the WCF programming model.