Have you ever had the need to customize json serialization and deserialization of your WCF rest endpoint? I have, in particular to replace the WCF datetime format by generalized time format. By default, most json serializers serialize datetime objects to a generalized time format, this could lead to problems if you are creating a WCF rest endpoint which only supports input of microsofts datetime implementation like this \/Date(1293034567877)\/. A way to solve this problem is to replace the behavior of your rest endpoint by a custom one that uses newtonsofts json serializer instead.

To do this we have to create a message formatter class by implementing IDispatchMessageFormatter:

public class NewtonsoftJsonDispatchFormatter : IDispatchMessageFormatter
{
	public void DeserializeRequest( Message message, object[] parameters ) { ... }
	public Message SerializeReply( MessageVersion messageVersion, object[] parameters, object result ) { ... }
}

This class will be using newtonsofts json serializer to serialize and deserialize messages. Next we have to create a behavior class that extends WebHttpBehavior:

public class NewtonsoftJsonBehavior : WebHttpBehavior
{
	protected override IDispatchMessageFormatter GetRequestDispatchFormatter( OperationDescription operationDescription, ServiceEndpoint endpoint )
	{
		return new NewtonsoftJsonDispatchFormatter( operationDescription, true );
	}

	protected override IDispatchMessageFormatter GetReplyDispatchFormatter( OperationDescription operationDescription, ServiceEndpoint endpoint )
	{
		return new NewtonsoftJsonDispatchFormatter( operationDescription, false );
	}
}

The only thing left to do is tell the WCF endpoint to use this new behavior. One way to do that is using your web.config to connect the new behavior to your endpoint. In order to do that we have to create a behavior extension class by extending the BehaviorExtensionElement class:

public class NewtonsoftJsonBehaviorExtension : BehaviorExtensionElement
{
	public override Type BehaviorType
	{
		get { return typeof( NewtonsoftJsonBehavior ); }
	}

	protected override object CreateBehavior()
	{
		return new NewtonsoftJsonBehavior();
	}
}

And create a content type mapper:

public class NewtonsoftJsonContentTypeMapper : WebContentTypeMapper
{
	public override WebContentFormat GetMessageFormatForContentType( string contentType )
	{
		return WebContentFormat.Raw;
	}
}

Now when configuring your binding you can simply setup an extension like this:

<extensions>
  <behaviorExtensions>
    <add name="newtonsoftJsonBehavior" type="MyApp.NameSpace.NewtonsoftJsonBehaviorExtension, MyApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
  </behaviorExtensions>
</extensions>

Setup your enpoint behavior like this:

<endpointBehaviors>
  <behavior name="restEndPointBehavior">
    <webHttp helpEnabled="false" defaultBodyStyle="Bare" defaultOutgoingResponseFormat="Json" faultExceptionEnabled="false" />
    <newtonsoftJsonBehavior/>
  </behavior>
</endpointBehaviors>

Configure a webHttpBinding to use our custom contentTypeMapper:

<bindings>
  <webHttpBinding>
    <binding name="restWebHttpBinding" contentTypeMapper="MyApp.NameSpace.NewtonsoftJsonContentTypeMapper, MyApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
  </webHttpBinding>
</bindings>

And finally setting up our endpoint like this:

<services>
  <service name="MyApp.NameSpace.Service" behaviorConfiguration="restServiceBehavior">
    <endpoint address="" behaviorConfiguration="restEndPointBehavior" binding="webHttpBinding" bindingConfiguration="restWebHttpBinding" contract="MyApp.NameSpace.IService" />
  </service>
</services>

Your WCF rest endpoint is now using your own message formatter to serialize and deserialize messages! Below is a reallife example of using newtonsofts json serializer for this implementation. There are some other cool features when using newtonsofts json serializer. For example if your service method has a byte[] parameter, you can just post a Base64 encoded string and it will be automatically deserialized to a byte[] on the server.

public 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?" );
		}

		var bodyReader = message.GetReaderAtBodyContents();
		bodyReader.ReadStartElement( "Binary" );
		byte[] rawBody = bodyReader.ReadContentAsBase64();
		var ms = new MemoryStream( rawBody );

		var sr = new StreamReader( ms );
		var 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 )
			{
				var 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;
		var serializer = new Newtonsoft.Json.JsonSerializer();

		using ( var ms = new MemoryStream() )
		{
			using ( var 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();
				}
			}
		}

		System.ServiceModel.Channels.Message replyMessage = System.ServiceModel.Channels.Message.CreateMessage( messageVersion, operation.Messages[1].Action, new RawBodyWriter( body ) );
		replyMessage.Properties.Add( WebBodyFormatMessageProperty.Name, new WebBodyFormatMessageProperty( WebContentFormat.Raw ) );
		var respProp = new HttpResponseMessageProperty();
		respProp.Headers[HttpResponseHeader.ContentType] = "application/json";
		replyMessage.Properties.Add( HttpResponseMessageProperty.Name, respProp );
		return replyMessage;
	}
}

public 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();
	}
}

public class NewtonsoftJsonBehavior : WebHttpBehavior
{
	public override void Validate( ServiceEndpoint endpoint )
	{
		base.Validate( endpoint );

		var elements = endpoint.Binding.CreateBindingElements();
		var 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 );
		}
	}

	private void ValidateOperation( OperationDescription operation )
	{
		if ( operation.Messages.Count > 1 )
		{
			if ( operation.Messages[1].Body.Parts.Count > 0 )
			{
				throw new InvalidOperationException( "Operations cannot have out/ref parameters." );
			}
		}

		WebMessageBodyStyle bodyStyle = this.GetBodyStyle( operation );
		int inputParameterCount = operation.Messages[0].Body.Parts.Count;
		if ( !this.IsGetOperation( operation ) )
		{
			var wrappedRequest = bodyStyle == WebMessageBodyStyle.Wrapped || bodyStyle == WebMessageBodyStyle.WrappedRequest;
			if ( inputParameterCount == 1 && wrappedRequest )
			{
				throw new InvalidOperationException( "Wrapped body style for single parameters not implemented in this behavior." );
			}
		}

		var wrappedResponse = bodyStyle == WebMessageBodyStyle.Wrapped || bodyStyle == WebMessageBodyStyle.WrappedResponse;
		var isVoidReturn = operation.Messages.Count == 1 || operation.Messages[1].Body.ReturnValue.Type == typeof( void );
		if ( !isVoidReturn && wrappedResponse )
		{
			throw new InvalidOperationException( "Wrapped response not implemented in this behavior." );
		}
	}

	private WebMessageBodyStyle GetBodyStyle( OperationDescription operation )
	{
		var wga = operation.Behaviors.Find<WebGetAttribute>();
		if ( wga != null )
		{
			return wga.BodyStyle;
		}

		var wia = operation.Behaviors.Find<WebInvokeAttribute>();
		if ( wia != null )
		{
			return wia.BodyStyle;
		}

		return this.DefaultBodyStyle;
	}

	private bool IsGetOperation( OperationDescription operation )
	{
		var wga = operation.Behaviors.Find<WebInvokeAttribute>();
		if ( wga != null )
		{
			return true;
		}

		var wia = operation.Behaviors.Find();
		if ( wia != null )
		{
			return wia.Method == "HEAD";
		}

		return false;
	}
}

public class NewtonsoftJsonBehaviorExtension : BehaviorExtensionElement
{
	public override Type BehaviorType
	{
		get { return typeof( NewtonsoftJsonBehavior ); }
	}

	protected override object CreateBehavior()
	{
		return new NewtonsoftJsonBehavior();
	}
}

public class NewtonsoftJsonContentTypeMapper : WebContentTypeMapper
{
	public override WebContentFormat GetMessageFormatForContentType( string contentType )
	{
		return WebContentFormat.Raw;
	}
}

Author ITQ

More posts by ITQ
21 March 2014

Join the discussion 13 Comments

  • Abu Yussuf says:

    Thanks. It was very useful.

  • Tom Carpe says:

    Just FYI, the code does not compile in my environment due to certain generics and the Find method. I was abl eto stitch it together with help from the code at this article: http://www.getcodesamples.com/src/5C848E96/A9F60B61

    Thanks for putting this out there. I hope that someone may implement something as a Nuget package eventually.

  • Martin says:

    Hi. The calls to “operation.Behaviors.Find()” seem to be missing the generic parameter i.e. operation.Behaviors.Find(). Can you possibly clarify what they should be? Thanks

  • Martin says:

    Ah I see after posting that comment that the > and < have been removed in my text too…

  • Martin says:

    Solved:
    var wga = operation.Behaviors.Find<WebGetAttribute>();
    var wia = operation.Behaviors.Find<WebInvokeAttribute>();
    var webEncoder = elements.Find<WebHttpBinding>();

  • dave chong says:

    will this work with wcf .net 3.5?

    • Hi Dave, this code is initially written in .NET 4.5. All the base classes are also available in .NET 3.5, so theoretically it shouldn’t be a problem.

      • Dave Chong says:

        Hi Jeroen, thanks for your response! I was wondering if your solution supports UriTemplates? There was a post made by a Microsoft Developer named Carlos Figueira who posted a very similar solution but claimed his solution did not support UriTemplates. I can send you the url to his post if you would like.

        thanks
        Dave

      • Dave Chong says:

        Hi Jeroen,

        I’m hitting what might be a major roadblock with .NET 3.5. I’m trying to setup the binding for restWebHttpBinding. The attribute contentTypeMapper is not allowed. If I save the web.config anyways. I get an error in the browser.

        Unrecognized attribute ‘contentTypeMapper’. Note that attribute names are case-sensitive.

        I looked up the .NET API for WebHttpBinding.ContentTypeMapper Property and it says it was introduced in .NET Framework 4.0.

        Is there a way around this? I think I might be sunk.

        please reply

        thanks
        Dave

  • Dave Chong says:

    Hi Jeroen,

    I got your solution to work in .NET 3.5. I just had to find alternate ways of setting up the contentTypeMapper in the web.Config. I’m in the very early stages of testing and seems to work ok. Even the WSDL works without any future modifications!

    I’ll keep you informed.

  • Dave Chong says:

    With .NET 3.5 the web.config has to be dealt with differently as some attributes are not supported so things need to done bit different.

    I’ve listed changes vs the original post below.

    Setup your end point behavior like this:

    <!—->

    Configure a webHttpBinding to use our custom contentTypeMapper:

    And finally setting up our endpoint like this:

    I hope someone finds this useful.

    Dave

  • Dave Chong says:

    Hmmm, looks like my changes were blocked out during my last post. I’ll email Jeroen my changes.

Leave a Reply