I have a simple WCF REST service:
[ServiceContract]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
public class Service1
{
[WebGet(UriTemplate = "{id}")]
public SampleItem Get(string id)
{
return new SampleItem() { Id = Int32.Parse(id), StringValue = "Hello" };
}
}
There is not constrain about the media that the service should return.
When I send a request specifying json
format, it returns JSON:
GET http://localhost/RestService/4 HTTP/1.1
User-Agent: Fiddler
Accept: application/json
Host: localhost
HTTP/1.1 200 OK
Cache-Control: private
Content-Length: 30
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/7.5
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Sun, 02 Oct 2011 18:06:47 GMT
{"Id":4,"StringValue":"Hello"}
When I specify xml
, it returns XML:
GET http://localhost/RestService/4 HTTP/1.1
User-Agent: Fiddler
Accept: application/xml
Host: localhost
HTTP/1.1 200 OK
Cache-Control: private
Content-Length: 194
Content-Type: application/xml; charset=utf-8
Server: Microsoft-IIS/7.5
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Sun, 02 Oct 2011 18:06:35 GMT
<SampleItem xmlns="http://schemas.datacontract.org/2004/07/RestPrototype.Service" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"><Id>4</Id><StringValue>Hello</StringValue></SampleItem>
So far so good, the problem is that the service doesn't return a Vary
HTTP header to say that the content has been negotiated and that the Accept
http header has been a determinant factor.
Should not it be like this?:
GET http://localhost/RestService/4 HTTP/1.1
User-Agent: Fiddler
Accept: application/json
Host: localhost
HTTP/1.1 200 OK
Cache-Control: private
Content-Length: 30
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/7.5
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Vary:Accept
Date: Sun, 02 Oct 2011 18:06:47 GMT
{"Id":4,"StringValue":"Hello"}
As far as I know, in terms of caching, the "Vary" header will tell intermediate caches that the response is generated based on the URI and the Accept
HTTP header. Otherwise, a proxy could cache a json response, and use it for somebody that is asking xml.
There is any way to make WCF REST put this header automatically?
Thanks.
You can use a custom message inspector to add the Vary
header to the responses. Based on the automatic formatting rules for WCF WebHTTP, the order is 1) Accept header; 2) Content-Type of request message; 3) default setting in the operation and 4) default setting in the behavior itself. Only the first two are dependent on the request (thus influencing the Vary
header), and for your scenario (caching), only GET are interesting, so we can discard the incoming Content-Type as well. So writing such an inspector is fairly simple: if the AutomaticFormatSelectionEnabled
property is set, then we add the Vary: Accept
header for the responses of all GET requests - the code below does that. If you want to include the content-type (for non-GET requests as well), you can modify the inspector to look at the incoming request as well.
public class Post_0acbfef2_16a3_440a_88d6_e0d7fcf90a8e
{
[DataContract(Name = "Person", Namespace = "")]
public class Person
{
[DataMember]
public string Name { get; set; }
[DataMember]
public int Age { get; set; }
}
[ServiceContract]
public class MyContentNegoService
{
[WebGet(ResponseFormat = WebMessageFormat.Xml)]
public Person ResponseFormatXml()
{
return new Person { Name = "John Doe", Age = 33 };
}
[WebGet(ResponseFormat = WebMessageFormat.Json)]
public Person ResponseFormatJson()
{
return new Person { Name = "John Doe", Age = 33 };
}
[WebGet]
public Person ContentNegotiated()
{
return new Person { Name = "John Doe", Age = 33 };
}
[WebInvoke]
public Person ContentNegotiatedPost(Person person)
{
return person;
}
}
class MyVaryAddingInspector : IEndpointBehavior, IDispatchMessageInspector
{
public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{
WebHttpBehavior webBehavior = endpoint.Behaviors.Find<WebHttpBehavior>();
if (webBehavior != null && webBehavior.AutomaticFormatSelectionEnabled)
{
endpointDispatcher.DispatchRuntime.MessageInspectors.Add(this);
}
}
public void Validate(ServiceEndpoint endpoint)
{
}
public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
{
HttpRequestMessageProperty prop;
prop = (HttpRequestMessageProperty)request.Properties[HttpRequestMessageProperty.Name];
if (prop.Method == "GET")
{
// we shouldn't cache non-GET requests, so only returning this for such requests
return "Accept";
}
return null;
}
public void BeforeSendReply(ref Message reply, object correlationState)
{
string varyHeader = correlationState as string;
if (varyHeader != null)
{
HttpResponseMessageProperty prop;
prop = reply.Properties[HttpResponseMessageProperty.Name] as HttpResponseMessageProperty;
if (prop != null)
{
prop.Headers[HttpResponseHeader.Vary] = varyHeader;
}
}
}
}
public static void SendGetRequest(string uri, string acceptHeader)
{
SendRequest(uri, "GET", null, null, acceptHeader);
}
public static void SendRequest(string uri, string method, string contentType, string body, string acceptHeader)
{
Console.Write("{0} request to {1}", method, uri.Substring(uri.LastIndexOf('/')));
if (contentType != null)
{
Console.Write(" with Content-Type:{0}", contentType);
}
if (acceptHeader == null)
{
Console.WriteLine(" (no Accept header)");
}
else
{
Console.WriteLine(" (with Accept: {0})", acceptHeader);
}
HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(uri);
req.Method = method;
if (contentType != null)
{
req.ContentType = contentType;
Stream reqStream = req.GetRequestStream();
byte[] bodyBytes = Encoding.UTF8.GetBytes(body);
reqStream.Write(bodyBytes, 0, bodyBytes.Length);
reqStream.Close();
}
if (acceptHeader != null)
{
req.Accept = acceptHeader;
}
HttpWebResponse resp;
try
{
resp = (HttpWebResponse)req.GetResponse();
}
catch (WebException e)
{
resp = (HttpWebResponse)e.Response;
}
Console.WriteLine("HTTP/{0} {1} {2}", resp.ProtocolVersion, (int)resp.StatusCode, resp.StatusDescription);
foreach (string headerName in resp.Headers.AllKeys)
{
Console.WriteLine("{0}: {1}", headerName, resp.Headers[headerName]);
}
Console.WriteLine();
Stream respStream = resp.GetResponseStream();
Console.WriteLine(new StreamReader(respStream).ReadToEnd());
Console.WriteLine();
Console.WriteLine(" *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* ");
Console.WriteLine();
}
public static void Test()
{
string baseAddress = "http://" + Environment.MachineName + ":8000/Service";
ServiceHost host = new ServiceHost(typeof(MyContentNegoService), new Uri(baseAddress));
ServiceEndpoint endpoint = host.AddServiceEndpoint(typeof(MyContentNegoService), new WebHttpBinding(), "");
endpoint.Behaviors.Add(new WebHttpBehavior { AutomaticFormatSelectionEnabled = true });
endpoint.Behaviors.Add(new MyVaryAddingInspector());
host.Open();
Console.WriteLine("Host opened");
foreach (string operation in new string[] { "ResponseFormatJson", "ResponseFormatXml", "ContentNegotiated" })
{
foreach (string acceptHeader in new string[] { null, "application/json", "text/xml", "text/json" })
{
SendGetRequest(baseAddress + "/" + operation, acceptHeader);
}
}
Console.WriteLine("Sending some POST requests with content-nego (but no Vary in response)");
string jsonBody = "{\"Name\":\"John Doe\",\"Age\":33}";
SendRequest(baseAddress + "/ContentNegotiatedPost", "POST", "text/json", jsonBody, "text/xml");
SendRequest(baseAddress + "/ContentNegotiatedPost", "POST", "text/json", jsonBody, "text/json");
Console.Write("Press ENTER to close the host");
Console.ReadLine();
host.Close();
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With