Formateadores personalizados en ASP.NET Core Web API

Por Kirk Larkin y Tom Dykstra.

ASP.NET Core MVC admite el intercambio de datos en las API Web con formateadores de entrada y salida. El enlace de modelos usa formateadores de entrada. Los formateadores de salida se usan para dar formato a las respuestas.

El marco proporciona formateadores de entrada y salida integrados para JSON y XML. Proporciona un formateador de salida integrado para texto sin formato, pero no proporciona un formateador de entrada para texto sin formato.

En este artículo se muestra cómo agregar compatibilidad con formatos adicionales mediante la creación de formateadores personalizados. Para obtener un ejemplo de un formateador de entrada de texto sin formato personalizado, vea TextPlainInputFormatter en GitHub.

Vea o descargue el código de ejemplo (cómo descargarlo)

Cuándo usar formateadores personalizados

Use un formateador personalizado para agregar compatibilidad con un tipo de contenido que los formateadores integrados no controlan.

Información general sobre cómo utilizar a un formateador personalizado

Para crear un formateador personalizado:

  • Para serializar los datos enviados al cliente, cree una clase de formateador de salida.
  • Para deserializar los datos recibidos del cliente, cree una clase de formateador de entrada.
  • Agregue instancias de clases de formateadores a las InputFormatters OutputFormatters colecciones y en MvcOptions .

Cómo crear una clase de formateador personalizado

Para crear un formateador:

El código siguiente muestra la VcardOutputFormatter clase del ejemplo:

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 de la clase base adecuada

Para los tipos de medios de texto (por ejemplo, vCard), derive de la TextInputFormatter clase TextOutputFormatter base o .

public class VcardOutputFormatter : TextOutputFormatter

Para los tipos binarios, derive de la InputFormatter OutputFormatter clase base o .

Especificar las codificaciones y los tipos de medios válidos

En el constructor, especifique tipos de medios y codificaciones válidos. Para ello, agréguelos a las colecciones SupportedMediaTypes y SupportedEncodings.

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

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

Una clase de formateador no puede usar la inserción de constructores para sus dependencias. Por ejemplo, ILogger<VcardOutputFormatter> no se puede agregar como parámetro al constructor. Para acceder a los servicios, use el objeto de contexto que se pasa a los métodos . En un ejemplo de código de este artículo y en el ejemplo se muestra cómo hacerlo.

Invalidar CanReadType y CanWriteType

Especifique el tipo desde el que se deserializa o desde el que se reemplazan los CanReadType CanWriteType métodos o . Por ejemplo, crear texto de tarjeta virtual a partir Contact de un tipo y viceversa.

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

Método CanWriteResult

En algunos escenarios, CanWriteResult se debe invalidar en lugar de CanWriteType . Use CanWriteResult si las condiciones siguientes son verdaderas:

  • El método action devuelve una clase de modelo.
  • Hay clases derivadas que pueden obtenerse en tiempo de ejecución.
  • La clase derivada devuelta por la acción debe conocerse en tiempo de ejecución.

Por ejemplo, suponga que el método de acción:

  • Signature devuelve un Person tipo.
  • Puede devolver un Student tipo o que deriva de Instructor Person .

Para que el formateador controle solo Student objetos, compruebe el tipo de Object en el objeto de contexto proporcionado al método CanWriteResult . Cuando el método de acción devuelve IActionResult :

  • No es necesario usar CanWriteResult .
  • El CanWriteType método recibe el tipo en tiempo de ejecución.

Invalidación de ReadRequestBodyAsync y WriteResponseBodyAsync

La deserialización o serialización se realiza en ReadRequestBodyAsync o WriteResponseBodyAsync . En el ejemplo siguiente se muestra cómo obtener servicios del contenedor de inserción de dependencias. Los servicios no se pueden obtener a partir de parámetros de constructor.

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

Cómo configurar MVC para usar a un formateador personalizado

Para utilizar un formateador personalizado, debe agregar una instancia de la clase de formateador a la colección InputFormatters o OutputFormatters.

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

Los formateadores se evalúan en el orden en que se insertaron. El primero de ellos tiene prioridad.

La clase VcardInputFormatter completa

El código siguiente muestra la VcardInputFormatter clase del ejemplo:

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

Pruebas de la aplicación

Ejecute la aplicación de ejemplo de este artículo, que implementa formateadores básicos de entrada y salida de vCard. La aplicación lee y escribe tarjetas virtuales similares a las siguientes:

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

Para ver la salida de la tarjeta virtual, ejecute la aplicación y envíe una solicitud Get con el encabezado Accept text/vcard a https://localhost:5001/api/contacts .

Para agregar una vCard a la colección en memoria de contactos:

  • Envíe una Post solicitud a con una herramienta como /api/contacts Postman.
  • Establezca el encabezado Content-Type en text/vcard.
  • Establezca vCard texto en el cuerpo, con el formato del ejemplo anterior.

Recursos adicionales