ASP.NET Core Web API のカスタム フォーマッタ

ASP.NET Core MVC は、入力と出力のフォーマッタを使用した Web API でのデータ交換をサポートしています。 入力フォーマッタは、モデル バインドによって使用されます。 出力フォーマッタは、応答の書式設定に使用されます。

フレームワークには、JSON および XML 用の組み込みの入力および出力フォーマッタが用意されています。 プレーン テキスト用の組み込みの出力フォーマッタが用意されていますが、プレーン テキスト用の入力フォーマッタは用意されていません。

この記事では、カスタム フォーマッタを作成して、追加形式のサポートを追加する方法を示します。 カスタムのプレーン テキスト入力フォーマッタの例については、GitHub の TextPlainInputFormatter を参照してください。

サンプル コードを表示またはダウンロードします (ダウンロード方法)。

カスタム フォーマッタを使用するタイミング

カスタム フォーマッタを使用して、組み込みのフォーマッタによって処理されていないコンテンツ タイプのサポートを追加します。

カスタム フォーマッタを作成する方法の概要

カスタム フォーマッタ クラスを作成するには:

  • クライアントに送信されるデータをシリアル化するには、出力フォーマッタ クラスを作成します。
  • クライアントから受信したデータを逆シリアル化するには、入力フォーマッタ クラスを作成します。
  • フォーマッタ クラスのインスタンスを MvcOptions 内の InputFormatters および OutputFormatters コレクションに追加します。

カスタム フォーマッタを作成する

フォーマッタを作成する場合は、次のようにします。

次のコードは、サンプル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 メソッド

一部のシナリオでは、CanWriteType ではなく CanWriteResult をオーバーライドする必要があります。 次のすべての条件が満たされている場合は、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 のようなツールを使用して /api/contactsPost 要求を送信します。
  • Content-Type ヘッダーを text/vcard に設定します。
  • 前の例のように書式設定された vCard テキストを本文に設定します。

その他のリソース

ASP.NET Core MVC は、入力と出力のフォーマッタを使用した Web API でのデータ交換をサポートしています。 入力フォーマッタは、モデル バインドによって使用されます。 出力フォーマッタは、応答の書式設定に使用されます。

フレームワークには、JSON および XML 用の組み込みの入力および出力フォーマッタが用意されています。 プレーン テキスト用の組み込みの出力フォーマッタが用意されていますが、プレーン テキスト用の入力フォーマッタは用意されていません。

この記事では、カスタム フォーマッタを作成して、追加形式のサポートを追加する方法を示します。 カスタムのプレーン テキスト入力フォーマッタの例については、GitHub の TextPlainInputFormatter を参照してください。

サンプル コードを表示またはダウンロードします (ダウンロード方法)。

カスタム フォーマッタを使用するタイミング

カスタム フォーマッタを使用して、組み込みのフォーマッタによって処理されていないコンテンツ タイプのサポートを追加します。

カスタム フォーマッタを作成する方法の概要

カスタム フォーマッタ クラスを作成するには:

  • クライアントに送信されるデータをシリアル化するには、出力フォーマッタ クラスを作成します。
  • クライアントから受信したデータを逆シリアル化するには、入力フォーマッタ クラスを作成します。
  • フォーマッタ クラスのインスタンスを MvcOptions 内の InputFormatters および OutputFormatters コレクションに追加します。

カスタム フォーマッタを作成する

フォーマッタを作成する場合は、次のようにします。

次のコードは、サンプル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 メソッド

一部のシナリオでは、CanWriteType ではなく CanWriteResult をオーバーライドする必要があります。 次のすべての条件が満たされている場合は、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 を追加するには:

  • Postman のようなツールを使用して /api/contactsPost 要求を送信します。
  • Content-Type ヘッダーを text/vcard に設定します。
  • 前の例のように書式設定された vCard テキストを本文に設定します。

その他のリソース