Partilhar via


Formatters personalizados na API Web ASP.NET Core

ASP.NET Core MVC suporta a troca de dados em APIs da Web usando os formatadores de entrada e saída. Os formatadores de entrada são usados pelos Vinculação de Modelo. Os formatters de saída são usados para formatar respostas.

A estrutura fornece formatadores integrados de entrada e saída para JSON e XML. Ele fornece um formatador de saída interno para texto sem formatação, mas não fornece um formatador de entrada para texto sem formatação.

Este artigo mostra como adicionar suporte para formatos adicionais criando formatters personalizados. Para obter um exemplo de um formatador de entrada de texto simples personalizado, consulte TextPlainInputFormatter no GitHub.

Visualizar ou descarregar amostra de código (como descarregar)

Quando usar um formatador personalizado

Utilize um formatador personalizado para adicionar suporte a um tipo de conteúdo que não é manipulado pelos formatadores internos.

Visão geral de como criar um formatador personalizado

Para criar um formatador personalizado:

  • Para serializar dados enviados ao cliente, crie uma classe de formatador de saída.
  • Para desserializar dados recebidos do cliente, crie uma classe de formatador de entrada.
  • Adicione instâncias de classes de formatador às coleções InputFormatters e OutputFormatters em MvcOptions.

Criar um formatador personalizado

Para criar um formatador:

O código a seguir mostra a VcardOutputFormatter classe do exemplo:

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

Derivar da classe base apropriada

Para tipos de mídia de texto (por exemplo, vCard), derive da classe base TextInputFormatter ou TextOutputFormatter:

public class VcardOutputFormatter : TextOutputFormatter

Para tipos binários, derive da classe base InputFormatter ou OutputFormatter.

Especificar tipos de mídia e codificações suportados

No construtor, especifique os tipos de mídia suportados e codificações, adicionando às coleções SupportedMediaTypes e SupportedEncodings:

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

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

Uma classe de formatador não pode usar a injeção de construtor para suas dependências. Por exemplo, ILogger<VcardOutputFormatter> não pode ser adicionado como um parâmetro para o construtor. Para acessar serviços, use o objeto de contexto que é passado para os métodos. Um exemplo de código neste artigo e o exemplo mostram como fazer isso.

Substituir CanReadType e CanWriteType

Especifique o tipo para desserializar ou serializar substituindo os CanReadType métodos ou CanWriteType . Por exemplo, para criar texto vCard a partir de um tipo Contact e vice-versa:

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

O método CanWriteResult

Em alguns cenários, CanWriteResult deve ser substituído em vez de CanWriteType. Use CanWriteResult se as seguintes condições forem verdadeiras:

  • O método action retorna uma classe de modelo.
  • Há classes derivadas que podem ser retornadas em tempo de execução.
  • A classe derivada retornada pela ação deve ser conhecida em tempo de execução.

Por exemplo, suponha o método de ação:

  • A assinatura retorna um tipo Person.
  • Pode retornar um Student ou Instructor tipo que deriva de Person.

Para que o formatador manipule apenas objetos Student, verifique o tipo de Object no objeto de contexto fornecido ao método CanWriteResult. Quando o método action retorna IActionResult:

  • Não é necessário usar CanWriteResult.
  • O CanWriteType método recebe o tipo de tempo de execução.

Sobrescrever ReadRequestBodyAsync e WriteResponseBodyAsync

A desserialização ou serialização é realizada em ReadRequestBodyAsync ou WriteResponseBodyAsync. O exemplo a seguir mostra como obter serviços do contêiner de injeção de dependência. Os serviços não podem ser obtidos a partir dos parâmetros do construtor:

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

Configurar o MVC para usar um formatador personalizado

Para usar um formatador personalizado, adicione uma instância da classe formatador à coleção MvcOptions.InputFormatters ou MvcOptions.OutputFormatters.

var builder = WebApplication.CreateBuilder(args);

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

Os formatadores são avaliados pela ordem em que são inseridos, sendo que o primeiro tem precedência.

A classe completa VcardInputFormatter

O código a seguir mostra a VcardInputFormatter classe do exemplo:

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

Testar a aplicação

Execute a aplicação de exemplo deste artigo, que implementa formatadores de entrada e saída básicos de vCard. O aplicativo lê e grava vCards semelhante ao seguinte formato:

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

Para ver a saída do vCard, execute o aplicativo e envie uma solicitação Get com o cabeçalho text/vcard Accept para https://localhost:<port>/api/contacts.

Para adicionar um vCard à coleção de contatos na memória:

  • Envie uma Post solicitação para /api/contacts com uma ferramenta como http-repl.
  • Defina o Content-Type cabeçalho como text/vcard.
  • Defina vCard o texto no corpo, formatado como o exemplo anterior.

Recursos adicionais

ASP.NET Core MVC suporta a troca de dados em APIs da Web usando os formatadores de entrada e saída. Os formatadores de entrada são usados pelos Vinculação de Modelo. Os formatters de saída são usados para formatar respostas.

A estrutura fornece formatadores integrados de entrada e saída para JSON e XML. Ele fornece um formatador de saída interno para texto sem formatação, mas não fornece um formatador de entrada para texto sem formatação.

Este artigo mostra como adicionar suporte para formatos adicionais criando formatters personalizados. Para obter um exemplo de um formatador de entrada de texto simples personalizado, consulte TextPlainInputFormatter no GitHub.

Visualizar ou descarregar amostra de código (como descarregar)

Quando usar um formatador personalizado

Utilize um formatador personalizado para adicionar suporte a um tipo de conteúdo que não é manipulado pelos formatadores internos.

Visão geral de como criar um formatador personalizado

Para criar um formatador personalizado:

  • Para serializar dados enviados ao cliente, crie uma classe de formatador de saída.
  • Para desserializar dados recebidos do cliente, crie uma classe de formatador de entrada.
  • Adicione instâncias de classes de formatador às coleções InputFormatters e OutputFormatters em MvcOptions.

Criar um formatador personalizado

Para criar um formatador:

O código a seguir mostra a VcardOutputFormatter classe do exemplo:

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

Derivar da classe base apropriada

Para tipos de mídia de texto (por exemplo, vCard), derive da classe base TextInputFormatter ou TextOutputFormatter:

public class VcardOutputFormatter : TextOutputFormatter

Para tipos binários, derive da classe base InputFormatter ou OutputFormatter.

Especificar tipos de mídia e codificações suportados

No construtor, especifique os tipos de mídia suportados e codificações, adicionando às coleções SupportedMediaTypes e SupportedEncodings:

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

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

Uma classe de formatador não pode usar a injeção de construtor para suas dependências. Por exemplo, ILogger<VcardOutputFormatter> não pode ser adicionado como um parâmetro para o construtor. Para acessar serviços, use o objeto de contexto que é passado para os métodos. Um exemplo de código neste artigo e o exemplo mostram como fazer isso.

Substituir CanReadType e CanWriteType

Especifique o tipo para desserializar ou serializar substituindo os CanReadType métodos ou CanWriteType . Por exemplo, para criar texto vCard a partir de um tipo Contact e vice-versa:

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

O método CanWriteResult

Em alguns cenários, CanWriteResult deve ser substituído em vez de CanWriteType. Use CanWriteResult se as seguintes condições forem verdadeiras:

  • O método action retorna uma classe de modelo.
  • Há classes derivadas que podem ser retornadas em tempo de execução.
  • A classe derivada retornada pela ação deve ser conhecida em tempo de execução.

Por exemplo, suponha o método de ação:

  • A assinatura retorna um tipo Person.
  • Pode retornar um Student ou Instructor tipo que deriva de Person.

Para que o formatador manipule apenas objetos Student, verifique o tipo de Object no objeto de contexto fornecido ao método CanWriteResult. Quando o método action retorna IActionResult:

  • Não é necessário usar CanWriteResult.
  • O CanWriteType método recebe o tipo de tempo de execução.

Sobrescrever ReadRequestBodyAsync e WriteResponseBodyAsync

A desserialização ou serialização é realizada em ReadRequestBodyAsync ou WriteResponseBodyAsync. O exemplo a seguir mostra como obter serviços do contêiner de injeção de dependência. Os serviços não podem ser obtidos a partir dos parâmetros do construtor:

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

Configurar o MVC para usar um formatador personalizado

Para usar um formatador personalizado, adicione uma instância da classe formatador à coleção MvcOptions.InputFormatters ou MvcOptions.OutputFormatters.

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

Formatadores são avaliados pela ordem em que são inseridos. A primeira tem precedência.

A classe completa VcardInputFormatter

O código a seguir mostra a VcardInputFormatter classe do exemplo:

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

Testar a aplicação

Execute a aplicação de exemplo deste artigo, que implementa formatadores de entrada e saída básicos de vCard. O aplicativo lê e grava vCards semelhante ao seguinte formato:

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

Para ver a saída do vCard, execute o aplicativo e envie uma solicitação Get com o cabeçalho text/vcard Accept para https://localhost:5001/api/contacts.

Para adicionar um vCard à coleção de contatos na memória:

  • Envie uma Post solicitação para /api/contacts com uma ferramenta como curl.
  • Defina o Content-Type cabeçalho como text/vcard.
  • Defina vCard o texto no corpo, formatado como o exemplo anterior.

Recursos adicionais