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 https://server/Foo/3782982/json and get json, or specify https://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     {

    3 

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

    5 

    6 

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

    8             : base(singletonInstance, baseAddresses) { }

    9 

   10 

   11         public MagicContentTypeSelectingServiceHost() : base() { }

   12 

   13 

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

   24 

   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));

   33 

   34                         WebGetAttribute wga =

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

   36 

   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)));

   48 

   49                             }

   50 

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

   52 

   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.

   57 

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

   59 

   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;

   69 

   70                                     if (wga.IsRequestFormatSetExplicitly)

   71                                         wga2.RequestFormat = wga.RequestFormat;

   72 

   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";

   79 

   80                                     od.Behaviors.Add(wga2);

   81 

   82                                 }

   83                                 else

   84                                     od.Behaviors.Add(b);

   85                             }

   86 

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

   88                                 od.Messages.Add(md);

   89 

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

   91                                 od.Faults.Add(fd);

   92 

   93                             od.SyncMethod = opDesc.SyncMethod;

   94 

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

   96                             opsToAdd.Add(od);

   97                         }

   98                     }

   99                 }

  100 

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

  106 

  107             TraceMe("");

  108             TraceMe("");

  109         }

  110 

  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.

Cheers!

 

[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.]

MagicContentTypeSelectingServiceHost.cs