Форматирование HTTP-ответа в ASP.NET WebAPI

ASP.NET WebAPI имеет две модели работы с контентом, который отдается клиенту - управлять непосредственно содержимым ответа или возвращать в качестве результата CLR-объект и предоставить WebAPI возможность самостоятельно решить в каком виде отдавать эти данные. Последний вариант на мой взгляд предпочтительнее, поскольку оставляет инфраструктурные вопросы платформе и позволяет думать только о логике. В таком случае встает вопрос о том, как WebAPI решает в каком виде эти данные уйдут на клиент, а также как на это поведение повлиять.

Возьмем для примера тривиальный WebAPI контроллер:

public class UsersController : ApiController
{
  [HttpGet]
  public IEnumerable<User> Get()
  {
    return new[]
    {
      new User {Name = "User1", Email = "[email protected]"},
      new User {Name = "User2", Email = "[email protected]"},
      new User {Name = "User3", Email = "[email protected]"},
      new User {Name = "User4", Email = "[email protected]"},
    };
  }
}

В качестве результата действие Get() отдает коллекцию объектов. В процессе обработки запроса WebAPI предстоит решить вопрос в каком виде отдавать эти данные - JSON, XML или каком-либо ещё? Для этого в инфраструктуре WebAPI имеется механизм, который называется Media Formatters. Задача этих сервисов заключается в форматировании объектов в нужном виде. Процесс обработки запроса выглядит следующим образом:

  1. При поступлении входящего запроса от клиента анализируется HTTP-заголовок Accept.
  2. Всем существующим форматировщикам передается это значение и предлагается сформировать ответ для этого запроса.
  3. Если форматировщик способен работать с данным форматом, он генерирует результат, который передается клиенту.
  4. Форматировщик может быть ориентирован не только на работу с конкретными типами контента, но и на на работу с конкретными типами объектов.

Чтобы убедиться в корректности этой схемы, можно запустить приложение и отправить ему пару HTTP-запросов:

Запрос:

GET http://localhost:40139/api/users/get HTTP/1.1
User-Agent: Fiddler
Host: localhost:40139
Accept: application/json

Ответ:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.0
Content-Length: 173

[{"Name":"User1","Email":"[email protected]"},{"Name":"User2", ...

Второй вариант:

Запрос:

GET http://localhost:40139/api/users/get HTTP/1.1
User-Agent: Fiddler
Host: localhost:40139
Accept: application/xml

Ответ:

HTTP/1.1 200 OK
Content-Type: application/xml; charset=utf-8
Server: Microsoft-IIS/8.0
Content-Length: 400

<ArrayOfUser ...><User><Email>[email protected]</Email><Name>User1... 

Как видно, WebAPI по-разному формирует ответ для разных значений HTTP-заголовка Accept.

Это поведение можно изменить и добавить свой форматировщик. Например, сделаем форматировщик, отдающий результат в текстовом формате (text/plain).

Нам потребуется объект, унаследованный от MediaTypeFormatter. Нужно также указать тип ответа и тип объектов, с которыми может работать форматировщик:

public class TextFormatter : BufferedMediaTypeFormatter
{
  public TextFormatter()
  {
    SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain"));
  }

  public override bool CanReadType(Type type)
  {
    return false;
  }

  public override bool CanWriteType(Type type)
  {
    return type == typeof(IEnumerable<User>);
  }

Для формирования результата следует переопределить метод WriteToStream(), который имеет доступ к исходному объекту (который получен из контроллера), текущему HTTP-запросу, а также потоку для генерации результата:

public class TextFormatter : BufferedMediaTypeFormatter
{
  // ...

  public override void WriteToStream(Type type, object value, Stream writeStream, HttpContent content)
  {
    var items = (IEnumerable<User>)value;

    using (var writer = new StreamWriter(writeStream))
    {
      foreach (var item in items)
      {
        writer.WriteLine("Name: {0}, Email: {1}", item.Name, item.Email);
      }
    }
  }

Наконец, нужно зарегистрировать этот форматировщик в инфраструктуре WebAPI:

public static class WebApiConfig
{
  public static void Register(HttpConfiguration config)
  {
    config.Formatters.Add(new TextFormatter());

Теперь все запросы, заголовок Accept которых содержит text/plain, а результат работы контроллера возвращает IEnumerable<User>, будут обрабатываться этим объектом. Это легко проверить, выполнив ещё один HTTP-запрос:

Запрос:

GET http://localhost:40139/api/users/get HTTP/1.1
User-Agent: Fiddler
Host: localhost:40139
Accept: text/plain

Ответ:

HTTP/1.1 200 OK
Content-Type: text/plain
Server: Microsoft-IIS/8.0
Content-Length: 148

Name: User1, Email: [email protected]
Name: User2, Email: [email protected]
Name: User3, Email: [email protected]
Name: User4, Email: [email protected]