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 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 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:
- Derive la clase de la clase base adecuada. La aplicación de ejemplo se deriva de TextOutputFormatter y TextInputFormatter.
- Especifique en el constructor los tipos de medios y codificaciones admitidos.
- Invalide los métodos CanReadType y CanWriteType.
- Invalide los métodos ReadRequestBodyAsync y WriteResponseBodyAsync.
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
oInstructor
que derive dePerson
.
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
entext/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 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 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:
- Derive la clase de la clase base adecuada. La aplicación de ejemplo se deriva de TextOutputFormatter y TextInputFormatter.
- Especifique en el constructor los tipos de medios y codificaciones admitidos.
- Invalide los métodos CanReadType y CanWriteType.
- Invalide los métodos ReadRequestBodyAsync y WriteResponseBodyAsync.
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
oInstructor
que derive dePerson
.
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 solicitud de
Post
a/api/contacts
con una herramienta como curl. - Establezca el encabezado
Content-Type
entext/vcard
. - Establezca
vCard
texto en el cuerpo, con formato similar al ejemplo anterior.