ASP.NET Core Web API 中的自定义格式化程序
ASP.NET Core MVC 使用输入和输出格式化程序支持 Web API 中的数据交换。 模型绑定使用输入格式化程序。 格式响应使用输出格式化程序。
该框架为 JSON 和 XML 提供内置的输入和输出格式化程序。 它为纯文本提供内置的输出格式化程序,但不为纯文本提供输入格式化程序。
本文展示如何通过创建自定义格式化程序,添加对其他格式的支持。 有关自定义纯文本输入格式化程序的示例,请参阅 GitHub 上的 TextPlainInputFormatter。
何时使用自定义格式化程序
使用自定义格式化程序为内置格式化程序无法处理的内容类型添加支持。
有关如何使用自定义格式化程序的概述
若要创建自定义格式化程序:
- 以序列化发送到客户端的数据,请创建输出格式化程序类。
- 以反序列化从客户端收到的数据,请创建输入格式化程序类。
- 将格式化程序类的实例添加到 MvcOptions 中的 InputFormatters 和 OutputFormatters 集合。
创建自定义格式化程序
若要创建格式化程序,请执行以下操作:
- 从相应的基类中派生类。 示例应用派生自 TextOutputFormatter 和 TextInputFormatter。
- 在构造函数中指定有效的媒体类型和编码。
- 替代 CanReadType 和 CanWriteType 方法。
- 替代 ReadRequestBodyAsync 和 WriteResponseBodyAsync 方法。
以下代码展示了示例中的 VcardOutputFormatter
类:
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);
}
}
从相应的基类中派生
对于文本媒体类型(例如 vCard),从 TextInputFormatter 或 TextOutputFormatter 基类派生。
public class VcardOutputFormatter : TextOutputFormatter
对于二进制类型,从 InputFormatter 或 OutputFormatter 基类派生。
指定支持的媒体类型和编码
在构造函数中,通过添加到 SupportedMediaTypes 和 SupportedEncodings 集合来指定支持的媒体类型和编码。
public VcardOutputFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));
SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode);
}
格式化程序类不能对其依赖项使用构造函数注入。 例如,ILogger<VcardOutputFormatter>
不能作为参数添加到构造函数。 若要访问服务,请使用传入方法的上下文对象。 本文中的代码示例和示例展示了如何执行此操作。
替代 CanReadType 和 CanWriteType
通过替代 CanReadType 或 CanWriteType 方法,指定要反序列化为或从其序列化的类型。 例如,从 Contact
类型创建 vCard 文本,反之亦然。
protected override bool CanWriteType(Type? type)
=> typeof(Contact).IsAssignableFrom(type)
|| typeof(IEnumerable<Contact>).IsAssignableFrom(type);
CanWriteResult 方法
在某些情况下,必须替代 CanWriteResult 而不是 CanWriteType。 如果满足以下条件,则使用 CanWriteResult
:
- 操作方法返回模型类。
- 存在可能在运行时返回的派生类。
- 操作返回的派生类必须在运行时已知。
例如,假定操作方法:
- 签名返回
Person
类型。 - 可以返回派生自
Person
的Student
或Instructor
类型。
若要让格式化程序仅处理 Student
对象,请检查提供给 CanWriteResult
方法的上下文对象中的 Object 类型。 当操作方法返回 IActionResult 时:
- 无需使用
CanWriteResult
。 CanWriteType
方法接收运行时类型。
替代 ReadRequestBodyAsync 和 WriteResponseBodyAsync
反序列化或序列化在 ReadRequestBodyAsync 或 WriteResponseBodyAsync 中执行。 以下示例展示如何从依赖项注入容器中获取服务。 无法从构造函数参数中获取服务。
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);
}
如何将 MVC 配置为使用自定义格式化程序
若要使用自定义格式化程序,请将格式化程序类的实例添加到 MvcOptions.InputFormatters 或 MvcOptions.OutputFormatters 集合:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(options =>
{
options.InputFormatters.Insert(0, new VcardInputFormatter());
options.OutputFormatters.Insert(0, new VcardOutputFormatter());
});
格式化程序按插入顺序进行评估,其中第一个格式设置优先。
完整的 VcardInputFormatter
类
以下代码展示了示例中的 VcardInputFormatter
类:
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;
}
}
测试应用程序
运行本文的示例应用,它将实现基本的 vCard 输入和输出格式化程序。 该应用读取和写入类似于以下格式的 vCard:
BEGIN:VCARD
VERSION:2.1
N:Davolio;Nancy
FN:Nancy Davolio
END:VCARD
若要查看 vCard 输出,请运行应用并向 https://localhost:<port>/api/contacts
发送带有 Accept 标头 text/vcard
的 Get 请求。
若要将 vCard 添加到内存中的联系人集合,请执行以下操作:
- 使用 http-repl 等工具向
/api/contacts
发送Post
请求。 - 将
Content-Type
标头设置为text/vcard
。 - 设置主体中的
vCard
文本,格式与前面的示例类似。
其他资源
ASP.NET Core MVC 使用输入和输出格式化程序支持 Web API 中的数据交换。 模型绑定使用输入格式化程序。 格式响应使用输出格式化程序。
该框架为 JSON 和 XML 提供内置的输入和输出格式化程序。 它为纯文本提供内置的输出格式化程序,但不为纯文本提供输入格式化程序。
本文展示如何通过创建自定义格式化程序,添加对其他格式的支持。 有关自定义纯文本输入格式化程序的示例,请参阅 GitHub 上的 TextPlainInputFormatter。
何时使用自定义格式化程序
使用自定义格式化程序为内置格式化程序无法处理的内容类型添加支持。
有关如何使用自定义格式化程序的概述
若要创建自定义格式化程序:
- 以序列化发送到客户端的数据,请创建输出格式化程序类。
- 以反序列化从客户端收到的数据,请创建输入格式化程序类。
- 将格式化程序类的实例添加到 MvcOptions 中的 InputFormatters 和 OutputFormatters 集合。
创建自定义格式化程序
若要创建格式化程序,请执行以下操作:
- 从相应的基类中派生类。 示例应用派生自 TextOutputFormatter 和 TextInputFormatter。
- 在构造函数中指定有效的媒体类型和编码。
- 替代 CanReadType 和 CanWriteType 方法。
- 替代 ReadRequestBodyAsync 和 WriteResponseBodyAsync 方法。
以下代码展示了示例中的 VcardOutputFormatter
类:
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);
}
}
从相应的基类中派生
对于文本媒体类型(例如 vCard),从 TextInputFormatter 或 TextOutputFormatter 基类派生。
public class VcardOutputFormatter : TextOutputFormatter
对于二进制类型,从 InputFormatter 或 OutputFormatter 基类派生。
指定支持的媒体类型和编码
在构造函数中,通过添加到 SupportedMediaTypes 和 SupportedEncodings 集合来指定支持的媒体类型和编码。
public VcardOutputFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/vcard"));
SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode);
}
格式化程序类不能对其依赖项使用构造函数注入。 例如,ILogger<VcardOutputFormatter>
不能作为参数添加到构造函数。 若要访问服务,请使用传入方法的上下文对象。 本文中的代码示例和示例展示了如何执行此操作。
替代 CanReadType 和 CanWriteType
通过替代 CanReadType 或 CanWriteType 方法,指定要反序列化为或从其序列化的类型。 例如,从 Contact
类型创建 vCard 文本,反之亦然。
protected override bool CanWriteType(Type type)
{
return typeof(Contact).IsAssignableFrom(type) ||
typeof(IEnumerable<Contact>).IsAssignableFrom(type);
}
CanWriteResult 方法
在某些情况下,必须替代 CanWriteResult 而不是 CanWriteType。 如果满足以下条件,则使用 CanWriteResult
:
- 操作方法返回模型类。
- 存在可能在运行时返回的派生类。
- 操作返回的派生类必须在运行时已知。
例如,假定操作方法:
- 签名返回
Person
类型。 - 可以返回派生自
Person
的Student
或Instructor
类型。
若要让格式化程序仅处理 Student
对象,请检查提供给 CanWriteResult
方法的上下文对象中的 Object 类型。 当操作方法返回 IActionResult 时:
- 无需使用
CanWriteResult
。 CanWriteType
方法接收运行时类型。
替代 ReadRequestBodyAsync 和 WriteResponseBodyAsync
反序列化或序列化在 ReadRequestBodyAsync 或 WriteResponseBodyAsync 中执行。 以下示例展示如何从依赖项注入容器中获取服务。 无法从构造函数参数中获取服务。
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);
}
如何将 MVC 配置为使用自定义格式化程序
若要使用自定义格式化程序,请将格式化程序类的实例添加到 MvcOptions.InputFormatters 或 MvcOptions.OutputFormatters 集合:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options =>
{
options.InputFormatters.Insert(0, new VcardInputFormatter());
options.OutputFormatters.Insert(0, new VcardOutputFormatter());
});
}
按格式化程序的插入顺序对其进行计算。 第一个优先。
完整的 VcardInputFormatter
类
以下代码展示了示例中的 VcardInputFormatter
类:
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;
}
}
测试应用程序
运行本文的示例应用,它将实现基本的 vCard 输入和输出格式化程序。 该应用读取和写入类似于以下格式的 vCard:
BEGIN:VCARD
VERSION:2.1
N:Davolio;Nancy
FN:Nancy Davolio
END:VCARD
若要查看 vCard 输出,请运行应用并向 https://localhost:5001/api/contacts
发送带有 Accept 标头 text/vcard
的 Get 请求。
若要将 vCard 添加到内存中的联系人集合,请执行以下操作:
- 使用 curl 等工具向
/api/contacts
发送Post
请求。 - 将
Content-Type
标头设置为text/vcard
。 - 设置主体中的
vCard
文本,格式与前面的示例类似。