Как использовать встроенный форматер xml или json для настраиваемого значения заголовка приема в.Net Core 2.0

Обновление: я загрузил небольшой тестовый проект на github: ссылка

Я создаю небольшой веб-сервис с.Net Core 2 и хотел бы дать клиентам возможность указать, нужна ли им навигационная информация в ответе или нет. Веб-интерфейс должен поддерживать только xml и json, но было бы неплохо, если бы клиенты могли использовать Accept: application/xml+hateoas или Accept: application/json+hateoas в своем запросе.

Я попытался настроить свой метод AddMvc следующим образом:

public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc(options =>
        {
            options.RespectBrowserAcceptHeader = true;
            options.ReturnHttpNotAcceptable = true;
            options.FormatterMappings.SetMediaTypeMappingForFormat(
                "xml", MediaTypeHeaderValue.Parse("application/xml"));
            options.FormatterMappings.SetMediaTypeMappingForFormat(
                "json", MediaTypeHeaderValue.Parse("application/json"));
            options.FormatterMappings.SetMediaTypeMappingForFormat(
                "xml+hateoas", MediaTypeHeaderValue.Parse("application/xml"));
            options.FormatterMappings.SetMediaTypeMappingForFormat(
                "json+hateoas", MediaTypeHeaderValue.Parse("application/json"));
        })            
        .AddJsonOptions(options => {
            // Force Camel Case to JSON
            options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        })
        .AddXmlSerializerFormatters()
        .AddXmlDataContractSerializerFormatters()
        ;

И я использую заголовок accept в моих методах контроллера, чтобы различать нормальный ответ xml/json и hateoas-подобный ответ, например так:

[HttpGet]
[Route("GetAllSomething")]
public async Task<IActionResult> GetAllSomething([FromHeader(Name = "Accept")]string accept)
{
...
bool generateLinks = !string.IsNullOrWhiteSpace(accept) && accept.ToLower().EndsWith("hateoas");
...
if (generateLinks)
{
    AddNavigationLink(Url.Link("GetSomethingById", new { Something.Id }), "self", "GET");
}
...
}

Итак, короче говоря, я не хочу создавать собственные средства форматирования, потому что единственная "настраиваемая" вещь - это включение или исключение навигационных ссылок в моем ответе, но сам ответ должен быть xml или json на основе значения заголовка Accept.

Мой класс модели выглядит следующим образом (в основном это строки и базовые значения):

[DataContract]
public class SomethingResponse
{
    [DataMember]
    public int Id { get; private set; }

При вызове моего сервиса из Fiddler я получил следующие результаты для различных значений Accept:

  1. Примите: application/json -> Код состояния 200 только с запрошенными данными.
  2. Принять: application/json+hateoas -> Код статуса 406 (Недопустимо).
  3. Принять: application/xml -> Код состояния 504. [Fiddler] ReadResponse() не выполнен: сервер не вернул полный ответ на этот запрос. Сервер вернул 468 байт.
  4. Принять: application/xml+hateoas -> Код состояния 406 (Недопустимо).

Может кто-нибудь сказать мне, какая настройка неверна?

1 ответ

Решение

Отображение формата в Media Type (SetMediaTypeMappingForFormat звонки) работает не так, как вы ожидаете. Это отображение не использует Accept Заголовок в запросе. Он читает запрошенный формат из параметра с именем format в данных маршрута или строке запроса URL. Вы также должны пометить свой контроллер или действие с FormatFilter приписывать. Есть несколько хороших статей о форматировании ответов, основанных на FormatFilter атрибут, проверьте здесь и здесь.

Чтобы исправить текущие сопоставления форматов, вы должны сделать следующее:

  1. Переименуйте формат, чтобы он не содержал знака плюс. Специальный + Персонаж доставит вам неприятности при передаче по URL. Лучше заменить его на -:

    options.FormatterMappings.SetMediaTypeMappingForFormat(
        "xml-hateoas", MediaTypeHeaderValue.Parse("application/xml"));
    options.FormatterMappings.SetMediaTypeMappingForFormat(
        "json-hateoas", MediaTypeHeaderValue.Parse("application/json"));
    
  2. добавлять format Параметр к маршруту:

    [Route("GetAllSomething/{format}")]
    
  3. Формат, используемый для отображения формата, не может быть извлечен из Accept заголовок, так что вы передадите его в URL. Так как вам нужно знать формат логики в вашем контроллере, вы можете отобразить выше format от параметра маршрута до действия, чтобы избежать дублирования в Accept заголовок:

    public async Task<IActionResult> GetAllSomething(string format)
    

    Теперь вам не нужно передавать требуемый формат в Accept заголовок, потому что формат будет отображен из URL запроса.

  4. Отметить контроллер или действие с помощью FormatFilter приписывать.

    Заключительное действие:

    [HttpGet]
    [Route("GetAllSomething/{format}")]
    [FormatFilter]
    public async Task<IActionResult> GetAllSomething(string format)
    {
        bool generateLinks = !string.IsNullOrWhiteSpace(format) && format.ToLower().EndsWith("hateoas");
    
        //  ...
    
        return await Task.FromResult(Ok(new SomeModel { SomeProperty = "Test" }));
    }
    

Теперь, если вы запрашиваете URL /GetAllSomething/xml-hateoas (даже с отсутствующим Accept заголовок), FormatFilter будет карта format ценность xml-hateoas в application/xml и XML-форматер будет использоваться для ответа. Запрошенный формат также будет доступен в format параметр GetAllSomething действие.

Пример проекта с отображениями форматирования на GitHub

Помимо сопоставлений форматеров, вы могли бы достичь своей цели, добавив новые поддерживаемые типы мультимедиа в существующие средства форматирования типов мультимедиа. Поддерживаемые типы носителей хранятся в OutputFormatter.SupportedMediaTypes коллекции и заполняются в конструкторе конкретного выходного форматера, например XmlSerializerOutputFormatter, Вы можете создать экземпляр средства форматирования самостоятельно (вместо использования AddXmlSerializerFormatters добавочный номер) и добавьте необходимые типы SupportedMediaTypes коллекция. Чтобы настроить JSON форматер, который добавлен по умолчанию, просто найдите его экземпляр в options.OutputFormatters:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options =>
        {
            options.RespectBrowserAcceptHeader = true;
            options.ReturnHttpNotAcceptable = true;

            options.InputFormatters.Add(new XmlSerializerInputFormatter());
            var xmlOutputFormatter = new XmlSerializerOutputFormatter();
            xmlOutputFormatter.SupportedMediaTypes.Add("application/xml+hateoas");
            options.OutputFormatters.Add(xmlOutputFormatter);

            var jsonOutputFormatter = options.OutputFormatters.OfType<JsonOutputFormatter>().FirstOrDefault();
            jsonOutputFormatter?.SupportedMediaTypes.Add("application/json+hateoas");
        })
        .AddJsonOptions(options => {
            // Force Camel Case to JSON
            options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        })
        .AddXmlDataContractSerializerFormatters();
}

В этом случае GetAllSomething должно быть таким же, как в вашем первоначальном вопросе. Вы также должны передать необходимый формат в Accept заголовок, например Accept: application/xml+hateoas,

Пример проекта с пользовательскими типами мультимедиа на GitHub

Другие вопросы по тегам