Bagikan melalui


Pemformat kustom di ASP.NET Core Web API

ASP.NET Core MVC mendukung pertukaran data di API Web menggunakan pemformat input dan output. Pemformat input digunakan oleh Pengikatan Model. Formatter output digunakan untuk memformat respons.

Kerangka kerja ini menyediakan pemformat input dan output bawaan untuk JSON dan XML. Ini menyediakan formatter output bawaan untuk teks biasa, tetapi tidak menyediakan formatter input untuk teks biasa.

Artikel ini memperlihatkan cara menambahkan dukungan untuk format tambahan dengan membuat pemformat kustom. Untuk contoh pemformat input teks biasa kustom, lihat TextPlainInputFormatter di GitHub.

Melihat atau mengunduh kode sampel (cara mengunduh)

Kapan menggunakan pemformat kustom

Gunakan formatter kustom untuk menambahkan dukungan untuk tipe konten yang tidak ditangani oleh pemformat bawaan.

Gambaran umum cara membuat pemformat kustom

Untuk membuat formatter kustom:

  • Untuk menserialisasikan data yang dikirim ke klien, buat kelas pemformat output.
  • Untuk deserialisasi data yang diterima dari klien, buat kelas pemformat input.
  • Tambahkan instans kelas formatter ke InputFormatters koleksi dan OutputFormatters di MvcOptions.

Membuat pemformat kustom

Untuk membuat pemformat:

Kode berikut menunjukkan VcardOutputFormatter kelas dari sampel:

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

Berasal dari kelas dasar yang sesuai

Untuk jenis media teks (misalnya, vCard), berasal dari TextInputFormatter kelas dasar atau TextOutputFormatter :

public class VcardOutputFormatter : TextOutputFormatter

Untuk jenis biner, berasal dari InputFormatter kelas atau OutputFormatter dasar.

Tentukan jenis media dan pengodean yang didukung

Di konstruktor, tentukan jenis media dan pengodean yang didukung dengan menambahkan ke SupportedMediaTypes koleksi dan SupportedEncodings :

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

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

Kelas formatter tidak dapat menggunakan injeksi konstruktor untuk dependensinya. Misalnya, ILogger<VcardOutputFormatter> tidak dapat ditambahkan sebagai parameter ke konstruktor. Untuk mengakses layanan, gunakan objek konteks yang diteruskan ke metode . Contoh kode dalam artikel ini dan sampel menunjukkan cara melakukan ini.

Mengganti CanReadType dan CanWriteType

Tentukan jenis yang akan dideserialisasi ke dalam atau serialisasi dengan mengambil CanReadType alih metode atau CanWriteType . Misalnya, untuk membuat teks vCard dari Contact jenis dan sebaliknya:

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

Metode CanWriteResult

Dalam beberapa skenario, CanWriteResult harus ditimpa daripada CanWriteType. Gunakan CanWriteResult jika kondisi berikut ini benar:

  • Metode tindakan mengembalikan kelas model.
  • Ada kelas turunan yang mungkin dikembalikan pada runtime.
  • Kelas turunan yang dikembalikan oleh tindakan harus diketahui saat runtime.

Misalnya, misalkan metode tindakan:

  • Tanda tangan mengembalikan Person jenis.
  • Dapat mengembalikan Student atau Instructor jenis yang berasal dari Person.

Agar pemformat hanya Student menangani objek, periksa jenis Object dalam objek konteks yang disediakan untuk CanWriteResult metode . Ketika metode tindakan mengembalikan IActionResult:

  • Tidak perlu menggunakan CanWriteResult.
  • Metode CanWriteType menerima jenis runtime.

Mengambil alih ReadRequestBodyAsync dan WriteResponseBodyAsync

Deserialisasi atau serialisasi dilakukan di ReadRequestBodyAsync atau WriteResponseBodyAsync. Contoh berikut menunjukkan cara mendapatkan layanan dari kontainer injeksi dependensi. Layanan tidak dapat diperoleh dari parameter konstruktor:

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

Mengonfigurasi MVC untuk menggunakan pemformat kustom

Untuk menggunakan formatter kustom, tambahkan instans kelas formatter ke MvcOptions.InputFormatters koleksi atau MvcOptions.OutputFormatters :

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers(options =>
{
    options.InputFormatters.Insert(0, new VcardInputFormatter());
    options.OutputFormatters.Insert(0, new VcardOutputFormatter());
});

Formatter dievaluasi dalam urutan yang disisipkan, di mana yang pertama diutamakan.

Kelas lengkap VcardInputFormatter

Kode berikut menunjukkan VcardInputFormatter kelas dari sampel:

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

Menguji aplikasi

Jalankan aplikasi sampel untuk artikel ini, yang mengimplementasikan pemformat input dan output vCard dasar. Aplikasi membaca dan menulis vCard yang mirip dengan format berikut:

BEGIN:VCARD
VERSION:2.1
N:Davolio;Nancy
FN:Nancy Davolio
END:VCARD

Untuk melihat output vCard, jalankan aplikasi dan kirim permintaan Dapatkan dengan header text/vcard Terima ke https://localhost:<port>/api/contacts.

Untuk menambahkan vCard ke kumpulan kontak dalam memori:

  • Post Kirim permintaan ke /api/contacts dengan alat seperti http-repl.
  • Atur header Content-Type ke text/vcard.
  • Atur vCard teks dalam isi, diformat seperti contoh sebelumnya.

Sumber Daya Tambahan:

ASP.NET Core MVC mendukung pertukaran data di API Web menggunakan pemformat input dan output. Pemformat input digunakan oleh Pengikatan Model. Formatter output digunakan untuk memformat respons.

Kerangka kerja ini menyediakan pemformat input dan output bawaan untuk JSON dan XML. Ini menyediakan formatter output bawaan untuk teks biasa, tetapi tidak menyediakan formatter input untuk teks biasa.

Artikel ini memperlihatkan cara menambahkan dukungan untuk format tambahan dengan membuat pemformat kustom. Untuk contoh pemformat input teks biasa kustom, lihat TextPlainInputFormatter di GitHub.

Melihat atau mengunduh kode sampel (cara mengunduh)

Kapan menggunakan pemformat kustom

Gunakan formatter kustom untuk menambahkan dukungan untuk tipe konten yang tidak ditangani oleh pemformat bawaan.

Gambaran umum cara membuat pemformat kustom

Untuk membuat formatter kustom:

  • Untuk menserialisasikan data yang dikirim ke klien, buat kelas pemformat output.
  • Untuk deserialisasi data yang diterima dari klien, buat kelas pemformat input.
  • Tambahkan instans kelas formatter ke InputFormatters koleksi dan OutputFormatters di MvcOptions.

Membuat pemformat kustom

Untuk membuat pemformat:

Kode berikut menunjukkan VcardOutputFormatter kelas dari sampel:

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

Berasal dari kelas dasar yang sesuai

Untuk jenis media teks (misalnya, vCard), berasal dari TextInputFormatter kelas dasar atau TextOutputFormatter :

public class VcardOutputFormatter : TextOutputFormatter

Untuk jenis biner, berasal dari InputFormatter kelas atau OutputFormatter dasar.

Tentukan jenis media dan pengodean yang didukung

Di konstruktor, tentukan jenis media dan pengodean yang didukung dengan menambahkan ke SupportedMediaTypes koleksi dan SupportedEncodings :

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

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

Kelas formatter tidak dapat menggunakan injeksi konstruktor untuk dependensinya. Misalnya, ILogger<VcardOutputFormatter> tidak dapat ditambahkan sebagai parameter ke konstruktor. Untuk mengakses layanan, gunakan objek konteks yang diteruskan ke metode . Contoh kode dalam artikel ini dan sampel menunjukkan cara melakukan ini.

Mengganti CanReadType dan CanWriteType

Tentukan jenis yang akan dideserialisasi ke dalam atau serialisasi dengan mengambil CanReadType alih metode atau CanWriteType . Misalnya, untuk membuat teks vCard dari Contact jenis dan sebaliknya:

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

Metode CanWriteResult

Dalam beberapa skenario, CanWriteResult harus ditimpa daripada CanWriteType. Gunakan CanWriteResult jika kondisi berikut ini benar:

  • Metode tindakan mengembalikan kelas model.
  • Ada kelas turunan yang mungkin dikembalikan pada runtime.
  • Kelas turunan yang dikembalikan oleh tindakan harus diketahui saat runtime.

Misalnya, misalkan metode tindakan:

  • Tanda tangan mengembalikan Person jenis.
  • Dapat mengembalikan Student atau Instructor jenis yang berasal dari Person.

Agar pemformat hanya Student menangani objek, periksa jenis Object dalam objek konteks yang disediakan untuk CanWriteResult metode . Ketika metode tindakan mengembalikan IActionResult:

  • Tidak perlu menggunakan CanWriteResult.
  • Metode CanWriteType menerima jenis runtime.

Mengambil alih ReadRequestBodyAsync dan WriteResponseBodyAsync

Deserialisasi atau serialisasi dilakukan di ReadRequestBodyAsync atau WriteResponseBodyAsync. Contoh berikut menunjukkan cara mendapatkan layanan dari kontainer injeksi dependensi. Layanan tidak dapat diperoleh dari parameter konstruktor:

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

Mengonfigurasi MVC untuk menggunakan pemformat kustom

Untuk menggunakan formatter kustom, tambahkan instans kelas formatter ke MvcOptions.InputFormatters koleksi atau MvcOptions.OutputFormatters :

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers(options =>
    {
        options.InputFormatters.Insert(0, new VcardInputFormatter());
        options.OutputFormatters.Insert(0, new VcardOutputFormatter());
    });
}

Pemformat dievaluasi dalam urutan Anda menyisipkannya. Yang pertama lebih diutamakan.

Kelas lengkap VcardInputFormatter

Kode berikut menunjukkan VcardInputFormatter kelas dari sampel:

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

Menguji aplikasi

Jalankan aplikasi sampel untuk artikel ini, yang mengimplementasikan pemformat input dan output vCard dasar. Aplikasi membaca dan menulis vCard yang mirip dengan format berikut:

BEGIN:VCARD
VERSION:2.1
N:Davolio;Nancy
FN:Nancy Davolio
END:VCARD

Untuk melihat output vCard, jalankan aplikasi dan kirim permintaan Dapatkan dengan header text/vcard Terima ke https://localhost:5001/api/contacts.

Untuk menambahkan vCard ke kumpulan kontak dalam memori:

  • Post Kirim permintaan ke /api/contacts dengan alat seperti curl.
  • Atur header Content-Type ke text/vcard.
  • Atur vCard teks dalam isi, diformat seperti contoh sebelumnya.

Sumber Daya Tambahan: