WCF Extensibility – Message Formatters
This post is part of a series about WCF extensibility points. For a list of all previous posts and planned future ones, go to the index page .
Message formatters are the component which do the translation between CLR operations and the WCF Message object – their role is to convert all the operation parameters and return values (possibly via serialization) into a Message on output, and deconstruct the message into parameter and return values on input. Anytime a new format is to be used (be it a new serialization format for currently supported CLR types, or supporting a new CLR type altogether), a message formatter is the interface you’d implement to enable this scenario. For example, even though the type System.IO.Stream is not serializable (it’s even abstract), WCF allows it to be used as the input or return values for methods (for example, in the WCF REST Raw programming model) and a formatter deals with converting it into messages. Like the message inspectors, there are two versions, one for the server side (IDispatchMessageFormatter), and one for the client side (IClientMessageFormatter).
Among the extensibility points listed in this series, the message formatters are the first kind to be required in the WCF pipeline – a service does not need to have any service / endpoint / contract / operation behavior, nor any message / parameter inspector. If the user doesn’t add any of those, the client / service will just work (some behaviors are automatically added to the operations when one adds an endpoint, but that’s an implementation detail for WCF – they aren’t strictly necessary). Formatters, on the other hand, are required (to bridge the gap between the message world and the operations using CLR types). If we don’t add any behaviors to the service / endpoint / contract / operation that sets a formatter, WCF will add one formatter to the operation (usually via the DataContractSerializerOperationBehavior, which is added by default in all operation contracts).
Public implementations in WCF
None. As with most of the runtime extensibility points, there are no public implementations of either the dispatch or the client message formatter. There are a lot of internal implementations, though, such as a formatter which converts operation parameters into a message (both using the DataContractSerializer and the XmlSerializer), formatters which map parameters to the HTTP URI (used in REST operations with UriTemplate), among others.
Interface declaration
- public interface IDispatchMessageFormatter
- {
- void DeserializeRequest(Message message, object[] parameters);
- Message SerializeReply(MessageVersion messageVersion, object[] parameters, object result);
- }
- public interface IClientMessageFormatter
- {
- Message SerializeRequest(MessageVersion messageVersion, object[] parameters);
- object DeserializeReply(Message message, object[] parameters);
- }
The formatter interfaces each have two methods, one to convert between input outgoing CLR objects into the Message object (serialize), and one to take the incoming message object apart and break it down into incoming CLR objects (deserialize).
At the server side, after the the message was decoded, passed through the channel stack and other extensibility points and is about to be dispatched to the service operation, the method DeserializeRequest is called to deserialize the message object into an array of parameters to be passed to the operation. The object argument “parameters” will have been allocated, so the implementation of the formatter only needs to read the message and set the operation parameters in their correct order in the array passed to it. After the operation is invoked (and any parameter inspector has a chance to change the operation outputs), SerializeReply is called, to take the operation return, plus the final value of any out/ref parameters which the operation may have, and package them into a Message object to be sent down the WCF channel stack.
At the client side the mirror happens: when the operation is called (and after the parameter inspector has a chance to change the inputs), SerializeRequest is called to package all the input parameters into the outgoing message object. After the message is sent to the server and the response is decoded by the client’s encoder, DeserializeReply is called to open the message object and populate the return value and any possible out/ref parameters.
One small parenthesis about out/ref parameters: all parameters in WCF calls are passed by value – a copy of the value is sent across the wire. If an operation declares a parameter with a ref modifier (ByRef in Visual Basic), the service will receive a copy (i.e., by value) of that parameter, and then return another copy of the value of the parameter at the end of the method. This second copy will be deserialized at the client into a new instance of that type. So even if you pass an instance of your type by reference to a WCF service, and the service doesn’t change it at all, the value at the end of the call at the client side the reference will point to a different object.
- [ServiceContract]
- public interface ITest
- {
- [OperationContract]
- void UpdatePerson(ref Person person);
- [OperationContract]
- void InOutRef(int x, ref int y, out int z, out int w);
- }
- public class Client
- {
- public static void Main()
- {
- ChannelFactory<ITest> factory = new ChannelFactory<ITest>();
- ITest proxy = factory.CreateChannel();
- Person p1 = new Person();
- Person p2 = p1;
- Console.WriteLine(object.ReferenceEquals(p1, p2)); // true
- proxy.UpdatePerson(ref p2);
- Console.WriteLine(object.ReferenceEquals(p1, p2)); // false, even if service doesn't change reference
- }
- }
Going back to the formatters, the list of “input” parameters (those in the *Request) operations are either the input (ByVal) parameters or reference (ByRef without <Out>). The list of “output” parameters are ones marked with ref (ByRef) and out (<Out> ByRef). In the operation InOutRef above, the parameters x and y are considered to be input parameters, so the parameters array in the *Request operations will have two objects). The parameters y, z and w are considered to be output parameters, so the parameters array in the *Reply operations will have three elements.
How to add message formatters
At the server side: the formatter is a property of the operation runtime, so at the server side it’s available at the DispatchOperation object. This object is typically accessed via an operation behavior, or by traversing the operation list from an endpoint behavior. both in the ApplyDispatchBehavior method. Another way is if you’re creating a REST endpoints (i.e., one with a behavior which inherits from WebHttpBehavior). WebHttpBehavior exposes two protected methods, GetRequestDispatchFormatter and GetReplyDispatchFormatter, which can be overridden to to return the formatter to be used by the operation.
- public class MyDispatchMessageFormatter : IDispatchMessageFormatter
- {
- OperationDescription operation;
- public MyDispatchMessageFormatter(OperationDescription operation) { ... }
- }
- public class MyRestBehavior : WebHttpBehavior
- {
- protected override IDispatchMessageFormatter GetReplyDispatchFormatter(OperationDescription operationDescription, ServiceEndpoint endpoint)
- {
- return new MyDispatchMessageFormatter(operationDescription);
- }
- }
At the client side: Similarly, the operation formatter at the client side is available at the ClientOperation object, typically accessed via an operation or endpoint behavior. And for behaviors inheriting from WebHttpBehavior, there are two protected methods, GetRequestClientFormatter and GetReplyClientFormatter, which can be used to return the operation formatter as well.
- public class MyClientFormatter : IClientMessageFormatter
- {
- }
- public class MyOperationBehavior : IOperationBehavior
- {
- public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
- {
- clientOperation.Formatter = new MyClientFormatter();
- }
- }
A quick note about wrapping the default WCF formatter. As I mentioned before, the default formatter is usually added by the DataContractSerializerOperationBehavior when it’s executed. If we want to use that default formatter, and wrap it in our own formatter, we need to make sure that the DataContractSerializerOperationBehavior is a position in the operation behavior list which is executed before our own operation behavior is. Take this seemingly problem-free example:
- [ServiceContract]
- public interface ITest
- {
- [OperationContract]
- [MyOperationBehavior]
- int Add(int x, int y);
- }
- public class Service : ITest
- {
- public int Add(int x, int y) { return x + y; }
- }
- public class MyClientFormatter : IClientMessageFormatter
- {
- IClientMessageFormatter inner;
- public MyClientFormatter(IClientMessageFormatter inner)
- {
- if (inner == null) throw new ArgumentNullException("inner");
- this.inner = inner;
- }
- public object DeserializeReply(Message message, object[] parameters)
- {
- return this.inner.DeserializeReply(message, parameters);
- }
- public Message SerializeRequest(MessageVersion messageVersion, object[] parameters)
- {
- return this.inner.SerializeRequest(messageVersion, parameters);
- }
- }
- public class MyOperationBehaviorAttribute : Attribute, IOperationBehavior
- {
- public void AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters)
- {
- }
- public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
- {
- clientOperation.Formatter = new MyClientFormatter(clientOperation.Formatter);
- }
- public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
- {
- }
- public void Validate(OperationDescription operationDescription)
- {
- }
- }
- class Program
- {
- static void Main(string[] args)
- {
- string baseAddress = "https://" + Environment.MachineName + ":8000/Service";
- ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress));
- host.AddServiceEndpoint(typeof(ITest), new BasicHttpBinding(), "");
- host.Open();
- ChannelFactory<ITest> factory = new ChannelFactory<ITest>(new BasicHttpBinding(), new EndpointAddress(baseAddress));
- foreach (var opBehavior in factory.Endpoint.Contract.Operations[0].Behaviors)
- {
- Console.WriteLine(opBehavior);
- }
- ITest proxy = factory.CreateChannel();
- Console.WriteLine(proxy.Add(3, 4));
- }
- }
The call to CreateChannel in the channel factory will fail – the constructor of MyClientFormatter will throw. That’s because the order of the operation behaviors (printed in the code) is the following:
System.ServiceModel.Dispatcher.OperationInvokerBehavior
MyOperationBehaviorAttribute
System.ServiceModel.OperationBehaviorAttribute
System.ServiceModel.Description.DataContractSerializerOperationBehavior
System.ServiceModel.Description.DataContractSerializerOperationGenerator
I have no idea why attribute-based operation behaviors are added in this position, but the fact is that this doesn’t work. So for this scenario we cannot use attribute-based behaviors, we need to add it “manually”, as shown below.
- [ServiceContract]
- public interface ITest
- {
- [OperationContract]
- //[MyOperationBehavior]
- int Add(int x, int y);
- }
- class Program
- {
- static void Main(string[] args)
- {
- string baseAddress = "https://" + Environment.MachineName + ":8000/Service";
- ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress));
- ServiceEndpoint endpoint = host.AddServiceEndpoint(typeof(ITest), new BasicHttpBinding(), "");
- endpoint.Contract.Operations.Find("Add").Behaviors.Add(new MyOperationBehaviorAttribute());
- host.Open();
- ChannelFactory<ITest> factory = new ChannelFactory<ITest>(new BasicHttpBinding(), new EndpointAddress(baseAddress));
- ITest proxy = factory.CreateChannel();
- Console.WriteLine(proxy.Add(3, 4));
- }
- }
After doing that, the code runs and works fine.
Real world scenario: supporting different data and serialization formats
The first version of WCF (.NET Framework 3.0) supported essentially SOAP endpoints – an XML payload following some rules defined in the SOAP specification (you actually could do POX/REST, but it was awfully hard). On the next version (.NET Framework 3.5), the REST programming model was added and that started supporting a whole new set of formats, mostly notably POX (plain-old XML, no SOAP involved), JSON (based on the DataContractJsonSerializer) and a special “raw” mode, where one could declare a single Stream parameter in an operation for receiving arbitrary data, or returning a Stream value for returning arbitrary data. The raw mode can be used to support essentially all data and serialization formats, but that comes at a price that the operation itself has to deal with serializing / deserializing the parameters. Also, there are scenarios (from the forums) where the user needs to expose the same operation in non-REST endpoints (or even non-HTTP, as in the scenario described in the forum post), where the Stream trick won’t work. This example will expand on the answer to the forum question and provide formatters which will use a custom serializer (in this case, the JSON.NET one, available from codeplex at https://json.codeplex.com).
And the usual disclaimer: this is a sample for illustrating the topic of this post, this is not production-ready code. I tested it for a few contracts and it worked, but I cannot guarantee that it will work for all scenarios (please let me know if you find a bug or something missing). Also, for simplicity sake it doesn’t have a lot of error handling which a production-level code would, and it isn’t fully integrated with the WCF REST pipeline (i.e., it doesn’t support UriTemplates, it doesn’t integrate with the REST help page, doesn’t support out/ref parameters,, etc.).
First, some data contracts to set up the scenario. The service will process people and their pets. The data contracts are decorated both with WCF serialization attributes (DataContract, DataMember), and with the JSON.NET serialization attributes (JsonObejct, JsonProperty, JsonConverter) so that it can be mapped according to both formats.
- [DataContract]
- [Newtonsoft.Json.JsonObject(MemberSerialization = Newtonsoft.Json.MemberSerialization.OptIn)]
- public class Person
- {
- [DataMember(Order = 1), Newtonsoft.Json.JsonProperty]
- public string FirstName;
- [DataMember(Order = 2), Newtonsoft.Json.JsonProperty]
- public string LastName;
- [DataMember(Order = 3),
- Newtonsoft.Json.JsonProperty,
- Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.IsoDateTimeConverter))]
- public DateTime BirthDate;
- [DataMember(Order = 4), Newtonsoft.Json.JsonProperty]
- public List<Pet> Pets;
- [DataMember(Order = 5), Newtonsoft.Json.JsonProperty]
- public int Id;
- }
- [DataContract, Newtonsoft.Json.JsonObject(MemberSerialization = Newtonsoft.Json.MemberSerialization.OptIn)]
- public class Pet
- {
- [DataMember(Order = 1), Newtonsoft.Json.JsonProperty]
- public string Name;
- [DataMember(Order = 2), Newtonsoft.Json.JsonProperty]
- public string Color;
- [DataMember(Order = 3), Newtonsoft.Json.JsonProperty]
- public string Markings;
- [DataMember(Order = 4), Newtonsoft.Json.JsonProperty]
- public int Id;
- }
Next the interface definition, and the service implementation. It’s not very interesting, but I added operations using both GET and POST methods, and operations with single and multiple parameters (the goal of this sample is to demonstrate the formatter, not a specific service, so I think I can slack off on the service example a little ).
- [ServiceContract]
- public interface ITestService
- {
- [WebGet, OperationContract]
- Person GetPerson();
- [WebInvoke, OperationContract]
- Pet EchoPet(Pet pet);
- [WebInvoke(BodyStyle = WebMessageBodyStyle.WrappedRequest), OperationContract]
- int Add(int x, int y);
- }
- public class Service : ITestService
- {
- public Person GetPerson()
- {
- return new Person
- {
- FirstName = "First",
- LastName = "Last",
- BirthDate = new DateTime(1993, 4, 17, 2, 51, 37, 47, DateTimeKind.Local),
- Id = 0,
- Pets = new List<Pet>
- {
- new Pet { Name= "Generic Pet 1", Color = "Beige", Id = 0, Markings = "Some markings" },
- new Pet { Name= "Generic Pet 2", Color = "Gold", Id = 0, Markings = "Other markings" },
- },
- };
- }
- public Pet EchoPet(Pet pet)
- {
- return pet;
- }
- public int Add(int x, int y)
- {
- return x + y;
- }
- }
Now the behavior which will set everything together. Since this is a REST endpoint, instead of writing a new endpoint, I’ll simply inherit from WebHttpBehavior, and override the protected methods to return the client and server formatters. Notice that in some cases when there are no parameters or for GET requests (where the parameters are in the query string, not in the request body), so in those cases I’ll simply reuse the default formatter from the base class. Another logic which is implemented at the behavior is validation – we know what our formatter doesn’t support, so we’ll simply throw while the service (or the client channel) is being opened, giving the user a clear message explaining why the behavior failed the validation (some of the validation code is not in the snippet below, but it can be found in the source code for this post).
- public class NewtonsoftJsonBehavior : WebHttpBehavior
- {
- public override void Validate(ServiceEndpoint endpoint)
- {
- base.Validate(endpoint);
- BindingElementCollection elements = endpoint.Binding.CreateBindingElements();
- WebMessageEncodingBindingElement webEncoder = elements.Find<WebMessageEncodingBindingElement>();
- if (webEncoder == null)
- {
- throw new InvalidOperationException("This behavior must be used in an endpoint with the WebHttpBinding (or a custom binding with the WebMessageEncodingBindingElement).");
- }
- foreach (OperationDescription operation in endpoint.Contract.Operations)
- {
- this.ValidateOperation(operation);
- }
- }
- protected override IDispatchMessageFormatter GetRequestDispatchFormatter(OperationDescription operationDescription, ServiceEndpoint endpoint)
- {
- if (this.IsGetOperation(operationDescription))
- {
- // no change for GET operations
- return base.GetRequestDispatchFormatter(operationDescription, endpoint);
- }
- if (operationDescription.Messages[0].Body.Parts.Count == 0)
- {
- // nothing in the body, still use the default
- return base.GetRequestDispatchFormatter(operationDescription, endpoint);
- }
- return new NewtonsoftJsonDispatchFormatter(operationDescription, true);
- }
- protected override IDispatchMessageFormatter GetReplyDispatchFormatter(OperationDescription operationDescription, ServiceEndpoint endpoint)
- {
- if (operationDescription.Messages.Count == 1 || operationDescription.Messages[1].Body.ReturnValue.Type == typeof(void))
- {
- return base.GetReplyDispatchFormatter(operationDescription, endpoint);
- }
- else
- {
- return new NewtonsoftJsonDispatchFormatter(operationDescription, false);
- }
- }
- protected override IClientMessageFormatter GetRequestClientFormatter(OperationDescription operationDescription, ServiceEndpoint endpoint)
- {
- if (operationDescription.Behaviors.Find<WebGetAttribute>() != null)
- {
- // no change for GET operations
- return base.GetRequestClientFormatter(operationDescription, endpoint);
- }
- else
- {
- WebInvokeAttribute wia = operationDescription.Behaviors.Find<WebInvokeAttribute>();
- if (wia != null)
- {
- if (wia.Method == "HEAD")
- {
- // essentially a GET operation
- return base.GetRequestClientFormatter(operationDescription, endpoint);
- }
- }
- }
- if (operationDescription.Messages[0].Body.Parts.Count == 0)
- {
- // nothing in the body, still use the default
- return base.GetRequestClientFormatter(operationDescription, endpoint);
- }
- return new NewtonsoftJsonClientFormatter(operationDescription, endpoint);
- }
- protected override IClientMessageFormatter GetReplyClientFormatter(OperationDescription operationDescription, ServiceEndpoint endpoint)
- {
- if (operationDescription.Messages.Count == 1 || operationDescription.Messages[1].Body.ReturnValue.Type == typeof(void))
- {
- return base.GetReplyClientFormatter(operationDescription, endpoint);
- }
- else
- {
- return new NewtonsoftJsonClientFormatter(operationDescription, endpoint);
- }
- }
- }
On to the formatters. I’ll start with the dispatch formatter. As the new formatter was only added for POST operations with at least one parameter, we can safely assume that the messages will not be empty, and the request will be stored in the message body. But the message body is always represented in XML, so in order to support a custom format, we need to either define a mapping between that format and XML (which is what was done internally for supporting JSON in 3.5) – and implement XML readers and writers to support this mapping – or reuse one of the existing formats for which we already have a mapping for. And as in the raw programming model, the raw format (described in the post about message inspectors) is a great candidate for it – we can get the raw bytes from the request (and write raw bytes in the response) and parse them using our custom format. We must ensure, however, that the incoming message actually was read with the raw encoder (by default messages with JSON content type are read with the JSON encoder, we need a custom content type mapper to ensure that they are read as raw messages).
- class NewtonsoftJsonDispatchFormatter : IDispatchMessageFormatter
- {
- OperationDescription operation;
- Dictionary<string, int> parameterNames;
- public NewtonsoftJsonDispatchFormatter(OperationDescription operation, bool isRequest)
- {
- this.operation = operation;
- if (isRequest)
- {
- int operationParameterCount = operation.Messages[0].Body.Parts.Count;
- if (operationParameterCount > 1)
- {
- this.parameterNames = new Dictionary<string, int>();
- for (int i = 0; i < operationParameterCount; i++)
- {
- this.parameterNames.Add(operation.Messages[0].Body.Parts[i].Name, i);
- }
- }
- }
- }
- public void DeserializeRequest(Message message, object[] parameters)
- {
- object bodyFormatProperty;
- if (!message.Properties.TryGetValue(WebBodyFormatMessageProperty.Name, out bodyFormatProperty) ||
- (bodyFormatProperty as WebBodyFormatMessageProperty).Format != WebContentFormat.Raw)
- {
- throw new InvalidOperationException("Incoming messages must have a body format of Raw. Is a ContentTypeMapper set on the WebHttpBinding?");
- }
- XmlDictionaryReader bodyReader = message.GetReaderAtBodyContents();
- bodyReader.ReadStartElement("Binary");
- byte[] rawBody = bodyReader.ReadContentAsBase64();
- MemoryStream ms = new MemoryStream(rawBody);
- StreamReader sr = new StreamReader(ms);
- Newtonsoft.Json.JsonSerializer serializer = new Newtonsoft.Json.JsonSerializer();
- if (parameters.Length == 1)
- {
- // single parameter, assuming bare
- parameters[0] = serializer.Deserialize(sr, operation.Messages[0].Body.Parts[0].Type);
- }
- else
- {
- // multiple parameter, needs to be wrapped
- Newtonsoft.Json.JsonReader reader = new Newtonsoft.Json.JsonTextReader(sr);
- reader.Read();
- if (reader.TokenType != Newtonsoft.Json.JsonToken.StartObject)
- {
- throw new InvalidOperationException("Input needs to be wrapped in an object");
- }
- reader.Read();
- while (reader.TokenType == Newtonsoft.Json.JsonToken.PropertyName)
- {
- string parameterName = reader.Value as string;
- reader.Read();
- if (this.parameterNames.ContainsKey(parameterName))
- {
- int parameterIndex = this.parameterNames[parameterName];
- parameters[parameterIndex] = serializer.Deserialize(reader, this.operation.Messages[0].Body.Parts[parameterIndex].Type);
- }
- else
- {
- reader.Skip();
- }
- reader.Read();
- }
- reader.Close();
- }
- sr.Close();
- ms.Close();
- }
- public Message SerializeReply(MessageVersion messageVersion, object[] parameters, object result)
- {
- byte[] body;
- Newtonsoft.Json.JsonSerializer serializer = new Newtonsoft.Json.JsonSerializer();
- using (MemoryStream ms = new MemoryStream())
- {
- using (StreamWriter sw = new StreamWriter(ms, Encoding.UTF8))
- {
- using (Newtonsoft.Json.JsonWriter writer = new Newtonsoft.Json.JsonTextWriter(sw))
- {
- writer.Formatting = Newtonsoft.Json.Formatting.Indented;
- serializer.Serialize(writer, result);
- sw.Flush();
- body = ms.ToArray();
- }
- }
- }
- Message replyMessage = Message.CreateMessage(messageVersion, operation.Messages[1].Action, new RawBodyWriter(body));
- replyMessage.Properties.Add(WebBodyFormatMessageProperty.Name, new WebBodyFormatMessageProperty(WebContentFormat.Raw));
- HttpResponseMessageProperty respProp = new HttpResponseMessageProperty();
- respProp.Headers[HttpResponseHeader.ContentType] = "application/json";
- replyMessage.Properties.Add(HttpResponseMessageProperty.Name, respProp);
- return replyMessage;
- }
- }
When the formatter is serializing the reply of the message , it will create a new message using a “raw” body writer, which is a simple implementation of the BodyWriter class. It will then set the body format property to ensure that the raw encoder will be used when converting that message into bytes on the wire.
- class RawBodyWriter : BodyWriter
- {
- byte[] content;
- public RawBodyWriter(byte[] content)
- : base(true)
- {
- this.content = content;
- }
- protected override void OnWriteBodyContents(XmlDictionaryWriter writer)
- {
- writer.WriteStartElement("Binary");
- writer.WriteBase64(content, 0, content.Length);
- writer.WriteEndElement();
- }
- }
The client formatter is similar to the service one. On reply, since it doesn’t support multiple return values (no out/ref support implemented), it simply gets the bytes from the message and feeds them to the JSON.NET deserializer. When serializing the request, the formatter will either simply serialize a single parameter if it’s the only one, or it will wrap it in a JSON object with properties named by the operation parameter name. The last thing it does on serialization is to again set the body format property (to ensure that the raw encoder will be used for that message) and also set the “To” header of the message (which is required for REST messages, since it uses manual addressing on the transport).
- class NewtonsoftJsonClientFormatter : IClientMessageFormatter
- {
- OperationDescription operation;
- Uri operationUri;
- public NewtonsoftJsonClientFormatter(OperationDescription operation, ServiceEndpoint endpoint)
- {
- this.operation = operation;
- string endpointAddress = endpoint.Address.Uri.ToString();
- if (!endpointAddress.EndsWith("/"))
- {
- endpointAddress = endpointAddress + "/";
- }
- this.operationUri = new Uri(endpointAddress + operation.Name);
- }
- public object DeserializeReply(Message message, object[] parameters)
- {
- object bodyFormatProperty;
- if (!message.Properties.TryGetValue(WebBodyFormatMessageProperty.Name, out bodyFormatProperty) ||
- (bodyFormatProperty as WebBodyFormatMessageProperty).Format != WebContentFormat.Raw)
- {
- throw new InvalidOperationException("Incoming messages must have a body format of Raw. Is a ContentTypeMapper set on the WebHttpBinding?");
- }
- XmlDictionaryReader bodyReader = message.GetReaderAtBodyContents();
- Newtonsoft.Json.JsonSerializer serializer = new Newtonsoft.Json.JsonSerializer();
- bodyReader.ReadStartElement("Binary");
- byte[] body = bodyReader.ReadContentAsBase64();
- using (MemoryStream ms = new MemoryStream(body))
- {
- using (StreamReader sr = new StreamReader(ms))
- {
- Type returnType = this.operation.Messages[1].Body.ReturnValue.Type;
- return serializer.Deserialize(sr, returnType);
- }
- }
- }
- public Message SerializeRequest(MessageVersion messageVersion, object[] parameters)
- {
- byte[] body;
- Newtonsoft.Json.JsonSerializer serializer = new Newtonsoft.Json.JsonSerializer();
- using (MemoryStream ms = new MemoryStream())
- {
- using (StreamWriter sw = new StreamWriter(ms, Encoding.UTF8))
- {
- using (Newtonsoft.Json.JsonWriter writer = new Newtonsoft.Json.JsonTextWriter(sw))
- {
- writer.Formatting = Newtonsoft.Json.Formatting.Indented;
- if (parameters.Length == 1)
- {
- // Single parameter, assuming bare
- serializer.Serialize(sw, parameters[0]);
- }
- else
- {
- writer.WriteStartObject();
- for (int i = 0; i < this.operation.Messages[0].Body.Parts.Count; i++)
- {
- writer.WritePropertyName(this.operation.Messages[0].Body.Parts[i].Name);
- serializer.Serialize(writer, parameters[0]);
- }
- writer.WriteEndObject();
- }
- writer.Flush();
- sw.Flush();
- body = ms.ToArray();
- }
- }
- }
- Message requestMessage = Message.CreateMessage(messageVersion, operation.Messages[0].Action, new RawBodyWriter(body));
- requestMessage.Headers.To = operationUri;
- requestMessage.Properties.Add(WebBodyFormatMessageProperty.Name, new WebBodyFormatMessageProperty(WebContentFormat.Raw));
- HttpRequestMessageProperty reqProp = new HttpRequestMessageProperty();
- reqProp.Headers[HttpRequestHeader.ContentType] = "application/json";
- requestMessage.Properties.Add(HttpRequestMessageProperty.Name, reqProp);
- return requestMessage;
- }
- }
Now for testing the formatters. The first thing we need is the content type mapper, to ensure that all incoming requests will be read by the raw encoder. Its implementation is quite simple.
- class MyRawMapper : WebContentTypeMapper
- {
- public override WebContentFormat GetMessageFormatForContentType(string contentType)
- {
- return WebContentFormat.Raw;
- }
- }
Now for the test itself. I’m using a helper method (SendRequest) to send arbitrary HTTP requests to the server, but I’ll skip it here (it’s just setting up HttpWebRequest and displaying the results from HttpWebResponse). The test code sets up a service and adds two endpoints, one “normal” (SOAP) and one JSON.NET enabled
- static void Main(string[] args)
- {
- string baseAddress = "https://" + Environment.MachineName + ":8000/Service";
- ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress));
- host.AddServiceEndpoint(typeof(ITestService), new BasicHttpBinding(), "soap");
- WebHttpBinding webBinding = new WebHttpBinding();
- webBinding.ContentTypeMapper = new MyRawMapper();
- host.AddServiceEndpoint(typeof(ITestService), webBinding, "json").Behaviors.Add(new NewtonsoftJsonBehavior());
- Console.WriteLine("Opening the host");
- host.Open();
- ChannelFactory<ITestService> factory = new ChannelFactory<ITestService>(new BasicHttpBinding(), new EndpointAddress(baseAddress + "/soap"));
- ITestService proxy = factory.CreateChannel();
- Console.WriteLine(proxy.GetPerson());
- SendRequest(baseAddress + "/json/GetPerson", "GET", null, null);
- SendRequest(baseAddress + "/json/EchoPet", "POST", "application/json", "{\"Name\":\"Fido\",\"Color\":\"Black and white\",\"Markings\":\"None\",\"Id\":1}");
- SendRequest(baseAddress + "/json/Add", "POST", "application/json", "{\"x\":111,\"z\":null,\"w\":[1,2],\"v\":{\"a\":1},\"y\":222}");
- Console.WriteLine("Now using the client formatter");
- ChannelFactory<ITestService> newFactory = new ChannelFactory<ITestService>(webBinding, new EndpointAddress(baseAddress + "/json"));
- newFactory.Endpoint.Behaviors.Add(new NewtonsoftJsonBehavior());
- ITestService newProxy = newFactory.CreateChannel();
- Console.WriteLine(newProxy.Add(444, 555));
- Console.WriteLine(newProxy.EchoPet(new Pet { Color = "gold", Id = 2, Markings = "Collie", Name = "Lassie" }));
- Console.WriteLine(newProxy.GetPerson());
- Console.WriteLine("Press ENTER to close");
- Console.ReadLine();
- host.Close();
- Console.WriteLine("Host closed");
- }
And that’s it for the formatter example!
New WCF Web API
One note that I want to leave here is that it’s not too easy to integrate any new formats in the existing WCF REST pipeline. In the example above I had to deal with formatters, Message objects, take a dependency on a content type mapper, just to be able to use this different serializer. And even after all of this, it’s not completely integrated with the pipeline – UriTemplates aren’t supported, for example. In an upcoming release, however, the WCF team is working hard to make WCF really a great platform for hosting HTTP services, and that includes a new platform (built on top of the current one) for HTTP services specifically. In the new WCF Web API (you can preview the bits on the Codeplex site) implementing such a scenario is a lot simpler, and other features (such as UriTemplate) will just continue to work after the new formatter is plugged in. Glenn Block has posted a formatter that does exactly that on the thread https://wcf.codeplex.com/discussions/255873, you decide which one is easier :)
Coming up
Operation selectors.
Carlos Figueira
https://blogs.msdn.com/carlosfigueira
Twitter: @carlos_figueira https://twitter.com/carlos_figueira