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 輸出,請執行應用程式,並將具有 Accept 標頭 text/vcard
的 Get 要求傳送至 https://localhost:<port>/api/contacts
。
將 vCard 新增至連絡人的記憶體內部集合:
- 使用 http-repl 這類工具,以將
Post
要求傳送至/api/contacts
。 - 將
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 輸出,請執行應用程式,並將具有 Accept 標頭 text/vcard
的 Get 要求傳送至 https://localhost:5001/api/contacts
。
將 vCard 新增至連絡人的記憶體內部集合:
- 使用 curl 這類工具,以將
Post
要求傳送至/api/contacts
。 - 將
Content-Type
標頭設定為text/vcard
。 - 在本文中設定
vCard
文字,且格式類似上述範例。