Share via


Formateadores personalizados en ASP.NET Core Web API

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 JS ON 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 ver un ejemplo de formateador de entrada de texto plano personalizado, consulta TextPlainInputFormatter en GitHub.

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

Cuándo utilizar un formateador personalizado

Utilice un formateador personalizado para añadir compatibilidad con un tipo de contenido que no sea gestionado por los formateadores integrados.

Cómo crear 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.
  • Añade instancias de clases de formateadores a las colecciones InputFormatters y OutputFormatters en MvcOptions.

Creación de un 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)
        => 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 soporte de texto (por ejemplo, vCard), derive de TextInputFormatter o TextOutputFormatter la clase base:

public class VcardOutputFormatter : TextOutputFormatter

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

Especifique los tipos de medios y codificaciones compatibles

En el constructor, especifica los tipos de medios y codificaciones soportados añadiendo a las colecciones SupportedMediaTypes y SupportedEncodings:

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

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

Una clase 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, utilice el objeto de contexto que se pasa a los métodos. Un ejemplo de código de este artículo y el ejemplo muestran cómo hacerlo.

Invalidar CanReadType y CanWriteType

Especifica el tipo de deserialización o serialización mediante los métodos CanReadType o CanWriteType. Por ejemplo, quizá solo pueda crear texto de vCard desde un tipo Contact y viceversa:

protected override bool CanWriteType(Type? type)
    => 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 de acción devuelve una clase de modelo.
  • Hay clases derivadas que pueden ser devueltas en tiempo de ejecución.
  • La clase derivada devuelta por la acción debe conocerse en tiempo de ejecución.

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

  • La firma devuelve un tipo Person.
  • Puede devolver un tipo Student o Instructor que derive de Person.

Para que el formateador maneje sólo 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.

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

Configurar MVC para utilizar un formateador personalizado

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

var builder = WebApplication.CreateBuilder(args);

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

Los formateadores se evalúan en el orden en que se insertan, donde el primero 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)
        => 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;
    }
}

Prueba 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 vCards de forma similar al formato siguiente:

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

Para ver la salida vCard, ejecute la aplicación y envíe una solicitud Get con encabezado Aceptar text/vcard a https://localhost:<port>/api/contacts.

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

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

Recursos adicionales

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 JS ON 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 ver un ejemplo de formateador de entrada de texto plano personalizado, consulta TextPlainInputFormatter en GitHub.

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

Cuándo utilizar un formateador personalizado

Utilice un formateador personalizado para añadir compatibilidad con un tipo de contenido que no sea gestionado por los formateadores integrados.

Cómo crear 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.
  • Añade instancias de clases de formateadores a las colecciones InputFormatters y OutputFormatters en MvcOptions.

Creación de un 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 soporte de texto (por ejemplo, vCard), derive de TextInputFormatter o TextOutputFormatter la clase base:

public class VcardOutputFormatter : TextOutputFormatter

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

Especifique los tipos de medios y codificaciones compatibles

En el constructor, especifica los tipos de medios y codificaciones soportados añadiendo a las colecciones SupportedMediaTypes y SupportedEncodings:

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

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

Una clase 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, utilice el objeto de contexto que se pasa a los métodos. Un ejemplo de código de este artículo y el ejemplo muestran cómo hacerlo.

Invalidar CanReadType y CanWriteType

Especifica el tipo de deserialización o serialización mediante los métodos CanReadType o CanWriteType. Por ejemplo, quizá solo pueda crear texto de vCard desde un tipo Contact 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 de acción devuelve una clase de modelo.
  • Hay clases derivadas que pueden ser devueltas en tiempo de ejecución.
  • La clase derivada devuelta por la acción debe conocerse en tiempo de ejecución.

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

  • La firma devuelve un tipo Person.
  • Puede devolver un tipo Student o Instructor que derive de Person.

Para que el formateador maneje sólo 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.

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

Configurar MVC para utilizar un formateador personalizado

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

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

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

Prueba 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 vCards de forma similar al formato siguiente:

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

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

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

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

Recursos adicionales