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
InputFormattersOutputFormatterscolecciones y en MvcOptions .
Cómo crear una clase de 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 tipos de medios válidos y codificaciones en el constructor.
- 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 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
Persontipo. - Puede devolver un
Studenttipo o que deriva deInstructorPerson.
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
CanWriteTypemé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
Postsolicitud a con una herramienta como/api/contactsPostman. - Establezca el encabezado
Content-Typeentext/vcard. - Establezca
vCardtexto en el cuerpo, con el formato del ejemplo anterior.