Partilhar via


Referência rápida de APIs mínimas

Observação

Esta não é a versão mais recente deste artigo. Para a versão atual, consulte a versão .NET 9 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 .NET Core. Para a versão atual, consulte a versão .NET 9 deste artigo.

Importante

Estas informações referem-se a um produto de pré-lançamento que pode ser substancialmente modificado antes de ser lançado comercialmente. A Microsoft não oferece garantias, expressas ou implícitas, em relação às informações fornecidas aqui.

Para a versão atual, consulte a versão .NET 9 deste artigo.

Este documento:

As APIs mínimas consistem em:

WebApplication

O código a seguir é gerado por um modelo ASP.NET Core:

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

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

app.Run();

O código anterior pode ser criado por meio de dotnet new web na linha de comando ou selecionando o modelo da Web vazio no Visual Studio.

O código a seguir cria um WebApplication (app) sem criar explicitamente um WebApplicationBuilder:

var app = WebApplication.Create(args);

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

app.Run();

WebApplication.Create inicializa uma nova instância da classe WebApplication com padrões pré-configurados.

WebApplication adiciona automaticamente o seguinte middleware em Minimal API applications dependendo de determinadas condições:

  • UseDeveloperExceptionPage é adicionado primeiro quando o HostingEnvironment é "Development".
  • UseRouting é adicionado em segundo lugar se o código do usuário ainda não tiver chamado UseRouting e se houver pontos de extremidade configurados, por exemplo, app.MapGet.
  • UseEndpoints é acrescentado no final do pipeline de middleware se qualquer endpoint estiver configurado.
  • UseAuthentication é adicionado imediatamente após UseRouting se o código do usuário ainda não tiver chamado UseAuthentication e se IAuthenticationSchemeProvider puder ser detetado no provedor de serviços. IAuthenticationSchemeProvider é adicionado por padrão ao usar AddAuthenticatione os serviços são detetados usando IServiceProviderIsService.
  • UseAuthorization será adicionado em seguida se o código do usuário ainda não tiver chamado UseAuthorization e se IAuthorizationHandlerProvider puder ser detetado no provedor de serviços. IAuthorizationHandlerProvider é adicionado por padrão ao usar AddAuthorizatione os serviços são detetados usando IServiceProviderIsService.
  • O middleware e os endpoints configurados pelo utilizador são adicionados entre UseRouting e UseEndpoints.

O código a seguir é efetivamente o que o middleware automático que está sendo adicionado ao aplicativo produz:

if (isDevelopment)
{
    app.UseDeveloperExceptionPage();
}

app.UseRouting();

if (isAuthenticationConfigured)
{
    app.UseAuthentication();
}

if (isAuthorizationConfigured)
{
    app.UseAuthorization();
}

// user middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// end user middleware/endpoints

app.UseEndpoints(e => {});

Em alguns casos, a configuração de middleware padrão não está correta para o aplicativo e requer modificação. Por exemplo, UseCors deve ser chamado antes de UseAuthentication e UseAuthorization. A aplicação precisa chamar UseAuthentication e UseAuthorization se UseCors for chamado.

app.UseCors();
app.UseAuthentication();
app.UseAuthorization();

Se o middleware deve ser executado antes que ocorra a correspondência de rota, UseRouting deve ser chamado e o middleware colocado antes da chamada para UseRouting. UseEndpoints não é necessário neste caso, pois é adicionado automaticamente conforme descrito anteriormente:

app.Use((context, next) =>
{
    return next(context);
});

app.UseRouting();

// other middleware and endpoints

Ao adicionar um middleware de terminal:

  • O middleware deve ser adicionado após UseEndpoints.
  • O aplicativo precisa chamar UseRouting e UseEndpoints para que o middleware do terminal possa ser colocado no local correto.
app.UseRouting();

app.MapGet("/", () => "hello world");

app.UseEndpoints(e => {});

app.Run(context =>
{
    context.Response.StatusCode = 404;
    return Task.CompletedTask;
});

O middleware de terminal é executado se nenhum endpoint lidar com a solicitação.

Trabalhar com portas

Quando um aplicativo Web é criado com o Visual Studio ou dotnet new, um arquivo de Properties/launchSettings.json é criado que especifica as portas às quais o aplicativo responde. Nos exemplos de configuração de porta a seguir, a execução do aplicativo do Visual Studio retorna uma caixa de diálogo de erro Unable to connect to web server 'AppName'. Visual Studio retorna um erro porque ele está esperando a porta especificada em Properties/launchSettings.json, mas o aplicativo está usando a porta especificada por app.Run("http://localhost:3000"). Execute os seguintes exemplos de alteração de porta a partir da linha de comandos.

As seções a seguir definem a porta à qual o aplicativo responde.

var app = WebApplication.Create(args);

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

app.Run("http://localhost:3000");

No código anterior, o aplicativo responde à porta 3000.

Várias portas

No código a seguir, o aplicativo responde à porta 3000 e 4000.

var app = WebApplication.Create(args);

app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");

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

app.Run();

Definir a porta a partir da linha de comando

O comando a seguir faz com que o aplicativo responda à porta 7777:

dotnet run --urls="https://localhost:7777"

Se o ponto de extremidade Kestrel também estiver configurado no arquivo appsettings.json, a URL especificada do arquivo appsettings.json será usada. Para obter mais informações, consulte Kestrel configuração do ponto de extremidade

Leia a porta do ambiente

O código a seguir lê a porta do ambiente:

var app = WebApplication.Create(args);

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";

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

app.Run($"http://localhost:{port}");

A maneira preferida de definir a porta do ambiente é usar a variável de ambiente ASPNETCORE_URLS, que é mostrada na seção a seguir.

Definir as portas através da variável de ambiente ASPNETCORE_URLS

A variável de ambiente ASPNETCORE_URLS está disponível para definir a porta:

ASPNETCORE_URLS=http://localhost:3000

ASPNETCORE_URLS suporta vários URLs:

ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000

Ouça em todas as interfaces

Os exemplos a seguir demonstram a escuta em todas as interfaces

Disponível em: http://*:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://*:3000");

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

app.Run();

http://+:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://+:3000");

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

app.Run();

http://0.0.0.0:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://0.0.0.0:3000");

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

app.Run();

Ouça em todas as interfaces usando ASPNETCORE_URLS

Os exemplos anteriores podem usar ASPNETCORE_URLS

ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005

Ouça em todas as interfaces usando ASPNETCORE_HTTPS_PORTS

Os exemplos anteriores podem usar ASPNETCORE_HTTPS_PORTS e ASPNETCORE_HTTP_PORTS.

ASPNETCORE_HTTP_PORTS=3000;5005
ASPNETCORE_HTTPS_PORTS=5000

Para obter mais informações, consulte Configurar endpoints para o servidor web do ASP.NET CoreKestrel

Especificar HTTPS com certificado de desenvolvimento

var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

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

app.Run();

Para obter mais informações sobre o certificado de desenvolvimento, consulte Confiar no certificado de desenvolvimento HTTPS do ASP.NET Core no Windows e macOS.

Especificar HTTPS usando um certificado personalizado

As seções a seguir mostram como especificar o certificado personalizado usando o arquivo appsettings.json e via configuração.

Especifique o certificado personalizado com appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "Certificates": {
      "Default": {
        "Path": "cert.pem",
        "KeyPath": "key.pem"
      }
    }
  }
}

Especifique o certificado personalizado por meio da configuração

var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

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

app.Run();

Usar as APIs de certificado

using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(httpsOptions =>
    {
        var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
        var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");

        httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath, 
                                         keyPath);
    });
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

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

app.Run();

Ler o ambiente

var app = WebApplication.Create(args);

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/oops");
}

app.MapGet("/", () => "Hello World");
app.MapGet("/oops", () => "Oops! An error happened.");

app.Run();

Para obter mais informações sobre como usar o ambiente, consulte Usar vários ambientes no ASP.NET Core

Configuração

O código a seguir lê do sistema de configuração:

var app = WebApplication.Create(args);

var message = app.Configuration["HelloKey"] ?? "Config failed!";

app.MapGet("/", () => message);

app.Run();

Para obter mais informações, consulte Configuração no ASP.NET Core

Registo

O código a seguir grava uma mensagem no log durante o arranque da aplicação.

var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

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

app.Run();

Para obter mais informações, consulte Registo de logs no .NET Core e no ASP.NET Core

Aceder ao contentor de injeção de dependência (DI)

O código a seguir mostra como obter serviços do contêiner DI durante a inicialização do aplicativo:


var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())
{
    var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
    sampleService.DoSomething();
}

app.Run();

O código a seguir mostra como acessar chaves do contêiner DI usando o atributo [FromKeyedServices]:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");

var app = builder.Build();

app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));

app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) => smallCache.Get("date"));

app.Run();

public interface ICache
{
    object Get(string key);
}
public class BigCache : ICache
{
    public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache
{
    public object Get(string key) => $"Resolving {key} from small cache.";
}

Para obter mais informações sobre DI, consulte Injeção de dependência no ASP.NET Core.

WebApplicationBuilder

Esta seção contém código de exemplo usando WebApplicationBuilder.

Alterar a raiz do conteúdo, o nome do aplicativo e o ambiente

O código a seguir define a raiz do conteúdo, o nome do aplicativo e o ambiente:

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging,
    WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();

WebApplication.CreateBuilder inicializa uma nova instância da classe WebApplicationBuilder com padrões pré-configurados.

Para obter mais informações, consulte ASP.NET Visão geral dos fundamentos principais

Alterar a raiz do conteúdo, o nome do aplicativo e o ambiente usando variáveis de ambiente ou linha de comando

A tabela a seguir mostra a variável de ambiente e o argumento de linha de comando usados para alterar a raiz do conteúdo, o nome do aplicativo e o ambiente:

Funcionalidade Variável de ambiente Argumento de linha de comando
Nome do aplicativo ASPNETCORE_APPLICATIONNAME --nome_da_aplicação
Nome do ambiente ASPNETCORE_ENVIRONMENT --ambiente
Raiz do conteúdo ASPNETCORE_CONTENTROOT --contentRoot

Adicionar provedores de configuração

O exemplo a seguir adiciona o provedor de configuração INI:

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();

Para obter informações detalhadas, consulte Provedores de configuração de arquivo em Configuração no ASP.NET Core.

Ler configuração

Por padrão, o WebApplicationBuilder lê a configuração de várias fontes, incluindo:

  • appSettings.json e appSettings.{environment}.json
  • Variáveis de ambiente
  • A linha de comando

Para obter uma lista completa das fontes de configuração lidas, consulte de configuração padrão em Configuration in ASP.NET Core.

O código a seguir lê HelloKey da configuração e mostra o valor no endpoint /. Se o valor de configuração for null, "Hello" será atribuído a message:

var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();

Ler o ambiente

var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    Console.WriteLine($"Running in development.");
}

var app = builder.Build();

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

app.Run();

Adicionar fornecedores de registo

var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();

var app = builder.Build();

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

app.Run();

Adicionar serviços

var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.
builder.Services.AddMemoryCache();

// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();

Personalizar o IHostBuilder

Os métodos de extensão existentes no IHostBuilder podem ser acessados usando a propriedade Host:

var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();

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

app.Run();

Personalizar o IWebHostBuilder

Os métodos de extensão em IWebHostBuilder podem ser acessados usando a propriedade WebApplicationBuilder.WebHost.

var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();

Alterar a raiz da Web

Por padrão, a raiz da Web é relativa à raiz de conteúdo na pasta wwwroot. O diretório raiz da Web é onde o middleware de arquivos estáticos procura arquivos estáticos. A raiz da Web pode ser alterada com WebHostOptions, a linha de comando ou o método UseWebRoot:

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    // Look for static files in webroot
    WebRootPath = "webroot"
});

var app = builder.Build();

app.Run();

Container de injeção de dependências (DI) personalizado

O exemplo a seguir usa Autofac:

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();

Adicionar Middleware

Qualquer middleware existente do ASP.NET Core pode ser configurado no WebApplication:

var app = WebApplication.Create(args);

// Setup the file server to serve static files.
app.UseFileServer();

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

app.Run();

Para obter mais informações, consulte ASP.NET Core Middleware

Página de exceção do desenvolvedor

WebApplication.CreateBuilder inicializa uma nova instância da classe WebApplicationBuilder com padrões pré-configurados. A página de exceção do desenvolvedor está habilitada nos padrões pré-configurados. Quando o código a seguir é executado no ambiente de desenvolvimento , navegar até / renderiza uma página amigável que mostra a exceção.

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
    throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});

app.Run();

ASP.NET Core Middleware

A tabela a seguir lista alguns dos middleware usados com freqüência com APIs mínimas.

Middleware Descrição API
Autenticação Fornece suporte à autenticação. UseAuthentication
Autorização Fornece suporte de autorização. UseAuthorization
CORS Configura o compartilhamento de recursos entre origens. UseCors
manipulador de exceções Lida globalmente com exceções geradas pelo pipeline de middleware. UseExceptionHandler
Encaminhamento de Cabeçalhos Encaminha cabeçalhos proxy para a solicitação atual. UseForwardedHeaders
Redirecionamento HTTPS Redireciona todas as solicitações HTTP para HTTPS. UseHttpsRedirection
Segurança Estrita de Transporte HTTP (HSTS) Middleware de aprimoramento de segurança que adiciona um cabeçalho de resposta especial. UseHsts
Registo de Pedidos Fornece suporte para registrar solicitações e respostas HTTP. UseHttpLogging
Tempo limite de solicitação Fornece suporte para configurar tempos limite de solicitação, padrão global e por ponto de extremidade. UseRequestTimeouts
Registo de Pedidos do W3C Fornece suporte para registrar solicitações e respostas HTTP no formato W3C. UseW3CLogging
Cache de Resposta Fornece suporte para respostas em cache. UseResponseCaching
Compressão de Resposta Fornece suporte para compressão de respostas. UseResponseCompression
Sessão Fornece suporte para gerenciar sessões de usuário. UseSession
arquivos estáticos Fornece suporte para servir arquivos estáticos e navegação em diretórios. UseStaticFiles, UseFileServer
WebSockets Habilita o protocolo WebSockets. UseWebSockets

As seções a seguir abrangem o tratamento de solicitações: roteamento, vinculação de parâmetros e respostas.

Roteamento

Um WebApplication configurado suporta Map{Verb} e MapMethods em que {Verb} é um método HTTP com caixa de camelo, como Get, Post, Put ou Delete:

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

app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");

app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" }, 
                          () => "This is an options or head request ");

app.Run();

Os Delegate argumentos passados para esses métodos são chamados de "manipuladores de rota".

Manipuladores de rota

Os manipuladores de rota são métodos que são executados quando a rota corresponde. Os manipuladores de rota podem ser uma expressão lambda, uma função local, um método de instância ou um método estático. Os manipuladores de rota podem ser síncronos ou assíncronos.

Expressão lambda

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

app.MapGet("/inline", () => "This is an inline lambda");

var handler = () => "This is a lambda variable";

app.MapGet("/", handler);

app.Run();

Função local

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

string LocalFunction() => "This is local function";

app.MapGet("/", LocalFunction);

app.Run();

Método de instância

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

var handler = new HelloHandler();

app.MapGet("/", handler.Hello);

app.Run();

class HelloHandler
{
    public string Hello()
    {
        return "Hello Instance method";
    }
}

Método estático

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

app.MapGet("/", HelloHandler.Hello);

app.Run();

class HelloHandler
{
    public static string Hello()
    {
        return "Hello static method";
    }
}

Ponto final definido fora do Program.cs

As APIs mínimas não precisam estar localizadas em Program.cs.

Program.cs

using MinAPISeparateFile;

var builder = WebApplication.CreateSlimBuilder(args);

var app = builder.Build();

TodoEndpoints.Map(app);

app.Run();

TodoEndpoints.cs

namespace MinAPISeparateFile;

public static class TodoEndpoints
{
    public static void Map(WebApplication app)
    {
        app.MapGet("/", async context =>
        {
            // Get all todo items
            await context.Response.WriteAsJsonAsync(new { Message = "All todo items" });
        });

        app.MapGet("/{id}", async context =>
        {
            // Get one todo item
            await context.Response.WriteAsJsonAsync(new { Message = "One todo item" });
        });
    }
}

Consulte também Grupos de rotas mais adiante neste artigo.

Os pontos de extremidade podem receber nomes para gerar URLs para o ponto de extremidade. Usar um ponto de extremidade nomeado evita ter que codificar caminhos em um aplicativo:

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

app.MapGet("/hello", () => "Hello named route")
   .WithName("hi");

app.MapGet("/", (LinkGenerator linker) => 
        $"The link to the hello route is {linker.GetPathByName("hi", values: null)}");

app.Run();

O código anterior mostra The link to the hello route is /hello a partir do ponto final /.

NOTA: Os nomes dos pontos finais são sensíveis a maiúsculas e minúsculas.

Nomes de pontos finais:

  • Deve ser globalmente único.
  • São usados como o id de operação OpenAPI quando o suporte OpenAPI está ativado. Para obter mais informações, consulte OpenAPI.

Parâmetros de rota

Os parâmetros de rota podem ser capturados como parte da definição do padrão de rota:

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

app.MapGet("/users/{userId}/books/{bookId}", 
    (int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");

app.Run();

O código anterior retorna The user id is 3 and book id is 7 do URI /users/3/books/7.

O manipulador de rotas pode declarar os parâmetros a serem capturados. Quando uma solicitação é feita para uma rota com parâmetros declarados para captura, os parâmetros são analisados e passados para o manipulador. Isso facilita a captura dos valores de forma segura. No código anterior, userId e bookId são int.

No código anterior, se qualquer valor de rota não puder ser convertido em um int, uma exceção será lançada. A solicitação GET /users/hello/books/3 lança a seguinte exceção:

BadHttpRequestException: Failed to bind parameter "int userId" from "hello".

Curinga e pegar todas as rotas

A seguinte rota abrangente retorna Routing to hello do endpoint '/posts/hello':

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

app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");

app.Run();

Restrições de rota

As restrições de rota restringem o comportamento correspondente de uma rota.

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

app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");

app.Run();

A tabela a seguir demonstra os modelos de rota anteriores e seu comportamento:

Modelo de rota Exemplo de URI correspondente
/todos/{id:int} /todos/1
/todos/{text} /todos/something
/posts/{slug:regex(^[a-z0-9_-]+$)} /posts/mypost

Para obter mais informações, consulte referência sobre restrições de rota no Roteamento no ASP.NET Core.

Grupos de rotas

O método de extensão MapGroup ajuda a organizar grupos de pontos de extremidade com um prefixo comum. Ele reduz o código repetitivo e permite personalizar grupos inteiros de endpoints com uma única chamada para métodos como RequireAuthorization e WithMetadata que adicionam metadados de endpoint .

Por exemplo, o código a seguir cria dois grupos semelhantes de pontos de extremidade:

app.MapGroup("/public/todos")
    .MapTodosApi()
    .WithTags("Public");

app.MapGroup("/private/todos")
    .MapTodosApi()
    .WithTags("Private")
    .AddEndpointFilterFactory(QueryPrivateTodos)
    .RequireAuthorization();


EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
    var dbContextIndex = -1;

    foreach (var argument in factoryContext.MethodInfo.GetParameters())
    {
        if (argument.ParameterType == typeof(TodoDb))
        {
            dbContextIndex = argument.Position;
            break;
        }
    }

    // Skip filter if the method doesn't have a TodoDb parameter.
    if (dbContextIndex < 0)
    {
        return next;
    }

    return async invocationContext =>
    {
        var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
        dbContext.IsPrivate = true;

        try
        {
            return await next(invocationContext);
        }
        finally
        {
            // This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
            dbContext.IsPrivate = false;
        }
    };
}
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
    group.MapGet("/", GetAllTodos);
    group.MapGet("/{id}", GetTodo);
    group.MapPost("/", CreateTodo);
    group.MapPut("/{id}", UpdateTodo);
    group.MapDelete("/{id}", DeleteTodo);

    return group;
}

Nesse cenário, você pode usar um endereço relativo para o cabeçalho Location no resultado 201 Created.

public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
    await database.AddAsync(todo);
    await database.SaveChangesAsync();

    return TypedResults.Created($"{todo.Id}", todo);
}

O primeiro grupo de endpoints só corresponderá a pedidos com prefixo /public/todos e será acessível sem necessitar de qualquer autenticação. O segundo grupo de pontos de extremidade só corresponderá a solicitações prefixadas com /private/todos e exigirá autenticação.

A fábrica de filtros de ponto de extremidade QueryPrivateTodos é uma função local que modifica os parâmetros do manipulador de rotas TodoDb para permitir o acesso e o armazenamento de dados de tarefas privadas.

Os grupos de rotas também suportam grupos aninhados e padrões de prefixo complexos com parâmetros e restrições de rota. No exemplo a seguir, o manipulador de rotas mapeado para o grupo user pode capturar os parâmetros de rota {org} e {group} definidos nos prefixos do grupo externo.

O prefixo também pode estar vazio. Isso pode ser útil para adicionar metadados ou filtros de ponto de extremidade a um grupo de pontos de extremidade sem alterar o padrão de rota.

var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");

Adicionar filtros ou metadados a um grupo comporta-se da mesma forma que adicioná-los individualmente a cada ponto de extremidade antes de adicionar quaisquer filtros ou metadados adicionais que possam ter sido adicionados a um grupo interno ou ponto de extremidade específico.

var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");

inner.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/inner group filter");
    return next(context);
});

outer.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/outer group filter");
    return next(context);
});

inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("MapGet filter");
    return next(context);
});

No exemplo acima, o filtro externo registrará a solicitação de entrada antes do filtro interno, mesmo que ele tenha sido adicionado em segundo lugar. Como os filtros foram aplicados a grupos diferentes, a ordem em que foram adicionados em relação uns aos outros não importa. Os filtros de ordem adicionados são importantes se aplicados ao mesmo grupo ou ponto de extremidade específico.

Uma solicitação para /outer/inner/ registrará o seguinte:

/outer group filter
/inner group filter
MapGet filter

Vinculação de parâmetros

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 de ligaçã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 with Minimal APIs.

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 armazenados na fila do armazenamento de Queue do Azure ou armazenados no armazenamento de Blob 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.

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. metadados de OpenAPI são inferidos para parâmetros de formulário para oferecer suporte à integração com 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 cadeia de caracteres de cabeçalhos e cadeias de caracteres 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á duas 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.

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
}

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 status
{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.

Respostas

Os manipuladores de rota suportam os seguintes tipos de valores de retorno:

  1. IResult baseado - Inclui Task<IResult> e ValueTask<IResult>
  2. string - Inclui Task<string> e ValueTask<string>
  3. T (Qualquer outro tipo) - Inclui Task<T> e ValueTask<T>
Valor de retorno Comportamento Tipo de conteúdo
IResult O framework chama IResult.ExecuteAsync Decidido pela implementação IResult
string A estrutura grava a cadeia de caracteres diretamente na resposta text/plain
T (Qualquer outro tipo) A estrutura JSON serializa a resposta application/json

Para obter um guia mais detalhado sobre os valores de retorno do manipulador de rotas, consulte Criar respostas em Aplicativos de API mínimos

Exemplo de valores de retorno

valores de retorno de string

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

Valores de retorno JSON

app.MapGet("/hello", () => new { Message = "Hello World" });

Retornar TypedResults

O código a seguir retorna um TypedResults:

app.MapGet("/hello", () => TypedResults.Ok(new Message() {  Text = "Hello World!" }));

Devolver TypedResults é preferível a devolver Results. Para obter mais informações, consulte TypedResults vs Results.

Valores de retorno IResult

app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));

O exemplo a seguir usa os tipos de resultado internos para personalizar a resposta:

app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
         await db.Todos.FindAsync(id) 
         is Todo todo
         ? Results.Ok(todo) 
         : Results.NotFound())
   .Produces<Todo>(StatusCodes.Status200OK)
   .Produces(StatusCodes.Status404NotFound);

JSON

app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));

Código de status personalizado

app.MapGet("/405", () => Results.StatusCode(405));

Texto

app.MapGet("/text", () => Results.Text("This is some text"));

Transmissão

var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

Consulte Criar respostas em Aplicativos de API mínima para obter mais exemplos.

Redirecionamento

app.MapGet("/old-path", () => Results.Redirect("/new-path"));

Ficheiro

app.MapGet("/download", () => Results.File("myfile.text"));

Resultados incorporados

Existem auxiliares de resultados comuns nas classes Results e TypedResults estáticas. Devolver TypedResults é preferível a devolver Results. Para obter mais informações, consulte TypedResults vs Results.

Modificando cabeçalhos

Use o objeto HttpResponse para modificar cabeçalhos de resposta:

app.MapGet("/", (HttpContext context) => {
    // Set a custom header
    context.Response.Headers["X-Custom-Header"] = "CustomValue";

    // Set a known header
    context.Response.Headers.CacheControl = $"public,max-age=3600";

    return "Hello World";
});

Personalização dos resultados

Os aplicativos podem controlar as respostas implementando um tipo de IResult personalizado. O código a seguir é um exemplo de um tipo de resultado HTML:

using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}

Recomendamos adicionar um método de extensão ao Microsoft.AspNetCore.Http.IResultExtensions para tornar esses resultados personalizados mais detetáveis.

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

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();

Resultados digitados

A interface IResult pode representar valores retornados de APIs mínimas que não utilizam o suporte implícito para JSON serializando o objeto retornado para a resposta HTTP. A classe estática Results é usada para criar objetos de IResult variáveis que representam diferentes tipos de respostas. Por exemplo, definir o código de status da resposta ou redirecionar para outra URL.

Os tipos que implementam IResult são públicos, permitindo asserções de tipo durante o teste. Por exemplo:

[TestClass()]
public class WeatherApiTests
{
    [TestMethod()]
    public void MapWeatherApiTest()
    {
        var result = WeatherApi.GetAllWeathers();
        Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
    }      
}

Você pode examinar os tipos de retorno dos métodos correspondentes na classe estática TypedResults para encontrar o tipo público IResult correto para o qual fazer o casting.

Consulte Criar respostas em Aplicativos de API mínima para obter mais exemplos.

Filtros

Para obter mais informações, consulte Filtros de em aplicativos de API mínima.

Autorização

As rotas podem ser protegidas usando políticas de autorização. Estes podem ser declarados através do atributo [Authorize] ou usando o método RequireAuthorization:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

O código anterior pode ser escrito com RequireAuthorization:

app.MapGet("/auth", () => "This endpoint requires authorization")
   .RequireAuthorization();

O exemplo a seguir usa de autorização baseada em política:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/admin", [Authorize("AdminsOnly")] () => 
                             "The /admin endpoint is for admins only.");

app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
   .RequireAuthorization("AdminsOnly");

app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

Permitir que utilizadores não autenticados acedam a um endpoint

O [AllowAnonymous] permite que utilizadores não autenticados acedam a interfaces:

app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");


app.MapGet("/login2", () => "This endpoint also for all roles.")
   .AllowAnonymous();

CORS

As rotas podem ser CORS ativadas usando políticas CORS. CORS pode ser declarado através do atributo [EnableCors] ou usando o método RequireCors. Os exemplos a seguir habilitam o CORS:

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

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

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

app.Run();
using Microsoft.AspNetCore.Cors;

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

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

app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () => 
                           "This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
                     .RequireCors(MyAllowSpecificOrigins);

app.Run();

Para obter mais informações, consulte Habilitar solicitações entre origens (CORS) no ASP.NET Core

ValidateScopes e ValidateOnBuild

ValidateScopes e ValidateOnBuild são ativados por padrão no ambiente de desenvolvimento , mas desativados em outros ambientes.

Quando ValidateOnBuild é true, o contêiner DI valida a configuração do serviço em tempo de compilação. Se a configuração do serviço for inválida, a compilação falhará na inicialização do aplicativo, em vez de no tempo de execução quando o serviço for solicitado.

Quando ValidateScopes é true, o contêiner DI valida que um serviço com escopo não foi resolvido a partir do escopo raiz. Resolver um serviço com escopo a partir do escopo raiz pode resultar em um vazamento de memória porque o serviço é retido na memória por mais tempo do que o escopo da solicitação.

ValidateScopes e ValidateOnBuild são falsos por padrão em modos que não são de desenvolvimento por motivos de desempenho.

O código a seguir mostra ValidateScopes está habilitado por padrão no modo de desenvolvimento, mas desabilitado no modo de versão:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<MyScopedService>();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    Console.WriteLine("Development environment");
}
else
{
    Console.WriteLine("Release environment");
}

app.MapGet("/", context =>
{
    // Intentionally getting service provider from app, not from the request
    // This causes an exception from attempting to resolve a scoped service
    // outside of a scope.
    // Throws System.InvalidOperationException:
    // 'Cannot resolve scoped service 'MyScopedService' from root provider.'
    var service = app.Services.GetRequiredService<MyScopedService>();
    return context.Response.WriteAsync("Service resolved");
});

app.Run();

public class MyScopedService { }

O código a seguir mostra ValidateOnBuild está habilitado por padrão no modo de desenvolvimento, mas desabilitado no modo de versão:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<MyScopedService>();
builder.Services.AddScoped<AnotherService>();

// System.AggregateException: 'Some services are not able to be constructed (Error
// while validating the service descriptor 'ServiceType: AnotherService Lifetime:
// Scoped ImplementationType: AnotherService': Unable to resolve service for type
// 'BrokenService' while attempting to activate 'AnotherService'.)'
var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    Console.WriteLine("Development environment");
}
else
{
    Console.WriteLine("Release environment");
}

app.MapGet("/", context =>
{
    var service = context.RequestServices.GetRequiredService<MyScopedService>();
    return context.Response.WriteAsync("Service resolved correctly!");
});

app.Run();

public class MyScopedService { }

public class AnotherService
{
    public AnotherService(BrokenService brokenService) { }
}

public class BrokenService { }

O código a seguir desativa ValidateScopes e ValidateOnBuild em Development:

var builder = WebApplication.CreateBuilder(args);

if (builder.Environment.IsDevelopment())
{
    Console.WriteLine("Development environment");
    // Doesn't detect the validation problems because ValidateScopes is false.
    builder.Host.UseDefaultServiceProvider(options =>
    {
        options.ValidateScopes = false;
        options.ValidateOnBuild = false;
    });
}

Ver também

Este documento:

As APIs mínimas consistem em:

WebApplication

O código a seguir é gerado por um modelo ASP.NET Core:

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

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

app.Run();

O código anterior pode ser criado por meio de dotnet new web na linha de comando ou selecionando o modelo da Web vazio no Visual Studio.

O código a seguir cria um WebApplication (app) sem criar explicitamente um WebApplicationBuilder:

var app = WebApplication.Create(args);

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

app.Run();

WebApplication.Create inicializa uma nova instância da classe WebApplication com padrões pré-configurados.

WebApplication adiciona automaticamente o seguinte middleware em Minimal API applications dependendo de determinadas condições:

  • UseDeveloperExceptionPage é adicionado primeiro quando o HostingEnvironment é "Development".
  • UseRouting é adicionado em segundo lugar se o código do usuário ainda não tiver chamado UseRouting e se houver pontos de extremidade configurados, por exemplo, app.MapGet.
  • UseEndpoints é adicionado ao final do pipeline de middleware se algum endpoint estiver configurado.
  • UseAuthentication é adicionado imediatamente após UseRouting se o código do usuário ainda não tiver chamado UseAuthentication e se IAuthenticationSchemeProvider puder ser detetado no provedor de serviços. IAuthenticationSchemeProvider é adicionado por padrão ao usar AddAuthenticatione os serviços são detetados usando IServiceProviderIsService.
  • UseAuthorization será adicionado em seguida se o código do usuário ainda não tiver chamado UseAuthorization e se IAuthorizationHandlerProvider puder ser detetado no provedor de serviços. IAuthorizationHandlerProvider é adicionado por padrão ao usar AddAuthorizatione os serviços são detetados usando IServiceProviderIsService.
  • O middleware e os pontos de extremidade configurados pelo usuário são adicionados entre UseRouting e UseEndpoints.

O código a seguir é efetivamente o que o middleware automático que está sendo adicionado ao aplicativo produz:

if (isDevelopment)
{
    app.UseDeveloperExceptionPage();
}

app.UseRouting();

if (isAuthenticationConfigured)
{
    app.UseAuthentication();
}

if (isAuthorizationConfigured)
{
    app.UseAuthorization();
}

// user middleware/endpoints
app.CustomMiddleware(...);
app.MapGet("/", () => "hello world");
// end user middleware/endpoints

app.UseEndpoints(e => {});

Em alguns casos, a configuração de middleware padrão não está correta para o aplicativo e requer modificação. Por exemplo, UseCors deve ser chamado antes de UseAuthentication e UseAuthorization. A aplicação precisa chamar UseAuthentication e UseAuthorization se UseCors for chamado.

app.UseCors();
app.UseAuthentication();
app.UseAuthorization();

Se o middleware deverá ser executado antes que a correspondência de rota ocorra, UseRouting deverá ser chamado e o middleware deverá ser colocado antes da chamada para UseRouting. UseEndpoints não é necessário neste caso, pois é adicionado automaticamente conforme descrito anteriormente:

app.Use((context, next) =>
{
    return next(context);
});

app.UseRouting();

// other middleware and endpoints

Ao adicionar um middleware de terminal:

  • O middleware deve ser adicionado após UseEndpoints.
  • O aplicativo precisa chamar UseRouting e UseEndpoints para que o middleware do terminal possa ser colocado no local correto.
app.UseRouting();

app.MapGet("/", () => "hello world");

app.UseEndpoints(e => {});

app.Run(context =>
{
    context.Response.StatusCode = 404;
    return Task.CompletedTask;
});

O middleware de terminal é um middleware que é executado se nenhum ponto de extremidade lidar com a solicitação.

Trabalhar com portas

Quando um aplicativo Web é criado com o Visual Studio ou dotnet new, um arquivo de Properties/launchSettings.json é criado que especifica as portas às quais o aplicativo responde. Nos exemplos de configuração de porta a seguir, a execução do aplicativo do Visual Studio retorna uma caixa de diálogo de erro Unable to connect to web server 'AppName'. Visual Studio retorna um erro porque ele está esperando a porta especificada em Properties/launchSettings.json, mas o aplicativo está usando a porta especificada por app.Run("http://localhost:3000"). Execute os seguintes exemplos de alteração de porta a partir da linha de comando.

As seções a seguir definem a porta à qual o aplicativo responde.

var app = WebApplication.Create(args);

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

app.Run("http://localhost:3000");

No código anterior, o aplicativo responde à porta 3000.

Várias portas

No código a seguir, o aplicativo responde à porta 3000 e 4000.

var app = WebApplication.Create(args);

app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");

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

app.Run();

Definir a porta a partir da linha de comando

O comando a seguir faz com que o aplicativo responda à porta 7777:

dotnet run --urls="https://localhost:7777"

Se o ponto de extremidade Kestrel também estiver configurado no arquivo appsettings.json, a URL especificada do arquivo appsettings.json será usada. Para mais informações, consulte a configuração do ponto de extremidade Kestrel

Leia a porta do ambiente

O código a seguir lê a porta do ambiente:

var app = WebApplication.Create(args);

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";

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

app.Run($"http://localhost:{port}");

A maneira preferida de definir a porta do ambiente é usar a variável de ambiente ASPNETCORE_URLS, que é mostrada na seção a seguir.

Definir as portas através da variável de ambiente ASPNETCORE_URLS

A variável de ambiente ASPNETCORE_URLS está disponível para definir a porta:

ASPNETCORE_URLS=http://localhost:3000

ASPNETCORE_URLS suporta vários URLs:

ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000

Para obter mais informações sobre como usar o ambiente, consulte Usar vários ambientes no ASP.NET Core

Ouça em todas as interfaces

Os exemplos a seguir demonstram a escuta em todas as interfaces

Disponível em: http://*:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://*:3000");

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

app.Run();

http://+:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://+:3000");

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

app.Run();

http://0.0.0.0:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://0.0.0.0:3000");

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

app.Run();

Ouça em todas as interfaces usando ASPNETCORE_URLS

Os exemplos anteriores podem usar ASPNETCORE_URLS

ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005

Especificar HTTPS com certificado de desenvolvimento

var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

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

app.Run();

Para obter mais informações sobre o certificado de desenvolvimento, consulte Confiar no certificado de desenvolvimento HTTPS do ASP.NET Core no Windows e macOS.

Especificar HTTPS usando um certificado personalizado

As seções a seguir mostram como especificar o certificado personalizado usando o arquivo appsettings.json e via configuração.

Especifique o certificado personalizado com appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "Certificates": {
      "Default": {
        "Path": "cert.pem",
        "KeyPath": "key.pem"
      }
    }
  }
}

Especifique o certificado personalizado por meio da configuração

var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

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

app.Run();

Usar as APIs de certificado

using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(httpsOptions =>
    {
        var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
        var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");

        httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath, 
                                         keyPath);
    });
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

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

app.Run();

Configuração

O código a seguir lê do sistema de configuração:

var app = WebApplication.Create(args);

var message = app.Configuration["HelloKey"] ?? "Config failed!";

app.MapGet("/", () => message);

app.Run();

Para obter mais informações, consulte Configuração no ASP.NET Core

Registo

O seguinte código escreve uma mensagem no log ao iniciar a aplicação:

var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

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

app.Run();

Para obter mais informações, consulte Registo de logs no .NET Core e no ASP.NET Core

Acessar o contêiner de injeção de dependência (DI)

O código a seguir mostra como obter serviços do contêiner DI durante a inicialização do aplicativo:


var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())
{
    var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
    sampleService.DoSomething();
}

app.Run();

Para mais informações, consulte Injeção de Dependências no ASP.NET Core.

WebApplicationBuilder

Esta seção contém código de exemplo usando WebApplicationBuilder.

Alterar a raiz do conteúdo, o nome do aplicativo e o ambiente

O código a seguir define a raiz do conteúdo, o nome do aplicativo e o ambiente:

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging,
    WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();

WebApplication.CreateBuilder inicializa uma nova instância da classe WebApplicationBuilder com padrões pré-configurados.

Para obter mais informações, consulte ASP.NET Visão geral dos fundamentos principais

Alterar a raiz do conteúdo, o nome do aplicativo e o ambiente por variáveis de ambiente ou linha de comando

A tabela a seguir mostra a variável de ambiente e o argumento de linha de comando usados para alterar a raiz do conteúdo, o nome do aplicativo e o ambiente:

Funcionalidade Variável de ambiente Argumento de linha de comando
Nome do aplicativo ASPNETCORE_APPLICATIONNAME --nome_da_aplicação
Nome do ambiente ASPNETCORE_ENVIRONMENT --ambiente
Raiz do conteúdo ASPNETCORE_CONTENTROOT --contentRoot

Adicionar provedores de configuração

O exemplo a seguir adiciona o provedor de configuração INI:

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();

Para obter informações detalhadas, consulte Provedores de configuração de arquivo em Configuração no ASP.NET Core.

Ler configuração

Por padrão, o WebApplicationBuilder lê a configuração de várias fontes, incluindo:

  • appSettings.json e appSettings.{environment}.json
  • Variáveis de ambiente
  • A linha de comando

O código a seguir lê HelloKey da configuração e exibe o valor no endpoint /. Se o valor de configuração for null, "Hello" será atribuído a message:

var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();

Para obter uma lista completa das fontes de configuração lidas, consulte configuração padrão em Configuração no ASP.NET Core

Adicionar fornecedores de registo

var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();

var app = builder.Build();

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

app.Run();

Adicionar serviços

var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.
builder.Services.AddMemoryCache();

// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();

Personalizar o IHostBuilder

Os métodos de extensão existentes no IHostBuilder podem ser acessados usando a propriedade Host:

var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();

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

app.Run();

Personalizar o IWebHostBuilder

Os métodos de extensão em IWebHostBuilder podem ser acessados usando a propriedade WebApplicationBuilder.WebHost.

var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();

alterar a raiz da web

Por padrão, a raiz web é relativa à raiz de conteúdo na pasta wwwroot. A raiz da Web é onde o middleware de arquivos estáticos procura arquivos estáticos. A raiz da Web pode ser alterada através de WebHostOptions, da linha de comando ou do método UseWebRoot:

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    // Look for static files in webroot
    WebRootPath = "webroot"
});

var app = builder.Build();

app.Run();

Contentor personalizado de injeção de dependências (DI)

O exemplo a seguir usa Autofac:

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();

Adicionar middleware

Qualquer middleware existente do ASP.NET Core pode ser configurado no WebApplication:

var app = WebApplication.Create(args);

// Setup the file server to serve static files.
app.UseFileServer();

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

app.Run();

Para obter mais informações, consulte ASP.NET Core Middleware

Página de exceção do desenvolvedor

WebApplication.CreateBuilder inicializa uma nova instância da classe WebApplicationBuilder com padrões pré-configurados. A página de exceção do desenvolvedor está habilitada nos padrões pré-configurados. Quando o código a seguir é executado no ambiente de desenvolvimento , navegar até / renderiza uma página amigável que mostra a exceção.

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
    throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});

app.Run();

ASP.NET Core Middleware

A tabela a seguir lista alguns dos middleware usados com freqüência com APIs mínimas.

Middleware Descrição Interface de Programação de Aplicações (API)
Autenticação Fornece suporte à autenticação. UseAuthentication
Autorização Fornece suporte de autorização. UseAuthorization
CORS Configura o compartilhamento de recursos entre origens. UseCors
Manipulador de Exceções Lida globalmente com exceções geradas pelo pipeline de middleware. UseExceptionHandler
Cabeçalhos Encaminhados Encaminha cabeçalhos com proxy para a solicitação atual. UseForwardedHeaders
Redirecionamento HTTPS Redireciona todas as solicitações HTTP para HTTPS. UseHttpsRedirection
Segurança Rígida de Transporte HTTP (HSTS) Middleware de aprimoramento de segurança que adiciona um cabeçalho de resposta especial. UseHsts
Registo de Pedidos Fornece suporte para registrar solicitações e respostas HTTP. UseHttpLogging
Tempos limite de solicitações Fornece suporte para configurar tempos limite de solicitação, padrão global e por ponto de extremidade. UseRequestTimeouts
Registo de Solicitações do W3C Fornece suporte para registrar solicitações e respostas HTTP no formato W3C. UseW3CLogging
Cache de Resposta Fornece suporte para respostas em cache. UseResponseCaching
compressão de resposta Fornece suporte para compressão de respostas. UseResponseCompression
Sessão Fornece suporte para gerenciar sessões de usuário. UseSession
arquivos estáticos Fornece suporte para servir arquivos estáticos e navegação em diretórios. UseStaticFiles, UseFileServer
WebSockets Habilita o protocolo WebSockets. UseWebSockets

As seções a seguir abrangem o tratamento de solicitações: roteamento, vinculação de parâmetros e respostas.

Roteamento

Um WebApplication configurado suporta Map{Verb} e MapMethods em que {Verb} é um método HTTP com caixa de camelo, como Get, Post, Put ou Delete:

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

app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");

app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" }, 
                          () => "This is an options or head request ");

app.Run();

Os Delegate argumentos passados para esses métodos são chamados de "manipuladores de rota".

Manipuladores de rota

Os manipuladores de rota são métodos que são executados quando a rota corresponde. Os manipuladores de rota podem ser uma expressão lambda, uma função local, um método de instância ou um método estático. Os manipuladores de rota podem ser síncronos ou assíncronos.

Expressão lambda

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

app.MapGet("/inline", () => "This is an inline lambda");

var handler = () => "This is a lambda variable";

app.MapGet("/", handler);

app.Run();

Função local

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

string LocalFunction() => "This is local function";

app.MapGet("/", LocalFunction);

app.Run();

Método de instância

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

var handler = new HelloHandler();

app.MapGet("/", handler.Hello);

app.Run();

class HelloHandler
{
    public string Hello()
    {
        return "Hello Instance method";
    }
}

Método estático

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

app.MapGet("/", HelloHandler.Hello);

app.Run();

class HelloHandler
{
    public static string Hello()
    {
        return "Hello static method";
    }
}

Ponto final definido fora do Program.cs

As APIs mínimas não precisam estar localizadas em Program.cs.

Program.cs

using MinAPISeparateFile;

var builder = WebApplication.CreateSlimBuilder(args);

var app = builder.Build();

TodoEndpoints.Map(app);

app.Run();

TodoEndpoints.cs

namespace MinAPISeparateFile;

public static class TodoEndpoints
{
    public static void Map(WebApplication app)
    {
        app.MapGet("/", async context =>
        {
            // Get all todo items
            await context.Response.WriteAsJsonAsync(new { Message = "All todo items" });
        });

        app.MapGet("/{id}", async context =>
        {
            // Get one todo item
            await context.Response.WriteAsJsonAsync(new { Message = "One todo item" });
        });
    }
}

Consulte também Grupos de rotas mais adiante neste artigo.

Os pontos de extremidade podem receber nomes para gerar URLs para o ponto de extremidade. Usar um ponto de extremidade nomeado evita ter que codificar caminhos em um aplicativo:

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

app.MapGet("/hello", () => "Hello named route")
   .WithName("hi");

app.MapGet("/", (LinkGenerator linker) => 
        $"The link to the hello route is {linker.GetPathByName("hi", values: null)}");

app.Run();

O código anterior mostra The link to the hello route is /hello a partir do ponto de extremidade /.

NOTA: Os nomes dos pontos finais diferenciam maiúsculas de minúsculas.

Nomes de pontos finais:

  • Deve ser globalmente único.
  • São usados como o id de operação OpenAPI quando o suporte OpenAPI está ativado. Para obter mais informações, consulte OpenAPI.

Parâmetros de rota

Os parâmetros de rota podem ser capturados como parte da definição do padrão de rota:

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

app.MapGet("/users/{userId}/books/{bookId}", 
    (int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");

app.Run();

O código anterior retorna The user id is 3 and book id is 7 do URI /users/3/books/7.

O manipulador de rotas pode declarar os parâmetros a serem capturados. Quando uma solicitação é feita para uma rota com parâmetros declarados para captura, os parâmetros são analisados e passados para o manipulador. Isso facilita a captura dos valores de forma segura. No código anterior, userId e bookId são int.

No código anterior, se qualquer valor de rota não puder ser convertido em um int, uma exceção será lançada. A solicitação GET /users/hello/books/3 lança a seguinte exceção:

BadHttpRequestException: Failed to bind parameter "int userId" from "hello".

Curinga e pegar todas as rotas

A seguinte rota catch-all retorna Routing to hello do endpoint '/posts/hello':

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

app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");

app.Run();

Restrições de rota

As restrições de rota restringem o comportamento correspondente de uma rota.

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

app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");

app.Run();

A tabela a seguir demonstra os modelos de rota anteriores e seu comportamento:

Modelo de rota Exemplo de URI correspondente
/todos/{id:int} /todos/1
/todos/{text} /todos/something
/posts/{slug:regex(^[a-z0-9_-]+$)} /posts/mypost

Para obter mais informações, consulte de referência de restrição de rota em Roteamento no ASP.NET Core.

Grupos de rotas

O método de extensão MapGroup ajuda a organizar grupos de pontos de extremidade com um prefixo comum. Ele reduz o código repetitivo e permite personalizar grupos inteiros de endpoints com uma única chamada para métodos como RequireAuthorization e WithMetadata, que adicionam metadados de endpoint .

Por exemplo, o código a seguir cria dois grupos semelhantes de pontos de extremidade:

app.MapGroup("/public/todos")
    .MapTodosApi()
    .WithTags("Public");

app.MapGroup("/private/todos")
    .MapTodosApi()
    .WithTags("Private")
    .AddEndpointFilterFactory(QueryPrivateTodos)
    .RequireAuthorization();


EndpointFilterDelegate QueryPrivateTodos(EndpointFilterFactoryContext factoryContext, EndpointFilterDelegate next)
{
    var dbContextIndex = -1;

    foreach (var argument in factoryContext.MethodInfo.GetParameters())
    {
        if (argument.ParameterType == typeof(TodoDb))
        {
            dbContextIndex = argument.Position;
            break;
        }
    }

    // Skip filter if the method doesn't have a TodoDb parameter.
    if (dbContextIndex < 0)
    {
        return next;
    }

    return async invocationContext =>
    {
        var dbContext = invocationContext.GetArgument<TodoDb>(dbContextIndex);
        dbContext.IsPrivate = true;

        try
        {
            return await next(invocationContext);
        }
        finally
        {
            // This should only be relevant if you're pooling or otherwise reusing the DbContext instance.
            dbContext.IsPrivate = false;
        }
    };
}
public static RouteGroupBuilder MapTodosApi(this RouteGroupBuilder group)
{
    group.MapGet("/", GetAllTodos);
    group.MapGet("/{id}", GetTodo);
    group.MapPost("/", CreateTodo);
    group.MapPut("/{id}", UpdateTodo);
    group.MapDelete("/{id}", DeleteTodo);

    return group;
}

Nesse cenário, pode-se usar um endereço relativo para o cabeçalho Location no resultado 201 Created.

public static async Task<Created<Todo>> CreateTodo(Todo todo, TodoDb database)
{
    await database.AddAsync(todo);
    await database.SaveChangesAsync();

    return TypedResults.Created($"{todo.Id}", todo);
}

O primeiro grupo de endpoints irá apenas corresponder a pedidos prefixados com /public/todos e são acessíveis sem qualquer autenticação. O segundo grupo de pontos de extremidade só corresponderá a solicitações prefixadas com /private/todos e exigirá autenticação.

O filtro de fábrica do endpoint QueryPrivateTodos é uma função local que modifica os parâmetros do manipulador de rotas TodoDb para permitir o acesso e armazenamento de dados privados de tarefas.

Os grupos de rotas também suportam grupos aninhados e padrões de prefixo complexos com parâmetros e restrições de rota. No exemplo a seguir, o manipulador de rotas mapeado para o grupo user pode capturar os parâmetros de rota {org} e {group} definidos pelos prefixos do grupo externo.

O prefixo também pode estar vazio. Isso pode ser útil para adicionar metadados ou filtros de ponto de extremidade a um grupo de pontos de extremidade sem alterar o padrão de rota.

var all = app.MapGroup("").WithOpenApi();
var org = all.MapGroup("{org}");
var user = org.MapGroup("{user}");
user.MapGet("", (string org, string user) => $"{org}/{user}");

Adicionar filtros ou metadados a um grupo comporta-se da mesma forma que adicioná-los individualmente a cada ponto de extremidade antes de adicionar quaisquer filtros ou metadados adicionais que possam ter sido adicionados a um grupo interno ou ponto de extremidade específico.

var outer = app.MapGroup("/outer");
var inner = outer.MapGroup("/inner");

inner.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/inner group filter");
    return next(context);
});

outer.AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("/outer group filter");
    return next(context);
});

inner.MapGet("/", () => "Hi!").AddEndpointFilter((context, next) =>
{
    app.Logger.LogInformation("MapGet filter");
    return next(context);
});

No exemplo acima, o filtro externo registrará a solicitação de entrada antes do filtro interno, mesmo que ele tenha sido adicionado em segundo lugar. Como os filtros foram aplicados a grupos diferentes, a ordem em que foram adicionados em relação uns aos outros não importa. Os filtros de ordem adicionados são importantes se aplicados ao mesmo grupo ou ponto de extremidade específico.

Uma solicitação para /outer/inner/ registrará o seguinte:

/outer group filter
/inner group filter
MapGet filter

Vinculação de parâmetros

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 rota
  • string de consulta
  • Cabeçalho
  • Corpo (como 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 ligam implicitamente a partir do corpo. Para vincular a partir do corpo (como JSON) para esses métodos HTTP, vincular explicitamente com [FromBody] ou ler 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 nativamente no .NET 6 e 7.

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

A associação de parâmetros para APIs mínimas é realizada através de injeção de dependência quando o tipo é 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

Os valores nulos e 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 vincular o parâmetro "Nullable<int> pageNumber" de "dois".
/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 utilizador 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 de blobs ou enfileire os dados em um fornecedor de filas.
  • Processe os dados armazenados com um processo de trabalho ou função de nuvem.

Por exemplo, os dados podem ser enfileirados para de armazenamento de filas do Azure ou armazenados em de armazenamento de Blob 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 que o corpo é lido, ele não é rebobinável. 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 ficheiro autenticado são suportadas usando um cabeçalho Authorization, um certificado de cliente ou um cabeçalho cookie.

Não há suporte integrado para antifalsificação no ASP.NET Core 7.0. Antifalsificação está disponível no ASP.NET Core 8.0 e em versões posteriores. No entanto, ele pode ser implementado usando o serviço IAntiforgery.

Vincular matrizes e valores de strings de cabeçalhos e de consultas

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 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 ponto de extremidade 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 utiliza 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 código a seguir mostra endpoints 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 seguinte código mostra os endpoints refatorados usando AsParameters e os anteriores struct e classes:

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.

Encadernação personalizada

Há duas 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.

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
}

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 status
{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
Erro 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 Vinculativa

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 da string 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 possui 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 do pedido

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.

Respostas

Os manipuladores de rota suportam os seguintes tipos de valores de retorno:

  1. Base IResult - Isto inclui Task<IResult> e ValueTask<IResult>
  2. string - Inclui Task<string> e ValueTask<string>
  3. T (Qualquer outro tipo) - Inclui Task<T> e ValueTask<T>
Valor de retorno Comportamento Tipo de conteúdo
IResult A estrutura chama IResult.ExecuteAsync Decidido pela implementação IResult
string A estrutura grava a cadeia de caracteres diretamente na resposta text/plain
T (Qualquer outro tipo) A estrutura JSON serializa a resposta application/json

Para obter um guia mais detalhado sobre os valores de retorno do manipulador de rotas, consulte Criar respostas em Aplicativos de API mínimos

Exemplo de valores de retorno

valores de retorno de string

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

Valores de retorno JSON

app.MapGet("/hello", () => new { Message = "Hello World" });

Retornar TypedResults

O código a seguir retorna um TypedResults:

app.MapGet("/hello", () => TypedResults.Ok(new Message() {  Text = "Hello World!" }));

Devolver TypedResults é preferível a devolver Results. Para obter mais informações, consulte TypedResults vs Results.

Valores de retorno de IResult

app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));

O exemplo a seguir usa os tipos de resultado internos para personalizar a resposta:

app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
         await db.Todos.FindAsync(id) 
         is Todo todo
         ? Results.Ok(todo) 
         : Results.NotFound())
   .Produces<Todo>(StatusCodes.Status200OK)
   .Produces(StatusCodes.Status404NotFound);

JSON

app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));

Código de status personalizado

app.MapGet("/405", () => Results.StatusCode(405));

Texto

app.MapGet("/text", () => Results.Text("This is some text"));

Transmissão

var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

Consulte Criar respostas em Aplicativos de API mínima para obter mais exemplos.

Redirecionamento

app.MapGet("/old-path", () => Results.Redirect("/new-path"));

Ficheiro

app.MapGet("/download", () => Results.File("myfile.text"));

Resultados incorporados

Existem auxiliares de resultados comuns nas classes Results e TypedResults estáticas. Devolver TypedResults é preferível a devolver Results. Para obter mais informações, consulte TypedResults vs Results.

Personalização dos resultados

Os aplicativos podem controlar as respostas implementando um tipo de IResult personalizado. O código a seguir é um exemplo de um tipo de resultado HTML:

using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}

Recomendamos adicionar um método de extensão ao Microsoft.AspNetCore.Http.IResultExtensions para tornar esses resultados personalizados mais detetáveis.

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

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();

Resultados digitados

A interface IResult pode representar valores retornados de APIs mínimas que não utilizam o suporte implícito para JSON serializando o objeto retornado para a resposta HTTP. A classe estática Results é usada para criar objetos de IResult variáveis que representam diferentes tipos de respostas. Por exemplo, definir o código de status da resposta ou redirecionar para outra URL.

Os tipos que implementam IResult são públicos, permitindo asserções de tipo durante o teste. Por exemplo:

[TestClass()]
public class WeatherApiTests
{
    [TestMethod()]
    public void MapWeatherApiTest()
    {
        var result = WeatherApi.GetAllWeathers();
        Assert.IsInstanceOfType(result, typeof(Ok<WeatherForecast[]>));
    }      
}

Você pode examinar os tipos de retorno dos métodos correspondentes na classe estática TypedResults para encontrar o tipo público IResult correto para o qual efetuar a conversão.

Consulte Criar respostas em Aplicativos de API mínima para obter mais exemplos.

Filtros

Consulte Filtros em aplicações de API mínima

Autorização

As rotas podem ser protegidas usando políticas de autorização. Estes podem ser declarados através do atributo [Authorize] ou usando o método RequireAuthorization:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

O código anterior pode ser escrito com RequireAuthorization:

app.MapGet("/auth", () => "This endpoint requires authorization")
   .RequireAuthorization();

O exemplo a seguir usa a autorização baseada em política :

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/admin", [Authorize("AdminsOnly")] () => 
                             "The /admin endpoint is for admins only.");

app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
   .RequireAuthorization("AdminsOnly");

app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

Permitir que utilizadores não autenticados acessem um endpoint.

O [AllowAnonymous] permite que utilizadores não autenticados acedam aos pontos de extremidade.

app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");


app.MapGet("/login2", () => "This endpoint also for all roles.")
   .AllowAnonymous();

CORS

As rotas podem ser CORS ativadas usando políticas CORS. CORS pode ser declarado através do atributo [EnableCors] ou usando o método RequireCors. Os exemplos a seguir habilitam o CORS:

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

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

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

app.Run();
using Microsoft.AspNetCore.Cors;

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

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

app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () => 
                           "This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
                     .RequireCors(MyAllowSpecificOrigins);

app.Run();

Para obter mais informações, consulte Habilitar solicitações entre origens (CORS) no ASP.NET Core

Ver também

Este documento:

As APIs mínimas consistem em:

WebApplication

O código a seguir é gerado por um modelo ASP.NET Core:

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

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

app.Run();

O código anterior pode ser criado por meio de dotnet new web na linha de comando ou selecionando o modelo da Web vazio no Visual Studio.

O código a seguir cria um WebApplication (app) sem criar explicitamente um WebApplicationBuilder:

var app = WebApplication.Create(args);

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

app.Run();

WebApplication.Create inicializa uma nova instância da classe WebApplication com padrões pré-configurados.

Trabalhar com portas

Quando um aplicativo Web é criado com o Visual Studio ou dotnet new, um arquivo de Properties/launchSettings.json é criado que especifica as portas às quais o aplicativo responde. Nos exemplos de configuração de porta a seguir, a execução do aplicativo do Visual Studio retorna uma caixa de diálogo de erro Unable to connect to web server 'AppName'. Execute os seguintes exemplos de alteração de porta a partir da linha de comandos.

As seções a seguir definem a porta à qual o aplicativo responde.

var app = WebApplication.Create(args);

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

app.Run("http://localhost:3000");

No código anterior, o aplicativo responde à porta 3000.

Várias portas

No código a seguir, o aplicativo responde à porta 3000 e 4000.

var app = WebApplication.Create(args);

app.Urls.Add("http://localhost:3000");
app.Urls.Add("http://localhost:4000");

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

app.Run();

Definir a porta a partir da linha de comando

O comando a seguir faz com que o aplicativo responda à porta 7777:

dotnet run --urls="https://localhost:7777"

Se o ponto de extremidade Kestrel também estiver configurado no arquivo appsettings.json, a URL especificada do arquivo appsettings.json será usada. Para obter mais informações, consulte a configuração do ponto de extremidade Kestrel

Leia a porta do ambiente de execução

O código a seguir lê a porta do ambiente:

var app = WebApplication.Create(args);

var port = Environment.GetEnvironmentVariable("PORT") ?? "3000";

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

app.Run($"http://localhost:{port}");

A maneira preferida de definir a porta do ambiente é usar a variável de ambiente ASPNETCORE_URLS, que é mostrada na seção a seguir.

Definir as portas através da variável de ambiente ASPNETCORE_URLS

A variável de ambiente ASPNETCORE_URLS está disponível para definir a porta:

ASPNETCORE_URLS=http://localhost:3000

ASPNETCORE_URLS suporta vários URLs:

ASPNETCORE_URLS=http://localhost:3000;https://localhost:5000

Ouça em todas as interfaces

Os exemplos a seguir demonstram a escuta em todas as interfaces

Disponível em: http://*:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://*:3000");

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

app.Run();

http://+:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://+:3000");

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

app.Run();

http://0.0.0.0:3000

var app = WebApplication.Create(args);

app.Urls.Add("http://0.0.0.0:3000");

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

app.Run();

Ouça em todas as interfaces usando ASPNETCORE_URLS

Os exemplos anteriores podem usar ASPNETCORE_URLS

ASPNETCORE_URLS=http://*:3000;https://+:5000;http://0.0.0.0:5005

Especificar HTTPS com certificado de desenvolvimento

var app = WebApplication.Create(args);

app.Urls.Add("https://localhost:3000");

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

app.Run();

Para obter mais informações sobre o certificado de desenvolvimento, consulte Confiar no certificado de desenvolvimento HTTPS do ASP.NET Core no Windows e macOS.

Especificar HTTPS usando um certificado personalizado

As seções a seguir mostram como especificar o certificado personalizado usando o arquivo appsettings.json e via configuração.

Especifique o certificado personalizado com appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Kestrel": {
    "Certificates": {
      "Default": {
        "Path": "cert.pem",
        "KeyPath": "key.pem"
      }
    }
  }
}

Especifique o certificado personalizado por meio da configuração

var builder = WebApplication.CreateBuilder(args);

// Configure the cert and the key
builder.Configuration["Kestrel:Certificates:Default:Path"] = "cert.pem";
builder.Configuration["Kestrel:Certificates:Default:KeyPath"] = "key.pem";

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

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

app.Run();

Usar as APIs de certificado

using System.Security.Cryptography.X509Certificates;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.ConfigureKestrel(options =>
{
    options.ConfigureHttpsDefaults(httpsOptions =>
    {
        var certPath = Path.Combine(builder.Environment.ContentRootPath, "cert.pem");
        var keyPath = Path.Combine(builder.Environment.ContentRootPath, "key.pem");

        httpsOptions.ServerCertificate = X509Certificate2.CreateFromPemFile(certPath, 
                                         keyPath);
    });
});

var app = builder.Build();

app.Urls.Add("https://localhost:3000");

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

app.Run();

Ler o ambiente

var app = WebApplication.Create(args);

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/oops");
}

app.MapGet("/", () => "Hello World");
app.MapGet("/oops", () => "Oops! An error happened.");

app.Run();

Para obter mais informações sobre como usar o ambiente, consulte Usar vários ambientes no ASP.NET Core

Configuração

O código a seguir lê do sistema de configuração:

var app = WebApplication.Create(args);

var message = app.Configuration["HelloKey"] ?? "Hello";

app.MapGet("/", () => message);

app.Run();

Para obter mais informações, consulte Configuração no ASP.NET Core

Registo

O código a seguir escreve uma mensagem no registo ao iniciar a aplicação:

var app = WebApplication.Create(args);

app.Logger.LogInformation("The app started");

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

app.Run();

Para obter mais informações, consulte Registo de log no .NET Core e no ASP.NET Core

Aceder ao Contêiner de Injeção de Dependência (DI)

O código a seguir mostra como obter serviços do contêiner DI durante a inicialização do aplicativo:


var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddScoped<SampleService>();

var app = builder.Build();

app.MapControllers();

using (var scope = app.Services.CreateScope())
{
    var sampleService = scope.ServiceProvider.GetRequiredService<SampleService>();
    sampleService.DoSomething();
}

app.Run();

Para obter mais informações, veja injeção de dependências no ASP.NET Core.

WebApplicationBuilder

Esta seção contém código de exemplo usando WebApplicationBuilder.

Alterar a raiz do conteúdo, o nome do aplicativo e o ambiente

O código a seguir define a raiz do conteúdo, o nome do aplicativo e o ambiente:

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ApplicationName = typeof(Program).Assembly.FullName,
    ContentRootPath = Directory.GetCurrentDirectory(),
    EnvironmentName = Environments.Staging,
    WebRootPath = "customwwwroot"
});

Console.WriteLine($"Application Name: {builder.Environment.ApplicationName}");
Console.WriteLine($"Environment Name: {builder.Environment.EnvironmentName}");
Console.WriteLine($"ContentRoot Path: {builder.Environment.ContentRootPath}");
Console.WriteLine($"WebRootPath: {builder.Environment.WebRootPath}");

var app = builder.Build();

WebApplication.CreateBuilder inicializa uma nova instância da classe WebApplicationBuilder com padrões pré-configurados.

Para obter mais informações, consulte ASP.NET Visão geral dos fundamentos principais

Alterar a raiz do conteúdo, o nome do aplicativo e o ambiente por variáveis de ambiente ou linha de comando

A tabela a seguir mostra a variável de ambiente e o argumento de linha de comando usados para alterar a raiz do conteúdo, o nome do aplicativo e o ambiente:

Característica Variável de ambiente Argumento de linha de comando
Nome do aplicativo ASPNETCORE_APPLICATIONNAME --nome_da_aplicação
Nome do ambiente ASPNETCORE_ENVIRONMENT --ambiente
Raiz do conteúdo ASPNETCORE_CONTENTROOT --contentRoot

Adicionar provedores de configuração

O exemplo a seguir adiciona o provedor de configuração INI:

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddIniFile("appsettings.ini");

var app = builder.Build();

Para obter informações detalhadas, consulte Provedores de configuração de arquivo em Configuração no ASP.NET Core.

Ler configuração

Por padrão, o WebApplicationBuilder lê a configuração de várias fontes, incluindo:

  • appSettings.json e appSettings.{environment}.json
  • Variáveis de ambiente
  • A linha de comando

Para obter uma lista completa das fontes de configuração lidas, consulte Configuração padrão em Configuração no ASP.NET Core

O código abaixo lê HelloKey da configuração e exibe o valor no endereço final /. Se o valor de configuração for null, "Hello" será atribuído a message:

var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();

Ler o ambiente

var builder = WebApplication.CreateBuilder(args);

var message = builder.Configuration["HelloKey"] ?? "Hello";

var app = builder.Build();

app.MapGet("/", () => message);

app.Run();

Adicionar fornecedores de registo

var builder = WebApplication.CreateBuilder(args);

// Configure JSON logging to the console.
builder.Logging.AddJsonConsole();

var app = builder.Build();

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

app.Run();

Adicionar serviços

var builder = WebApplication.CreateBuilder(args);

// Add the memory cache services.
builder.Services.AddMemoryCache();

// Add a custom scoped service.
builder.Services.AddScoped<ITodoRepository, TodoRepository>();
var app = builder.Build();

Personalizar o IHostBuilder

Os métodos de extensão existentes no IHostBuilder podem ser acessados usando a propriedade Host:

var builder = WebApplication.CreateBuilder(args);

// Wait 30 seconds for graceful shutdown.
builder.Host.ConfigureHostOptions(o => o.ShutdownTimeout = TimeSpan.FromSeconds(30));

var app = builder.Build();

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

app.Run();

Personalizar o IWebHostBuilder

Os métodos de extensão em IWebHostBuilder podem ser acessados usando a propriedade WebApplicationBuilder.WebHost.

var builder = WebApplication.CreateBuilder(args);

// Change the HTTP server implemenation to be HTTP.sys based
builder.WebHost.UseHttpSys();

var app = builder.Build();

app.MapGet("/", () => "Hello HTTP.sys");

app.Run();

Alterar a raiz da Web

Por padrão, a raiz da Web é relativa à raiz de conteúdo na pasta wwwroot. A raiz do servidor web é onde o middleware de arquivos estáticos procura por arquivos estáticos. A raiz da Web pode ser alterada com WebHostOptions, a linha de comando ou com o método UseWebRoot:

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    // Look for static files in webroot
    WebRootPath = "webroot"
});

var app = builder.Build();

app.Run();

Container de injeção de dependência (DI) personalizado

O exemplo a seguir usa Autofac:

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());

// Register services directly with Autofac here. Don't
// call builder.Populate(), that happens in AutofacServiceProviderFactory.
builder.Host.ConfigureContainer<ContainerBuilder>(builder => builder.RegisterModule(new MyApplicationModule()));

var app = builder.Build();

Adicionar middleware

Qualquer middleware existente do ASP.NET Core pode ser configurado no WebApplication:

var app = WebApplication.Create(args);

// Setup the file server to serve static files.
app.UseFileServer();

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

app.Run();

Para obter mais informações, consulte ASP.NET Core Middleware

Página de exceção do desenvolvedor

WebApplication.CreateBuilder inicializa uma nova instância da classe WebApplicationBuilder com padrões pré-configurados. A página de exceção do desenvolvedor está habilitada nos padrões pré-configurados. Quando o código a seguir é executado no ambiente de desenvolvimento , navegar até / renderiza uma página amigável que mostra a exceção.

var builder = WebApplication.CreateBuilder(args);

var app = builder.Build();

app.MapGet("/", () =>
{
    throw new InvalidOperationException("Oops, the '/' route has thrown an exception.");
});

app.Run();

ASP.NET Core Middleware

A tabela a seguir lista alguns dos middleware usados com freqüência com APIs mínimas.

Middleware Descrição API
Autenticação Fornece suporte à autenticação. UseAuthentication
Autorização Fornece suporte de autorização. UseAuthorization
CORS Configura o compartilhamento de recursos entre origens. UseCors
manipulador de exceções Globalmente lida com exceções geradas pelo pipeline de middleware. UseExceptionHandler
Cabeçalhos encaminhados Encaminha cabeçalhos com proxy para a solicitação atual. UseForwardedHeaders
Redirecionamento HTTPS Redireciona todas as solicitações HTTP para HTTPS. UseHttpsRedirection
Segurança Rigorosa do Transporte HTTP (HSTS) Middleware de aprimoramento de segurança que adiciona um cabeçalho de resposta especial. UseHsts
Registo de Pedidos Fornece suporte para registrar solicitações e respostas HTTP. UseHttpLogging
Registo de Pedidos W3C Fornece suporte para registrar solicitações e respostas HTTP no formato W3C. UseW3CLogging
Armazenamento em cache de resposta Fornece suporte para respostas em cache. UseResponseCaching
Compressão de Resposta Fornece suporte para compressão de respostas. UseResponseCompression
Sessão Fornece suporte para gerenciar sessões de usuário. UseSession
Arquivos Estáticos Fornece suporte para servir arquivos estáticos e navegação em diretórios. UseStaticFiles, UseFileServer
WebSockets Habilita o protocolo WebSockets. UseWebSockets

Tratamento de pedidos

As seções a seguir abordam roteamento, vinculação de parâmetros e respostas.

Roteamento

Um WebApplication configurado suporta Map{Verb} e MapMethods:

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

app.MapGet("/", () => "This is a GET");
app.MapPost("/", () => "This is a POST");
app.MapPut("/", () => "This is a PUT");
app.MapDelete("/", () => "This is a DELETE");

app.MapMethods("/options-or-head", new[] { "OPTIONS", "HEAD" }, 
                          () => "This is an options or head request ");

app.Run();

Manipuladores de rota

Os manipuladores de rota são métodos que são executados quando a rota coincide. Os manipuladores de rota podem ser funções de qualquer tipo, incluindo síncronas ou assíncronas. Os manipuladores de rota podem ser uma expressão lambda, uma função local, um método de instância ou um método estático.

Expressão lambda

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

app.MapGet("/inline", () => "This is an inline lambda");

var handler = () => "This is a lambda variable";

app.MapGet("/", handler);

app.Run();

Função local

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

string LocalFunction() => "This is local function";

app.MapGet("/", LocalFunction);

app.Run();

Método de instância

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

var handler = new HelloHandler();

app.MapGet("/", handler.Hello);

app.Run();

class HelloHandler
{
    public string Hello()
    {
        return "Hello Instance method";
    }
}

Método estático

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

app.MapGet("/", HelloHandler.Hello);

app.Run();

class HelloHandler
{
    public static string Hello()
    {
        return "Hello static method";
    }
}

Os pontos de extremidade podem receber nomes para gerar URLs para o ponto de extremidade. Usar um ponto de extremidade nomeado evita ter que codificar caminhos em um aplicativo:

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

app.MapGet("/hello", () => "Hello named route")
   .WithName("hi");

app.MapGet("/", (LinkGenerator linker) => 
        $"The link to the hello route is {linker.GetPathByName("hi", values: null)}");

app.Run();

O código anterior exibe The link to the hello endpoint is /hello do ponto de extremidade /.

NOTA: Os nomes dos pontos finais são sensíveis a maiúsculas e minúsculas.

Nomes de pontos finais:

  • Deve ser globalmente único.
  • São usados como o id de operação OpenAPI quando o suporte OpenAPI está ativado. Para obter mais informações, consulte OpenAPI.

Parâmetros de rota

Os parâmetros de rota podem ser capturados como parte da definição do padrão de rota:

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

app.MapGet("/users/{userId}/books/{bookId}", 
    (int userId, int bookId) => $"The user id is {userId} and book id is {bookId}");

app.Run();

O código anterior retorna The user id is 3 and book id is 7 do URI /users/3/books/7.

O manipulador de rotas pode declarar os parâmetros a serem capturados. Quando uma solicitação é feita para uma rota com parâmetros declarados para serem capturados, os parâmetros são analisados e passados para o manipulador. Isso facilita a captura dos valores de forma segura. No código anterior, userId e bookId são int.

No código anterior, se qualquer valor de rota não puder ser convertido em um int, uma exceção será lançada. A solicitação GET /users/hello/books/3 lança a seguinte exceção:

BadHttpRequestException: Failed to bind parameter "int userId" from "hello".

Curinga e pegar todas as rotas

A seguinte rota de captura geral retorna Routing to hello do endpoint '/posts/hello':

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

app.MapGet("/posts/{*rest}", (string rest) => $"Routing to {rest}");

app.Run();

Restrições de rota

As restrições de rota restringem o comportamento correspondente de uma rota.

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

app.MapGet("/todos/{id:int}", (int id) => db.Todos.Find(id));
app.MapGet("/todos/{text}", (string text) => db.Todos.Where(t => t.Text.Contains(text)));
app.MapGet("/posts/{slug:regex(^[a-z0-9_-]+$)}", (string slug) => $"Post {slug}");

app.Run();

A tabela a seguir demonstra os modelos de rota anteriores e seu comportamento:

Modelo de rota Exemplo de URI correspondente
/todos/{id:int} /todos/1
/todos/{text} /todos/something
/posts/{slug:regex(^[a-z0-9_-]+$)} /posts/mypost

Para obter mais informações, consulte a referência de restrição de rota no roteamento no ASP.NET Core.

Vinculação de parâmetros

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 rota
  • Sequência de caracteres de consulta
  • Cabeçalho
  • Corpo (como JSON)
  • Serviços prestados por injeção de dependência
  • Personalizado

Observação

A vinculação a partir de valores de formulário não é suportada nativamente no .NET.

O manipulador de rotas GET de exemplo 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 ligam implicitamente a partir do corpo. Para vincular a partir do corpo (como JSON) para esses métodos HTTP, vincular explicitamente com [FromBody] ou ler a partir do HttpRequest.

O seguinte exemplo de manipulador de rota POST utiliza uma origem de ligação no 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 de exemplo a seguir mostram como ler dados de 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 denominado id
page string 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 a partir de valores de formulário não é suportada nativamente no .NET.

Ligação de parâmetros com DI

A vinculação de parâmetros para APIs mínimas vincula parâmetros por meio de de injeção de dependência quando o tipo é 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

Os valores nulos e padrões anteriores 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 vincular o parâmetro "Nullable<int> pageNumber" de "dois".
/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);
    

Vinculação personalizada

Há duas 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.

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
}

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 status
{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]
  2. Tipos especiais
    1. HttpContext
    2. HttpRequest (HttpContext.Request)
    3. HttpResponse (HttpContext.Response)
    4. ClaimsPrincipal (HttpContext.User)
    5. CancellationToken (HttpContext.RequestAborted)
  3. O tipo de parâmetro tem um método BindAsync válido.
  4. Tipo de parâmetro é uma cadeia de caracteres ou tem um método TryParse 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.

Personalizar a vinculação JSON

A fonte de vinculação de corpo usa System.Text.Json para desserialização. Não é possível alterar esse padrão, mas a vinculação pode ser personalizada usando outras técnicas descritas anteriormente. Para personalizar as opções do serializador JSON, use um código semelhante ao seguinte:

using Microsoft.AspNetCore.Http.Json;

var builder = WebApplication.CreateBuilder(args);

// Configure JSON options.
builder.Services.Configure<JsonOptions>(options =>
{
    options.SerializerOptions.IncludeFields = true;
});

var app = builder.Build();

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

app.Run();

class Product
{
    // These are public fields, not properties.
    public int Id;
    public string? Name;
}

O código anterior:

  • Configura as opções JSON padrão de entrada e saída.
  • Retorna o seguinte JSON
    {
      "id": 1,
      "name": "Joe Smith"
    }
    
    Ao publicar
    {
      "Id": 1,
      "Name": "Joe Smith"
    }
    

Leia o corpo do pedido

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.

Respostas

Os manipuladores de rota suportam os seguintes tipos de valores de retorno:

  1. IResult com base - Inclui Task<IResult> e ValueTask<IResult>
  2. string - Inclui Task<string> e ValueTask<string>
  3. T (Qualquer outro tipo) - Inclui Task<T> e ValueTask<T>
Valor de retorno Comportamento Tipo de conteúdo
IResult A estrutura chama IResult.ExecuteAsync Decidido pela implementação do IResult
string A estrutura grava a cadeia de caracteres diretamente na resposta text/plain
T (Qualquer outro tipo) A estrutura irá serializar a resposta em JSON application/json

Exemplo de valores de retorno

valores de retorno de cadeia de caracteres

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

Valores de retorno JSON

app.MapGet("/hello", () => new { Message = "Hello World" });

Valores de retorno IResult

app.MapGet("/hello", () => Results.Ok(new { Message = "Hello World" }));

O exemplo a seguir usa os tipos de resultado internos para personalizar a resposta:

app.MapGet("/api/todoitems/{id}", async (int id, TodoDb db) =>
         await db.Todos.FindAsync(id) 
         is Todo todo
         ? Results.Ok(todo) 
         : Results.NotFound())
   .Produces<Todo>(StatusCodes.Status200OK)
   .Produces(StatusCodes.Status404NotFound);
JSON
app.MapGet("/hello", () => Results.Json(new { Message = "Hello World" }));
Código de status personalizado
app.MapGet("/405", () => Results.StatusCode(405));
Texto
app.MapGet("/text", () => Results.Text("This is some text"));
Streaming
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var proxyClient = new HttpClient();
app.MapGet("/pokemon", async () => 
{
    var stream = await proxyClient.GetStreamAsync("http://contoso/pokedex.json");
    // Proxy the response as JSON
    return Results.Stream(stream, "application/json");
});

app.Run();
Redirecionamento
app.MapGet("/old-path", () => Results.Redirect("/new-path"));
Ficheiro
app.MapGet("/download", () => Results.File("myfile.text"));

Resultados incorporados

Existem auxiliares de resultados comuns na classe estática Microsoft.AspNetCore.Http.Results.

Descrição Tipo de resposta Código de estado API
Escrever uma resposta JSON com opções avançadas application/json 200 Resultados.Json
Escrever uma resposta JSON application/json 200 Results.Ok
Escrever uma resposta de texto texto/simples (padrão), configurável 200 Results.Text
Escreva a resposta como bytes application/octet-stream (padrão), configurável 200 Results.Bytes
Gravar um fluxo de bytes na resposta application/octet-stream (padrão), configurável 200 Resultados.Fluxo
Transmitir um arquivo para a resposta para download com o cabeçalho de disposição de conteúdo application/octet-stream (padrão), configurável 200 Resultados.Ficheiro
Defina o código de status como 404, com uma resposta JSON opcional N/A 404 Results.NotFound
Defina o código de status como 204 N/A 204 Resultados.SemConteúdo
Defina o código de status como 422, com uma resposta JSON opcional N/A 422 Resultados.ImpossívelProcessarEntidade
Defina o código de status como 400, com uma resposta JSON opcional N/A 400 Results.BadRequest
Defina o código de status como 409, com uma resposta JSON opcional N/A 409 Resultados.Conflito
Escrever um objeto JSON de detalhes do problema para a resposta N/A 500 (padrão), configurável Resultados.Problema
Especificar um objeto JSON de detalhes do problema na resposta com erros de validação N/A N/D, configurável Results.ValidationProblem

Personalização dos resultados

Os aplicativos podem controlar as respostas implementando um tipo de IResult personalizado. O código a seguir é um exemplo de um tipo de resultado HTML:

using System.Net.Mime;
using System.Text;
static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}

class HtmlResult : IResult
{
    private readonly string _html;

    public HtmlResult(string html)
    {
        _html = html;
    }

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}

Recomendamos adicionar um método de extensão ao Microsoft.AspNetCore.Http.IResultExtensions para tornar esses resultados personalizados mais detetáveis.

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

app.MapGet("/html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

app.Run();

Autorização

As rotas podem ser protegidas usando políticas de autorização. Estes podem ser declarados através do atributo [Authorize] ou usando o método RequireAuthorization:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/auth", [Authorize] () => "This endpoint requires authorization.");
app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

O código anterior pode ser escrito com RequireAuthorization:

app.MapGet("/auth", () => "This endpoint requires authorization")
   .RequireAuthorization();

O exemplo a seguir usa autorização baseada em política :

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using WebRPauth.Data;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization(o => o.AddPolicy("AdminsOnly", 
                                  b => b.RequireClaim("admin", "true")));

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

var app = builder.Build();

app.UseAuthorization();

app.MapGet("/admin", [Authorize("AdminsOnly")] () => 
                             "The /admin endpoint is for admins only.");

app.MapGet("/admin2", () => "The /admin2 endpoint is for admins only.")
   .RequireAuthorization("AdminsOnly");

app.MapGet("/", () => "This endpoint doesn't require authorization.");
app.MapGet("/Identity/Account/Login", () => "Sign in page at this endpoint.");

app.Run();

Permitir que utilizadores não autenticados acedam um endpoint

O [AllowAnonymous] permite que utilizadores não autenticados acedam a endereços:

app.MapGet("/login", [AllowAnonymous] () => "This endpoint is for all roles.");


app.MapGet("/login2", () => "This endpoint also for all roles.")
   .AllowAnonymous();

CORS

As rotas podem ser CORS ativadas usando políticas CORS. CORS pode ser declarado através do atributo [EnableCors] ou usando o método RequireCors. Os exemplos a seguir habilitam o CORS:

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

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

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

app.Run();
using Microsoft.AspNetCore.Cors;

const string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCors(options =>
{
    options.AddPolicy(name: MyAllowSpecificOrigins,
                      builder =>
                      {
                          builder.WithOrigins("http://example.com",
                                              "http://www.contoso.com");
                      });
});

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

app.MapGet("/cors", [EnableCors(MyAllowSpecificOrigins)] () => 
                           "This endpoint allows cross origin requests!");
app.MapGet("/cors2", () => "This endpoint allows cross origin requests!")
                     .RequireCors(MyAllowSpecificOrigins);

app.Run();

Para obter mais informações, consulte Ativar solicitações entre origens (CORS) no ASP.NET Core

Ver também

suporte a OpenAPI em APIs mínimas