Dela via


Anpassade formatters i ASP.NET Core Web API

ASP.NET Core MVC stöder datautbyte i webb-API:er med hjälp av in- och utdataformaterare. Indataformaterare används av modellbindning. Utdataformaterare används för att formatera svar.

Ramverket tillhandahåller inbyggda in- och utdataformaterare för JSON och XML. Den innehåller en inbyggd utdataformaterare för oformaterad text, men ger ingen indataformaterare för oformaterad text.

Den här artikeln visar hur du lägger till stöd för ytterligare format genom att skapa anpassade formatters. Ett exempel på en anpassad formateringsformaterare för oformaterad text finns i TextPlainInputFormatter på GitHub.

Visa eller ladda ned exempelkod (hur du laddar ned)

När du ska använda en anpassad formatering

Använd en anpassad formatering för att lägga till stöd för en innehållstyp som inte hanteras av de inbyggda formatrarna.

Översikt över hur du skapar en anpassad formatterare

Så här skapar du en anpassad formatering:

  • För serialisering av data som skickas till klienten skapar du en formateringsklass för utdata.
  • För att deserialisera data som tas emot från klienten skapar du en indataformateringsklass.
  • Lägg till instanser av formateringsklasser till samlingarna InputFormatters och OutputFormatters i MvcOptions.

Skapa en anpassad formatering

Så här skapar du en formaterare:

Följande kod visar VcardOutputFormatter klassen från exemplet:

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

Härled från lämplig basklass

För textmedietyper (till exempel vCard) härled från TextInputFormatter eller TextOutputFormatter basklassen:

public class VcardOutputFormatter : TextOutputFormatter

För binära typer härleds från basklassen InputFormatter eller OutputFormatter .

Ange medietyper och kodningar som stöds

I konstruktorn anger du medietyper och kodningar som stöds genom att lägga till i samlingarna SupportedMediaTypes och SupportedEncodings :

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

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

En formateringsklass kan inte använda konstruktorinmatning för sina beroenden. Det går till exempel ILogger<VcardOutputFormatter> inte att lägga till som en parameter i konstruktorn. För att få åtkomst till tjänster använder du kontextobjektet som skickas till metoderna. Ett kodexempel i den här artikeln och exemplet visar hur du gör detta.

Åsidosätt CanReadType och CanWriteType

Ange vilken typ du vill deserialisera till eller serialisera från genom att åsidosätta metoderna CanReadType eller CanWriteType. Om du till exempel vill skapa vCard-text från en Contact typ och vice versa:

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

Metoden CanWriteResult

I vissa scenarier måste CanWriteResult åsidosättas istället för CanWriteType. Använd CanWriteResult om följande villkor är uppfyllda:

  • Åtgärdsmetoden returnerar en modellklass.
  • Det finns härledda klasser som kan returneras under körning.
  • Den härledda klass som returneras av åtgärden måste vara känd vid körning.

Anta till exempel att åtgärdsmetoden:

  • Signaturen returnerar en Person datatyp.
  • Kan returnera en Student eller Instructor typ som härleds från Person.

För att formateraren endast ska hantera Student objekt, kontrollera typen av Object i det kontextobjekt som tillhandahålls till CanWriteResult metoden. När åtgärdsmetoden returnerar IActionResult:

  • Det är inte nödvändigt att använda CanWriteResult.
  • Metoden CanWriteType tar emot körningstypen.

Åsidosätt ReadRequestBodyAsync och WriteResponseBodyAsync

Deserialisering eller serialisering utförs i ReadRequestBodyAsync eller WriteResponseBodyAsync. I följande exempel visas hur du hämtar tjänster från containern för beroendeinjektion. Tjänster kan inte hämtas från konstruktorparametrar:

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

Konfigurera MVC för att använda en anpassad formatering

Om du vill använda en anpassad formaterare lägger du till en instans av formateringsklassen MvcOptions.InputFormatters i eller MvcOptions.OutputFormatters -samlingen:

var builder = WebApplication.CreateBuilder(args);

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

Formaterare utvärderas i den ordning de infogas, och den första ges företräde.

Den fullständiga VcardInputFormatter klassen

Följande kod visar VcardInputFormatter klassen från exemplet:

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

Testa appen

Kör exempelappen för den här artikeln, som implementerar grundläggande in- och utdataformaterare för virtuella kort. Appen läser och skriver visitkort som liknar följande format:

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

Om du vill se utdata från ett visitkort kör du appen och skickar en Get-begäran med accepthuvud text/vcard till https://localhost:<port>/api/contacts.

Så här lägger du till ett virtuellt kort i den minnesinterna samlingen med kontakter:

  • Skicka en Post begäran till /api/contacts med ett verktyg som http-repl.
  • Ställ in Content-Type-rubriken till text/vcard.
  • Ange vCard text i brödtexten, formaterad som föregående exempel.

Ytterligare resurser

ASP.NET Core MVC stöder datautbyte i webb-API:er med hjälp av in- och utdataformaterare. Indataformaterare används av modellbindning. Utdataformaterare används för att formatera svar.

Ramverket tillhandahåller inbyggda in- och utdataformaterare för JSON och XML. Den innehåller en inbyggd utdataformaterare för oformaterad text, men ger ingen indataformaterare för oformaterad text.

Den här artikeln visar hur du lägger till stöd för ytterligare format genom att skapa anpassade formatters. Ett exempel på en anpassad formateringsformaterare för oformaterad text finns i TextPlainInputFormatter på GitHub.

Visa eller ladda ned exempelkod (hur du laddar ned)

När du ska använda en anpassad formatering

Använd en anpassad formatering för att lägga till stöd för en innehållstyp som inte hanteras av de inbyggda formatrarna.

Översikt över hur du skapar en anpassad formatterare

Så här skapar du en anpassad formatering:

  • För serialisering av data som skickas till klienten skapar du en formateringsklass för utdata.
  • För att deserialisera data som tas emot från klienten skapar du en indataformateringsklass.
  • Lägg till instanser av formateringsklasser till samlingarna InputFormatters och OutputFormatters i MvcOptions.

Skapa en anpassad formatering

Så här skapar du en formaterare:

Följande kod visar VcardOutputFormatter klassen från exemplet:

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

Härled från lämplig basklass

För textmedietyper (till exempel vCard) härled från TextInputFormatter eller TextOutputFormatter basklassen:

public class VcardOutputFormatter : TextOutputFormatter

För binära typer härleds från basklassen InputFormatter eller OutputFormatter .

Ange medietyper och kodningar som stöds

I konstruktorn anger du medietyper och kodningar som stöds genom att lägga till i samlingarna SupportedMediaTypes och SupportedEncodings :

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

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

En formateringsklass kan inte använda konstruktorinmatning för sina beroenden. Det går till exempel ILogger<VcardOutputFormatter> inte att lägga till som en parameter i konstruktorn. För att få åtkomst till tjänster använder du kontextobjektet som skickas till metoderna. Ett kodexempel i den här artikeln och exemplet visar hur du gör detta.

Åsidosätt CanReadType och CanWriteType

Ange vilken typ du vill deserialisera till eller serialisera från genom att åsidosätta metoderna CanReadType eller CanWriteType. Om du till exempel vill skapa vCard-text från en Contact typ och vice versa:

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

Metoden CanWriteResult

I vissa scenarier måste CanWriteResult åsidosättas istället för CanWriteType. Använd CanWriteResult om följande villkor är uppfyllda:

  • Åtgärdsmetoden returnerar en modellklass.
  • Det finns härledda klasser som kan returneras under körning.
  • Den härledda klass som returneras av åtgärden måste vara känd vid körning.

Anta till exempel att åtgärdsmetoden:

  • Signaturen returnerar en Person datatyp.
  • Kan returnera en Student eller Instructor typ som härleds från Person.

För att formateraren endast ska hantera Student objekt, kontrollera typen av Object i det kontextobjekt som tillhandahålls till CanWriteResult metoden. När åtgärdsmetoden returnerar IActionResult:

  • Det är inte nödvändigt att använda CanWriteResult.
  • Metoden CanWriteType tar emot körningstypen.

Åsidosätt ReadRequestBodyAsync och WriteResponseBodyAsync

Deserialisering eller serialisering utförs i ReadRequestBodyAsync eller WriteResponseBodyAsync. I följande exempel visas hur du hämtar tjänster från containern för beroendeinjektion. Tjänster kan inte hämtas från konstruktorparametrar:

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

Konfigurera MVC för att använda en anpassad formatering

Om du vill använda en anpassad formaterare lägger du till en instans av formateringsklassen MvcOptions.InputFormatters i eller MvcOptions.OutputFormatters -samlingen:

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

Formaterare utvärderas i den ordning du infogar dem. Den första har företräde.

Den fullständiga VcardInputFormatter klassen

Följande kod visar VcardInputFormatter klassen från exemplet:

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

Testa appen

Kör exempelappen för den här artikeln, som implementerar grundläggande in- och utdataformaterare för virtuella kort. Appen läser och skriver visitkort som liknar följande format:

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

Om du vill se utdata från ett visitkort kör du appen och skickar en Get-begäran med accepthuvud text/vcard till https://localhost:5001/api/contacts.

Så här lägger du till ett virtuellt kort i den minnesinterna samlingen med kontakter:

  • Skicka en Post begäran till /api/contacts med ett verktyg som curl.
  • Ställ in Content-Type-rubriken till text/vcard.
  • Ange vCard text i brödtexten, formaterad som föregående exempel.

Ytterligare resurser