Benutzerdefinierte Formatierer in Web-APIs in ASP.NET Core

ASP.NET Core MVC unterstützt den Datenaustausch in Web-APIs mithilfe von Eingabe- und Ausgabeformatierern. Eingabeformatierer werden von der Modellbindung verwendet. Ausgabeformatierer werden zum Formatieren von Antworten verwendet.

Das Framework stellt integrierte Eingabe- und Ausgabeformatierer für JSON und XML bereit. Es stellt einen integrierten Ausgabeformatierer für Nur-Text bereit, jedoch keinen Eingabeformatierer.

In diesem Artikel wird dargestellt, wie Sie die Unterstützung von zusätzlichen Formaten hinzufügen, indem Sie benutzerdefinierte Formatierer erstellen. Ein Beispiel für einen benutzerdefinierten Formatierer für Nur-Text-Eingaben finden Sie unter TextPlainInputFormatter auf GitHub.

Anzeigen oder Herunterladen von Beispielcode (Vorgehensweise zum Herunterladen)

Wann wird ein benutzerdefinierter Formatierer verwendet?

Verwenden Sie einen benutzerdefinierten Formatierer, um Unterstützung für einen Inhaltstyp hinzuzufügen, der von den integrierten Formatieren nicht verarbeitet wird.

Übersicht über die Erstellung eines benutzerdefinierten Formatierers

So erstellen Sie einen benutzerdefinierten Formatierer:

  • Erstellen Sie zum Serialisieren von Daten, die an den Client gesendet werden, eine Ausgabeformatiererklasse.
  • Erstellen Sie zum Deserialisieren von Daten, die vom Client empfangen werden, eine Eingabeformatiererklasse.
  • Fügen Sie Instanzen der Formatiererklassen zu den Sammlungen InputFormatters und OutputFormatters in MvcOptions hinzu.

Erstellen eines benutzerdefinierten Formatierers

Erstellen eines Formatierers:

Der folgende Code zeigt die VcardOutputFormatter-Klasse aus dem Beispiel:

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

Ableiten von der entsprechenden Basisklasse

Verwenden Sie bei Textmedientypen (z. B. vCard) Ableitungen aus den Basisklassen TextInputFormatter oder TextOutputFormatter:

public class VcardOutputFormatter : TextOutputFormatter

Bei Binärtypen verwenden Sie Ableitungen aus den Basisklassen InputFormatter oder OutputFormatter.

Angeben unterstützter Medientypen und Codierungen

Geben Sie unterstützte Medientypen und Codierungen im Konstruktor an, indem Sie sie den Sammlungen SupportedMediaTypes und SupportedEncodings hinzufügen:

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

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

Eine Formatiererklasse kann keine Konstruktorinjektion für ihre Abhängigkeiten verwenden. Beispielsweise kann ILogger<VcardOutputFormatter> nicht als Parameter zum Konstruktor hinzugefügt werden. Verwenden Sie für den Zugriff auf Dienste das Kontextobjekt, das an die Methoden übergeben wird. Ein Codebeispiel in diesem Artikel sowie dieses Beispiel veranschaulichen die Vorgehensweise.

Überschreiben von CanReadType und CanWriteType

Geben Sie den Typ an, in den deserialisiert bzw. aus dem serialisiert werden soll, indem Sie die Methoden CanReadType oder CanWriteType überschreiben. Um beispielsweise vCard-Text aus einem Contact-Typ und umgekehrt zu erstellen, gehen Sie folgendermaßen vor:

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

Die CanWriteResult-Methode

In einigen Szenarien muss CanWriteResult überschrieben werden, nicht CanWriteType. Verwenden Sie CanWriteResult, wenn alle der folgenden Bedingungen zutreffen:

  • Die Aktionsmethode gibt eine Modellklasse zurück.
  • Es gibt abgeleitete Klassen, die möglicherweise zur Laufzeit zurückgegeben werden.
  • Die von der Aktion zurückgegebene abgeleitete Klasse muss zur Laufzeit bekannt sein.

Angenommen, für die Aktionsmethode gilt Folgendes:

  • Die Signatur gibt einen Person-Typ zurück.
  • Sie kann einen Student- oder einen Instructor-Typ zurückgeben, der aus Person abgeleitet ist.

Damit der Formatierer nur Student-Objekte verarbeitet, überprüfen Sie den Typ von Object in dem Kontextobjekt, das an die CanWriteResult-Methode übergeben wurde. Wenn die Aktionsmethode IActionResult zurückgibt, gilt Folgendes:

  • Es ist nicht notwendig, CanWriteResult zu verwenden.
  • Die CanWriteType-Methode empfängt den Laufzeittyp.

Überschreiben von ReadRequestBodyAsync und WriteResponseBodyAsync

Die Deserialisierung oder Serialisierung wird in ReadRequestBodyAsync oder WriteResponseBodyAsync ausgeführt. Das folgende Beispiel zeigt, wie Sie Dienste aus dem Abhängigkeitsinjektionscontainer abrufen. Dienste können nicht aus Konstruktorparametern abgerufen werden:

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

Konfigurieren von MVC zum Verwenden eines benutzerdefinierten Formatierers

Um einen benutzerdefinierten Formatierer zu verwenden, fügen Sie der Sammlung MvcOptions.InputFormatters oder MvcOptions.OutputFormatters eine Instanz der Formatiererklasse hinzu:

var builder = WebApplication.CreateBuilder(args);

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

Formatierer werden in der Reihenfolge ausgewertet, in der sie eingefügt werden, wobei der erste Vorrang hat.

Die vollständige VcardInputFormatter-Klasse

Der folgende Code zeigt die VcardInputFormatter-Klasse aus dem Beispiel:

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

Die App testen

Führen Sie die Beispiel-App für diesen Artikel aus, die grundlegende Ein- und Ausgabeformatierer für vCard hinzufügt. Die App liest und schreibt vCards ähnlich dem folgenden Format:

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

Um die vCard-Ausgabe anzuzeigen, führen Sie die App aus, und senden Sie eine get-Anforderung mit dem Accept-Header text/vcard an https://localhost:<port>/api/contacts.

So fügen Sie der In-Memory-Sammlung von Kontakten eine vCard hinzu:

  • Senden Sie eine Post-Anforderung mithilfe eines Tools wie http-repl an /api/contacts.
  • Legen Sie den Header Content-Type auf text/vcard fest.
  • Fügen Sie im Haupttext vCard-Text hinzu, der wie im obigen Beispiel formatiert ist.

Zusätzliche Ressourcen

ASP.NET Core MVC unterstützt den Datenaustausch in Web-APIs mithilfe von Eingabe- und Ausgabeformatierern. Eingabeformatierer werden von der Modellbindung verwendet. Ausgabeformatierer werden zum Formatieren von Antworten verwendet.

Das Framework stellt integrierte Eingabe- und Ausgabeformatierer für JSON und XML bereit. Es stellt einen integrierten Ausgabeformatierer für Nur-Text bereit, jedoch keinen Eingabeformatierer.

In diesem Artikel wird dargestellt, wie Sie die Unterstützung von zusätzlichen Formaten hinzufügen, indem Sie benutzerdefinierte Formatierer erstellen. Ein Beispiel für einen benutzerdefinierten Formatierer für Nur-Text-Eingaben finden Sie unter TextPlainInputFormatter auf GitHub.

Anzeigen oder Herunterladen von Beispielcode (Vorgehensweise zum Herunterladen)

Wann wird ein benutzerdefinierter Formatierer verwendet?

Verwenden Sie einen benutzerdefinierten Formatierer, um Unterstützung für einen Inhaltstyp hinzuzufügen, der von den integrierten Formatieren nicht verarbeitet wird.

Übersicht über die Erstellung eines benutzerdefinierten Formatierers

So erstellen Sie einen benutzerdefinierten Formatierer:

  • Erstellen Sie zum Serialisieren von Daten, die an den Client gesendet werden, eine Ausgabeformatiererklasse.
  • Erstellen Sie zum Deserialisieren von Daten, die vom Client empfangen werden, eine Eingabeformatiererklasse.
  • Fügen Sie Instanzen der Formatiererklassen zu den Sammlungen InputFormatters und OutputFormatters in MvcOptions hinzu.

Erstellen eines benutzerdefinierten Formatierers

Erstellen eines Formatierers:

Der folgende Code zeigt die VcardOutputFormatter-Klasse aus dem Beispiel:

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

Ableiten von der entsprechenden Basisklasse

Verwenden Sie bei Textmedientypen (z. B. vCard) Ableitungen aus den Basisklassen TextInputFormatter oder TextOutputFormatter:

public class VcardOutputFormatter : TextOutputFormatter

Bei Binärtypen verwenden Sie Ableitungen aus den Basisklassen InputFormatter oder OutputFormatter.

Angeben unterstützter Medientypen und Codierungen

Geben Sie unterstützte Medientypen und Codierungen im Konstruktor an, indem Sie sie den Sammlungen SupportedMediaTypes und SupportedEncodings hinzufügen:

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

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

Eine Formatiererklasse kann keine Konstruktorinjektion für ihre Abhängigkeiten verwenden. Beispielsweise kann ILogger<VcardOutputFormatter> nicht als Parameter zum Konstruktor hinzugefügt werden. Verwenden Sie für den Zugriff auf Dienste das Kontextobjekt, das an die Methoden übergeben wird. Ein Codebeispiel in diesem Artikel sowie dieses Beispiel veranschaulichen die Vorgehensweise.

Überschreiben von CanReadType und CanWriteType

Geben Sie den Typ an, in den deserialisiert bzw. aus dem serialisiert werden soll, indem Sie die Methoden CanReadType oder CanWriteType überschreiben. Um beispielsweise vCard-Text aus einem Contact-Typ und umgekehrt zu erstellen, gehen Sie folgendermaßen vor:

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

Die CanWriteResult-Methode

In einigen Szenarien muss CanWriteResult überschrieben werden, nicht CanWriteType. Verwenden Sie CanWriteResult, wenn alle der folgenden Bedingungen zutreffen:

  • Die Aktionsmethode gibt eine Modellklasse zurück.
  • Es gibt abgeleitete Klassen, die möglicherweise zur Laufzeit zurückgegeben werden.
  • Die von der Aktion zurückgegebene abgeleitete Klasse muss zur Laufzeit bekannt sein.

Angenommen, für die Aktionsmethode gilt Folgendes:

  • Die Signatur gibt einen Person-Typ zurück.
  • Sie kann einen Student- oder einen Instructor-Typ zurückgeben, der aus Person abgeleitet ist.

Damit der Formatierer nur Student-Objekte verarbeitet, überprüfen Sie den Typ von Object in dem Kontextobjekt, das an die CanWriteResult-Methode übergeben wurde. Wenn die Aktionsmethode IActionResult zurückgibt, gilt Folgendes:

  • Es ist nicht notwendig, CanWriteResult zu verwenden.
  • Die CanWriteType-Methode empfängt den Laufzeittyp.

Überschreiben von ReadRequestBodyAsync und WriteResponseBodyAsync

Die Deserialisierung oder Serialisierung wird in ReadRequestBodyAsync oder WriteResponseBodyAsync ausgeführt. Das folgende Beispiel zeigt, wie Sie Dienste aus dem Abhängigkeitsinjektionscontainer abrufen. Dienste können nicht aus Konstruktorparametern abgerufen werden:

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

Konfigurieren von MVC zum Verwenden eines benutzerdefinierten Formatierers

Um einen benutzerdefinierten Formatierer zu verwenden, fügen Sie der Sammlung MvcOptions.InputFormatters oder MvcOptions.OutputFormatters eine Instanz der Formatiererklasse hinzu:

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

Formatierer werden in der Reihenfolge überprüft, in der Sie sie einfügen. Der erste hat Vorrang.

Die vollständige VcardInputFormatter-Klasse

Der folgende Code zeigt die VcardInputFormatter-Klasse aus dem Beispiel:

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

Die App testen

Führen Sie die Beispiel-App für diesen Artikel aus, die grundlegende Ein- und Ausgabeformatierer für vCard hinzufügt. Die App liest und schreibt vCards ähnlich dem folgenden Format:

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

Um die vCard-Ausgabe anzuzeigen, führen Sie die App aus, und senden Sie eine get-Anforderung mit dem Accept-Header text/vcard an https://localhost:5001/api/contacts.

So fügen Sie der In-Memory-Sammlung von Kontakten eine vCard hinzu:

  • Senden Sie eine Post-Anforderung mithilfe eines Tools wie Postman an /api/contacts.
  • Legen Sie den Header Content-Type auf text/vcard fest.
  • Fügen Sie im Haupttext vCard-Text hinzu, der wie im obigen Beispiel formatiert ist.

Zusätzliche Ressourcen