ASP.NET Core Web API 中的自訂格式器

ASP.NET Core MVC 支援 Web API 中使用輸入和輸出格式器的資料交換。 模型繫結會使用輸入格式器。 輸出格式器用來格式化回應

此架構提供 JSON 和 XML 的內建輸入和輸出格式器。 其提供純文字的內建輸出格式器,但未提供純文字的輸入格式器。

本文說明如何藉由建立自訂的格式器來新增對其他格式的支援。 如需自訂純文字輸入格式器範例,請參閱 GitHub 上的 TextPlainInputFormatter

檢視或下載範例程式碼 \(英文\) (如何下載)

自訂格式器的使用時機

使用自訂格式器,以新增內建格式器所未處理內容類型的支援。

如何建立自訂格式器的概觀

建立自訂格式器:

  • 若要序列化傳送至用戶端的資料,請建立輸出格式器類別。
  • 若要還原序列化接收自用戶端的資料,請建立輸入格式器類別。
  • 將格式器類別的執行個體新增至 MvcOptions 中的 InputFormattersOutputFormatters 集合。

建立自訂格式器

若要建立格式器:

下列程式碼顯示範例中的 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),衍生自 TextInputFormatterTextOutputFormatter 基底類別:

public class VcardOutputFormatter : TextOutputFormatter

針對二進位類型,衍生自 InputFormatterOutputFormatter 基底類別。

指定所支援的媒體類型和編碼

在建構函式中,新增至 SupportedMediaTypesSupportedEncodings 集合,即可指定所支援的媒體類型和編碼:

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

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

格式器類別「無法」對其相依性使用建構函式插入。 例如,ILogger<VcardOutputFormatter> 無法新增為建構函式的參數。 若要存取服務,請使用可傳入方法的內容物件。 本文中的程式碼範例和範例顯示如何執行此作業。

覆寫 CanReadType 和 CanWriteType

覆寫 CanReadTypeCanWriteType 方法,即可指定要序列化或還原序列化的類型。 例如,透過 Contact 類型建立 vCard 文字,反之亦然:

protected override bool CanWriteType(Type? type)
    => typeof(Contact).IsAssignableFrom(type)
        || typeof(IEnumerable<Contact>).IsAssignableFrom(type);

CanWriteResult 方法

在某些情況下,必須覆寫 CanWriteResult,而不是 CanWriteType。 如果符合下列所有條件,請使用 CanWriteResult

  • 動作方法會傳回模型類別。
  • 在執行階段,可能會傳回衍生類別。
  • 在執行階段,必須知道動作所傳回的衍生類別。

例如,假設動作方法:

  • 簽章會傳回 Person 類型。
  • 可以傳回衍生自 PersonStudentInstructor 類型。

針對只處理 Student 物件的格式器,請檢查您提供給 CanWriteResult 方法之內容物件中的 Object 類型。 動作方法傳回 IActionResult 時:

  • 不需要使用 CanWriteResult
  • CanWriteType 方法會接收執行階段類型。

覆寫 ReadRequestBodyAsync 和 WriteResponseBodyAsync

還原序列化或序列化是在 ReadRequestBodyAsyncWriteResponseBodyAsync 中執行。 下列範例顯示如何從相依性插入容器中取得服務。 無法從建構函式參數取得服務:

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.InputFormattersMvcOptions.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 新增至連絡人的記憶體內部集合:

  • Post使用 HTTP-repl 之類的工具將要求傳送至 /api/contacts
  • Content-Type 標頭設定為 text/vcard
  • 在本文中設定 vCard 文字,且格式類似上述範例。

其他資源

ASP.NET Core MVC 支援 Web API 中使用輸入和輸出格式器的資料交換。 模型繫結會使用輸入格式器。 輸出格式器用來格式化回應

此架構提供 JSON 和 XML 的內建輸入和輸出格式器。 其提供純文字的內建輸出格式器,但未提供純文字的輸入格式器。

本文說明如何藉由建立自訂的格式器來新增對其他格式的支援。 如需自訂純文字輸入格式器範例,請參閱 GitHub 上的 TextPlainInputFormatter

檢視或下載範例程式碼 \(英文\) (如何下載)

自訂格式器的使用時機

使用自訂格式器,以新增內建格式器所未處理內容類型的支援。

如何建立自訂格式器的概觀

建立自訂格式器:

  • 若要序列化傳送至用戶端的資料,請建立輸出格式器類別。
  • 若要還原序列化接收自用戶端的資料,請建立輸入格式器類別。
  • 將格式器類別的執行個體新增至 MvcOptions 中的 InputFormattersOutputFormatters 集合。

建立自訂格式器

若要建立格式器:

下列程式碼顯示範例中的 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),衍生自 TextInputFormatterTextOutputFormatter 基底類別:

public class VcardOutputFormatter : TextOutputFormatter

針對二進位類型,衍生自 InputFormatterOutputFormatter 基底類別。

指定所支援的媒體類型和編碼

在建構函式中,新增至 SupportedMediaTypesSupportedEncodings 集合,即可指定所支援的媒體類型和編碼:

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

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

格式器類別「無法」對其相依性使用建構函式插入。 例如,ILogger<VcardOutputFormatter> 無法新增為建構函式的參數。 若要存取服務,請使用可傳入方法的內容物件。 本文中的程式碼範例和範例顯示如何執行此作業。

覆寫 CanReadType 和 CanWriteType

覆寫 CanReadTypeCanWriteType 方法,即可指定要序列化或還原序列化的類型。 例如,透過 Contact 類型建立 vCard 文字,反之亦然:

protected override bool CanWriteType(Type type)
{
    return typeof(Contact).IsAssignableFrom(type) ||
        typeof(IEnumerable<Contact>).IsAssignableFrom(type);
}

CanWriteResult 方法

在某些情況下,必須覆寫 CanWriteResult,而不是 CanWriteType。 如果符合下列所有條件,請使用 CanWriteResult

  • 動作方法會傳回模型類別。
  • 在執行階段,可能會傳回衍生類別。
  • 在執行階段,必須知道動作所傳回的衍生類別。

例如,假設動作方法:

  • 簽章會傳回 Person 類型。
  • 可以傳回衍生自 PersonStudentInstructor 類型。

針對只處理 Student 物件的格式器,請檢查您提供給 CanWriteResult 方法之內容物件中的 Object 類型。 動作方法傳回 IActionResult 時:

  • 不需要使用 CanWriteResult
  • CanWriteType 方法會接收執行階段類型。

覆寫 ReadRequestBodyAsync 和 WriteResponseBodyAsync

還原序列化或序列化是在 ReadRequestBodyAsyncWriteResponseBodyAsync 中執行。 下列範例顯示如何從相依性插入容器中取得服務。 無法從建構函式參數取得服務:

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.InputFormattersMvcOptions.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 新增至連絡人的記憶體內部集合:

  • 使用 Postman 這類工具,以將 Post 要求傳送至 /api/contacts
  • Content-Type 標頭設定為 text/vcard
  • 在本文中設定 vCard 文字,且格式類似上述範例。

其他資源