Udostępnij za pośrednictwem


Niestandardowe elementy formatujące w internetowym interfejsie API platformy ASP.NET Core

ASP.NET Core MVC obsługuje wymianę danych w internetowych interfejsach API przy użyciu formatów wejściowych i wyjściowych. Formatery danych wejściowych są używane przez powiązanie modelu. Formatery danych wyjściowych służą do formatowania odpowiedzi.

Platforma udostępnia wbudowane formatery wejściowe i wyjściowe dla formatu JSON i XML. Udostępnia wbudowany formater danych wyjściowych dla zwykłego tekstu, ale nie udostępnia formatera wejściowego zwykłego tekstu.

W tym artykule pokazano, jak dodać obsługę dodatkowych formatów, tworząc niestandardowe formatery. Aby zapoznać się z przykładem niestandardowego formatowania danych wejściowych w postaci zwykłego tekstu, zobacz TextPlainInputFormatter w witrynie GitHub.

Wyświetl lub pobierz przykładowy kod (jak pobrać)

Kiedy używać niestandardowego programu formatującego

Użyj niestandardowego formatera, aby dodać obsługę typu zawartości, który nie jest obsługiwany przez wbudowane formatery.

Omówienie tworzenia niestandardowego modułu formatującego

Aby utworzyć niestandardowy formater:

  • W przypadku serializacji danych wysyłanych do klienta utwórz klasę formatera danych wyjściowych.
  • W przypadku deserializacji danych odebranych od klienta utwórz klasę formatatora danych wejściowych.
  • Dodaj wystąpienia klas formaterów do InputFormatters kolekcji i OutputFormatters w pliku MvcOptions.

Tworzenie niestandardowego programu formatującego

Aby utworzyć program formatujący:

Poniższy kod przedstawia klasę VcardOutputFormatter z przykładu:

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

Pochodne od odpowiedniej klasy bazowej

W przypadku typów multimediów tekstowych (na przykład vCard) pochodzą z TextInputFormatter klasy podstawowej lub TextOutputFormatter :

public class VcardOutputFormatter : TextOutputFormatter

W przypadku typów binarnych pochodzą z klasy bazowej InputFormatter lub OutputFormatter .

Określanie obsługiwanych typów i kodowań multimediów

W konstruktorze określ obsługiwane typy i kodowania multimediów, dodając do SupportedMediaTypes kolekcji i SupportedEncodings :

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

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

Klasa formatatora nie może używać iniekcji konstruktora dla jego zależności. Na przykład ILogger<VcardOutputFormatter> nie można dodać go jako parametru do konstruktora. Aby uzyskać dostęp do usług, użyj obiektu kontekstu, który jest przekazywany do metod. Przykładowy kod w tym artykule i przykład pokazujący, jak to zrobić.

Zastąp wartości CanReadType i CanWriteType

Określ typ do deserializacji do lub serializacji z, przesłaniając CanReadType metody lub CanWriteType . Na przykład aby utworzyć tekst vCard na podstawie Contact typu i na odwrót:

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

Metoda CanWriteResult

W niektórych scenariuszach CanWriteResult należy zastąpić zamiast CanWriteType. Użyj CanWriteResult , jeśli spełnione są następujące warunki:

  • Metoda akcji zwraca klasę modelu.
  • Istnieją klasy pochodne, które mogą być zwracane w czasie wykonywania.
  • Klasa pochodna zwracana przez akcję musi być znana w czasie wykonywania.

Załóżmy na przykład, że metoda akcji:

  • Podpis zwraca Person typ.
  • Może zwrócić typ Student lub Instructor pochodzący z klasy Person.

Aby program formatujący obsługiwał tylko Student obiekty, sprawdź typ Object obiektu kontekstu dostarczonego do CanWriteResult metody . Gdy metoda akcji zwraca wartość IActionResult:

  • Nie jest konieczne użycie polecenia CanWriteResult.
  • Metoda CanWriteType odbiera typ środowiska uruchomieniowego.

Zastąpij metodę ReadRequestBodyAsync i WriteResponseBodyAsync

Deserializacja lub serializacja jest wykonywana w programie ReadRequestBodyAsync lub WriteResponseBodyAsync. W poniższym przykładzie pokazano, jak pobrać usługi z kontenera wstrzykiwania zależności. Nie można uzyskać usług z parametrów konstruktora:

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

Konfigurowanie wzorca MVC do używania niestandardowego formatowania

Aby użyć niestandardowego formatatora, dodaj wystąpienie klasy formatatora MvcOptions.InputFormatters do kolekcji lub MvcOptions.OutputFormatters :

var builder = WebApplication.CreateBuilder(args);

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

Formatery są oceniane w kolejności, w której pierwszy ma pierwszeństwo.

Kompletna VcardInputFormatter klasa

Poniższy kod przedstawia klasę VcardInputFormatter z przykładu:

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

Testowanie aplikacji

Uruchom przykładową aplikację dla tego artykułu, która implementuje podstawowe formatery danych wejściowych i wyjściowych karty wirtualnej. Aplikacja odczytuje i zapisuje karty wirtualne podobne do następującego formatu:

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

Aby wyświetlić dane wyjściowe karty vCard, uruchom aplikację i wyślij żądanie Get z nagłówkiem text/vcard Accept do .https://localhost:<port>/api/contacts

Aby dodać kartę vCard do kolekcji kontaktów w pamięci:

  • Post Wyślij żądanie do /api/contacts przy użyciu narzędzia, takiego jak http-repl.
  • Content-Type Ustaw nagłówek na text/vcard.
  • Ustaw vCard tekst w treści, sformatowany tak jak w poprzednim przykładzie.

Dodatkowe zasoby

ASP.NET Core MVC obsługuje wymianę danych w internetowych interfejsach API przy użyciu formatów wejściowych i wyjściowych. Formatery danych wejściowych są używane przez powiązanie modelu. Formatery danych wyjściowych służą do formatowania odpowiedzi.

Platforma udostępnia wbudowane formatery wejściowe i wyjściowe dla formatu JSON i XML. Udostępnia wbudowany formater danych wyjściowych dla zwykłego tekstu, ale nie udostępnia formatera wejściowego zwykłego tekstu.

W tym artykule pokazano, jak dodać obsługę dodatkowych formatów, tworząc niestandardowe formatery. Aby zapoznać się z przykładem niestandardowego formatowania danych wejściowych w postaci zwykłego tekstu, zobacz TextPlainInputFormatter w witrynie GitHub.

Wyświetl lub pobierz przykładowy kod (jak pobrać)

Kiedy używać niestandardowego programu formatującego

Użyj niestandardowego formatera, aby dodać obsługę typu zawartości, który nie jest obsługiwany przez wbudowane formatery.

Omówienie tworzenia niestandardowego modułu formatującego

Aby utworzyć niestandardowy formater:

  • W przypadku serializacji danych wysyłanych do klienta utwórz klasę formatera danych wyjściowych.
  • W przypadku deserializacji danych odebranych od klienta utwórz klasę formatatora danych wejściowych.
  • Dodaj wystąpienia klas formaterów do InputFormatters kolekcji i OutputFormatters w pliku MvcOptions.

Tworzenie niestandardowego programu formatującego

Aby utworzyć program formatujący:

Poniższy kod przedstawia klasę VcardOutputFormatter z przykładu:

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

Pochodne od odpowiedniej klasy bazowej

W przypadku typów multimediów tekstowych (na przykład vCard) pochodzą z TextInputFormatter klasy podstawowej lub TextOutputFormatter :

public class VcardOutputFormatter : TextOutputFormatter

W przypadku typów binarnych pochodzą z klasy bazowej InputFormatter lub OutputFormatter .

Określanie obsługiwanych typów i kodowań multimediów

W konstruktorze określ obsługiwane typy i kodowania multimediów, dodając do SupportedMediaTypes kolekcji i SupportedEncodings :

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

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

Klasa formatatora nie może używać iniekcji konstruktora dla jego zależności. Na przykład ILogger<VcardOutputFormatter> nie można dodać go jako parametru do konstruktora. Aby uzyskać dostęp do usług, użyj obiektu kontekstu, który jest przekazywany do metod. Przykładowy kod w tym artykule i przykład pokazujący, jak to zrobić.

Zastąp wartości CanReadType i CanWriteType

Określ typ do deserializacji do lub serializacji z, przesłaniając CanReadType metody lub CanWriteType . Na przykład aby utworzyć tekst vCard na podstawie Contact typu i na odwrót:

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

Metoda CanWriteResult

W niektórych scenariuszach CanWriteResult należy zastąpić zamiast CanWriteType. Użyj CanWriteResult , jeśli spełnione są następujące warunki:

  • Metoda akcji zwraca klasę modelu.
  • Istnieją klasy pochodne, które mogą być zwracane w czasie wykonywania.
  • Klasa pochodna zwracana przez akcję musi być znana w czasie wykonywania.

Załóżmy na przykład, że metoda akcji:

  • Podpis zwraca Person typ.
  • Może zwrócić typ Student lub Instructor pochodzący z klasy Person.

Aby program formatujący obsługiwał tylko Student obiekty, sprawdź typ Object obiektu kontekstu dostarczonego do CanWriteResult metody . Gdy metoda akcji zwraca wartość IActionResult:

  • Nie jest konieczne użycie polecenia CanWriteResult.
  • Metoda CanWriteType odbiera typ środowiska uruchomieniowego.

Zastąpij metodę ReadRequestBodyAsync i WriteResponseBodyAsync

Deserializacja lub serializacja jest wykonywana w programie ReadRequestBodyAsync lub WriteResponseBodyAsync. W poniższym przykładzie pokazano, jak pobrać usługi z kontenera wstrzykiwania zależności. Nie można uzyskać usług z parametrów konstruktora:

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

Konfigurowanie wzorca MVC do używania niestandardowego formatowania

Aby użyć niestandardowego formatatora, dodaj wystąpienie klasy formatatora MvcOptions.InputFormatters do kolekcji lub MvcOptions.OutputFormatters :

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

Formatery są oceniane w kolejności ich wstawiania. Pierwszy z nich ma pierwszeństwo.

Kompletna VcardInputFormatter klasa

Poniższy kod przedstawia klasę VcardInputFormatter z przykładu:

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

Testowanie aplikacji

Uruchom przykładową aplikację dla tego artykułu, która implementuje podstawowe formatery danych wejściowych i wyjściowych karty wirtualnej. Aplikacja odczytuje i zapisuje karty wirtualne podobne do następującego formatu:

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

Aby wyświetlić dane wyjściowe karty vCard, uruchom aplikację i wyślij żądanie Get z nagłówkiem text/vcard Accept do .https://localhost:5001/api/contacts

Aby dodać kartę vCard do kolekcji kontaktów w pamięci:

  • Post Wyślij żądanie do /api/contacts za pomocą narzędzia, takiego jak curl.
  • Content-Type Ustaw nagłówek na text/vcard.
  • Ustaw vCard tekst w treści, sformatowany tak jak w poprzednim przykładzie.

Dodatkowe zasoby