Как использовать встроенный форматер 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:
- Примите: application/json -> Код состояния 200 только с запрошенными данными.
- Принять: application/json+hateoas -> Код статуса 406 (Недопустимо).
- Принять: application/xml -> Код состояния 504. [Fiddler] ReadResponse() не выполнен: сервер не вернул полный ответ на этот запрос. Сервер вернул 468 байт.
- Принять: application/xml+hateoas -> Код состояния 406 (Недопустимо).
Может кто-нибудь сказать мне, какая настройка неверна?
1 ответ
Отображение формата в Media Type (SetMediaTypeMappingForFormat
звонки) работает не так, как вы ожидаете. Это отображение не использует Accept
Заголовок в запросе. Он читает запрошенный формат из параметра с именем format
в данных маршрута или строке запроса URL. Вы также должны пометить свой контроллер или действие с FormatFilter
приписывать. Есть несколько хороших статей о форматировании ответов, основанных на FormatFilter
атрибут, проверьте здесь и здесь.
Чтобы исправить текущие сопоставления форматов, вы должны сделать следующее:
Переименуйте формат, чтобы он не содержал знака плюс. Специальный
+
Персонаж доставит вам неприятности при передаче по URL. Лучше заменить его на-
:options.FormatterMappings.SetMediaTypeMappingForFormat( "xml-hateoas", MediaTypeHeaderValue.Parse("application/xml")); options.FormatterMappings.SetMediaTypeMappingForFormat( "json-hateoas", MediaTypeHeaderValue.Parse("application/json"));
добавлять
format
Параметр к маршруту:[Route("GetAllSomething/{format}")]
Формат, используемый для отображения формата, не может быть извлечен из
Accept
заголовок, так что вы передадите его в URL. Так как вам нужно знать формат логики в вашем контроллере, вы можете отобразить вышеformat
от параметра маршрута до действия, чтобы избежать дублирования вAccept
заголовок:public async Task<IActionResult> GetAllSomething(string format)
Теперь вам не нужно передавать требуемый формат в
Accept
заголовок, потому что формат будет отображен из URL запроса.Отметить контроллер или действие с помощью
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