WCF Extensibility – IContractBehavior
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.
Going down the list of behaviors, the second one to be covered is the IContractBehavior. Like the other ones, it can be used to inspect or change the contract description and its runtime for all endpoints which have that contract. For example, the behavior can validate the contract against the binding to ensure that the it’s used the contract is only used with appropriate, or it can modify the runtime for all its operations in a centralized location. Unlike IServiceBehavior mentioned in the previous post, the IContractBehavior is also used to change the client runtime.
Public implementations in WCF
- DeliveryRequirementsAttribute: Defines ordering requirements for the binding which uses the contract, such as whether the contract requires ordered or queued delivery.
- ServiceMetadataContractBehavior (new in 4.0): Determines whether endpoints with the given contract should be exposed in the service metadata.
- JavascriptCallbackBehaviorAttribute: For REST/JSON endpoints, defines the URL query string parameter name which needs to be passed to change the response from “normal” JSON to JSONP.
Interface declaration
- public interface IContractBehavior
- {
- void Validate(ContractDescription contractDescription, ServiceEndpoint endpoint);
- void ApplyDispatchBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, DispatchRuntime dispatchRuntime);
- void ApplyClientBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, ClientRuntime clientRuntime);
- void AddBindingParameters(ContractDescription contractDescription, ServiceEndpoint endpoint, BindingParameterCollection bindingParameters);
- }
As was mentioned in the post about Behaviors, you can use the Validate method to ensure that the contract doesn’t violate the validation logic on the behavior. One example is the DeliveryRequirementsAttribute, which throws if the behavior requires ordered message delivery but it’s not there (i.e., using HTTP, MSMQ), or if the behavior says that it cannot have queued delivery but the binding provides that.
AddBindingParameters is used to pass information to the binding elements themselves. It’s not used as often, but one (not too common) example would be for a contract, after validating that the binding does not contain any message encoding binding elements, to add one in the binding parameters at this method, thus guaranteeing that it would always use that encoding type.
ApplyDispatchBehavior is the method which is most used in the server side. It’s called right after the runtime was initialized, so at that point the code has access to the listeners / dispatchers / runtime objects for the server, and can add / remove / modify those. The example below will use a contract behavior to change the serializer used by all of the operations in the contract.
ApplyClientBehavior is the counterpart to ApplyDispatchBehavior for the client side. After the client runtime is initialized, the method is called so that the code can update the client runtime objects.
How to add a contract behavior
Via the service / endpoint description (server) : On the server side, once you add an endpoint you can get a reference to it, then you can access its Contract property, and from there you can access the behavior collection.
- 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.Behaviors.Add(new MyContractBehavior());
Via the channel factory / generated proxy (client) : The ChannelFactory class has an Endpoint property which is of the same type as the one on the server. Likewise, generated proxies (using svcutil / add service reference) are subclasses of ClientBase<T>, which also has an Endpoint property which contains a reference to the contract description, and from there to the behavior collection. Just remember that, once the endpoint has been opened (for channel factories, when it’s been explicitly opened or a channel created; for generated clients when it’s been explicitly opened or an operation called), modifying the description will have no effect on the runtime, since it’s already been created).
- ChannelFactory<ITest> factory = new ChannelFactory<ITest>(binding, new EndpointAddress(address));
- factory.Endpoint.Contract.Behaviors.Add(new MyContractBehavior());
- ITest proxy = factory.CreateChannel();
Using attributes: if the contract behavior is derived from System.Attribute, it can be applied to either the contract interface or the service class, and it will be added to the contract description by WCF. If it’s applied to the contract interface, it will be added to the contract description in all endpoints which use that contract (both server and client). If the attribute is applied to the service implementation, it will be applied to all contracts implemented by the service (unless the attribute also implements the IContractBehaviorAttribute interface, in which case it can be restricted to a single contract type – see more information in the remarks section of the documentation for IContractBehavior.
- public class MyContractBehaviorAttribute : Attribute, IContractBehavior
- {
- // IContractBehavior implementation
- }
- [ServiceContract]
- [MyContractBehavior]
- public interface ICalculator
- {
- [OperationContract]
- int Add(int x, int y);
- }
Using configuration: Configuration is not an option for adding endpoint behaviors.
Real world scenario: using a new serializer
This has come up a few times in the forums (1, 2, 3, …). Most of the time it’s about forcing WCF to use XmlSerializer, or replace the default DataContractSerializer with the NetDataContractSerializer (to avoid having to deal with known types in inheritance-heavy scenarios). Other times people really need a new way of serializing / deserializing objects – for performance or size reasons, for example. In this example I have a custom serializer which knows how to serialize very compactly some data types which are tagged with a specific interface. The interface is a simple one with two methods: one for the object to serialize itself into a stream, and another for an object to initialize itself from a stream.
- public interface ICustomSerializable
- {
- void WriteTo(Stream stream);
- void InitializeFrom(Stream stream);
- }
And before I go any further, here goes 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 inputs and it worked, but I cannot guarantee that it will work for all types (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. Finally, this sample (as well as most other samples in this series) uses extensibility points other than the one for this post (e.g., operation behavior, custom serializers, etc.) which are necessary to get a realistic scenario going. I’ll briefly describe what they do, and leave to their specific entries a more detailed description of their behavior.
Below are a few types which implement this interface. They use some helper functions to read/write data in a compact binary format (the code for the helper can be found in the [Code for this post] link at the end). This is a format similar to the one used by the .NET XML Binary Format (the one used by the binary encoding in WCF).
- public class Product : ICustomSerializable
- {
- public string Name;
- public string Unit;
- public int UnitPrice;
- public void WriteTo(Stream stream)
- {
- FormatHelper.WriteString(stream, this.Name);
- FormatHelper.WriteString(stream, this.Unit);
- FormatHelper.WriteInt(stream, this.UnitPrice);
- }
- public void InitializeFrom(Stream stream)
- {
- this.Name = FormatHelper.ReadString(stream);
- this.Unit = FormatHelper.ReadString(stream);
- this.UnitPrice = FormatHelper.ReadInt(stream);
- }
- }
- public class Order : ICustomSerializable
- {
- public int Id;
- public Product[] Items;
- public DateTime Date;
- public void WriteTo(Stream stream)
- {
- FormatHelper.WriteInt(stream, this.Id);
- FormatHelper.WriteInt(stream, this.Items.Length);
- for (int i = 0; i < this.Items.Length; i++)
- {
- this.Items[i].WriteTo(stream);
- }
- FormatHelper.WriteLong(stream, this.Date.ToBinary());
- }
- public void InitializeFrom(Stream stream)
- {
- this.Id = FormatHelper.ReadInt(stream);
- int itemCount = FormatHelper.ReadInt(stream);
- this.Items = new Product[itemCount];
- for (int i = 0; i < itemCount; i++)
- {
- this.Items[i] = new Product();
- this.Items[i].InitializeFrom(stream);
- }
- this.Date = DateTime.FromBinary(FormatHelper.ReadLong(stream));
- }
- }
Now to the behavior. In order to easily share the behavior between client and server (after all, both sides need to agree on the serialization format), I’m implementing it as an attribute, so we can share the contract definition between the parties and it will be decorated with the behavior. The behavior will do two things: validate that all types which will use the new serializer can be created (i.e., they’re public types with a public, parameter-less constructor), and replace a behavior in all of the contract operations. The behavior which selects the serializer in WCF is the DataContractSerializerOperationBehavior (DCSOB), whose task is to add a formatter to the runtime which knows how to convert between the incoming / outgoing message and the parameters in the operations. We could create a new formatter to do that, but inheriting from DCSOB (even though we won’t be using the DataContractSerializer for our custom serialization) will save us time, as it exposes two helper virtual methods that makes it easier to replace the serializer).
- public class MyNewSerializerContractBehaviorAttribute : Attribute, IContractBehavior
- {
- public void AddBindingParameters(ContractDescription contractDescription, ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
- {
- }
- public void ApplyClientBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, ClientRuntime clientRuntime)
- {
- this.ReplaceSerializerOperationBehavior(contractDescription);
- }
- public void ApplyDispatchBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, DispatchRuntime dispatchRuntime)
- {
- this.ReplaceSerializerOperationBehavior(contractDescription);
- }
- public void Validate(ContractDescription contractDescription, ServiceEndpoint endpoint)
- {
- foreach (OperationDescription operation in contractDescription.Operations)
- {
- foreach (MessageDescription message in operation.Messages)
- {
- this.ValidateMessagePartDescription(message.Body.ReturnValue);
- foreach (MessagePartDescription part in message.Body.Parts)
- {
- this.ValidateMessagePartDescription(part);
- }
- foreach (MessageHeaderDescription header in message.Headers)
- {
- this.ValidateCustomSerializableType(header.Type);
- }
- }
- }
- }
- private void ValidateMessagePartDescription(MessagePartDescription part)
- {
- if (part != null)
- {
- this.ValidateCustomSerializableType(part.Type);
- }
- }
- private void ValidateCustomSerializableType(Type type)
- {
- if (typeof(ICustomSerializable).IsAssignableFrom(type))
- {
- if (!type.IsPublic)
- {
- throw new InvalidOperationException("Custom serialization is supported in public types only");
- }
- ConstructorInfo defaultConstructor = type.GetConstructor(new Type[0]);
- if (defaultConstructor == null)
- {
- throw new InvalidOperationException("Custom serializable types must have a public, parameterless constructor");
- }
- }
- }
- private void ReplaceSerializerOperationBehavior(ContractDescription contract)
- {
- foreach (OperationDescription od in contract.Operations)
- {
- for (int i = 0; i < od.Behaviors.Count; i++)
- {
- DataContractSerializerOperationBehavior dcsob = od.Behaviors[i] as DataContractSerializerOperationBehavior;
- if (dcsob != null)
- {
- od.Behaviors[i] = new MyNewSerializerOperationBehavior(od);
- }
- }
- }
- }
- class MyNewSerializerOperationBehavior : DataContractSerializerOperationBehavior
- {
- public MyNewSerializerOperationBehavior(OperationDescription operation) : base(operation) { }
- public override XmlObjectSerializer CreateSerializer(Type type, string name, string ns, IList<Type> knownTypes)
- {
- return new MyNewSerializer(type, base.CreateSerializer(type, name, ns, knownTypes));
- }
- public override XmlObjectSerializer CreateSerializer(Type type, XmlDictionaryString name, XmlDictionaryString ns, IList<Type> knownTypes)
- {
- return new MyNewSerializer(type, base.CreateSerializer(type, name, ns, knownTypes));
- }
- }
- }
Finally, the serializer. Implementing a new WCF serializer is a matter of finding a mapping between XML (the internal format used by WCF messages) and the format you want. For this optimized serialization, I’ll use a very simple mapping which writes the serialized object as binary data, wrapped inside a XML element. The mapping is done by subclassing XmlObjectSerializer, by overriding a few methods about reading / writing the object from / to XML reader / writers. Also, since there are types which can be used in the operation but aren’t ready for optimized serialization, the serializer is holding on to a reference to the original serializer from WCF, so that they can be used as well. So the implementation checks whether the type being serialized supports custom serialization; if it does, the new logic is used; otherwise it delegates the call to the original serializer for that type.
- public class MyNewSerializer : XmlObjectSerializer
- {
- const string MyPrefix = "new";
- Type type;
- bool isCustomSerialization;
- XmlObjectSerializer fallbackSerializer;
- public MyNewSerializer(Type type, XmlObjectSerializer fallbackSerializer)
- {
- this.type = type;
- this.isCustomSerialization = typeof(ICustomSerializable).IsAssignableFrom(type);
- this.fallbackSerializer = fallbackSerializer;
- }
- public override bool IsStartObject(XmlDictionaryReader reader)
- {
- if (this.isCustomSerialization)
- {
- return reader.LocalName == MyPrefix;
- }
- else
- {
- return this.fallbackSerializer.IsStartObject(reader);
- }
- }
- public override object ReadObject(XmlDictionaryReader reader, bool verifyObjectName)
- {
- if (this.isCustomSerialization)
- {
- object result = Activator.CreateInstance(this.type);
- MemoryStream ms = new MemoryStream(reader.ReadElementContentAsBase64());
- ((ICustomSerializable)result).InitializeFrom(ms);
- return result;
- }
- else
- {
- return this.fallbackSerializer.ReadObject(reader, verifyObjectName);
- }
- }
- public override void WriteEndObject(XmlDictionaryWriter writer)
- {
- if (this.isCustomSerialization)
- {
- writer.WriteEndElement();
- }
- else
- {
- this.fallbackSerializer.WriteEndObject(writer);
- }
- }
- public override void WriteObjectContent(XmlDictionaryWriter writer, object graph)
- {
- if (this.isCustomSerialization)
- {
- MemoryStream ms = new MemoryStream();
- ((ICustomSerializable)graph).WriteTo(ms);
- byte[] bytes = ms.ToArray();
- writer.WriteBase64(bytes, 0, bytes.Length);
- }
- else
- {
- this.fallbackSerializer.WriteObjectContent(writer, graph);
- }
- }
- public override void WriteStartObject(XmlDictionaryWriter writer, object graph)
- {
- if (this.isCustomSerialization)
- {
- writer.WriteStartElement(MyPrefix);
- }
- else
- {
- this.fallbackSerializer.WriteStartObject(writer, graph);
- }
- }
- }
Coming up
The remaining 2 behavior interfaces.
Comments
- Anonymous
June 27, 2012
Really great series of posts.....carlosfigueiraexpecting more articles...and any where these vedios are available......