Partilhar via


Vinculação de parâmetros em aplicativos de API mínima

Observação

Esta não é a versão mais recente deste artigo. Para a versão atual, consulte a versão .NET 10 deste artigo.

Advertência

Esta versão do ASP.NET Core não é mais suportada. Para obter mais informações, consulte a Política de suporte do .NET e do .NET Core. Para a versão atual, consulte a versão .NET 9 deste artigo.

A vinculação de parâmetros é o processo de conversão de dados de solicitação em parâmetros fortemente tipados que são expressos por manipuladores de rota. Uma fonte de vinculação determina de onde os parâmetros são vinculados. As fontes de vinculação podem ser explícitas ou inferidas com base no método HTTP e no tipo de parâmetro.

Fontes de vinculação suportadas:

  • Valores de itinerário
  • string de consulta
  • Cabeçalho
  • Corpo (em formato JSON)
  • Valores do formulário
  • Serviços prestados por injeção de dependência
  • Personalizado

O manipulador de rotas GET a seguir usa algumas dessas fontes de vinculação de parâmetros:

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();

app.MapGet("/{id}", (int id,
                     int page,
                     [FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
                     Service service) => { });

class Service { }

A tabela a seguir mostra a relação entre os parâmetros usados no exemplo anterior e as fontes de ligação associadas.

Parâmetro Origem da vinculação
id valor da rota
page seqüência de caracteres de consulta
customHeader cabeçalho
service Fornecido por injeção de dependência

Os métodos HTTP GET, HEAD, OPTIONSe DELETE não se vinculam implicitamente ao corpo. Para associar a partir do corpo (como JSON) para esses métodos HTTP, associe explicitamente com [FromBody] ou leia a partir do HttpRequest.

O seguinte exemplo de manipulador de rota POST usa uma fonte de ligação de corpo (como JSON) para o parâmetro person:

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapPost("/", (Person person) => { });

record Person(string Name, int Age);

Os parâmetros nos exemplos anteriores são todos vinculados a partir de dados de solicitação automaticamente. Para demonstrar a conveniência que a vinculação de parâmetros oferece, os manipuladores de rota a seguir mostram como ler os dados da solicitação diretamente da solicitação:

app.MapGet("/{id}", (HttpRequest request) =>
{
    var id = request.RouteValues["id"];
    var page = request.Query["page"];
    var customHeader = request.Headers["X-CUSTOM-HEADER"];

    // ...
});

app.MapPost("/", async (HttpRequest request) =>
{
    var person = await request.ReadFromJsonAsync<Person>();

    // ...
});

Vinculação explícita de parâmetros

Os atributos podem ser usados para declarar explicitamente de onde os parâmetros são vinculados.

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();


app.MapGet("/{id}", ([FromRoute] int id,
                     [FromQuery(Name = "p")] int page,
                     [FromServices] Service service,
                     [FromHeader(Name = "Content-Type")] string contentType) 
                     => {});

class Service { }

record Person(string Name, int Age);
Parâmetro Origem da vinculação
id valor da rota com o nome id
page seqüência de caracteres de consulta com o nome "p"
service Fornecido por injeção de dependência
contentType cabeçalho com o nome "Content-Type"

Vinculação explícita a partir de valores de formulário

O atributo [FromForm] vincula valores de formulário:

app.MapPost("/todos", async ([FromForm] string name,
    [FromForm] Visibility visibility, IFormFile? attachment, TodoDb db) =>
{
    var todo = new Todo
    {
        Name = name,
        Visibility = visibility
    };

    if (attachment is not null)
    {
        var attachmentName = Path.GetRandomFileName();

        using var stream = File.Create(Path.Combine("wwwroot", attachmentName));
        await attachment.CopyToAsync(stream);
    }

    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Ok();
});

// Remaining code removed for brevity.

Uma alternativa é usar o atributo [AsParameters] com um tipo personalizado que tenha propriedades anotadas com [FromForm]. Por exemplo, o código a seguir faz a ligação dos valores do formulário às propriedades da estrutura de registo NewTodoRequest.

app.MapPost("/ap/todos", async ([AsParameters] NewTodoRequest request, TodoDb db) =>
{
    var todo = new Todo
    {
        Name = request.Name,
        Visibility = request.Visibility
    };

    if (request.Attachment is not null)
    {
        var attachmentName = Path.GetRandomFileName();

        using var stream = File.Create(Path.Combine("wwwroot", attachmentName));
        await request.Attachment.CopyToAsync(stream);

        todo.Attachment = attachmentName;
    }

    db.Todos.Add(todo);
    await db.SaveChangesAsync();

    return Results.Ok();
});

// Remaining code removed for brevity.
public record struct NewTodoRequest([FromForm] string Name,
    [FromForm] Visibility Visibility, IFormFile? Attachment);

Para obter mais informações, consulte a seção sobre AsParameters mais adiante neste artigo.

O código de exemplo completo está no repositório AspNetCore.Docs.Samples.

Vinculação segura de IFormFile e IFormFileCollection

A vinculação de formulário complexo é suportada usando IFormFile e IFormFileCollection usando o [FromForm]:

using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

var app = builder.Build();
app.UseAntiforgery();

// Generate a form with an anti-forgery token and an /upload endpoint.
app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    var html = MyUtils.GenerateHtmlForm(token.FormFieldName, token.RequestToken!);
    return Results.Content(html, "text/html");
});

app.MapPost("/upload", async Task<Results<Ok<string>, BadRequest<string>>>
    ([FromForm] FileUploadForm fileUploadForm, HttpContext context,
                                                IAntiforgery antiforgery) =>
{
    await MyUtils.SaveFileWithName(fileUploadForm.FileDocument!,
              fileUploadForm.Name!, app.Environment.ContentRootPath);
    return TypedResults.Ok($"Your file with the description:" +
        $" {fileUploadForm.Description} has been uploaded successfully");
});

app.Run();

Os parâmetros vinculados à solicitação com [FromForm] incluem um token antifalsificação . O token antifalsificação é validado quando a solicitação é processada. Para obter mais informações, consulte Antifalsificação com APIs Mínimas.

Para obter mais informações, consulte vinculação de formulário em APIs mínimas.

O código de exemplo completo está no repositório AspNetCore.Docs.Samples.

Ligação de parâmetros com injeção de dependência

A vinculação de parâmetros para APIs mínimas liga parâmetros por meio de injeção de dependência quando o tipo está configurado como um serviço. Não é necessário aplicar explicitamente o atributo [FromServices] a um parâmetro. No código a seguir, ambas as ações retornam o tempo:

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IDateTime, SystemDateTime>();

var app = builder.Build();

app.MapGet("/",   (               IDateTime dateTime) => dateTime.Now);
app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();

Parâmetros opcionais

Os parâmetros declarados nos manipuladores de rota são tratados conforme necessário:

  • Se uma solicitação corresponder à rota, o manipulador de rota só será executado se todos os parâmetros necessários forem fornecidos na solicitação.
  • A falha em fornecer todos os parâmetros necessários resulta em um erro.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");

app.Run();
URI Resultado
/products?pageNumber=3 3 devolvidos
/products BadHttpRequestException: O parâmetro necessário "int pageNumber" não foi fornecido a partir da cadeia de caracteres de consulta.
/products/1 Erro HTTP 404, nenhuma rota correspondente

Para tornar pageNumber opcional, defina o tipo como opcional ou forneça um valor padrão:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";

app.MapGet("/products2", ListProducts);

app.Run();
URI Resultado
/products?pageNumber=3 3 devolvidos
/products 1 devolvido
/products2 1 devolvido

O valor nulo e o valor padrão precedentes aplicam-se a todas as fontes.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/products", (Product? product) => { });

app.Run();

O código anterior chama o método com um produto nulo se nenhum corpo de solicitação for enviado.

NOTA: Se dados inválidos forem fornecidos e o parâmetro for anulável, o manipulador de rotas não será executado.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

app.Run();
URI Resultado
/products?pageNumber=3 3 retornou
/products 1 retornou
/products?pageNumber=two BadHttpRequestException: Falha ao associar o parâmetro "Nullable<int> pageNumber" a partir de "two".
/products/two Erro HTTP 404, nenhuma rota correspondente

Consulte a seção Falhas de vinculação para obter mais informações.

Tipos especiais

Os seguintes tipos são vinculados sem atributos explícitos:

  • HttpContext: O contexto que contém todas as informações sobre a solicitação ou resposta HTTP atual:

    app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
    
  • HttpRequest e HttpResponse: A solicitação HTTP e a resposta HTTP:

    app.MapGet("/", (HttpRequest request, HttpResponse response) =>
        response.WriteAsync($"Hello World {request.Query["name"]}"));
    
  • CancellationToken: O token de cancelamento associado à solicitação HTTP atual:

    app.MapGet("/", async (CancellationToken cancellationToken) => 
        await MakeLongRunningRequestAsync(cancellationToken));
    
  • ClaimsPrincipal: O usuário associado à solicitação, vinculado a partir de HttpContext.User:

    app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
    

Vincular o corpo da solicitação como um Stream ou PipeReader

O corpo da solicitação pode ser vinculado como um Stream ou PipeReader para suportar eficientemente cenários em que o usuário precisa processar dados e:

  • Armazene os dados no armazenamento em blob ou adicione os dados a um provedor de fila.
  • Processe os dados armazenados com um processo de trabalho ou função de nuvem.

Por exemplo, os dados podem ser enfileirados no armazenamento de filas do Azure ou armazenados no armazenamento de blobs do Azure .

O código a seguir implementa uma fila em segundo plano:

using System.Text.Json;
using System.Threading.Channels;

namespace BackgroundQueueService;

class BackgroundQueue : BackgroundService
{
    private readonly Channel<ReadOnlyMemory<byte>> _queue;
    private readonly ILogger<BackgroundQueue> _logger;

    public BackgroundQueue(Channel<ReadOnlyMemory<byte>> queue,
                               ILogger<BackgroundQueue> logger)
    {
        _queue = queue;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var dataStream in _queue.Reader.ReadAllAsync(stoppingToken))
        {
            try
            {
                var person = JsonSerializer.Deserialize<Person>(dataStream.Span)!;
                _logger.LogInformation($"{person.Name} is {person.Age} " +
                                       $"years and from {person.Country}");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex.Message);
            }
        }
    }
}

class Person
{
    public string Name { get; set; } = String.Empty;
    public int Age { get; set; }
    public string Country { get; set; } = String.Empty;
}

O código a seguir vincula o corpo da solicitação a um Stream:

app.MapPost("/register", async (HttpRequest req, Stream body,
                                 Channel<ReadOnlyMemory<byte>> queue) =>
{
    if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // We're not above the message size and we have a content length, or
    // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
    // We add one to the message size so that we can detect when a chunked request body
    // is bigger than our configured max.
    var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

    var buffer = new byte[readSize];

    // Read at least that many bytes from the body.
    var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

    // We read more than the max, so this is a bad request.
    if (read > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // Attempt to send the buffer to the background queue.
    if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
    {
        return Results.Accepted();
    }

    // We couldn't accept the message since we're overloaded.
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

O código a seguir mostra o arquivo de Program.cs completo:

using System.Threading.Channels;
using BackgroundQueueService;

var builder = WebApplication.CreateBuilder(args);
// The max memory to use for the upload endpoint on this instance.
var maxMemory = 500 * 1024 * 1024;

// The max size of a single message, staying below the default LOH size of 85K.
var maxMessageSize = 80 * 1024;

// The max size of the queue based on those restrictions
var maxQueueSize = maxMemory / maxMessageSize;

// Create a channel to send data to the background queue.
builder.Services.AddSingleton<Channel<ReadOnlyMemory<byte>>>((_) =>
                     Channel.CreateBounded<ReadOnlyMemory<byte>>(maxQueueSize));

// Create a background queue service.
builder.Services.AddHostedService<BackgroundQueue>();
var app = builder.Build();

// curl --request POST 'https://localhost:<port>/register' --header 'Content-Type: application/json' --data-raw '{ "Name":"Samson", "Age": 23, "Country":"Nigeria" }'
// curl --request POST "https://localhost:<port>/register" --header "Content-Type: application/json" --data-raw "{ \"Name\":\"Samson\", \"Age\": 23, \"Country\":\"Nigeria\" }"
app.MapPost("/register", async (HttpRequest req, Stream body,
                                 Channel<ReadOnlyMemory<byte>> queue) =>
{
    if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // We're not above the message size and we have a content length, or
    // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
    // We add one to the message size so that we can detect when a chunked request body
    // is bigger than our configured max.
    var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

    var buffer = new byte[readSize];

    // Read at least that many bytes from the body.
    var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

    // We read more than the max, so this is a bad request.
    if (read > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // Attempt to send the buffer to the background queue.
    if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
    {
        return Results.Accepted();
    }

    // We couldn't accept the message since we're overloaded.
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

app.Run();
  • Ao ler dados, o Stream é o mesmo objeto que HttpRequest.Body.
  • O corpo da solicitação não é armazenado em buffer por padrão. Depois de o corpo ser lido, não pode ser retrocedido. O fluxo não pode ser lido várias vezes.
  • Os Stream e PipeReader não são utilizáveis fora do manipulador de ação mínimo, pois os buffers subjacentes serão descartados ou reutilizados.

Carregamentos de arquivos usando IFormFile e IFormFileCollection

Uploads de ficheiros usando IFormFile e IFormFileCollection em APIs mínimas exigem codificação multipart/form-data. O nome do parâmetro no manipulador de rota deve corresponder ao nome do campo de formulário na solicitação. APIs mínimas não suportam a vinculação de todo o corpo da solicitação diretamente a um IFormFile parâmetro sem codificação de formulário.

Se você precisar vincular todo o corpo da solicitação, por exemplo, ao trabalhar com JSON, dados binários ou outros tipos de conteúdo, consulte:

O código a seguir usa IFormFile e IFormFileCollection para carregar o arquivo:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapPost("/upload", async (IFormFile file) =>
{
    var tempFile = Path.GetTempFileName();
    app.Logger.LogInformation(tempFile);
    using var stream = File.OpenWrite(tempFile);
    await file.CopyToAsync(stream);
});

app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
    foreach (var file in myFiles)
    {
        var tempFile = Path.GetTempFileName();
        app.Logger.LogInformation(tempFile);
        using var stream = File.OpenWrite(tempFile);
        await file.CopyToAsync(stream);
    }
});

app.Run();

As solicitações de upload de arquivo autenticado são suportadas usando um cabeçalho de autorização , um certificado de cliente ou um cabeçalho cookie.

Vinculação a formulários com IFormCollection, IFormFile e IFormFileCollection

Há suporte para a vinculação de parâmetros baseados em formulário usando IFormCollection, IFormFilee IFormFileCollection. Os metadados do OpenAPI são inferidos para parâmetros de formulário para dar suporte à integração com o Swagger UI.

O código a seguir carrega arquivos usando a ligação inferida do tipo IFormFile:

using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

var app = builder.Build();
app.UseAntiforgery();

string GetOrCreateFilePath(string fileName, string filesDirectory = "uploadFiles")
{
    var directoryPath = Path.Combine(app.Environment.ContentRootPath, filesDirectory);
    Directory.CreateDirectory(directoryPath);
    return Path.Combine(directoryPath, fileName);
}

async Task UploadFileWithName(IFormFile file, string fileSaveName)
{
    var filePath = GetOrCreateFilePath(fileSaveName);
    await using var fileStream = new FileStream(filePath, FileMode.Create);
    await file.CopyToAsync(fileStream);
}

app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    var html = $"""
      <html>
        <body>
          <form action="/upload" method="POST" enctype="multipart/form-data">
            <input name="{token.FormFieldName}" type="hidden" value="{token.RequestToken}"/>
            <input type="file" name="file" placeholder="Upload an image..." accept=".jpg, 
                                                                            .jpeg, .png" />
            <input type="submit" />
          </form> 
        </body>
      </html>
    """;

    return Results.Content(html, "text/html");
});

app.MapPost("/upload", async Task<Results<Ok<string>,
   BadRequest<string>>> (IFormFile file, HttpContext context, IAntiforgery antiforgery) =>
{
    var fileSaveName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName);
    await UploadFileWithName(file, fileSaveName);
    return TypedResults.Ok("File uploaded successfully!");
});

app.Run();

Aviso: Ao implementar formulários, a aplicação deve evitarataques de falsificação de solicitação entre sites (XSRF/CSRF). No código anterior, o serviço IAntiforgery é usado para evitar ataques XSRF gerando e validando um token antifalsificação:

using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;

var builder = WebApplication.CreateBuilder();

builder.Services.AddAntiforgery();

var app = builder.Build();
app.UseAntiforgery();

string GetOrCreateFilePath(string fileName, string filesDirectory = "uploadFiles")
{
    var directoryPath = Path.Combine(app.Environment.ContentRootPath, filesDirectory);
    Directory.CreateDirectory(directoryPath);
    return Path.Combine(directoryPath, fileName);
}

async Task UploadFileWithName(IFormFile file, string fileSaveName)
{
    var filePath = GetOrCreateFilePath(fileSaveName);
    await using var fileStream = new FileStream(filePath, FileMode.Create);
    await file.CopyToAsync(fileStream);
}

app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    var html = $"""
      <html>
        <body>
          <form action="/upload" method="POST" enctype="multipart/form-data">
            <input name="{token.FormFieldName}" type="hidden" value="{token.RequestToken}"/>
            <input type="file" name="file" placeholder="Upload an image..." accept=".jpg, 
                                                                            .jpeg, .png" />
            <input type="submit" />
          </form> 
        </body>
      </html>
    """;

    return Results.Content(html, "text/html");
});

app.MapPost("/upload", async Task<Results<Ok<string>,
   BadRequest<string>>> (IFormFile file, HttpContext context, IAntiforgery antiforgery) =>
{
    var fileSaveName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName);
    await UploadFileWithName(file, fileSaveName);
    return TypedResults.Ok("File uploaded successfully!");
});

app.Run();

Para obter mais informações sobre ataques XSRF, consulte Antiforgery with Minimal APIs

Para obter mais informações, consulte vinculação de formulário em APIs mínimas;

Vincular a coleções e tipos complexos de formulários

A vinculação é suportada para:

  • Coleções, por exemplo, Lista e Dicionário
  • Tipos complexos, por exemplo, Todo ou Project

O código a seguir mostra:

  • Um ponto de extremidade mínimo que vincula uma entrada de formulário de várias partes a um objeto complexo.
  • Como usar os serviços antifalsificação para apoiar a geração e validação de tokens antifalsificação.
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAntiforgery();

var app = builder.Build();

app.UseAntiforgery();

app.MapGet("/", (HttpContext context, IAntiforgery antiforgery) =>
{
    var token = antiforgery.GetAndStoreTokens(context);
    var html = $"""
        <html><body>
           <form action="/todo" method="POST" enctype="multipart/form-data">
               <input name="{token.FormFieldName}" 
                                type="hidden" value="{token.RequestToken}" />
               <input type="text" name="name" />
               <input type="date" name="dueDate" />
               <input type="checkbox" name="isCompleted" value="true" />
               <input type="submit" />
               <input name="isCompleted" type="hidden" value="false" /> 
           </form>
        </body></html>
    """;
    return Results.Content(html, "text/html");
});

app.MapPost("/todo", async Task<Results<Ok<Todo>, BadRequest<string>>> 
               ([FromForm] Todo todo, HttpContext context, IAntiforgery antiforgery) =>
{
    try
    {
        await antiforgery.ValidateRequestAsync(context);
        return TypedResults.Ok(todo);
    }
    catch (AntiforgeryValidationException e)
    {
        return TypedResults.BadRequest("Invalid antiforgery token");
    }
});

app.Run();

class Todo
{
    public string Name { get; set; } = string.Empty;
    public bool IsCompleted { get; set; } = false;
    public DateTime DueDate { get; set; } = DateTime.Now.Add(TimeSpan.FromDays(1));
}

No código anterior:

  • O parâmetro de destino deve ser anotado com o atributo [FromForm] para diferenciar dos parâmetros que devem ser lidos do corpo JSON.
  • A vinculação de tipos complexos ou de coleção não é suportada para APIs mínimas compiladas com o Gerador de Delegados de Solicitação.
  • A marcação mostra uma entrada oculta adicional com um nome de isCompleted e um valor de false. Se a caixa de seleção isCompleted estiver marcada quando o formulário for enviado, os valores true e false serão enviados como valores. Se a caixa de seleção estiver desmarcada, somente o valor de entrada oculto false será enviado. O processo de vinculação de modelo ASP.NET Core lê apenas o primeiro valor ao vincular a um valor bool, o que resulta em true para caixas de seleção marcadas e false para caixas de seleção desmarcadas.

Um exemplo dos dados de formulário enviados para o ponto de extremidade anterior é o seguinte:

__RequestVerificationToken: CfDJ8Bveip67DklJm5vI2PF2VOUZ594RC8kcGWpTnVV17zCLZi1yrs-CSz426ZRRrQnEJ0gybB0AD7hTU-0EGJXDU-OaJaktgAtWLIaaEWMOWCkoxYYm-9U9eLV7INSUrQ6yBHqdMEE_aJpD4AI72gYiCqc
name: Walk the dog
dueDate: 2024-04-06
isCompleted: true
isCompleted: false

Vincular matrizes e valores de texto de cabeçalhos e strings de consulta

O código a seguir demonstra a vinculação de cadeias de caracteres de consulta a uma matriz de tipos primitivos, matrizes de cadeia de caracteres e StringValues:

// Bind query string values to a primitive type array.
// GET  /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) =>
                      $"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");

// Bind to a string array.
// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

// Bind to StringValues.
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

A vinculação de cadeias de caracteres de consulta ou valores de cabeçalho a uma matriz de tipos complexos é suportada quando o próprio tipo tem TryParse implementado. O código a seguir se liga a uma matriz de cadeia de caracteres e retorna todos os itens com as tags especificadas:

// GET /todoitems/tags?tags=home&tags=work
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>
{
    return await db.Todos
        .Where(t => tags.Select(i => i.Name).Contains(t.Tag.Name))
        .ToListAsync();
});

O código a seguir mostra o modelo e a implementação de TryParse necessária:

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    // This is an owned entity. 
    public Tag Tag { get; set; } = new();
}

[Owned]
public class Tag
{
    public string? Name { get; set; } = "n/a";

    public static bool TryParse(string? name, out Tag tag)
    {
        if (name is null)
        {
            tag = default!;
            return false;
        }

        tag = new Tag { Name = name };
        return true;
    }
}

O código a seguir se liga a uma matriz int:

// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
    return await db.Todos
        .Where(t => ids.Contains(t.Id))
        .ToListAsync();
});

Para testar o código anterior, adicione o seguinte endpoint para preencher o banco de dados com Todo itens.

// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
    await db.Todos.AddRangeAsync(todos);
    await db.SaveChangesAsync();

    return Results.Ok(todos);
});

Use uma ferramenta como HttpRepl para passar os seguintes dados para o ponto de extremidade anterior:

[
    {
        "id": 1,
        "name": "Have Breakfast",
        "isComplete": true,
        "tag": {
            "name": "home"
        }
    },
    {
        "id": 2,
        "name": "Have Lunch",
        "isComplete": true,
        "tag": {
            "name": "work"
        }
    },
    {
        "id": 3,
        "name": "Have Supper",
        "isComplete": true,
        "tag": {
            "name": "home"
        }
    },
    {
        "id": 4,
        "name": "Have Snacks",
        "isComplete": true,
        "tag": {
            "name": "N/A"
        }
    }
]

O código a seguir se liga à chave de cabeçalho X-Todo-Id e retorna os itens Todo com valores de Id correspondentes:

// GET /todoitems/header-ids
// The keys of the headers should all be X-Todo-Id with different values
app.MapGet("/todoitems/header-ids", async ([FromHeader(Name = "X-Todo-Id")] int[] ids, TodoDb db) =>
{
    return await db.Todos
        .Where(t => ids.Contains(t.Id))
        .ToListAsync();
});

Observação

Ao vincular um string[] de uma cadeia de caracteres de consulta, a ausência de qualquer valor de cadeia de caracteres de consulta correspondente resultará em uma matriz vazia em vez de um valor nulo.

Vinculação de parâmetros para listas de argumentos com [AsParameters]

AsParametersAttribute permite a ligação simples de parâmetros a tipos e não a vinculação de modelos complexos ou recursivos.

Considere o seguinte código:

using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());

app.MapGet("/todoitems/{id}",
                             async (int Id, TodoDb Db) =>
    await Db.Todos.FindAsync(Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());
// Remaining code removed for brevity.

Considere o seguinte endpoint GET:

app.MapGet("/todoitems/{id}",
                             async (int Id, TodoDb Db) =>
    await Db.Todos.FindAsync(Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());

Os seguintes struct podem ser usados para substituir os parâmetros destacados anteriores:

struct TodoItemRequest
{
    public int Id { get; set; }
    public TodoDb Db { get; set; }
}

O ponto de extremidade GET refatorado usa o precedente struct com o atributo AsParameters:

app.MapGet("/ap/todoitems/{id}",
                                async ([AsParameters] TodoItemRequest request) =>
    await request.Db.Todos.FindAsync(request.Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());

O seguinte código mostra pontos de extremidade adicionais na aplicação:

app.MapPost("/todoitems", async (TodoItemDTO Dto, TodoDb Db) =>
{
    var todoItem = new Todo
    {
        IsComplete = Dto.IsComplete,
        Name = Dto.Name
    };

    Db.Todos.Add(todoItem);
    await Db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/todoitems/{id}", async (int Id, TodoItemDTO Dto, TodoDb Db) =>
{
    var todo = await Db.Todos.FindAsync(Id);

    if (todo is null) return Results.NotFound();

    todo.Name = Dto.Name;
    todo.IsComplete = Dto.IsComplete;

    await Db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int Id, TodoDb Db) =>
{
    if (await Db.Todos.FindAsync(Id) is Todo todo)
    {
        Db.Todos.Remove(todo);
        await Db.SaveChangesAsync();
        return Results.Ok(new TodoItemDTO(todo));
    }

    return Results.NotFound();
});

As seguintes classes são usadas para refatorar as listas de parâmetros:

class CreateTodoItemRequest
{
    public TodoItemDTO Dto { get; set; } = default!;
    public TodoDb Db { get; set; } = default!;
}

class EditTodoItemRequest
{
    public int Id { get; set; }
    public TodoItemDTO Dto { get; set; } = default!;
    public TodoDb Db { get; set; } = default!;
}

O código a seguir mostra os pontos de extremidade refatorados usando AsParameters e os struct e classes anteriores:

app.MapPost("/ap/todoitems", async ([AsParameters] CreateTodoItemRequest request) =>
{
    var todoItem = new Todo
    {
        IsComplete = request.Dto.IsComplete,
        Name = request.Dto.Name
    };

    request.Db.Todos.Add(todoItem);
    await request.Db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/ap/todoitems/{id}", async ([AsParameters] EditTodoItemRequest request) =>
{
    var todo = await request.Db.Todos.FindAsync(request.Id);

    if (todo is null) return Results.NotFound();

    todo.Name = request.Dto.Name;
    todo.IsComplete = request.Dto.IsComplete;

    await request.Db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/ap/todoitems/{id}", async ([AsParameters] TodoItemRequest request) =>
{
    if (await request.Db.Todos.FindAsync(request.Id) is Todo todo)
    {
        request.Db.Todos.Remove(todo);
        await request.Db.SaveChangesAsync();
        return Results.Ok(new TodoItemDTO(todo));
    }

    return Results.NotFound();
});

Os seguintes tipos de record podem ser usados para substituir os parâmetros anteriores:

record TodoItemRequest(int Id, TodoDb Db);
record CreateTodoItemRequest(TodoItemDTO Dto, TodoDb Db);
record EditTodoItemRequest(int Id, TodoItemDTO Dto, TodoDb Db);

Usar um struct com AsParameters pode ser mais eficiente do que usar um tipo de record.

O código de exemplo completo no repositório AspNetCore.Docs.Samples.

Vinculação personalizada

Há três maneiras de personalizar a vinculação de parâmetros:

  1. Para fontes de vinculação de rota, consulta e cabeçalho, vincule tipos personalizados adicionando um método TryParse estático para o tipo.
  2. Controle o processo de vinculação implementando um método BindAsync em um tipo.
  3. Para cenários avançados, implemente a IBindableFromHttpContext<TSelf> interface para fornecer lógica de vinculação personalizada diretamente do HttpContext.

TryParse

TryParse tem duas APIs:

public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);

O código a seguir exibe Point: 12.3, 10.1 com o URI /map?Point=12.3,10.1:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");

app.Run();

public class Point
{
    public double X { get; set; }
    public double Y { get; set; }

    public static bool TryParse(string? value, IFormatProvider? provider,
                                out Point? point)
    {
        // Format is "(12.3,10.1)"
        var trimmedValue = value?.TrimStart('(').TrimEnd(')');
        var segments = trimmedValue?.Split(',',
                StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (segments?.Length == 2
            && double.TryParse(segments[0], out var x)
            && double.TryParse(segments[1], out var y))
        {
            point = new Point { X = x, Y = y };
            return true;
        }

        point = null;
        return false;
    }
}

BindAsync

BindAsync tem as seguintes APIs:

public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);

O código a seguir exibe SortBy:xyz, SortDirection:Desc, CurrentPage:99 com o URI /products?SortBy=xyz&SortDir=Desc&Page=99:

using System.Reflection;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
       $"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");

app.Run();

public class PagingData
{
    public string? SortBy { get; init; }
    public SortDirection SortDirection { get; init; }
    public int CurrentPage { get; init; } = 1;

    public static ValueTask<PagingData?> BindAsync(HttpContext context,
                                                   ParameterInfo parameter)
    {
        const string sortByKey = "sortBy";
        const string sortDirectionKey = "sortDir";
        const string currentPageKey = "page";

        Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
                                     ignoreCase: true, out var sortDirection);
        int.TryParse(context.Request.Query[currentPageKey], out var page);
        page = page == 0 ? 1 : page;

        var result = new PagingData
        {
            SortBy = context.Request.Query[sortByKey],
            SortDirection = sortDirection,
            CurrentPage = page
        };

        return ValueTask.FromResult<PagingData?>(result);
    }
}

public enum SortDirection
{
    Default,
    Asc,
    Desc
}

Vinculação de parâmetros personalizados com IBindableFromHttpContext

ASP.NET Core fornece suporte para vinculação de parâmetros personalizados em APIs mínimas usando a IBindableFromHttpContext<TSelf> interface. Essa interface, introduzida com os membros abstratos estáticos do C# 11, permite criar tipos que podem ser vinculados a partir de um contexto HTTP diretamente nos parâmetros do manipulador de rotas.

public interface IBindableFromHttpContext<TSelf>
    where TSelf : class, IBindableFromHttpContext<TSelf>
{
    static abstract ValueTask<TSelf?> BindAsync(HttpContext context, ParameterInfo parameter);
}

Ao implementar o IBindableFromHttpContext<TSelf>, pode criar tipos personalizados que manipulem a sua própria lógica de vinculação a partir do HttpContext. Quando um manipulador de rotas inclui um parâmetro desse tipo, a estrutura chama automaticamente o método estático BindAsync para criar a instância:

using CustomBindingExample;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseHttpsRedirection();

app.MapGet("/", () => "Hello, IBindableFromHttpContext example!");

app.MapGet("/custom-binding", (CustomBoundParameter param) =>
{
    return $"Value from custom binding: {param.Value}";
});

app.MapGet("/combined/{id}", (int id, CustomBoundParameter param) =>
{
    return $"ID: {id}, Custom Value: {param.Value}";
});

A seguir está um exemplo de implementação de um parâmetro personalizado que se liga a partir de um cabeçalho HTTP:

using System.Reflection;

namespace CustomBindingExample;

public class CustomBoundParameter : IBindableFromHttpContext<CustomBoundParameter>
{
    public string Value { get; init; } = default!;

    public static ValueTask<CustomBoundParameter?> BindAsync(HttpContext context, ParameterInfo parameter)
    {
        // Custom binding logic here
        // This example reads from a custom header
        var value = context.Request.Headers["X-Custom-Header"].ToString();
        
        // If no header was provided, you could fall back to a query parameter
        if (string.IsNullOrEmpty(value))
        {
            value = context.Request.Query["customValue"].ToString();
        }
        
        return ValueTask.FromResult<CustomBoundParameter?>(new CustomBoundParameter 
        {
            Value = value
        });
    }
}

Você também pode implementar a validação dentro de sua lógica de vinculação personalizada:

app.MapGet("/validated", (ValidatedParameter param) =>
{
    if (string.IsNullOrEmpty(param.Value))
    {
        return Results.BadRequest("Value cannot be empty");
    }
    
    return Results.Ok($"Validated value: {param.Value}");
});

Ver ou transferir o código de exemplo (como transferir)

Falhas de ligação

Quando a vinculação falha, a estrutura registra uma mensagem de depuração e retorna vários códigos de status para o cliente, dependendo do modo de falha.

Modo de falha Tipo de parâmetro anulável Origem da vinculação Código de estado
{ParameterType}.TryParse retorna false Sim rota/consulta/cabeçalho 400
{ParameterType}.BindAsync retorna null Sim Personalizado 400
{ParameterType}.BindAsync lança Não importa Personalizado 500
Falha ao desserializar o corpo JSON Não importa corpo 400
Tipo de conteúdo errado (não application/json) Não importa corpo 415

Precedência de vinculação

As regras para determinar uma fonte vinculativa a partir de um parâmetro:

  1. Atributo explícito definido no parâmetro (atributos From*) na seguinte ordem:
    1. Valores da rota: [FromRoute]
    2. Seqüência de caracteres de consulta: [FromQuery]
    3. Cabeçalho: [FromHeader]
    4. Corpo: [FromBody]
    5. Formulário: [FromForm]
    6. Serviço: [FromServices]
    7. Valores dos parâmetros: [AsParameters]
  2. Tipos especiais
    1. HttpContext
    2. HttpRequest (HttpContext.Request)
    3. HttpResponse (HttpContext.Response)
    4. ClaimsPrincipal (HttpContext.User)
    5. CancellationToken (HttpContext.RequestAborted)
    6. IFormCollection (HttpContext.Request.Form)
    7. IFormFileCollection (HttpContext.Request.Form.Files)
    8. IFormFile (HttpContext.Request.Form.Files[paramName])
    9. Stream (HttpContext.Request.Body)
    10. PipeReader (HttpContext.Request.BodyReader)
  3. O tipo de parâmetro tem um método BindAsync estático válido.
  4. Tipo de parâmetro é uma cadeia de caracteres ou tem um método TryParse estático válido.
    1. Se o nome do parâmetro existir no modelo de rota, por exemplo, app.Map("/todo/{id}", (int id) => {});, ele será vinculado à rota.
    2. Vinculado a partir da cadeia de caracteres de consulta.
  5. Se o tipo de parâmetro for um serviço fornecido por injeção de dependência, ele usará esse serviço como origem.
  6. O parâmetro é do corpo.

Configurar opções de desserialização JSON para vinculação de corpo

A fonte de vinculação de corpo usa System.Text.Json para desserialização. Não é possível alterar esse padrão, mas as opções de serialização e desserialização JSON podem ser configuradas.

Configurar opções de desserialização JSON globalmente

As opções que se aplicam globalmente a um aplicativo podem ser configuradas invocando ConfigureHttpJsonOptions. O exemplo a seguir inclui campos públicos e formatos de saída JSON.

var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.WriteIndented = true;
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/", (Todo todo) => {
    if (todo is not null) {
        todo.Name = todo.NameField;
    }
    return todo;
});

app.Run();

class Todo {
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "nameField":"Walk dog",
//    "isComplete":false
// }

Como o código de exemplo configura a serialização e a desserialização, ele pode ler NameField e incluir NameField no JSON de saída.

Configurar opções de desserialização JSON para um endpoint

ReadFromJsonAsync tem sobrecargas que aceitam um objeto JsonSerializerOptions. O exemplo a seguir inclui campos públicos e formatos de saída JSON.

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { 
    IncludeFields = true, 
    WriteIndented = true
};

app.MapPost("/", async (HttpContext context) => {
    if (context.Request.HasJsonContentType()) {
        var todo = await context.Request.ReadFromJsonAsync<Todo>(options);
        if (todo is not null) {
            todo.Name = todo.NameField;
        }
        return Results.Ok(todo);
    }
    else {
        return Results.BadRequest();
    }
});

app.Run();

class Todo
{
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "isComplete":false
// }

Como o código anterior aplica as opções personalizadas somente à desserialização, o JSON de saída exclui NameField.

Leia o corpo da requisição

Leia o corpo da solicitação diretamente usando um parâmetro HttpContext ou HttpRequest:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) =>
{
    var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());

    await using var writeStream = File.Create(filePath);
    await request.BodyReader.CopyToAsync(writeStream);
});

app.Run();

O código anterior:

  • Acede ao corpo da solicitação usando HttpRequest.BodyReader.
  • Copia o corpo da solicitação para um arquivo local.

A vinculação de parâmetros é o processo de conversão de dados de solicitação em parâmetros fortemente tipados que são expressos por manipuladores de rota. Uma fonte de vinculação determina de onde os parâmetros são vinculados. As fontes de vinculação podem ser explícitas ou inferidas com base no método HTTP e no tipo de parâmetro.

Fontes de vinculação suportadas:

  • Valores de itinerário
  • string de consulta
  • Cabeçalho
  • Corpo (em formato JSON)
  • Serviços prestados por injeção de dependência
  • Personalizado

A vinculação de valores de formulário não é suportada de forma nativa no .NET 6 e 7.

O manipulador de rotas GET a seguir usa algumas dessas fontes de vinculação de parâmetros:

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();

app.MapGet("/{id}", (int id,
                     int page,
                     [FromHeader(Name = "X-CUSTOM-HEADER")] string customHeader,
                     Service service) => { });

class Service { }

A tabela a seguir mostra a relação entre os parâmetros usados no exemplo anterior e as fontes de ligação associadas.

Parâmetro Origem da vinculação
id valor da rota
page seqüência de caracteres de consulta
customHeader cabeçalho
service Fornecido por injeção de dependência

Os métodos HTTP GET, HEAD, OPTIONSe DELETE não se vinculam implicitamente ao corpo. Para associar a partir do corpo (como JSON) para esses métodos HTTP, associe explicitamente com [FromBody] ou leia a partir do HttpRequest.

O seguinte exemplo de manipulador de rota POST usa uma fonte de ligação de corpo (como JSON) para o parâmetro person:

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapPost("/", (Person person) => { });

record Person(string Name, int Age);

Os parâmetros nos exemplos anteriores são todos vinculados a partir de dados de solicitação automaticamente. Para demonstrar a conveniência que a vinculação de parâmetros oferece, os manipuladores de rota a seguir mostram como ler os dados da solicitação diretamente da solicitação:

app.MapGet("/{id}", (HttpRequest request) =>
{
    var id = request.RouteValues["id"];
    var page = request.Query["page"];
    var customHeader = request.Headers["X-CUSTOM-HEADER"];

    // ...
});

app.MapPost("/", async (HttpRequest request) =>
{
    var person = await request.ReadFromJsonAsync<Person>();

    // ...
});

Vinculação explícita de parâmetros

Os atributos podem ser usados para declarar explicitamente de onde os parâmetros são vinculados.

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Added as service
builder.Services.AddSingleton<Service>();

var app = builder.Build();


app.MapGet("/{id}", ([FromRoute] int id,
                     [FromQuery(Name = "p")] int page,
                     [FromServices] Service service,
                     [FromHeader(Name = "Content-Type")] string contentType) 
                     => {});

class Service { }

record Person(string Name, int Age);
Parâmetro Origem da vinculação
id valor da rota com o nome id
page seqüência de caracteres de consulta com o nome "p"
service Fornecido por injeção de dependência
contentType cabeçalho com o nome "Content-Type"

Observação

A vinculação de valores de formulário não é suportada de forma nativa no .NET 6 e 7.

Ligação de parâmetros com injeção de dependência

A vinculação de parâmetros para APIs mínimas liga parâmetros por meio de injeção de dependência quando o tipo está configurado como um serviço. Não é necessário aplicar explicitamente o atributo [FromServices] a um parâmetro. No código a seguir, ambas as ações retornam o tempo:

using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IDateTime, SystemDateTime>();

var app = builder.Build();

app.MapGet("/",   (               IDateTime dateTime) => dateTime.Now);
app.MapGet("/fs", ([FromServices] IDateTime dateTime) => dateTime.Now);
app.Run();

Parâmetros opcionais

Os parâmetros declarados nos manipuladores de rota são tratados conforme necessário:

  • Se uma solicitação corresponder à rota, o manipulador de rota só será executado se todos os parâmetros necessários forem fornecidos na solicitação.
  • A falha em fornecer todos os parâmetros necessários resulta em um erro.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int pageNumber) => $"Requesting page {pageNumber}");

app.Run();
URI Resultado
/products?pageNumber=3 3 devolvidos
/products BadHttpRequestException: O parâmetro necessário "int pageNumber" não foi fornecido a partir da cadeia de caracteres de consulta.
/products/1 Erro HTTP 404, nenhuma rota correspondente

Para tornar pageNumber opcional, defina o tipo como opcional ou forneça um valor padrão:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

string ListProducts(int pageNumber = 1) => $"Requesting page {pageNumber}";

app.MapGet("/products2", ListProducts);

app.Run();
URI Resultado
/products?pageNumber=3 3 devolvidos
/products 1 devolvido
/products2 1 devolvido

O valor nulo e o valor padrão precedentes aplicam-se a todas as fontes.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/products", (Product? product) => { });

app.Run();

O código anterior chama o método com um produto nulo se nenhum corpo de solicitação for enviado.

NOTA: Se dados inválidos forem fornecidos e o parâmetro for anulável, o manipulador de rotas não será executado.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/products", (int? pageNumber) => $"Requesting page {pageNumber ?? 1}");

app.Run();
URI Resultado
/products?pageNumber=3 3 retornou
/products 1 retornou
/products?pageNumber=two BadHttpRequestException: Falha ao associar o parâmetro "Nullable<int> pageNumber" a partir de "two".
/products/two Erro HTTP 404, nenhuma rota correspondente

Consulte a seção Falhas de vinculação para obter mais informações.

Tipos especiais

Os seguintes tipos são vinculados sem atributos explícitos:

  • HttpContext: O contexto que contém todas as informações sobre a solicitação ou resposta HTTP atual:

    app.MapGet("/", (HttpContext context) => context.Response.WriteAsync("Hello World"));
    
  • HttpRequest e HttpResponse: A solicitação HTTP e a resposta HTTP:

    app.MapGet("/", (HttpRequest request, HttpResponse response) =>
        response.WriteAsync($"Hello World {request.Query["name"]}"));
    
  • CancellationToken: O token de cancelamento associado à solicitação HTTP atual:

    app.MapGet("/", async (CancellationToken cancellationToken) => 
        await MakeLongRunningRequestAsync(cancellationToken));
    
  • ClaimsPrincipal: O usuário associado à solicitação, vinculado a partir de HttpContext.User:

    app.MapGet("/", (ClaimsPrincipal user) => user.Identity.Name);
    

Vincular o corpo da solicitação como um Stream ou PipeReader

O corpo da solicitação pode ser vinculado como um Stream ou PipeReader para suportar eficientemente cenários em que o usuário precisa processar dados e:

  • Armazene os dados no armazenamento em blob ou adicione os dados a um provedor de fila.
  • Processe os dados armazenados com um processo de trabalho ou função de nuvem.

Por exemplo, os dados podem ser enfileirados no armazenamento de filas do Azure ou armazenados no armazenamento de blobs do Azure .

O código a seguir implementa uma fila em segundo plano:

using System.Text.Json;
using System.Threading.Channels;

namespace BackgroundQueueService;

class BackgroundQueue : BackgroundService
{
    private readonly Channel<ReadOnlyMemory<byte>> _queue;
    private readonly ILogger<BackgroundQueue> _logger;

    public BackgroundQueue(Channel<ReadOnlyMemory<byte>> queue,
                               ILogger<BackgroundQueue> logger)
    {
        _queue = queue;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await foreach (var dataStream in _queue.Reader.ReadAllAsync(stoppingToken))
        {
            try
            {
                var person = JsonSerializer.Deserialize<Person>(dataStream.Span)!;
                _logger.LogInformation($"{person.Name} is {person.Age} " +
                                       $"years and from {person.Country}");
            }
            catch (Exception ex)
            {
                _logger.LogError(ex.Message);
            }
        }
    }
}

class Person
{
    public string Name { get; set; } = String.Empty;
    public int Age { get; set; }
    public string Country { get; set; } = String.Empty;
}

O código a seguir vincula o corpo da solicitação a um Stream:

app.MapPost("/register", async (HttpRequest req, Stream body,
                                 Channel<ReadOnlyMemory<byte>> queue) =>
{
    if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // We're not above the message size and we have a content length, or
    // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
    // We add one to the message size so that we can detect when a chunked request body
    // is bigger than our configured max.
    var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

    var buffer = new byte[readSize];

    // Read at least that many bytes from the body.
    var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

    // We read more than the max, so this is a bad request.
    if (read > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // Attempt to send the buffer to the background queue.
    if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
    {
        return Results.Accepted();
    }

    // We couldn't accept the message since we're overloaded.
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

O código a seguir mostra o arquivo de Program.cs completo:

using System.Threading.Channels;
using BackgroundQueueService;

var builder = WebApplication.CreateBuilder(args);
// The max memory to use for the upload endpoint on this instance.
var maxMemory = 500 * 1024 * 1024;

// The max size of a single message, staying below the default LOH size of 85K.
var maxMessageSize = 80 * 1024;

// The max size of the queue based on those restrictions
var maxQueueSize = maxMemory / maxMessageSize;

// Create a channel to send data to the background queue.
builder.Services.AddSingleton<Channel<ReadOnlyMemory<byte>>>((_) =>
                     Channel.CreateBounded<ReadOnlyMemory<byte>>(maxQueueSize));

// Create a background queue service.
builder.Services.AddHostedService<BackgroundQueue>();
var app = builder.Build();

// curl --request POST 'https://localhost:<port>/register' --header 'Content-Type: application/json' --data-raw '{ "Name":"Samson", "Age": 23, "Country":"Nigeria" }'
// curl --request POST "https://localhost:<port>/register" --header "Content-Type: application/json" --data-raw "{ \"Name\":\"Samson\", \"Age\": 23, \"Country\":\"Nigeria\" }"
app.MapPost("/register", async (HttpRequest req, Stream body,
                                 Channel<ReadOnlyMemory<byte>> queue) =>
{
    if (req.ContentLength is not null && req.ContentLength > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // We're not above the message size and we have a content length, or
    // we're a chunked request and we're going to read up to the maxMessageSize + 1. 
    // We add one to the message size so that we can detect when a chunked request body
    // is bigger than our configured max.
    var readSize = (int?)req.ContentLength ?? (maxMessageSize + 1);

    var buffer = new byte[readSize];

    // Read at least that many bytes from the body.
    var read = await body.ReadAtLeastAsync(buffer, readSize, throwOnEndOfStream: false);

    // We read more than the max, so this is a bad request.
    if (read > maxMessageSize)
    {
        return Results.BadRequest();
    }

    // Attempt to send the buffer to the background queue.
    if (queue.Writer.TryWrite(buffer.AsMemory(0..read)))
    {
        return Results.Accepted();
    }

    // We couldn't accept the message since we're overloaded.
    return Results.StatusCode(StatusCodes.Status429TooManyRequests);
});

app.Run();
  • Ao ler dados, o Stream é o mesmo objeto que HttpRequest.Body.
  • O corpo da solicitação não é armazenado em buffer por padrão. Depois de o corpo ser lido, não pode ser retrocedido. O fluxo não pode ser lido várias vezes.
  • Os Stream e PipeReader não são utilizáveis fora do manipulador de ação mínimo, pois os buffers subjacentes serão descartados ou reutilizados.

Carregamentos de arquivos usando IFormFile e IFormFileCollection

O código a seguir usa IFormFile e IFormFileCollection para carregar o arquivo:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World!");

app.MapPost("/upload", async (IFormFile file) =>
{
    var tempFile = Path.GetTempFileName();
    app.Logger.LogInformation(tempFile);
    using var stream = File.OpenWrite(tempFile);
    await file.CopyToAsync(stream);
});

app.MapPost("/upload_many", async (IFormFileCollection myFiles) =>
{
    foreach (var file in myFiles)
    {
        var tempFile = Path.GetTempFileName();
        app.Logger.LogInformation(tempFile);
        using var stream = File.OpenWrite(tempFile);
        await file.CopyToAsync(stream);
    }
});

app.Run();

As solicitações de upload de arquivo autenticado são suportadas usando um cabeçalho de autorização , um certificado de cliente ou um cabeçalho cookie.

Não há suporte interno para antifalsificação no ASP.NET Core no .NET 7. A Antifalsificação está disponível no ASP.NET Core no .NET 8 ou posterior. No entanto, ele pode ser implementado usando o serviço IAntiforgery.

Vincular matrizes e valores de texto de cabeçalhos e strings de consulta

O código a seguir demonstra a vinculação de cadeias de caracteres de consulta a uma matriz de tipos primitivos, matrizes de cadeia de caracteres e StringValues:

// Bind query string values to a primitive type array.
// GET  /tags?q=1&q=2&q=3
app.MapGet("/tags", (int[] q) =>
                      $"tag1: {q[0]} , tag2: {q[1]}, tag3: {q[2]}");

// Bind to a string array.
// GET /tags2?names=john&names=jack&names=jane
app.MapGet("/tags2", (string[] names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

// Bind to StringValues.
// GET /tags3?names=john&names=jack&names=jane
app.MapGet("/tags3", (StringValues names) =>
            $"tag1: {names[0]} , tag2: {names[1]}, tag3: {names[2]}");

A vinculação de cadeias de caracteres de consulta ou valores de cabeçalho a uma matriz de tipos complexos é suportada quando o próprio tipo tem TryParse implementado. O código a seguir se liga a uma matriz de cadeia de caracteres e retorna todos os itens com as tags especificadas:

// GET /todoitems/tags?tags=home&tags=work
app.MapGet("/todoitems/tags", async (Tag[] tags, TodoDb db) =>
{
    return await db.Todos
        .Where(t => tags.Select(i => i.Name).Contains(t.Tag.Name))
        .ToListAsync();
});

O código a seguir mostra o modelo e a implementação de TryParse necessária:

public class Todo
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public bool IsComplete { get; set; }

    // This is an owned entity. 
    public Tag Tag { get; set; } = new();
}

[Owned]
public class Tag
{
    public string? Name { get; set; } = "n/a";

    public static bool TryParse(string? name, out Tag tag)
    {
        if (name is null)
        {
            tag = default!;
            return false;
        }

        tag = new Tag { Name = name };
        return true;
    }
}

O código a seguir se liga a uma matriz int:

// GET /todoitems/query-string-ids?ids=1&ids=3
app.MapGet("/todoitems/query-string-ids", async (int[] ids, TodoDb db) =>
{
    return await db.Todos
        .Where(t => ids.Contains(t.Id))
        .ToListAsync();
});

Para testar o código anterior, adicione o seguinte endpoint para preencher o banco de dados com Todo itens.

// POST /todoitems/batch
app.MapPost("/todoitems/batch", async (Todo[] todos, TodoDb db) =>
{
    await db.Todos.AddRangeAsync(todos);
    await db.SaveChangesAsync();

    return Results.Ok(todos);
});

Use uma ferramenta de teste de APIs como HttpRepl para passar os seguintes dados para o ponto de extremidade anterior.

[
    {
        "id": 1,
        "name": "Have Breakfast",
        "isComplete": true,
        "tag": {
            "name": "home"
        }
    },
    {
        "id": 2,
        "name": "Have Lunch",
        "isComplete": true,
        "tag": {
            "name": "work"
        }
    },
    {
        "id": 3,
        "name": "Have Supper",
        "isComplete": true,
        "tag": {
            "name": "home"
        }
    },
    {
        "id": 4,
        "name": "Have Snacks",
        "isComplete": true,
        "tag": {
            "name": "N/A"
        }
    }
]

O código a seguir se liga à chave de cabeçalho X-Todo-Id e retorna os itens Todo com valores de Id correspondentes:

// GET /todoitems/header-ids
// The keys of the headers should all be X-Todo-Id with different values
app.MapGet("/todoitems/header-ids", async ([FromHeader(Name = "X-Todo-Id")] int[] ids, TodoDb db) =>
{
    return await db.Todos
        .Where(t => ids.Contains(t.Id))
        .ToListAsync();
});

Observação

Ao vincular um string[] de uma cadeia de caracteres de consulta, a ausência de qualquer valor de cadeia de caracteres de consulta correspondente resultará em uma matriz vazia em vez de um valor nulo.

Vinculação de parâmetros para listas de argumentos com [AsParameters]

AsParametersAttribute permite a ligação simples de parâmetros a tipos e não a vinculação de modelos complexos ou recursivos.

Considere o seguinte código:

using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
var app = builder.Build();

app.MapGet("/todoitems", async (TodoDb db) =>
    await db.Todos.Select(x => new TodoItemDTO(x)).ToListAsync());

app.MapGet("/todoitems/{id}",
                             async (int Id, TodoDb Db) =>
    await Db.Todos.FindAsync(Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());
// Remaining code removed for brevity.

Considere o seguinte endpoint GET:

app.MapGet("/todoitems/{id}",
                             async (int Id, TodoDb Db) =>
    await Db.Todos.FindAsync(Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());

Os seguintes struct podem ser usados para substituir os parâmetros destacados anteriores:

struct TodoItemRequest
{
    public int Id { get; set; }
    public TodoDb Db { get; set; }
}

O ponto de extremidade GET refatorado usa o precedente struct com o atributo AsParameters:

app.MapGet("/ap/todoitems/{id}",
                                async ([AsParameters] TodoItemRequest request) =>
    await request.Db.Todos.FindAsync(request.Id)
        is Todo todo
            ? Results.Ok(new TodoItemDTO(todo))
            : Results.NotFound());

O seguinte código mostra pontos de extremidade adicionais na aplicação:

app.MapPost("/todoitems", async (TodoItemDTO Dto, TodoDb Db) =>
{
    var todoItem = new Todo
    {
        IsComplete = Dto.IsComplete,
        Name = Dto.Name
    };

    Db.Todos.Add(todoItem);
    await Db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/todoitems/{id}", async (int Id, TodoItemDTO Dto, TodoDb Db) =>
{
    var todo = await Db.Todos.FindAsync(Id);

    if (todo is null) return Results.NotFound();

    todo.Name = Dto.Name;
    todo.IsComplete = Dto.IsComplete;

    await Db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/todoitems/{id}", async (int Id, TodoDb Db) =>
{
    if (await Db.Todos.FindAsync(Id) is Todo todo)
    {
        Db.Todos.Remove(todo);
        await Db.SaveChangesAsync();
        return Results.Ok(new TodoItemDTO(todo));
    }

    return Results.NotFound();
});

As seguintes classes são usadas para refatorar as listas de parâmetros:

class CreateTodoItemRequest
{
    public TodoItemDTO Dto { get; set; } = default!;
    public TodoDb Db { get; set; } = default!;
}

class EditTodoItemRequest
{
    public int Id { get; set; }
    public TodoItemDTO Dto { get; set; } = default!;
    public TodoDb Db { get; set; } = default!;
}

O código a seguir mostra os pontos de extremidade refatorados usando AsParameters e os struct e classes anteriores:

app.MapPost("/ap/todoitems", async ([AsParameters] CreateTodoItemRequest request) =>
{
    var todoItem = new Todo
    {
        IsComplete = request.Dto.IsComplete,
        Name = request.Dto.Name
    };

    request.Db.Todos.Add(todoItem);
    await request.Db.SaveChangesAsync();

    return Results.Created($"/todoitems/{todoItem.Id}", new TodoItemDTO(todoItem));
});

app.MapPut("/ap/todoitems/{id}", async ([AsParameters] EditTodoItemRequest request) =>
{
    var todo = await request.Db.Todos.FindAsync(request.Id);

    if (todo is null) return Results.NotFound();

    todo.Name = request.Dto.Name;
    todo.IsComplete = request.Dto.IsComplete;

    await request.Db.SaveChangesAsync();

    return Results.NoContent();
});

app.MapDelete("/ap/todoitems/{id}", async ([AsParameters] TodoItemRequest request) =>
{
    if (await request.Db.Todos.FindAsync(request.Id) is Todo todo)
    {
        request.Db.Todos.Remove(todo);
        await request.Db.SaveChangesAsync();
        return Results.Ok(new TodoItemDTO(todo));
    }

    return Results.NotFound();
});

Os seguintes tipos de record podem ser usados para substituir os parâmetros anteriores:

record TodoItemRequest(int Id, TodoDb Db);
record CreateTodoItemRequest(TodoItemDTO Dto, TodoDb Db);
record EditTodoItemRequest(int Id, TodoItemDTO Dto, TodoDb Db);

Usar um struct com AsParameters pode ser mais eficiente do que usar um tipo de record.

O código de exemplo completo no repositório AspNetCore.Docs.Samples.

Vinculação personalizada

Há três maneiras de personalizar a vinculação de parâmetros:

  1. Para fontes de vinculação de rota, consulta e cabeçalho, vincule tipos personalizados adicionando um método TryParse estático para o tipo.
  2. Controle o processo de vinculação implementando um método BindAsync em um tipo.
  3. Para cenários avançados, implemente a IBindableFromHttpContext<TSelf> interface para fornecer lógica de vinculação personalizada diretamente do HttpContext.

TryParse

TryParse tem duas APIs:

public static bool TryParse(string value, out T result);
public static bool TryParse(string value, IFormatProvider provider, out T result);

O código a seguir exibe Point: 12.3, 10.1 com o URI /map?Point=12.3,10.1:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// GET /map?Point=12.3,10.1
app.MapGet("/map", (Point point) => $"Point: {point.X}, {point.Y}");

app.Run();

public class Point
{
    public double X { get; set; }
    public double Y { get; set; }

    public static bool TryParse(string? value, IFormatProvider? provider,
                                out Point? point)
    {
        // Format is "(12.3,10.1)"
        var trimmedValue = value?.TrimStart('(').TrimEnd(')');
        var segments = trimmedValue?.Split(',',
                StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (segments?.Length == 2
            && double.TryParse(segments[0], out var x)
            && double.TryParse(segments[1], out var y))
        {
            point = new Point { X = x, Y = y };
            return true;
        }

        point = null;
        return false;
    }
}

BindAsync

BindAsync tem as seguintes APIs:

public static ValueTask<T?> BindAsync(HttpContext context, ParameterInfo parameter);
public static ValueTask<T?> BindAsync(HttpContext context);

O código a seguir exibe SortBy:xyz, SortDirection:Desc, CurrentPage:99 com o URI /products?SortBy=xyz&SortDir=Desc&Page=99:

using System.Reflection;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// GET /products?SortBy=xyz&SortDir=Desc&Page=99
app.MapGet("/products", (PagingData pageData) => $"SortBy:{pageData.SortBy}, " +
       $"SortDirection:{pageData.SortDirection}, CurrentPage:{pageData.CurrentPage}");

app.Run();

public class PagingData
{
    public string? SortBy { get; init; }
    public SortDirection SortDirection { get; init; }
    public int CurrentPage { get; init; } = 1;

    public static ValueTask<PagingData?> BindAsync(HttpContext context,
                                                   ParameterInfo parameter)
    {
        const string sortByKey = "sortBy";
        const string sortDirectionKey = "sortDir";
        const string currentPageKey = "page";

        Enum.TryParse<SortDirection>(context.Request.Query[sortDirectionKey],
                                     ignoreCase: true, out var sortDirection);
        int.TryParse(context.Request.Query[currentPageKey], out var page);
        page = page == 0 ? 1 : page;

        var result = new PagingData
        {
            SortBy = context.Request.Query[sortByKey],
            SortDirection = sortDirection,
            CurrentPage = page
        };

        return ValueTask.FromResult<PagingData?>(result);
    }
}

public enum SortDirection
{
    Default,
    Asc,
    Desc
}

Vinculação de parâmetros personalizados com IBindableFromHttpContext

ASP.NET Core fornece suporte para vinculação de parâmetros personalizados em APIs mínimas usando a IBindableFromHttpContext<TSelf> interface. Essa interface, introduzida com os membros abstratos estáticos do C# 11, permite criar tipos que podem ser vinculados a partir de um contexto HTTP diretamente nos parâmetros do manipulador de rotas.

public interface IBindableFromHttpContext<TSelf>
    where TSelf : class, IBindableFromHttpContext<TSelf>
{
    static abstract ValueTask<TSelf?> BindAsync(HttpContext context, ParameterInfo parameter);
}

Ao implementar a IBindableFromHttpContext<TSelf> interface, você pode criar tipos personalizados que manipulam sua própria lógica de vinculação a partir do HttpContext. Quando um manipulador de rotas inclui um parâmetro desse tipo, a estrutura chama automaticamente o método estático BindAsync para criar a instância:

using CustomBindingExample;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseHttpsRedirection();

app.MapGet("/", () => "Hello, IBindableFromHttpContext example!");

app.MapGet("/custom-binding", (CustomBoundParameter param) =>
{
    return $"Value from custom binding: {param.Value}";
});

app.MapGet("/combined/{id}", (int id, CustomBoundParameter param) =>
{
    return $"ID: {id}, Custom Value: {param.Value}";
});

A seguir está um exemplo de implementação de um parâmetro personalizado que se liga a partir de um cabeçalho HTTP:

using System.Reflection;

namespace CustomBindingExample;

public class CustomBoundParameter : IBindableFromHttpContext<CustomBoundParameter>
{
    public string Value { get; init; } = default!;

    public static ValueTask<CustomBoundParameter?> BindAsync(HttpContext context, ParameterInfo parameter)
    {
        // Custom binding logic here
        // This example reads from a custom header
        var value = context.Request.Headers["X-Custom-Header"].ToString();
        
        // If no header was provided, you could fall back to a query parameter
        if (string.IsNullOrEmpty(value))
        {
            value = context.Request.Query["customValue"].ToString();
        }
        
        return ValueTask.FromResult<CustomBoundParameter?>(new CustomBoundParameter 
        {
            Value = value
        });
    }
}

Você também pode implementar a validação dentro de sua lógica de vinculação personalizada:

app.MapGet("/validated", (ValidatedParameter param) =>
{
    if (string.IsNullOrEmpty(param.Value))
    {
        return Results.BadRequest("Value cannot be empty");
    }
    
    return Results.Ok($"Validated value: {param.Value}");
});

Ver ou transferir o código de exemplo (como transferir)

Falhas de ligação

Quando a vinculação falha, a estrutura registra uma mensagem de depuração e retorna vários códigos de status para o cliente, dependendo do modo de falha.

Modo de falha Tipo de parâmetro anulável Origem da vinculação Código de estado
{ParameterType}.TryParse retorna false Sim rota/consulta/cabeçalho 400
{ParameterType}.BindAsync retorna null Sim Personalizado 400
{ParameterType}.BindAsync lança não importa Personalizado 500
Falha ao desserializar o corpo JSON não importa corpo 400
Tipo de conteúdo errado (não application/json) não importa corpo 415

Precedência de vinculação

As regras para determinar uma fonte vinculativa a partir de um parâmetro:

  1. Atributo explícito definido no parâmetro (atributos From*) na seguinte ordem:
    1. Valores da rota: [FromRoute]
    2. Seqüência de caracteres de consulta: [FromQuery]
    3. Cabeçalho: [FromHeader]
    4. Corpo: [FromBody]
    5. Serviço: [FromServices]
    6. Valores dos parâmetros: [AsParameters]
  2. Tipos especiais
    1. HttpContext
    2. HttpRequest (HttpContext.Request)
    3. HttpResponse (HttpContext.Response)
    4. ClaimsPrincipal (HttpContext.User)
    5. CancellationToken (HttpContext.RequestAborted)
    6. IFormFileCollection (HttpContext.Request.Form.Files)
    7. IFormFile (HttpContext.Request.Form.Files[paramName])
    8. Stream (HttpContext.Request.Body)
    9. PipeReader (HttpContext.Request.BodyReader)
  3. O tipo de parâmetro tem um método BindAsync estático válido.
  4. Tipo de parâmetro é uma cadeia de caracteres ou tem um método TryParse estático válido.
    1. Se o nome do parâmetro existir no modelo de rota. Em app.Map("/todo/{id}", (int id) => {});, id está ligado a partir da rota.
    2. Vinculado a partir da cadeia de caracteres de consulta.
  5. Se o tipo de parâmetro for um serviço fornecido por injeção de dependência, ele usará esse serviço como origem.
  6. O parâmetro é do corpo.

Configurar opções de desserialização JSON para vinculação de corpo

A fonte de vinculação de corpo usa System.Text.Json para desserialização. Não é possível alterar esse padrão, mas as opções de serialização e desserialização JSON podem ser configuradas.

Configurar opções de desserialização JSON globalmente

As opções que se aplicam globalmente a um aplicativo podem ser configuradas invocando ConfigureHttpJsonOptions. O exemplo a seguir inclui campos públicos e formatos de saída JSON.

var builder = WebApplication.CreateBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.WriteIndented = true;
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

app.MapPost("/", (Todo todo) => {
    if (todo is not null) {
        todo.Name = todo.NameField;
    }
    return todo;
});

app.Run();

class Todo {
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "nameField":"Walk dog",
//    "isComplete":false
// }

Como o código de exemplo configura a serialização e a desserialização, ele pode ler NameField e incluir NameField no JSON de saída.

Configurar opções de desserialização JSON para um endpoint

ReadFromJsonAsync tem sobrecargas que aceitam um objeto JsonSerializerOptions. O exemplo a seguir inclui campos públicos e formatos de saída JSON.

using System.Text.Json;

var app = WebApplication.Create();

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { 
    IncludeFields = true, 
    WriteIndented = true
};

app.MapPost("/", async (HttpContext context) => {
    if (context.Request.HasJsonContentType()) {
        var todo = await context.Request.ReadFromJsonAsync<Todo>(options);
        if (todo is not null) {
            todo.Name = todo.NameField;
        }
        return Results.Ok(todo);
    }
    else {
        return Results.BadRequest();
    }
});

app.Run();

class Todo
{
    public string? Name { get; set; }
    public string? NameField;
    public bool IsComplete { get; set; }
}
// If the request body contains the following JSON:
//
// {"nameField":"Walk dog", "isComplete":false}
//
// The endpoint returns the following JSON:
//
// {
//    "name":"Walk dog",
//    "isComplete":false
// }

Como o código anterior aplica as opções personalizadas somente à desserialização, o JSON de saída exclui NameField.

Leia o corpo da requisição

Leia o corpo da solicitação diretamente usando um parâmetro HttpContext ou HttpRequest:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapPost("/uploadstream", async (IConfiguration config, HttpRequest request) =>
{
    var filePath = Path.Combine(config["StoredFilesPath"], Path.GetRandomFileName());

    await using var writeStream = File.Create(filePath);
    await request.BodyReader.CopyToAsync(writeStream);
});

app.Run();

O código anterior:

  • Acede ao corpo da solicitação usando HttpRequest.BodyReader.
  • Copia o corpo da solicitação para um arquivo local.