WCF REST не возвращает заголовок ответа Vary при согласовании типа носителя

#wcf #http #wcf-rest

#wcf #http #wcf-rest

Вопрос:

У меня есть простая служба WCF REST:

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

В отношении носителя, который должна возвращать служба, нет ограничений.

Когда я отправляю запрос с указанием json формата, он возвращает 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"}
  

Когда я указываю xml , он возвращает 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>
  

Пока все хорошо, проблема в том, что служба не возвращает Vary заголовок HTTP, чтобы сообщить, что содержимое было согласовано и что заголовок Accept http был определяющим фактором.

Разве это не должно быть так?:

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

Насколько я знаю, с точки зрения кэширования, заголовок «Vary» сообщит промежуточным кэшам, что ответ генерируется на основе URI и заголовка Accept HTTP. В противном случае прокси-сервер может кэшировать ответ json и использовать его для кого-то, кто запрашивает xml.

Есть какой-либо способ заставить WCF REST автоматически помещать этот заголовок?

Спасибо.

Ответ №1:

Вы можете использовать пользовательский инспектор сообщений, чтобы добавить Vary заголовок к ответам. На основе правил автоматического форматирования для WCF webHttp порядок таков: 1) Заголовок Accept; 2) Тип содержимого сообщения запроса; 3) настройка по умолчанию в операции и 4) настройка по умолчанию в самом поведении. Только первые два зависят от запроса (что влияет на Vary заголовок), и для вашего сценария (кэширование) интересны только GET , поэтому мы также можем отказаться от типа входящего содержимого. Поэтому написать такой инспектор довольно просто: если AutomaticFormatSelectionEnabled свойство установлено, мы добавляем Vary: Accept заголовок для ответов на все запросы GET — это делает приведенный ниже код. Если вы хотите включить тип содержимого (также для запросов, не связанных с GET), вы можете изменить инспектор, чтобы он также просматривал входящий запрос.

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

Комментарии:

1. Пока трудно найти часть, которая фактически добавляет заголовок. Можно ли его использовать для добавления других заголовков ответа, например ‘Access-Control-Allow-Origin’

Ответ №2:

В веб-API WCF мы планируем добавить автоматическую настройку заголовка Vary во время подключения. На данный момент, если вы используете Web API, вы можете сделать это либо с помощью пользовательского обработчика операций, либо обработчика сообщений. Для WCF HTTP тогда лучше использовать инспектор сообщений, как рекомендовал Карлос.

Ответ №3:

Похоже, что привязка webHttpBinding была разработана в соответствии с моделью, описанной в this post, которая позволяет soap «сосуществовать» с конечными точками, отличными от soap. Подразумевается, что URL-адреса конечных точек кода в этой ссылке — каждая конечная точка предоставляет ресурс в виде одного типа содержимого. Конечные точки в этой ссылке настроены на поддержку soap, json и обычного XML через атрибут endpointBehaviors.

Ваш пример показывает, что webHttpBinding может поддерживать согласование содержимого, но это реализовано лишь частично, поскольку заголовок Vary не генерируется WCF. Если вы хотите использовать фреймворк, который более точно соответствует стилю архитектуры REST, посмотрите на причины, по которым вы можете захотеть использовать OpenRasta.

Комментарии:

1. Я бы не только сказал, что они не полностью соответствуют стилю архитектуры REST, я бы сказал, что они не следуют протоколу HTTP. Я посмотрю на этот проект, но, к сожалению, я вынужден использовать WCF REST. Спасибо!

2. К сожалению, утверждение RESTfulness (или нет) WCF может начать религиозную войну в наши дни. WCF абстрагирует транспорт связи (HTTP и другие), чтобы удовлетворить своей цели проектирования — отделить код реализации сервиса от так называемого «сантехнического» кода. Такая конструкция ограничивает встроенную поддержку HTTP в качестве платформы приложения. Именно в этом смысле я рекомендовал OpenRasta. Поскольку вы остаетесь с WCF, новый веб-API WCF, ориентированный на HTTP, может быть для вас альтернативным решением.

3. С веб-API WCF происходит то же самое. В любом случае, я думаю, что я собираюсь его использовать, это выглядит очень интересно. Спасибо!

Ответ №4:

Такое поведение, ИМХО, нарушает SHOULD в https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-p6-cache-16#section-3.5 . Я не вижу никаких оснований не отправлять Vary в случае согласованного ответа.

Я отправлю его в список HTTP WCF для уточнения / исправления и вернусь с ответом здесь.

Янв.

Комментарии:

1. Большое спасибо! Где этот список?, просто любопытно: D

2. Я не знаю, полезно ли это, но я пробовал использовать новый веб-API WCF, и происходит то же самое.