Formateurs personnalisés dans l’API web ASP.NET Core

ASP.NET Core MVC prend en charge l'échange de données dans les API Web à l'aide de formateurs d'entrée et de sortie. Les formateurs d'entrée sont utilisés par Model Binding. Les formateurs de sortie sont utilisés pour formater les réponses.

L’infrastructure fournit des formateurs d’entrée et de sortie intégrés pour JSON et XML. Il fournit un formateur de sortie intégré pour le texte brut, mais ne fournit pas de formateur d'entrée pour le texte brut.

Cet article montre comment ajouter la prise en charge de formats supplémentaires en créant des formateurs personnalisés. Pour un exemple de formateur de saisie de texte brut personnalisé, consultez TextPlainInputFormatter sur GitHub.

Affichez ou téléchargez l’exemple de code (procédure de téléchargement)

Quand utiliser un formateur personnalisé

Utilisez un formateur personnalisé pour ajouter la prise en charge d'un type de contenu qui n'est pas géré par les formateurs intégrés.

Présentation de la création d'un formateur personnalisé

Pour créer un formateur personnalisé :

  • Pour sérialiser les données envoyées au client, créez une classe de formateur de sortie.
  • Pour désérialiser les données reçues du client, créez une classe de formatage d'entrée.
  • Ajoutez des instances de classes de formateur aux collections InputFormatters et OutputFormatters dans MvcOptions.

Créer un formateur personnalisé

Pour créer un formateur :

Le code suivant montre la classe VcardOutputFormatter de l'exemple :

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

Effectuer une dérivation à partir de la classe de base appropriée

Pour les types de média texte (par exemple, vCard), dérivez de la classe De base TextInputFormatter or TextOutputFormatter :

public class VcardOutputFormatter : TextOutputFormatter

Pour les types binaires, dérivez de la classe de base InputFormatter ou OutputFormatter.

Spécifiez les types de médias et les encodages pris en charge

Dans le constructeur, spécifiez les types de médias et les encodages pris en charge en ajoutant aux collections SupportedMediaTypes et SupportedEncodings :

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

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

Une classe de formateur ne peut pas utiliser l'injection de constructeur pour ses dépendances. Par exemple, ILogger<VcardOutputFormatter> ne peut pas être ajouté en tant que paramètre au constructeur. Pour accéder aux services, utilisez l'objet de contexte qui est transmis aux méthodes. Un exemple de code dans cet article et l'exemple montrent comment procéder.

Remplacer CanReadType et CanWriteType

Spécifiez le type dans lequel désérialiser ou à partir duquel sérialiser en remplaçant les méthodes CanReadType ou CanWriteType. Par exemple, pour créer du texte vCard à partir d'un type Contact et vice versa :

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

Méthode CanWriteResult

Dans certains scénarios, CanWriteResult doit être remplacé plutôt que CanWriteType. Utilisez CanWriteResult si les conditions suivantes sont vraies :

  • La méthode d'action renvoie une classe de modèle.
  • Certaines classes dérivées peuvent être renvoyées lors de l'exécution.
  • La classe dérivée renvoyée par l'action doit être connue au moment de l'exécution.

Par exemple, supposons que la méthode d'action :

  • Signature renvoie un type Person.
  • Peut renvoyer un type Student ou Instructor qui dérive de Person.

Pour que le formateur ne gère que les objets Student, vérifiez le type de Object dans l'objet de contexte fourni à la méthode CanWriteResult. Lorsque la méthode d'action revient IActionResult:

  • Il n'est pas nécessaire d'utiliser CanWriteResult.
  • La méthode CanWriteType reçoit le type d'exécution.

Remplacer ReadRequestBodyAsync et WriteResponseBodyAsync

La désérialisation ou la sérialisation est effectuée dans ReadRequestBodyAsync ou WriteResponseBodyAsync. L'exemple suivant montre comment obtenir des services à partir du conteneur d'injection de dépendances. Les services ne peuvent pas être obtenus à partir des paramètres du constructeur :

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

Configurer MVC pour utiliser un formateur personnalisé

Pour utiliser un formateur personnalisé, ajoutez une instance de la classe de formateur à la collection MvcOptions.InputFormatters ou MvcOptions.OutputFormatters :

var builder = WebApplication.CreateBuilder(args);

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

Les formateurs sont évalués dans l'ordre dans lequel ils sont insérés, le premier ayant priorité.

La classe VcardInputFormatter complète

Le code suivant montre la classe VcardInputFormatter de l'exemple :

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

Test de l'application

Exécutez l'exemple d'application pour cet article, qui implémente les formateurs d'entrée et de sortie vCard de base. L'application lit et écrit des vCards similaires au format suivant :

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

Pour voir la sortie vCard, exécutez l'application et envoyez une requête Get avec l'en-tête Accept text/vcard à https://localhost:<port>/api/contacts.

Pour ajouter une vCard à la collection de contacts en mémoire :

  • Envoyez une requête Post à /api/contacts avec un outil comme http-repl.
  • Attribuez à l’en-tête Content-Type la valeur text/vcard.
  • Définissez le texte vCard dans le corps, formaté comme dans l'exemple précédent.

Ressources supplémentaires

ASP.NET Core MVC prend en charge l'échange de données dans les API Web à l'aide de formateurs d'entrée et de sortie. Les formateurs d'entrée sont utilisés par Model Binding. Les formateurs de sortie sont utilisés pour formater les réponses.

L’infrastructure fournit des formateurs d’entrée et de sortie intégrés pour JSON et XML. Il fournit un formateur de sortie intégré pour le texte brut, mais ne fournit pas de formateur d'entrée pour le texte brut.

Cet article montre comment ajouter la prise en charge de formats supplémentaires en créant des formateurs personnalisés. Pour un exemple de formateur de saisie de texte brut personnalisé, consultez TextPlainInputFormatter sur GitHub.

Affichez ou téléchargez l’exemple de code (procédure de téléchargement)

Quand utiliser un formateur personnalisé

Utilisez un formateur personnalisé pour ajouter la prise en charge d'un type de contenu qui n'est pas géré par les formateurs intégrés.

Présentation de la création d'un formateur personnalisé

Pour créer un formateur personnalisé :

  • Pour sérialiser les données envoyées au client, créez une classe de formateur de sortie.
  • Pour désérialiser les données reçues du client, créez une classe de formatage d'entrée.
  • Ajoutez des instances de classes de formateur aux collections InputFormatters et OutputFormatters dans MvcOptions.

Créer un formateur personnalisé

Pour créer un formateur :

Le code suivant montre la classe VcardOutputFormatter de l'exemple :

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

Effectuer une dérivation à partir de la classe de base appropriée

Pour les types de média texte (par exemple, vCard), dérivez de la classe De base TextInputFormatter or TextOutputFormatter :

public class VcardOutputFormatter : TextOutputFormatter

Pour les types binaires, dérivez de la classe de base InputFormatter ou OutputFormatter.

Spécifiez les types de médias et les encodages pris en charge

Dans le constructeur, spécifiez les types de médias et les encodages pris en charge en ajoutant aux collections SupportedMediaTypes et SupportedEncodings :

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

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

Une classe de formateur ne peut pas utiliser l'injection de constructeur pour ses dépendances. Par exemple, ILogger<VcardOutputFormatter> ne peut pas être ajouté en tant que paramètre au constructeur. Pour accéder aux services, utilisez l'objet de contexte qui est transmis aux méthodes. Un exemple de code dans cet article et l'exemple montrent comment procéder.

Remplacer CanReadType et CanWriteType

Spécifiez le type dans lequel désérialiser ou à partir duquel sérialiser en remplaçant les méthodes CanReadType ou CanWriteType. Par exemple, pour créer du texte vCard à partir d'un type Contact et vice versa :

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

Méthode CanWriteResult

Dans certains scénarios, CanWriteResult doit être remplacé plutôt que CanWriteType. Utilisez CanWriteResult si les conditions suivantes sont vraies :

  • La méthode d'action renvoie une classe de modèle.
  • Certaines classes dérivées peuvent être renvoyées lors de l'exécution.
  • La classe dérivée renvoyée par l'action doit être connue au moment de l'exécution.

Par exemple, supposons que la méthode d'action :

  • Signature renvoie un type Person.
  • Peut renvoyer un type Student ou Instructor qui dérive de Person.

Pour que le formateur ne gère que les objets Student, vérifiez le type de Object dans l'objet de contexte fourni à la méthode CanWriteResult. Lorsque la méthode d'action revient IActionResult:

  • Il n'est pas nécessaire d'utiliser CanWriteResult.
  • La méthode CanWriteType reçoit le type d'exécution.

Remplacer ReadRequestBodyAsync et WriteResponseBodyAsync

La désérialisation ou la sérialisation est effectuée dans ReadRequestBodyAsync ou WriteResponseBodyAsync. L'exemple suivant montre comment obtenir des services à partir du conteneur d'injection de dépendances. Les services ne peuvent pas être obtenus à partir des paramètres du constructeur :

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

Configurer MVC pour utiliser un formateur personnalisé

Pour utiliser un formateur personnalisé, ajoutez une instance de la classe de formateur à la collection MvcOptions.InputFormatters ou MvcOptions.OutputFormatters :

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

Les formateurs sont évalués dans l’ordre dans lequel vous les insérez. Le premier est prioritaire.

La classe VcardInputFormatter complète

Le code suivant montre la classe VcardInputFormatter de l'exemple :

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

Test de l'application

Exécutez l'exemple d'application pour cet article, qui implémente les formateurs d'entrée et de sortie vCard de base. L'application lit et écrit des vCards similaires au format suivant :

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

Pour voir la sortie vCard, exécutez l'application et envoyez une requête Get avec l'en-tête Accept text/vcard à https://localhost:5001/api/contacts.

Pour ajouter une vCard à la collection de contacts en mémoire :

  • Envoyer une requête Post à /api/contacts avec un outil tel que Curl.
  • Attribuez à l’en-tête Content-Type la valeur text/vcard.
  • Définissez le texte vCard dans le corps, formaté comme dans l'exemple précédent.

Ressources supplémentaires