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

Kirk LarkinTom Dykstra

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

架構會針對 JSON 和 XML 提供內建的輸入和輸出格式器。 它會為純文字提供內建的輸出格式器,但不提供純文字的輸入格式器。

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

查看或下載範例程式碼 (如何下載)

自訂格式器的使用時機

您可以使用自訂格式器來新增內建格式器未處理之內容類型的支援。

如何使用自訂格式器的概觀

若要建立自訂格式器:

  • 若要序列化傳送至用戶端的資料,請建立輸出格式器類別。
  • 若要還原序列化從用戶端接收的資料,請建立輸入格式器類別。
  • 將格式器類別的實例加入 InputFormattersOutputFormatters 中的和集合 MvcOptions

如何建立自訂格式器類別

若要建立格式器:

下列程式碼顯示 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

藉由覆寫或方法,指定要還原序列化或序列化的型別 CanReadType CanWriteType 。 例如,從類型建立 vCard 文字 Contact ,反之亦然。

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

CanWriteResult 方法

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

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

例如,假設動作方法:

  • Signature 會傳回 Person 型別。
  • 可以傳回 Student Instructor 衍生自的或類型 Person

若要讓格式器只處理 Student 物件,請 Object 在提供給方法的內容物件中,檢查的型別 CanWriteResult 。 當動作方法傳回時 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 以使用自訂格式器

若要使用自訂格式器,請將格式器類別的執行個體新增至 InputFormattersOutputFormatters 集合。

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);

系統會依據您插入格式器的順序進行評估。 第一個會優先使用。

完整 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 標頭的 Get 要求傳送 text/vcardhttps://localhost:5001/api/contacts

若要在記憶體中的連絡人集合中加入 vCard:

  • Post /api/contacts 使用 Postman 之類的工具將要求傳送至。
  • Content-Type 標頭設定為 text/vcard
  • 設定 vCard 主體中的文字,格式如上述範例所示。

其他資源