Пользовательские модули форматирования для веб-API в ASP.NET Core

ASP.NET Core MVC поддерживает обмен данными в веб-API с помощью форматировщиков ввода и вывода. Форматировщики ввода используются привязкой модели. Форматировщики вывода используются для форматирования откликов.

Платформа предоставляет встроенные форматировщики ввода и вывода для JSON и XML. Доступен только встроенный форматировщик вывода для обычного текста, но не форматировщик ввода для обычного текста.

В этой статье показано, как добавить поддержку дополнительных форматов, создав пользовательские модули форматирования. Пример пользовательского средства форматирования ввода обычного текста см. в разделе TextPlainInputFormatter на сайте GitHub.

Просмотреть или скачать образец кода (описание загрузки)

Когда следует использовать настраиваемое форматирование

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

Общие сведения о создании настраиваемого модуля форматирования

Создание настраиваемого модуля форматирования:

  • Для сериализации данных, отправленных клиенту, создайте класс форматирования выходных данных.
  • Для десериализации данных, полученных от клиента, создайте входной класс форматирования.
  • Добавьте экземпляры классов форматирования в InputFormatters коллекции и OutputFormatters коллекции.MvcOptions

Создание настраиваемого модуля форматирования

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

В следующем коде показан VcardOutputFormatter класс из примера:

public class VcardOutputFormatter : TextOutputFormatter
{
    public VcardOutputFormatter()
    {
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));

        SupportedEncodings.Add(Encoding.UTF8);
        SupportedEncodings.Add(Encoding.Unicode);
    }

    protected override bool CanWriteType(Type? type)
        => typeof(Contact).IsAssignableFrom(type)
            || typeof(IEnumerable<Contact>).IsAssignableFrom(type);

    public override async Task WriteResponseBodyAsync(
        OutputFormatterWriteContext context, Encoding selectedEncoding)
    {
        var httpContext = context.HttpContext;
        var serviceProvider = httpContext.RequestServices;

        var logger = serviceProvider.GetRequiredService<ILogger<VcardOutputFormatter>>();
        var buffer = new StringBuilder();

        if (context.Object is IEnumerable<Contact> contacts)
        {
            foreach (var contact in contacts)
            {
                FormatVcard(buffer, contact, logger);
            }
        }
        else
        {
            FormatVcard(buffer, (Contact)context.Object!, logger);
        }

        await httpContext.Response.WriteAsync(buffer.ToString(), selectedEncoding);
    }

    private static void FormatVcard(
        StringBuilder buffer, Contact contact, ILogger logger)
    {
        buffer.AppendLine("BEGIN:VCARD");
        buffer.AppendLine("VERSION:2.1");
        buffer.AppendLine($"N:{contact.LastName};{contact.FirstName}");
        buffer.AppendLine($"FN:{contact.FirstName} {contact.LastName}");
        buffer.AppendLine($"UID:{contact.Id}");
        buffer.AppendLine("END:VCARD");

        logger.LogInformation("Writing {FirstName} {LastName}",
            contact.FirstName, contact.LastName);
    }
}

Наследование от подходящего базового класса

Для типов текстовых носителей (например, vCard) наследуемого TextInputFormatter от базового класса:TextOutputFormatter

public class VcardOutputFormatter : TextOutputFormatter

Для двоичных типов наследуйте от InputFormatter базового OutputFormatter или базового класса.

Указание поддерживаемых типов носителей и кодировки

В конструкторе укажите поддерживаемые типы носителей и кодировки, добавив их SupportedMediaTypes в коллекции:SupportedEncodings

public VcardOutputFormatter()
{
    SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));

    SupportedEncodings.Add(Encoding.UTF8);
    SupportedEncodings.Add(Encoding.Unicode);
}

Класс форматирования не может использовать внедрение конструктора для зависимостей. Например, ILogger<VcardOutputFormatter> нельзя добавить в конструктор в качестве параметра. Чтобы получить доступ к службам, используйте объект контекста, передаваемый в методы. Пример кода в этой статье и пример показано, как это сделать.

Переопределение CanReadType и CanWriteType

Укажите тип для десериализации в или сериализовать из нее CanReadType , переопределив методы или CanWriteType методы. Например, чтобы создать текст vCard из Contact типа и наоборот:

protected override bool CanWriteType(Type? type)
    => typeof(Contact).IsAssignableFrom(type)
        || typeof(IEnumerable<Contact>).IsAssignableFrom(type);

Метод CanWriteResult

В некоторых сценариях CanWriteResult необходимо переопределить, CanWriteTypeа не переопределить. Используйте CanWriteResult, если выполняются указанные ниже условия.

  • Метод действия возвращает класс модели.
  • Существуют производные классы, которые могут быть возвращены во время выполнения.
  • Производный класс, возвращаемый действием, должен быть известен во время выполнения.

Например, предположим, что метод действия:

  • Сигнатура Person возвращает тип.
  • Может возвращать или Instructor тип, производный Student от Person.

Чтобы модуль форматирования обрабатывал только Student объекты, проверьте тип Object объекта контекста, предоставленного методу CanWriteResult . Когда метод действия возвращает IActionResult:

  • Использовать не нужно CanWriteResult.
  • Метод CanWriteType получает тип среды выполнения.

Переопределение ReadRequestBodyAsync и WriteResponseBodyAsync

Десериализация или сериализация выполняется в ReadRequestBodyAsync или WriteResponseBodyAsync. В следующем примере показано, как получить службы из контейнера внедрения зависимостей. Службы не могут быть получены из параметров конструктора:

public override async Task WriteResponseBodyAsync(
    OutputFormatterWriteContext context, Encoding selectedEncoding)
{
    var httpContext = context.HttpContext;
    var serviceProvider = httpContext.RequestServices;

    var logger = serviceProvider.GetRequiredService<ILogger<VcardOutputFormatter>>();
    var buffer = new StringBuilder();

    if (context.Object is IEnumerable<Contact> contacts)
    {
        foreach (var contact in contacts)
        {
            FormatVcard(buffer, contact, logger);
        }
    }
    else
    {
        FormatVcard(buffer, (Contact)context.Object!, logger);
    }

    await httpContext.Response.WriteAsync(buffer.ToString(), selectedEncoding);
}

private static void FormatVcard(
    StringBuilder buffer, Contact contact, ILogger logger)
{
    buffer.AppendLine("BEGIN:VCARD");
    buffer.AppendLine("VERSION:2.1");
    buffer.AppendLine($"N:{contact.LastName};{contact.FirstName}");
    buffer.AppendLine($"FN:{contact.FirstName} {contact.LastName}");
    buffer.AppendLine($"UID:{contact.Id}");
    buffer.AppendLine("END:VCARD");

    logger.LogInformation("Writing {FirstName} {LastName}",
        contact.FirstName, contact.LastName);
}

Настройка MVC для использования пользовательского средства форматирования

Чтобы использовать пользовательский модуль форматирования, добавьте экземпляр класса форматирования в коллекцию или MvcOptions.OutputFormatters в нееMvcOptions.InputFormatters:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>
{
    options.InputFormatters.Insert(0, new VcardInputFormatter());
    options.OutputFormatters.Insert(0, new VcardOutputFormatter());
});

Форматировщики вычисляются в том порядке, в котором они вставляются, где первый принимает приоритет.

Полный VcardInputFormatter класс

В следующем коде показан VcardInputFormatter класс из примера:

public class VcardInputFormatter : TextInputFormatter
{
    public VcardInputFormatter()
    {
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));

        SupportedEncodings.Add(Encoding.UTF8);
        SupportedEncodings.Add(Encoding.Unicode);
    }

    protected override bool CanReadType(Type type)
        => type == typeof(Contact);

    public override async Task<InputFormatterResult> ReadRequestBodyAsync(
        InputFormatterContext context, Encoding effectiveEncoding)
    {
        var httpContext = context.HttpContext;
        var serviceProvider = httpContext.RequestServices;

        var logger = serviceProvider.GetRequiredService<ILogger<VcardInputFormatter>>();

        using var reader = new StreamReader(httpContext.Request.Body, effectiveEncoding);
        string? nameLine = null;

        try
        {
            await ReadLineAsync("BEGIN:VCARD", reader, context, logger);
            await ReadLineAsync("VERSION:", reader, context, logger);

            nameLine = await ReadLineAsync("N:", reader, context, logger);

            var split = nameLine.Split(";".ToCharArray());
            var contact = new Contact(FirstName: split[1], LastName: split[0].Substring(2));

            await ReadLineAsync("FN:", reader, context, logger);
            await ReadLineAsync("END:VCARD", reader, context, logger);

            logger.LogInformation("nameLine = {nameLine}", nameLine);

            return await InputFormatterResult.SuccessAsync(contact);
        }
        catch
        {
            logger.LogError("Read failed: nameLine = {nameLine}", nameLine);
            return await InputFormatterResult.FailureAsync();
        }
    }

    private static async Task<string> ReadLineAsync(
        string expectedText, StreamReader reader, InputFormatterContext context,
        ILogger logger)
    {
        var line = await reader.ReadLineAsync();

        if (line is null || !line.StartsWith(expectedText))
        {
            var errorMessage = $"Looked for '{expectedText}' and got '{line}'";

            context.ModelState.TryAddModelError(context.ModelName, errorMessage);
            logger.LogError(errorMessage);

            throw new Exception(errorMessage);
        }

        return line;
    }
}

Тестирование приложения

Запустите пример приложения для этой статьи, который реализует базовые средства форматирования входных и выходных данных vCard. Приложение считывает и записывает виртуальные карточки, аналогичные следующему формату:

BEGIN:VCARD
VERSION:2.1
N:Davolio;Nancy
FN:Nancy Davolio
END:VCARD

Чтобы просмотреть выходные данные vCard, запустите приложение и отправьте запрос Get с заголовком text/vcard https://localhost:<port>/api/contactsAccept.

Чтобы добавить vCard в коллекцию контактов в памяти, выполните следующие действия.

  • Post Отправьте запрос /api/contacts с помощью средства, например http-repl.
  • В качестве заголовка Content-Type установите text/vcard.
  • Задайте vCard текст в тексте, отформатированный, как в предыдущем примере.

Дополнительные ресурсы

ASP.NET Core MVC поддерживает обмен данными в веб-API с помощью форматировщиков ввода и вывода. Форматировщики ввода используются привязкой модели. Форматировщики вывода используются для форматирования откликов.

Платформа предоставляет встроенные форматировщики ввода и вывода для JSON и XML. Доступен только встроенный форматировщик вывода для обычного текста, но не форматировщик ввода для обычного текста.

В этой статье показано, как добавить поддержку дополнительных форматов, создав пользовательские модули форматирования. Пример пользовательского средства форматирования ввода обычного текста см. в разделе TextPlainInputFormatter на сайте GitHub.

Просмотреть или скачать образец кода (описание загрузки)

Когда следует использовать настраиваемое форматирование

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

Общие сведения о создании настраиваемого модуля форматирования

Создание настраиваемого модуля форматирования:

  • Для сериализации данных, отправленных клиенту, создайте класс форматирования выходных данных.
  • Для десериализации данных, полученных от клиента, создайте входной класс форматирования.
  • Добавьте экземпляры классов форматирования в InputFormatters коллекции и OutputFormatters коллекции.MvcOptions

Создание настраиваемого модуля форматирования

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

В следующем коде показан VcardOutputFormatter класс из примера:

public class VcardOutputFormatter : TextOutputFormatter
{
    public VcardOutputFormatter()
    {
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));

        SupportedEncodings.Add(Encoding.UTF8);
        SupportedEncodings.Add(Encoding.Unicode);
    }

    protected override bool CanWriteType(Type type)
    {
        return typeof(Contact).IsAssignableFrom(type) ||
            typeof(IEnumerable<Contact>).IsAssignableFrom(type);
    }

    public override async Task WriteResponseBodyAsync(
        OutputFormatterWriteContext context, Encoding selectedEncoding)
    {
        var httpContext = context.HttpContext;
        var serviceProvider = httpContext.RequestServices;

        var logger = serviceProvider.GetRequiredService<ILogger<VcardOutputFormatter>>();
        var buffer = new StringBuilder();

        if (context.Object is IEnumerable<Contact> contacts)
        {
            foreach (var contact in contacts)
            {
                FormatVcard(buffer, contact, logger);
            }
        }
        else
        {
            FormatVcard(buffer, (Contact)context.Object, logger);
        }

        await httpContext.Response.WriteAsync(buffer.ToString(), selectedEncoding);
    }

    private static void FormatVcard(
        StringBuilder buffer, Contact contact, ILogger logger)
    {
        buffer.AppendLine("BEGIN:VCARD");
        buffer.AppendLine("VERSION:2.1");
        buffer.AppendLine($"N:{contact.LastName};{contact.FirstName}");
        buffer.AppendLine($"FN:{contact.FirstName} {contact.LastName}");
        buffer.AppendLine($"UID:{contact.Id}");
        buffer.AppendLine("END:VCARD");

        logger.LogInformation("Writing {FirstName} {LastName}",
            contact.FirstName, contact.LastName);
    }
}

Наследование от подходящего базового класса

Для типов текстовых носителей (например, vCard) наследуемого TextInputFormatter от базового класса:TextOutputFormatter

public class VcardOutputFormatter : TextOutputFormatter

Для двоичных типов наследуйте от InputFormatter базового OutputFormatter или базового класса.

Указание поддерживаемых типов носителей и кодировки

В конструкторе укажите поддерживаемые типы носителей и кодировки, добавив их SupportedMediaTypes в коллекции:SupportedEncodings

public VcardOutputFormatter()
{
    SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));

    SupportedEncodings.Add(Encoding.UTF8);
    SupportedEncodings.Add(Encoding.Unicode);
}

Класс форматирования не может использовать внедрение конструктора для зависимостей. Например, ILogger<VcardOutputFormatter> нельзя добавить в конструктор в качестве параметра. Чтобы получить доступ к службам, используйте объект контекста, передаваемый в методы. Пример кода в этой статье и пример показано, как это сделать.

Переопределение CanReadType и CanWriteType

Укажите тип для десериализации в или сериализовать из нее CanReadType , переопределив методы или CanWriteType методы. Например, чтобы создать текст vCard из Contact типа и наоборот:

protected override bool CanWriteType(Type type)
{
    return typeof(Contact).IsAssignableFrom(type) ||
        typeof(IEnumerable<Contact>).IsAssignableFrom(type);
}

Метод CanWriteResult

В некоторых сценариях CanWriteResult необходимо переопределить, CanWriteTypeа не переопределить. Используйте CanWriteResult, если выполняются указанные ниже условия.

  • Метод действия возвращает класс модели.
  • Существуют производные классы, которые могут быть возвращены во время выполнения.
  • Производный класс, возвращаемый действием, должен быть известен во время выполнения.

Например, предположим, что метод действия:

  • Сигнатура Person возвращает тип.
  • Может возвращать или Instructor тип, производный Student от Person.

Чтобы модуль форматирования обрабатывал только Student объекты, проверьте тип Object объекта контекста, предоставленного методу CanWriteResult . Когда метод действия возвращает IActionResult:

  • Использовать не нужно CanWriteResult.
  • Метод CanWriteType получает тип среды выполнения.

Переопределение ReadRequestBodyAsync и WriteResponseBodyAsync

Десериализация или сериализация выполняется в ReadRequestBodyAsync или WriteResponseBodyAsync. В следующем примере показано, как получить службы из контейнера внедрения зависимостей. Службы не могут быть получены из параметров конструктора:

public override async Task WriteResponseBodyAsync(
    OutputFormatterWriteContext context, Encoding selectedEncoding)
{
    var httpContext = context.HttpContext;
    var serviceProvider = httpContext.RequestServices;

    var logger = serviceProvider.GetRequiredService<ILogger<VcardOutputFormatter>>();
    var buffer = new StringBuilder();

    if (context.Object is IEnumerable<Contact> contacts)
    {
        foreach (var contact in contacts)
        {
            FormatVcard(buffer, contact, logger);
        }
    }
    else
    {
        FormatVcard(buffer, (Contact)context.Object, logger);
    }

    await httpContext.Response.WriteAsync(buffer.ToString(), selectedEncoding);
}

private static void FormatVcard(
    StringBuilder buffer, Contact contact, ILogger logger)
{
    buffer.AppendLine("BEGIN:VCARD");
    buffer.AppendLine("VERSION:2.1");
    buffer.AppendLine($"N:{contact.LastName};{contact.FirstName}");
    buffer.AppendLine($"FN:{contact.FirstName} {contact.LastName}");
    buffer.AppendLine($"UID:{contact.Id}");
    buffer.AppendLine("END:VCARD");

    logger.LogInformation("Writing {FirstName} {LastName}",
        contact.FirstName, contact.LastName);
}

Настройка MVC для использования пользовательского средства форматирования

Чтобы использовать пользовательский модуль форматирования, добавьте экземпляр класса форматирования в коллекцию или MvcOptions.OutputFormatters в нееMvcOptions.InputFormatters:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers(options =>
    {
        options.InputFormatters.Insert(0, new VcardInputFormatter());
        options.OutputFormatters.Insert(0, new VcardOutputFormatter());
    });
}

Модули форматирования обрабатываются в порядке добавления. Первый модуль имеет приоритет.

Полный VcardInputFormatter класс

В следующем коде показан VcardInputFormatter класс из примера:

public class VcardInputFormatter : TextInputFormatter
{
    public VcardInputFormatter()
    {
        SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));

        SupportedEncodings.Add(Encoding.UTF8);
        SupportedEncodings.Add(Encoding.Unicode);
    }

    protected override bool CanReadType(Type type)
    {
        return type == typeof(Contact);
    }

    public override async Task<InputFormatterResult> ReadRequestBodyAsync(
        InputFormatterContext context, Encoding effectiveEncoding)
    {
        var httpContext = context.HttpContext;
        var serviceProvider = httpContext.RequestServices;

        var logger = serviceProvider.GetRequiredService<ILogger<VcardInputFormatter>>();

        using var reader = new StreamReader(httpContext.Request.Body, effectiveEncoding);
        string nameLine = null;

        try
        {
            await ReadLineAsync("BEGIN:VCARD", reader, context, logger);
            await ReadLineAsync("VERSION:", reader, context, logger);

            nameLine = await ReadLineAsync("N:", reader, context, logger);

            var split = nameLine.Split(";".ToCharArray());
            var contact = new Contact
            {
                LastName = split[0].Substring(2),
                FirstName = split[1]
            };

            await ReadLineAsync("FN:", reader, context, logger);
            await ReadLineAsync("END:VCARD", reader, context, logger);

            logger.LogInformation("nameLine = {nameLine}", nameLine);

            return await InputFormatterResult.SuccessAsync(contact);
        }
        catch
        {
            logger.LogError("Read failed: nameLine = {nameLine}", nameLine);
            return await InputFormatterResult.FailureAsync();
        }
    }

    private static async Task<string> ReadLineAsync(
        string expectedText, StreamReader reader, InputFormatterContext context,
        ILogger logger)
    {
        var line = await reader.ReadLineAsync();

        if (!line.StartsWith(expectedText))
        {
            var errorMessage = $"Looked for '{expectedText}' and got '{line}'";

            context.ModelState.TryAddModelError(context.ModelName, errorMessage);
            logger.LogError(errorMessage);

            throw new Exception(errorMessage);
        }

        return line;
    }
}

Тестирование приложения

Запустите пример приложения для этой статьи, который реализует базовые средства форматирования входных и выходных данных vCard. Приложение считывает и записывает виртуальные карточки, аналогичные следующему формату:

BEGIN:VCARD
VERSION:2.1
N:Davolio;Nancy
FN:Nancy Davolio
END:VCARD

Чтобы просмотреть выходные данные vCard, запустите приложение и отправьте запрос Get с заголовком text/vcard https://localhost:5001/api/contactsAccept.

Чтобы добавить vCard в коллекцию контактов в памяти, выполните следующие действия.

  • Post Отправьте запрос /api/contacts с помощью средства, например curl.
  • В качестве заголовка Content-Type установите text/vcard.
  • Задайте vCard текст в тексте, отформатированный, как в предыдущем примере.

Дополнительные ресурсы