Varying Content-Type according to the URL in a WCF REST Service

My buddy Justin wrote about how to set the Content-Type headers in a WebGet method in a WCF REST app. Doing this would allow each WebGet method to specify its own Content-Type at runtime. 

After I summarized how to build a WCF REST app in a post a couple weeks ago, Kyle Beyer asked if there was a way to avoid hard-coding the Content-Type in the WebGet attribute on the method.

One question ... Do you know of a way to get a WCF service to honor the 'Content-Type' HTTP header instead of hard coding the content type via an attribute on the method? I would really like to create a service that has a single set of methods which returns JSON/XML based on the HTTP header(s) ... suggestions?

Taking a page from the White House Press spokesperson, rather than answering Kyle's question, I'm going to answer a different question, a question I would like to answer.  The question I will answer is, how can I create a service that has a single set of methods that returns JSON or XML depending on the URL tickled (and not on the Accept header)?

This is a little different than what Kyle wants, but it may be good enough.  What I mean is this:  a single method decorated with [WebGet] can deliver JSON or XML, depending on the URI.  We can specify http://server/Foo/3782982/json and get json, or specify http://server/Foo/3782982/xml and get plain-old-XML. 

We can do this without any extra code in the Operation method itself.  In fact the operation method doesn't care whether it is JSON or XML.  How do we pull this off?

My trick was to use a custom ServiceHost.  It automagically enables Content-Type selection by URL, for all WebGet
operations that are decorated with a special marker attribute.  (ionic.samples.WcfRest.DynamicContentType). 

The service host works its magic by cloning the OperationDescription for all operations in the service contract that are specially marked.  One copy of the OperationDescription gets WebMessageFormat.Xml and the other gets WebMessageFormat.Json. They get differing UriTemplates so WCF can disambiguate (double word score).  Both clones point to the same method in the service class, so there is no duplication of application code.

Here's what the service host looks like: 

    1     public class MagicContentTypeSelectingServiceHost : ServiceHost

    2     {


    4         public MagicContentTypeSelectingServiceHost(Type t, params Uri[] baseAddrs) : base(t, baseAddrs) { }



    7         public MagicContentTypeSelectingServiceHost(object singletonInstance, params Uri[] baseAddresses)

    8             : base(singletonInstance, baseAddresses) { }



   11         public MagicContentTypeSelectingServiceHost() : base() { }



   14         protected override void OnOpening()

   15         {

   16             TraceMe("");

   17             TraceMe("");

   18             ServiceEndpointCollection sec = this.Description.Endpoints;

   19             foreach (ServiceEndpoint se in sec)

   20             {

   21                 TraceMe("Endpoint: ");

   22                 TraceMe("  Address: {0}", se.Address.ToString());

   23                 TraceMe("  Contract: {0}", se.Contract.ToString());


   25                 var opsToAdd = new List<OperationDescription>();

   26                 foreach (OperationDescription opDesc in se.Contract.Operations)

   27                 {

   28                     object[] attrs = opDesc.SyncMethod.GetCustomAttributes(typeof(ionic.samples.WcfRest.DynamicContentType), false);

   29                     if ((attrs != null) && (attrs.Length == 1))

   30                     {

   31                         TraceMe("  operation: {0}", opDesc.Name);

   32                         TraceMe("    Marked with {0} attribute", typeof(ionic.samples.WcfRest.DynamicContentType));


   34                         WebGetAttribute wga =

   35                           opDesc.Behaviors.Find<WebGetAttribute>();


   37                         if (wga != null)

   38                         {

   39                             if (wga.IsResponseFormatSetExplicitly)

   40                             {

   41                                 throw new System.Exception

   42                                   (String.Format("On method '{0}', there are conflicting attributes. When using the " +

   43                                                   "custom service host {1}, on a method that is marked with {2}, the " +

   44                                                   "ResponseFormat in the WebGet attribute must be omitted.",

   45                                                   opDesc.Name,

   46                                                   this.GetType().ToString(),

   47                                                   typeof(ionic.samples.WcfRest.DynamicContentType)));


   49                             }


   51                             TraceMe("    Cloning this operation ...");


   53                             // Now, clone this OperationDescription.

   54                             // We can copy references to all properties, except those we are changing.

   55                             // The only thing that is changing is the WebGetAttribute, so

   56                             // we must actually new up one of those.


   58                             OperationDescription od = new OperationDescription(opDesc.Name + ".clone", opDesc.DeclaringContract);


   60                             string rootTemplate = wga.UriTemplate;

   61                             WebGetAttribute wga2 = null;

   62                             foreach (System.ServiceModel.Description.IOperationBehavior b in opDesc.Behaviors)

   63                             {

   64                                 if ((b as System.ServiceModel.Web.WebGetAttribute) != null)

   65                                 {

   66                                     wga2 = new WebGetAttribute();

   67                                     if (wga.IsBodyStyleSetExplicitly)

   68                                         wga2.BodyStyle = wga.BodyStyle;


   70                                     if (wga.IsRequestFormatSetExplicitly)

   71                                         wga2.RequestFormat = wga.RequestFormat;


   73                                     // Now, differentiate the two WebGetAttribute instances with the ResponseFormat.

   74                                     // The original OperationDescription gets XML, the clone gets JSON

   75                                     wga.ResponseFormat = WebMessageFormat.Xml;

   76                                     wga2.ResponseFormat = WebMessageFormat.Json;

   77                                     wga.UriTemplate = rootTemplate + "/xml";

   78                                     wga2.UriTemplate = rootTemplate + "/json";


   80                                     od.Behaviors.Add(wga2);


   82                                 }

   83                                 else

   84                                     od.Behaviors.Add(b);

   85                             }


   87                             foreach (System.ServiceModel.Description.MessageDescription md in opDesc.Messages)

   88                                 od.Messages.Add(md);


   90                             foreach (System.ServiceModel.Description.FaultDescription fd in opDesc.Faults)

   91                                 od.Faults.Add(fd);


   93                             od.SyncMethod = opDesc.SyncMethod;


   95                             // remember to add this OperationDescription to the service contract

   96                             opsToAdd.Add(od);

   97                         }

   98                     }

   99                 }


  101                 // add the cloned operation descriptions to the ServiceContract

  102                 foreach (OperationDescription od in opsToAdd)

  103                     se.Contract.Operations.Add(od);

  104             }

  105             base.OnOpening();


  107             TraceMe("");

  108             TraceMe("");

  109         }


  111     }

You can see on lines 58 through 96, the OperationDescription for a marked operation gets cloned. Then on line 103, the cloned operation gets added into the ServiceContract. 

The operation in the service interface looks the same as any operation, except it is marked with an attribute, like this:

    1       [OperationContract]

    2       [ionic.samples.WcfRest.DynamicContentType]

    3       [WebGet(

    4           BodyStyle = WebMessageBodyStyle.Bare,

    5         UriTemplate = "dyn/{orderId}")]

    6       ReplyMsg GetOrderInfoEx(string orderId);

The UriTemplate gets changed transparently at runtime by the custom service host.  The effective UriTemplate is "dyn/{orderId}/json" for the JSON flavor, and "dyn/{orderId}/xml for the plain-old-XML flavor.

This service host works in self-hosted apps as well as those hosted within IIS. Not exactly what Kyle asked for but this could do the trick for some of you.  The code is attached to this post.

Be sure to test thoroughly before you use this in production apps.



[Addendum: I think you may be able to modify this custom ServiceHost idea slightly to do Content-Type negotiation based on the Accept header. You may be able to use the a slightly modified version of this ServiceHost, that clones specially marked OperationDescriptions. Along with that, add an IEndpointBehavior that sets a modified OperationSelector. Then, within your own selector you could examine the request headers and then choose the operation you want.]


Comments (8)

  1. Kyle Beyer says:

    Very nice!  Although the code is a bit complex, I think this does provide a good option.  And you’ve already done the heavy lifting. 🙂

    It should be just as easy for consumers of a service to specify content type by appending to the URL as using HTTP headers and is probably less error prone.



  2. cheeso says:

    Jon asked – why is this custom service host better than just having two methods, each decorated with [WebGet], but using different UriTemplates and different ResponseFormats. (explicitly coding what the custom service host does behind the scenes).

        BodyStyle = WebMessageBodyStyle.Bare,
        UriTemplate = “dyn/{orderId}/json”)]
     ReplyMsg GetOrderInfoEx(string orderId){ return realOrderImp(orderId);}  
        BodyStyle = WebMessageBodyStyle.Bare,  
        UriTemplate = “dyn/{orderId}/xml”)]
     ReplyMsg GetOrderInfoExXML(string orderId){ return realOrderImp(orderId);}

    Explicitly coding it this way would be more clear and maintainable, but it becomes problematic when you have more than 3-5 methods.  You’d have to duplicate each one of ’em.  

    It’s a tradeoff.

  3. MtnNerd says:

    Very cool stuff….exactly the thing I’ve been trying to figure out how to pull off.  However I haven’t been able to get beyond this error on MagicContentTypeSelectingServiceHost start from console.  Any Ideas?



    The operations GetObject and GetObject.clone have the same action (  Every operation must have a unique action value.

  4. cheeso says:

    Hey Brad,

    Hmmmm, not sure why I did not run into that problem myself?  I think because I had no action at all on my methods.  Maybe you are doing something a little different.

    What you will have to do is clone the md in line 88 of the code block in the post, and in the clone, set the mdCloned.Action to md.Action + ".clone" or something similar. Then call od.Messages.Add(mdClone) ; .  

  5. I’ve done something similar where I hook in my own formatter depending on the Accepts request header:


  6. Damian Mehers made a comment on my blog post from April , but I felt it was worth a full reblog. Damian’s

  7. With the recent release of the WCF REST Starter Kit , Microsoft made implementing REST services via WCF

  8. Mike O'Brien says:

    Check out the WCF REST Contrib library:

    It handles the automatic serialization/deserialization of entity bodies based on the content-type and accept headers.

Skip to main content