Formattatori personalizzati nell'API Web ASP.NET Core

ASP.NET Core MVC supporta lo scambio di dati nelle API Web usando formattatori di input e output. I formattatori di input vengono usati dall'associazione di modelli. I formattatori di output vengono usati per formattare le risposte.

Il framework fornisce formattatori di input e output predefiniti per JSON e XML. Fornisce un formattatore di output predefinito per il testo normale, ma non fornisce un formattatore di input per il testo normale.

In questo articolo viene illustrato come aggiungere supporto per altri formati creando formattatori personalizzati. Per un esempio di formattatore di input di testo normale personalizzato, vedere TextPlainInputFormatter su GitHub.

Visualizzare o scaricare il codice di esempio (procedura per il download)

Quando usare un formattatore personalizzato

Usare un formattatore personalizzato per aggiungere il supporto per un tipo di contenuto non gestito dai formattatori predefiniti.

Panoramica di come creare un formattatore personalizzato

Per creare un formattatore personalizzato:

  • Per serializzare i dati inviati al client, creare una classe formattatore di output.
  • Per deserializzare i dati ricevuti dal client, creare una classe formattatore di input.
  • Aggiungere istanze di classi formattatore alle InputFormatters raccolte e OutputFormatters in MvcOptions.

Creare un formattatore personalizzato

Per creare un formattatore:

Il codice seguente illustra la VcardOutputFormatter classe dell'esempio:

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

Derivare dalla classe di base appropriata

Per i tipi di supporti di testo (ad esempio, vCard), derivano dalla TextInputFormatter classe base o TextOutputFormatter :

public class VcardOutputFormatter : TextOutputFormatter

Per i tipi binari, derivare dalla InputFormatter classe o OutputFormatter di base.

Specificare tipi di supporti e codifiche supportate

Nel costruttore specificare i tipi di supporti e le codifiche supportati aggiungendo alle SupportedMediaTypes raccolte e SupportedEncodings :

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

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

Una classe formattatore non può usare l'inserimento del costruttore per le relative dipendenze. Ad esempio, ILogger<VcardOutputFormatter> non può essere aggiunto come parametro al costruttore. Per accedere ai servizi, utilizzare l'oggetto contesto passato ai metodi . Un esempio di codice in questo articolo e l'esempio illustrano come eseguire questa operazione.

Eseguire l'override di CanReadType e CanWriteType

Specificare il tipo in cui deserializzare o serializzare da eseguendo l'override dei CanReadType metodi o CanWriteType . Ad esempio, per creare testo vCard da un Contact tipo e viceversa:

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

Metodo CanWriteResult

In alcuni scenari, CanWriteResult è necessario eseguire l'override anziché CanWriteType. Usare CanWriteResult se vengono soddisfatte le condizioni seguenti:

  • Il metodo di azione restituisce una classe del modello.
  • Esistono classi derivate che potrebbero essere restituite in fase di esecuzione.
  • La classe derivata restituita dall'azione deve essere nota in fase di esecuzione.

Si supponga, ad esempio, che il metodo di azione:

  • La firma restituisce un Person tipo.
  • Può restituire un Student tipo o Instructor che deriva da Person.

Affinché il formattatore gestisca solo Student gli oggetti, controllare il tipo di Object nell'oggetto di contesto fornito al CanWriteResult metodo . Quando il metodo di azione restituisce IActionResult:

  • Non è necessario usare CanWriteResult.
  • Il CanWriteType metodo riceve il tipo di runtime.

Eseguire l'override di ReadRequestBodyAsync e WriteResponseBodyAsync

La deserializzazione o la serializzazione viene eseguita in ReadRequestBodyAsync o WriteResponseBodyAsync. Nell'esempio seguente viene illustrato come ottenere servizi dal contenitore di inserimento delle dipendenze. I servizi non possono essere ottenuti dai parametri del costruttore:

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

Configurare MVC per l'uso di un formattatore personalizzato

Per usare un formattatore personalizzato, aggiungere un'istanza della classe formattatore alla MvcOptions.InputFormatters raccolta o MvcOptions.OutputFormatters :

var builder = WebApplication.CreateBuilder(args);

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

I formattatori vengono valutati nell'ordine in cui vengono inseriti, dove il primo ha la precedenza.

Classe completa VcardInputFormatter

Il codice seguente illustra la VcardInputFormatter classe dell'esempio:

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

Testare l'app

Eseguire l'app di esempio per questo articolo, che implementa formattatori di input e output vCard di base. L'app legge e scrive vCard in modo simile al formato seguente:

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

Per visualizzare l'output vCard, eseguire l'app e inviare una richiesta Get con l'intestazione text/vcard Accept a https://localhost:<port>/api/contacts.

Per aggiungere una vCard alla raccolta in memoria dei contatti:

  • Inviare una Post richiesta a /api/contacts con uno strumento come http-repl.
  • Impostare l'intestazione Content-Type su text/vcard.
  • Impostare vCard il testo nel corpo, formattato come nell'esempio precedente.

Risorse aggiuntive

ASP.NET Core MVC supporta lo scambio di dati nelle API Web usando formattatori di input e output. I formattatori di input vengono usati dall'associazione di modelli. I formattatori di output vengono usati per formattare le risposte.

Il framework fornisce formattatori di input e output predefiniti per JSON e XML. Fornisce un formattatore di output predefinito per il testo normale, ma non fornisce un formattatore di input per il testo normale.

In questo articolo viene illustrato come aggiungere supporto per altri formati creando formattatori personalizzati. Per un esempio di formattatore di input di testo normale personalizzato, vedere TextPlainInputFormatter su GitHub.

Visualizzare o scaricare il codice di esempio (procedura per il download)

Quando usare un formattatore personalizzato

Usare un formattatore personalizzato per aggiungere il supporto per un tipo di contenuto non gestito dai formattatori predefiniti.

Panoramica di come creare un formattatore personalizzato

Per creare un formattatore personalizzato:

  • Per serializzare i dati inviati al client, creare una classe formattatore di output.
  • Per deserializzare i dati ricevuti dal client, creare una classe formattatore di input.
  • Aggiungere istanze di classi formattatore alle InputFormatters raccolte e OutputFormatters in MvcOptions.

Creare un formattatore personalizzato

Per creare un formattatore:

Il codice seguente illustra la VcardOutputFormatter classe dell'esempio:

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

Derivare dalla classe di base appropriata

Per i tipi di supporti di testo (ad esempio, vCard), derivano dalla TextInputFormatter classe base o TextOutputFormatter :

public class VcardOutputFormatter : TextOutputFormatter

Per i tipi binari, derivare dalla InputFormatter classe o OutputFormatter di base.

Specificare tipi di supporti e codifiche supportate

Nel costruttore specificare i tipi di supporti e le codifiche supportati aggiungendo alle SupportedMediaTypes raccolte e SupportedEncodings :

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

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

Una classe formattatore non può usare l'inserimento del costruttore per le relative dipendenze. Ad esempio, ILogger<VcardOutputFormatter> non può essere aggiunto come parametro al costruttore. Per accedere ai servizi, utilizzare l'oggetto contesto passato ai metodi . Un esempio di codice in questo articolo e l'esempio illustrano come eseguire questa operazione.

Eseguire l'override di CanReadType e CanWriteType

Specificare il tipo in cui deserializzare o serializzare da eseguendo l'override dei CanReadType metodi o CanWriteType . Ad esempio, per creare testo vCard da un Contact tipo e viceversa:

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

Metodo CanWriteResult

In alcuni scenari, CanWriteResult è necessario eseguire l'override anziché CanWriteType. Usare CanWriteResult se vengono soddisfatte le condizioni seguenti:

  • Il metodo di azione restituisce una classe del modello.
  • Esistono classi derivate che potrebbero essere restituite in fase di esecuzione.
  • La classe derivata restituita dall'azione deve essere nota in fase di esecuzione.

Si supponga, ad esempio, che il metodo di azione:

  • La firma restituisce un Person tipo.
  • Può restituire un Student tipo o Instructor che deriva da Person.

Affinché il formattatore gestisca solo Student gli oggetti, controllare il tipo di Object nell'oggetto di contesto fornito al CanWriteResult metodo . Quando il metodo di azione restituisce IActionResult:

  • Non è necessario usare CanWriteResult.
  • Il CanWriteType metodo riceve il tipo di runtime.

Eseguire l'override di ReadRequestBodyAsync e WriteResponseBodyAsync

La deserializzazione o la serializzazione viene eseguita in ReadRequestBodyAsync o WriteResponseBodyAsync. Nell'esempio seguente viene illustrato come ottenere servizi dal contenitore di inserimento delle dipendenze. I servizi non possono essere ottenuti dai parametri del costruttore:

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

Configurare MVC per l'uso di un formattatore personalizzato

Per usare un formattatore personalizzato, aggiungere un'istanza della classe formattatore alla MvcOptions.InputFormatters raccolta o MvcOptions.OutputFormatters :

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

I formattatori vengono valutati nell'ordine d'inserimento. Il primo ha la precedenza.

Classe completa VcardInputFormatter

Il codice seguente illustra la VcardInputFormatter classe dell'esempio:

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

Testare l'app

Eseguire l'app di esempio per questo articolo, che implementa formattatori di input e output vCard di base. L'app legge e scrive vCard in modo simile al formato seguente:

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

Per visualizzare l'output vCard, eseguire l'app e inviare una richiesta Get con l'intestazione text/vcard Accept a https://localhost:5001/api/contacts.

Per aggiungere una vCard alla raccolta in memoria dei contatti:

  • Inviare una Post richiesta a /api/contacts con uno strumento come Postman.
  • Impostare l'intestazione Content-Type su text/vcard.
  • Impostare vCard il testo nel corpo, formattato come nell'esempio precedente.

Risorse aggiuntive