Treinamento
Módulo
Usar páginas, roteamento e layouts para melhorar a navegação no Blazor - Training
Saiba como otimizar a navegação no aplicativo, usar parâmetros da URL e criar layouts reutilizáveis em um aplicativo Web do Blazor.
Não há mais suporte para esse navegador.
Atualize o Microsoft Edge para aproveitar os recursos, o suporte técnico e as atualizações de segurança mais recentes.
De Ryan Nowak, Kirk Larkin e Rick Anderson
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.
Aviso
Esta versão do ASP.NET Core não tem mais suporte. Para obter mais informações, consulte a Política de Suporte do .NET e do .NET Core. Para a versão atual, consulte a versão .NET 9 deste artigo.
Importante
Essas informações relacionam-se ao produto de pré-lançamento, que poderá ser substancialmente modificado antes do lançamento comercial. A Microsoft não oferece nenhuma garantia, explícita ou implícita, quanto às informações fornecidas aqui.
Para a versão atual, consulte a versão .NET 9 deste artigo.
O roteamento é responsável por corresponder solicitações HTTP de entrada e expedir essas solicitações para os pontos de extremidade executáveis do aplicativo. Os pontos de extremidade são as unidades de código executável de manipulação de solicitações do aplicativo. Os pontos de extremidade são definidos no aplicativo e configurados quando o aplicativo é iniciado. O processo de correspondência de ponto de extremidade pode extrair valores da URL da solicitação e fornecer esses valores para processamento de solicitações. Usando as informações de ponto de extremidade do aplicativo, o roteamento também pode gerar URLs que são mapeadas para os pontos de extremidade.
Os aplicativos podem configurar o roteamento usando:
Este artigo aborda os detalhes de baixo nível do roteamento do ASP.NET Core. Para obter informações sobre como configurar o roteamento:
O código a seguir mostra um exemplo básico de roteamento:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
O exemplo anterior inclui um único ponto de extremidade usando o método MapGet:
GET
é enviada para a URL raiz /
: Hello World!
é gravado na resposta HTTP.GET
ou a URL raiz não for /
, nenhuma rota corresponderá e um HTTP 404 será retornado.O roteamento usa um par de middleware registrado por UseRouting e UseEndpoints:
UseRouting
adiciona a correspondência de rotas ao pipeline de middleware. Esse middleware examina o conjunto de pontos de extremidade definidos no aplicativo e seleciona a melhor correspondência com base na solicitação.UseEndpoints
adiciona a execução do ponto de extremidade ao pipeline de middleware. Ele executa o delegado associado ao ponto de extremidade selecionado.Normalmente, os aplicativos não precisam chamar UseRouting
ou UseEndpoints
. WebApplicationBuilder configura um pipeline de middleware, que encapsula o middleware adicionado em Program.cs
com UseRouting
e UseEndpoints
. No entanto, os aplicativos podem alterar a ordem na qual UseRouting
e UseEndpoints
são executados, chamando esses métodos explicitamente. Por exemplo, o código a seguir faz uma chamada explícita para UseRouting
:
app.Use(async (context, next) =>
{
// ...
await next(context);
});
app.UseRouting();
app.MapGet("/", () => "Hello World!");
No código anterior:
app.Use
registra um middleware personalizado, que é executado no início do pipeline.UseRouting
configura o middleware de correspondência de rotas a ser executado após o middleware personalizado.MapGet
é executado na extremidade do pipeline.Se o exemplo anterior não incluísse uma chamada para UseRouting
, o middleware personalizado seria executado após o middleware de correspondência de rotas.
Observação: As rotas adicionadas diretamente ao WebApplication são executadas no fim do pipeline.
O método MapGet
é usado para definir um ponto de extremidade. Um ponto de extremidade pode ser:
Os pontos de extremidade que podem ser correspondidos e executados pelo aplicativo são configurados no UseEndpoints
. Por exemplo, MapGet, MapPost e métodos semelhantes conectam os delegados de solicitação ao sistema de roteamento. Métodos adicionais podem ser usados para conectar os recursos de estrutura do ASP.NET Core ao sistema de roteamento:
O exemplo a seguir mostra o roteamento com um modelo de rota mais sofisticado:
app.MapGet("/hello/{name:alpha}", (string name) => $"Hello {name}!");
A cadeia de caracteres /hello/{name:alpha}
é um modelo de rota. Um modelo de rota é usado para configurar a forma como o ponto de extremidade é correspondido. Nesse caso, o modelo corresponde a:
/hello/Docs
/hello/
seguido de uma sequência de caracteres alfabéticos. :alpha
aplica uma restrição de rota que corresponde apenas a caracteres alfabéticos. As restrições de rota serão explicadas posteriormente neste artigo.O segundo segmento do caminho de URL, {name:alpha}
:
name
.O exemplo a seguir mostra o roteamento com as verificações de integridade e a autorização:
app.UseAuthentication();
app.UseAuthorization();
app.MapHealthChecks("/healthz").RequireAuthorization();
app.MapGet("/", () => "Hello World!");
O exemplo anterior demonstra como:
A chamada MapHealthChecks adiciona um ponto de extremidade de verificação de integridade. O encadeamento de RequireAuthorization para esta chamada anexa uma política de autorização ao ponto de extremidade.
Chamar UseAuthentication e UseAuthorization adiciona o middleware de autenticação e autorização. Esses programas de middleware são colocados entre UseRouting e UseEndpoints
para que possam:
UseRouting
.No exemplo anterior, há dois pontos de extremidade, mas apenas o ponto de extremidade de verificação de integridade tem uma política de autorização anexada. Se a solicitação corresponder ao ponto de extremidade de verificação de integridade, /healthz
, uma verificação de autorização será executada. Isso demonstra que os pontos de extremidade podem ter dados extras anexados. Esses dados extras são chamados de metadados de ponto de extremidade:
O sistema de roteamento se baseia no pipeline de middleware, adicionando o conceito de ponto de extremidade eficiente. Os pontos de extremidade representam as unidades da funcionalidade do aplicativo que são diferentes umas das outras em termos de roteamento, autorização e qualquer número de sistemas do ASP.NET Core.
Um ponto de extremidade do ASP.NET Core é:
O código a seguir mostra como recuperar e inspecionar o ponto de extremidade correspondente à solicitação atual:
app.Use(async (context, next) =>
{
var currentEndpoint = context.GetEndpoint();
if (currentEndpoint is null)
{
await next(context);
return;
}
Console.WriteLine($"Endpoint: {currentEndpoint.DisplayName}");
if (currentEndpoint is RouteEndpoint routeEndpoint)
{
Console.WriteLine($" - Route Pattern: {routeEndpoint.RoutePattern}");
}
foreach (var endpointMetadata in currentEndpoint.Metadata)
{
Console.WriteLine($" - Metadata: {endpointMetadata}");
}
await next(context);
});
app.MapGet("/", () => "Inspect Endpoint.");
O ponto de extremidade, se selecionado, pode ser recuperado a partir do HttpContext
. Suas propriedades podem ser inspecionadas. Os objetos de ponto de extremidade são imutáveis e não podem ser modificados após a criação. O tipo mais comum de ponto de extremidade é um RouteEndpoint. RouteEndpoint
inclui informações que permitem que ele seja selecionado pelo sistema de roteamento.
No código anterior, app.Use configura um middleware embutido.
O código a seguir mostra que, dependendo de onde app.Use
é chamado no pipeline, pode não haver um ponto de extremidade:
// Location 1: before routing runs, endpoint is always null here.
app.Use(async (context, next) =>
{
Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
await next(context);
});
app.UseRouting();
// Location 2: after routing runs, endpoint will be non-null if routing found a match.
app.Use(async (context, next) =>
{
Console.WriteLine($"2. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
await next(context);
});
// Location 3: runs when this endpoint matches
app.MapGet("/", (HttpContext context) =>
{
Console.WriteLine($"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
return "Hello World!";
}).WithDisplayName("Hello");
app.UseEndpoints(_ => { });
// Location 4: runs after UseEndpoints - will only run if there was no match.
app.Use(async (context, next) =>
{
Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
await next(context);
});
O exemplo anterior adiciona instruções Console.WriteLine
que mostram se um ponto de extremidade foi selecionado ou não. Para maior clareza, o exemplo atribui um nome de exibição ao ponto de extremidade /
fornecido.
O exemplo anterior também inclui chamadas para UseRouting
e UseEndpoints
para controlar exatamente quando esses programas de middleware são executados no pipeline.
Executar esse código com uma URL do /
exibe:
1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello
Executar esse código com qualquer outra URL exibe:
1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)
Essa saída demonstra que:
UseRouting
seja chamado.UseRouting
e UseEndpoints.UseEndpoints
é terminal quando uma correspondência é encontrada. O middleware de terminal será definido posteriormente neste artigo.UseEndpoints
é executado apenas quando nenhuma correspondência é encontrada.O middleware UseRouting
usa o método SetEndpoint para anexar o ponto de extremidade ao contexto atual. É possível substituir o middleware UseRouting
pela lógica personalizada e ainda obter os benefícios de usar pontos de extremidade. Os pontos de extremidade são primitivos de baixo nível, como o middleware, e não são acoplados à implementação de roteamento. A maioria dos aplicativos não precisa substituir UseRouting
pela lógica personalizada.
O middleware UseEndpoints
foi projetado para ser usado em conjunto com o middleware UseRouting
. A lógica principal para executar um ponto de extremidade não é complicada. Use GetEndpoint para recuperar o ponto de extremidade e, em seguida, invoque a propriedade RequestDelegate.
O código a seguir demonstra como o middleware pode influenciar ou reagir ao roteamento:
app.UseHttpMethodOverride();
app.UseRouting();
app.Use(async (context, next) =>
{
if (context.GetEndpoint()?.Metadata.GetMetadata<RequiresAuditAttribute>() is not null)
{
Console.WriteLine($"ACCESS TO SENSITIVE DATA AT: {DateTime.UtcNow}");
}
await next(context);
});
app.MapGet("/", () => "Audit isn't required.");
app.MapGet("/sensitive", () => "Audit required for sensitive data.")
.WithMetadata(new RequiresAuditAttribute());
public class RequiresAuditAttribute : Attribute { }
O exemplo anterior demonstra dois conceitos importantes:
UseRouting
para modificar os dados nos quais o roteamento opera.
UseRouting
e UseEndpoints para processar os resultados do roteamento, antes que o ponto de extremidade seja executado.
UseRouting
e UseEndpoints
: UseAuthorization
e UseCors
.O código anterior mostra um exemplo de um middleware personalizado que permite políticas por ponto de extremidade. O middleware grava um log de auditoria de acesso a dados confidenciais no console. O middleware pode ser configurado para auditar um ponto de extremidade com os metadados RequiresAuditAttribute
. Este exemplo demonstra um padrão de aceitação em que apenas os pontos de extremidade marcados como confidenciais são auditados. É possível definir essa lógica ao contrário, auditando tudo o que não está marcado como seguro, por exemplo. O sistema de metadados do ponto de extremidade é flexível. Essa lógica pode ser projetada da maneira que for adequada para o caso de uso.
O código de exemplo anterior destina-se a demonstrar os conceitos básicos dos pontos de extremidade. O exemplo não se destina ao uso de produção. Uma versão mais completa de um middleware de log de auditoria:
Os metadados da política de auditoria RequiresAuditAttribute
são definidos como um Attribute
para facilitar o uso com estruturas baseadas em classe, como controladores e SignalR. Ao usar a rota para o código:
A melhor prática para tipos de metadados é defini-los como interfaces ou atributos. As interfaces e os atributos permitem a reutilização de código. O sistema de metadados é flexível e não impõe limitações.
O exemplo a seguir demonstra o middleware de terminal e o roteamento:
// Approach 1: Terminal Middleware.
app.Use(async (context, next) =>
{
if (context.Request.Path == "/")
{
await context.Response.WriteAsync("Terminal Middleware.");
return;
}
await next(context);
});
app.UseRouting();
// Approach 2: Routing.
app.MapGet("/Routing", () => "Routing.");
O estilo de middleware mostrado com Approach 1:
é o middleware de terminal. Ele é chamado de middleware de terminal porque faz uma operação correspondente:
Path == "/"
para o middleware e Path == "/Routing"
para o roteamento.next
.Ele é chamado de middleware de terminal porque termina a pesquisa, executa algumas funcionalidades e retorna.
A lista a seguir compara o middleware de terminal com o roteamento:
next
.UseAuthorization
e UseCors
.
UseAuthorization
ou UseCors
exige uma interface manual com o sistema de autorização.Um ponto de extremidade define ambos:
O middleware de terminal pode ser uma ferramenta eficaz, mas pode exigir:
Considere a integração com o roteamento antes de gravar um middleware de terminal.
O middleware de terminal existente que é integrado ao Map ou MapWhen geralmente pode ser transformado em um ponto de extremidade com reconhecimento de roteamento. O MapHealthChecks demonstra o padrão para router-ware:
Map
e forneça o novo pipeline de middleware.Map
usando o método de extensão.O código a seguir mostra o uso do MapHealthChecks:
app.UseAuthentication();
app.UseAuthorization();
app.MapHealthChecks("/healthz").RequireAuthorization();
O exemplo anterior mostra por que retornar o objeto do construtor é importante. Retornar o objeto do construtor permite que o desenvolvedor do aplicativo configure políticas como autorização para o ponto de extremidade. Neste exemplo, o middleware de verificações de integridade não tem integração direta com o sistema de autorização.
O sistema de metadados foi criado em resposta aos problemas encontrados por autores de extensibilidade usando o middleware de terminal. É problemático para cada middleware implementar sua própria integração com o sistema de autorização.
Quando um middleware de roteamento é executado, ele define um Endpoint
e encaminha os valores para um recurso de solicitação no HttpContext a partir da solicitação atual:
HttpRequest.RouteValues
obtém a coleção de valores de rota.O Middleware que é executado após o middleware de roteamento pode inspecionar o ponto de extremidade e tomar providências. Por exemplo, um middleware de autorização pode interrogar a coleção de metadados do ponto de extremidade para uma política de autorização. Depois que todos os middlewares no pipeline de processamento da solicitação forem executados, o representante do ponto de extremidade selecionado será invocado.
O sistema de roteamento no roteamento de ponto de extremidade é responsável por todas as decisões de expedição. Como o middleware aplica políticas com base no ponto de extremidade selecionado, é importante que:
Aviso
Para compatibilidade com versões anteriores, quando o delegado do ponto de extremidade do Controlador ou do Razor Pages é executado, as propriedades do RouteContext.RouteData são definidas com os valores apropriados com base no processamento da solicitação executado até o momento.
O tipo RouteContext
será marcado como obsoleto em uma versão futura:
RouteData.Values
para HttpRequest.RouteValues
.RouteData.DataTokens
para recuperar IDataTokensMetadata nos metadados do ponto de extremidade.A correspondência de URL opera em um conjunto configurável de fases. Em cada fase, a saída é um conjunto de correspondências. O conjunto de correspondências pode ser reduzido ainda mais pela próxima fase. A implementação de roteamento não garante uma ordem de processamento para pontos de extremidade correspondentes. Todas as correspondências possíveis são processadas de uma só vez. As fases de correspondência de URL ocorrem na ordem a seguir. ASP.NET Core:
A lista de pontos de extremidade é priorizada de acordo com:
Todos os pontos de extremidade correspondentes são processados em cada fase até que o EndpointSelector seja atingido. O EndpointSelector
é a fase final. Ele escolhe o ponto de extremidade de prioridade mais alta nas correspondências como a melhor correspondência. Se houver outras correspondências com a mesma prioridade que a melhor correspondência, uma exceção de correspondência ambígua será gerada.
A precedência de rota é calculada com base em um modelo de rota mais específico que recebe uma prioridade mais alta. Por exemplo, considere os modelos /hello
e /{message}
:
/hello
./hello
é mais específico e, portanto, tem prioridade mais alta.Em geral, a precedência de rota escolhe a melhor correspondência para os tipos de esquemas de URL usados na prática. Use Order somente quando necessário para evitar uma ambiguidade.
Devido aos tipos de extensibilidade fornecidos pelo roteamento, não é possível que o sistema de roteamento calcule antecipadamente as rotas ambíguas. Considere um exemplo, como os modelos de rota /{message:alpha}
e /{message:int}
:
alpha
corresponde apenas a caracteres alfabéticos.int
corresponde apenas a números.Aviso
A ordem das operações dentro do UseEndpoints não influencia o comportamento do roteamento, com uma exceção. MapControllerRoute e MapAreaRoute atribuem automaticamente um valor de pedido aos pontos de extremidade com base na ordem em que são invocados. Isso simula o comportamento de longo prazo dos controladores, sem que o sistema de roteamento forneça as mesmas garantias que as implementações de roteamento mais antigas.
Roteamento de ponto de extremidade no ASP.NET Core:
A precedência de modelo de rota é um sistema que atribui a cada modelo de rota um valor com base na especificidade. A precedência de modelo de rota:
Por exemplo, considere os modelos /Products/List
e /Products/{id}
. Seria aceitável supor que /Products/List
é uma correspondência melhor do que /Products/{id}
para o caminho de URL /Products/List
. Isso funciona porque o segmento literal /List
é considerado com melhor precedência do que o segmento de parâmetro /{id}
.
Os detalhes de como funciona a precedência são acoplados à forma como os modelos de rota são definidos:
Geração de URL:
O roteamento de ponto de extremidade inclui a API LinkGenerator. LinkGenerator
é um serviço singleton disponível na DI. A API LinkGenerator
pode ser usada fora do contexto de uma solicitação em execução. O Mvc.IUrlHelper e os cenários que dependem do IUrlHelper, como Auxiliares de Marcação, Auxiliares de HTML e Resultados da Ação, usam a API LinkGenerator
internamente para fornecer as funcionalidades de geração de link.
O gerador de link é respaldado pelo conceito de um endereço e esquemas de endereço. Um esquema de endereço é uma maneira de determinar os pontos de extremidade que devem ser considerados para a geração de link. Por exemplo, os cenários de nome de rota e valores de rota com os quais muitos usuários estão familiarizados nos controladores e no Razor Pages são implementados como um esquema de endereço.
O gerador de link pode ser vinculado aos controladores e ao Razor Pages usando os seguintes métodos de extensão:
As sobrecargas desses métodos aceitam argumentos que incluem o HttpContext
. Esses métodos são funcionalmente equivalentes a Url.Action e Url.Page, mas oferecem mais flexibilidade e opções.
Os métodos GetPath*
são mais semelhantes a Url.Action
e Url.Page
, pois geram um URI que contém um caminho absoluto. Os métodos GetUri*
sempre geram um URI absoluto que contém um esquema e um host. Os métodos que aceitam um HttpContext
geram um URI no contexto da solicitação em execução. Os valores de rota de ambiente, o caminho base da URL, o esquema e o host da solicitação em execução são usados, a menos que sejam substituídos.
LinkGenerator é chamado com um endereço. A geração de um URI ocorre em duas etapas:
Os métodos fornecidos pelo LinkGenerator dão suporte a funcionalidades de geração de link padrão para qualquer tipo de endereço. A maneira mais conveniente usar o gerador de link é por meio de métodos de extensão que executam operações para um tipo de endereço específico:
Método de extensão | Descrição |
---|---|
GetPathByAddress | Gera um URI com um caminho absoluto com base nos valores fornecidos. |
GetUriByAddress | Gera um URI absoluto com base nos valores fornecidos. |
Aviso
Preste atenção às seguintes implicações da chamada de métodos LinkGenerator:
Use métodos de extensão de GetUri*
com cuidado em uma configuração de aplicativo que não valide o cabeçalho Host
das solicitações de entrada. Se o cabeçalho Host
das solicitações de entrada não é validado, uma entrada de solicitação não confiável pode ser enviada novamente ao cliente em URIs em uma exibição ou página. Recomendamos que todos os aplicativos de produção configurem seu servidor para validar o cabeçalho Host
com os valores válidos conhecidos.
Use LinkGenerator com cuidado no middleware em combinação com Map
ou MapWhen
. Map*
altera o caminho base da solicitação em execução, o que afeta a saída da geração de link. Todas as APIs de LinkGenerator permitem a especificação de um caminho base. Especifique um caminho base vazio para desfazer o efeito de Map*
na geração de link.
No exemplo a seguir, um middleware usa a API de LinkGenerator para criar um link para um método de ação que lista os produtos da loja. O uso do gerador de link com sua injeção em uma classe e uma chamada a GenerateLink
está disponível para qualquer classe em um aplicativo:
public class ProductsMiddleware
{
private readonly LinkGenerator _linkGenerator;
public ProductsMiddleware(RequestDelegate next, LinkGenerator linkGenerator) =>
_linkGenerator = linkGenerator;
public async Task InvokeAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Plain;
var productsPath = _linkGenerator.GetPathByAction("Products", "Store");
await httpContext.Response.WriteAsync(
$"Go to {productsPath} to see our products.");
}
}
Os tokens entre {}
definem os parâmetros de rota que serão associados, se a rota for correspondida. Mais de um parâmetro de rota pode ser definido em um segmento de rota, mas os parâmetros de rota precisam ser separados por um valor literal. Por exemplo:
{controller=Home}{action=Index}
não é uma rota válida, já que não há valor literal entre {controller}
e {action}
. Os parâmetros de rota devem ter um nome e podem ter atributos adicionais especificados.
Um texto literal diferente dos parâmetros de rota (por exemplo, {id}
) e do separador de caminho /
precisa corresponder ao texto na URL. A correspondência de texto não diferencia maiúsculas de minúsculas e se baseia na representação decodificada do caminho de URLs. Para encontrar a correspondência de um delimitador de parâmetro de rota literal {
ou }
, faça o escape do delimitador repetindo o caractere. Por exemplo {{
ou }}
.
Asterisco *
ou asterisco duplo **
:
blog/{**slug}
: blog/
e tenha qualquer valor depois dele.blog/
é atribuído ao valor de rota de campo de dados dinâmico.Aviso
Um parâmetro catch-all pode corresponder às rotas incorretamente devido a um bug no roteamento. Os aplicativos afetados por esse bug têm as seguintes características:
{**slug}"
Confira os bugs do GitHub 18677 e 16579, por exemplo, casos que atingiram esse bug.
Uma correção de aceitação para esse bug está contida no SDK do .NET Core 3.1.301 e posterior. O código a seguir define um comutador interno que corrige esse bug:
public static void Main(string[] args)
{
AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior",
true);
CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.
Os parâmetros catch-all também podem corresponder à cadeia de caracteres vazia.
O parâmetro catch-all faz o escape dos caracteres corretos quando a rota é usada para gerar uma URL, incluindo os caracteres separadores de caminho /
. Por exemplo, a rota foo/{*path}
com valores de rota { path = "my/path" }
gera foo/my%2Fpath
. Observe o escape da barra invertida. Para fazer a viagem de ida e volta dos caracteres separadores de caminho, use o prefixo do parâmetro da rota **
. A rota foo/{**path}
com { path = "my/path" }
gera foo/my/path
.
Padrões de URL que tentam capturar um nome de arquivo com uma extensão de arquivo opcional apresentam considerações adicionais. Por exemplo, considere o modelo files/{filename}.{ext?}
. Quando existem valores para filename
e ext
, ambos os valores são populados. Se apenas existir um valor para filename
na URL, a rota encontrará uma correspondência, pois o .
à direita é opcional. As URLs a seguir correspondem a essa rota:
/files/myFile.txt
/files/myFile
Os parâmetros de rota podem ter valores padrão, designados pela especificação do valor padrão após o nome do parâmetro separado por um sinal de igual (=
). Por exemplo, {controller=Home}
define Home
como o valor padrão de controller
. O valor padrão é usado se nenhum valor está presente na URL para o parâmetro. Os parâmetros de rota se tornam opcionais com o acréscimo de um ponto de interrogação (?
) ao final do nome do parâmetro. Por exemplo, id?
. A diferença entre valores opcionais e parâmetros de rota padrão é:
Os parâmetros de rota podem ter restrições que precisam corresponder ao valor de rota associado da URL. A adição de :
e do nome da restrição após o nome do parâmetro de rota especifica uma restrição embutida em um parâmetro de rota. Se a restrição exigir argumentos, eles ficarão entre parênteses (...)
após o nome da restrição. Várias restrições embutidas podem ser especificadas por meio do acréscimo de outros :
e do nome da restrição.
O nome da restrição e os argumentos são passados para o serviço IInlineConstraintResolver para criar uma instância de IRouteConstraint a ser usada no processamento de URL. Por exemplo, o modelo de rota blog/{article:minlength(10)}
especifica uma restrição minlength
com o argumento 10
. Para obter mais informações sobre as restrições de rota e uma lista das restrições fornecidas pela estrutura, confira a seção Restrições de rota.
Os parâmetros de rota também podem ter transformadores de parâmetro. Os transformadores de parâmetro transformam o valor de um parâmetro ao gerar links e fazer a correspondência de ações e páginas com URLs. Assim como as restrições, os transformadores de parâmetro podem ser adicionados embutidos a um parâmetro de rota colocando :
e o nome do transformador após o nome do parâmetro de rota. Por exemplo, o modelo de rota blog/{article:slugify}
especifica um transformador slugify
. Para obter mais informações sobre transformadores de parâmetro, confira a seção Transformadores de parâmetro.
A tabela a seguir demonstra modelos de rota de exemplo e seu comportamento:
Modelo de rota | URI de correspondência de exemplo | O URI da solicitação |
---|---|---|
hello |
/hello |
Somente corresponde ao caminho único /hello . |
{Page=Home} |
/ |
Faz a correspondência e define Page como Home . |
{Page=Home} |
/Contact |
Faz a correspondência e define Page como Contact . |
{controller}/{action}/{id?} |
/Products/List |
É mapeado para o controlador Products e a ação List . |
{controller}/{action}/{id?} |
/Products/Details/123 |
É mapeado para o controlador Products e a ação Details , e id definido como 123. |
{controller=Home}/{action=Index}/{id?} |
/ |
É mapeado para o controlador Home e o método Index . id é ignorado. |
{controller=Home}/{action=Index}/{id?} |
/Products |
É mapeado para o controlador Products e o método Index . id é ignorado. |
Em geral, o uso de um modelo é a abordagem mais simples para o roteamento. Restrições e padrões também podem ser especificados fora do modelo de rota.
Os segmentos complexos são processados correspondendo delimitadores literais da direita para a esquerda sem greedy. Por exemplo, [Route("/a{b}c{d}")]
é um segmento complexo.
Os segmentos complexos funcionam de uma maneira específica que deve ser compreendida para usá-los com êxito. O exemplo nesta seção demonstra por que segmentos complexos só funcionam bem quando o texto delimitador não aparece dentro dos valores de parâmetro. Usar um regex e extrair manualmente os valores é necessário para casos mais complexos.
Aviso
Ao usar System.Text.RegularExpressions para processar entradas não confiáveis, passe um tempo limite. Um usuário mal-intencionado pode fornecer entrada para RegularExpressions
, causando um ataque de negação de serviço. APIs ASP.NET Core Framework que usam RegularExpressions
passam um tempo limite.
Este é um resumo das etapas que o roteamento executa com o modelo /a{b}c{d}
e o caminho de URL /abcd
. O |
é usado para ajudar a visualizar como o algoritmo funciona:
c
. Portanto, /abcd
é pesquisado pela direita e localiza /ab|c|d
.d
) agora corresponde ao parâmetro de rota {d}
.a
. Portanto, /ab|c|d
é pesquisado começando de onde paramos e, em seguida, a
é encontrado em /|a|b|c|d
.b
) agora corresponde ao parâmetro de rota {b}
.Este é um exemplo de um caso negativo usando o mesmo modelo /a{b}c{d}
e o caminho de URL /aabcd
. O |
é usado para ajudar a visualizar como o algoritmo funciona. Esse caso não é uma correspondência, que é explicada pelo mesmo algoritmo:
c
. Portanto, /aabcd
é pesquisado pela direita e localiza /aab|c|d
.d
) agora corresponde ao parâmetro de rota {d}
.a
. Portanto, /aab|c|d
é pesquisado começando de onde paramos e, em seguida, a
é encontrado em /a|a|b|c|d
.b
) agora corresponde ao parâmetro de rota {b}
.a
, mas o algoritmo ficou fora do modelo de rota para analisar. Portanto, não é uma correspondência.Como o algoritmo correspondente é sem greedy:
Expressões regulares fornecem muito mais controle sobre o comportamento correspondente.
Correspondência gananciosa, também conhecida como o número máximo de tentativas de correspondência para encontrar a correspondência mais longa possível no texto de entrada que satisfaça o padrão regex. A correspondência não gananciosa, também conhecida como correspondência preguiçosa, busca a correspondência mais curta possível no texto de entrada que satisfaça o padrão regex.
O roteamento com caracteres especiais pode levar a resultados inesperados. Por exemplo, considere um controlador com o seguinte método de ação:
[HttpGet("{id?}/name")]
public async Task<ActionResult<string>> GetName(string id)
{
var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null || todoItem.Name == null)
{
return NotFound();
}
return todoItem.Name;
}
Quando string id
contém os seguintes valores codificados, podem ocorrer resultados inesperados:
ASCII | Encoded |
---|---|
/ |
%2F |
|
+ |
Os parâmetros de rota nem sempre são decodificados por URL. Esse problema poderá ser resolvido no futuro. Para obter mais informações, confira este tópico do GitHub;
As restrições de rota são executadas quando ocorre uma correspondência com a URL de entrada e é criado um token do caminho da URL em valores de rota. Em geral, as restrições da rota inspecionam o valor de rota associado por meio do modelo de rota e tomam uma decisão do tipo "verdadeiro ou falso" sobre se o valor é aceitável. Algumas restrições da rota usam dados fora do valor de rota para considerar se a solicitação pode ser encaminhada. Por exemplo, a HttpMethodRouteConstraint pode aceitar ou rejeitar uma solicitação de acordo com o verbo HTTP. As restrições são usadas em solicitações de roteamento e na geração de link.
Aviso
Não use restrições para a validação de entrada. Se as restrições forem usadas para validação de entrada, a entrada inválida resultará em uma resposta 404
Não Encontrado. A entrada inválida deve produzir uma Solicitação Inválida 400
com uma mensagem de erro apropriada. As restrições de rota são usadas para desfazer a ambiguidade entre rotas semelhantes, não para validar as entradas de uma rota específica.
A tabela a seguir demonstra restrições de rota de exemplo e seu comportamento esperado:
restrição | Exemplo | Correspondências de exemplo | Observações |
---|---|---|---|
int |
{id:int} |
123456789 , -123456789 |
Corresponde a qualquer inteiro |
bool |
{active:bool} |
true , FALSE |
Corresponde a true ou false . Não diferencia maiúsculas de minúsculas |
datetime |
{dob:datetime} |
2016-12-31 , 2016-12-31 7:32pm |
Corresponde a um valor válido DateTime na cultura invariável. Confira o aviso anterior. |
decimal |
{price:decimal} |
49.99 , -1,000.01 |
Corresponde a um valor válido decimal na cultura invariável. Confira o aviso anterior. |
double |
{weight:double} |
1.234 , -1,001.01e8 |
Corresponde a um valor válido double na cultura invariável. Confira o aviso anterior. |
float |
{weight:float} |
1.234 , -1,001.01e8 |
Corresponde a um valor válido float na cultura invariável. Confira o aviso anterior. |
guid |
{id:guid} |
CD2C1638-1638-72D5-1638-DEADBEEF1638 |
Corresponde a um valor Guid válido |
long |
{ticks:long} |
123456789 , -123456789 |
Corresponde a um valor long válido |
minlength(value) |
{username:minlength(4)} |
Rick |
A cadeia de caracteres deve ter, no mínimo, 4 caracteres |
maxlength(value) |
{filename:maxlength(8)} |
MyFile |
A cadeia de caracteres não pode ser maior que 8 caracteres |
length(length) |
{filename:length(12)} |
somefile.txt |
A cadeia de caracteres deve ter exatamente 12 caracteres |
length(min,max) |
{filename:length(8,16)} |
somefile.txt |
A cadeia de caracteres deve ter, pelo menos, 8 e não mais de 16 caracteres |
min(value) |
{age:min(18)} |
19 |
O valor inteiro deve ser, pelo menos, 18 |
max(value) |
{age:max(120)} |
91 |
O valor inteiro não deve ser maior que 120 |
range(min,max) |
{age:range(18,120)} |
91 |
O valor inteiro deve ser, pelo menos, 18, mas não maior que 120 |
alpha |
{name:alpha} |
Rick |
A cadeia de caracteres deve consistir em um ou mais caracteres alfabéticos, a -z e não diferencia maiúsculas de minúsculas. |
regex(expression) |
{ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} |
123-45-6789 |
A cadeia de caracteres deve corresponder à expressão regular. Confira as dicas sobre como definir uma expressão regular. |
required |
{name:required} |
Rick |
Usado para impor que um valor não parâmetro está presente durante a geração de URL |
Aviso
Ao usar System.Text.RegularExpressions para processar entradas não confiáveis, passe um tempo limite. Um usuário mal-intencionado pode fornecer entrada para RegularExpressions
, causando um ataque de negação de serviço. APIs ASP.NET Core Framework que usam RegularExpressions
passam um tempo limite.
Várias restrições delimitadas por dois-pontos podem ser aplicadas a um único parâmetro. Por exemplo, a restrição a seguir restringe um parâmetro para um valor inteiro de 1 ou maior:
[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }
Aviso
As restrições de rota que verificam a URL e são convertidas em um tipo CLR sempre usam a cultura invariável. Por exemplo, conversão para o tipo CLR int
ou DateTime
. Essas restrições consideram que a URL não é localizável. As restrições de rota fornecidas pela estrutura não modificam os valores armazenados nos valores de rota. Todos os valores de rota analisados com base na URL são armazenados como cadeias de caracteres. Por exemplo, a restrição float
tenta converter o valor de rota em um float, mas o valor convertido é usado somente para verificar se ele pode ser convertido em um float.
Aviso
Ao usar System.Text.RegularExpressions para processar entradas não confiáveis, passe um tempo limite. Um usuário mal-intencionado pode fornecer entrada para RegularExpressions
, causando um ataque de negação de serviço. APIs ASP.NET Core Framework que usam RegularExpressions
passam um tempo limite.
As expressões regulares podem ser especificadas como restrições embutidas usando a restrição de rota regex(...)
. Os métodos na família MapControllerRoute também aceitam um literal de objeto das restrições. Se esse formulário for usado, os valores de cadeia de caracteres serão interpretados como expressões regulares.
O código a seguir usa uma restrição regex embutida:
app.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
() => "Inline Regex Constraint Matched");
O código a seguir usa um literal de objeto para especificar uma restrição regex:
app.MapControllerRoute(
name: "people",
pattern: "people/{ssn}",
constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
defaults: new { controller = "People", action = "List" });
A estrutura do ASP.NET Core adiciona RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant
ao construtor de expressão regular. Confira RegexOptions para obter uma descrição desses membros.
As expressões regulares usam delimitadores e tokens semelhantes aos usados pelo roteamento e pela linguagem C#. Os tokens de expressão regular precisam ter escape. Para usar a expressão regular ^\d{3}-\d{2}-\d{4}$
em uma restrição embutida, use uma das seguintes opções:
\
fornecidos na cadeia de caracteres pelos caracteres \\
no arquivo de origem do C# para escapar do caractere de escape da cadeia de caracteres \
.Para fazer o escape dos caracteres de delimitador de parâmetro de roteamento {
, }
, [
, ]
, duplique os caracteres na expressão, por exemplo, {{
, }}
, [[
, ]]
. A tabela a seguir mostra uma expressão regular e a versão com escape:
Expressão regular | Expressão regular com escape |
---|---|
^\d{3}-\d{2}-\d{4}$ |
^\\d{{3}}-\\d{{2}}-\\d{{4}}$ |
^[a-z]{2}$ |
^[[a-z]]{{2}}$ |
As expressões regulares usadas no roteamento geralmente começam com o caractere ^
e correspondem à posição inicial da cadeia de caracteres. As expressões geralmente terminam com o caractere $
e correspondem ao final da cadeia de caracteres. Os caracteres ^
e $
garantem que a expressão regular corresponde a todo o valor do parâmetro de rota. Sem os caracteres ^
e $
, a expressão regular corresponde a qualquer subcadeia de caracteres na cadeia de caracteres, o que geralmente não é o desejado. A tabela a seguir fornece exemplos e explica por que eles encontram ou não uma correspondência:
Expression | String | Corresponder a | Comentar |
---|---|---|---|
[a-z]{2} |
hello | Sim | A subcadeia de caracteres corresponde |
[a-z]{2} |
123abc456 | Sim | A subcadeia de caracteres corresponde |
[a-z]{2} |
mz | Sim | Corresponde à expressão |
[a-z]{2} |
MZ | Sim | Não diferencia maiúsculas de minúsculas |
^[a-z]{2}$ |
hello | Não | Confira ^ e $ acima |
^[a-z]{2}$ |
123abc456 | Não | Confira ^ e $ acima |
Para saber mais sobre a sintaxe de expressões regulares, confira Expressões regulares do .NET Framework.
Para restringir um parâmetro a um conjunto conhecido de valores possíveis, use uma expressão regular. Por exemplo, {action:regex(^(list|get|create)$)}
apenas corresponde o valor da rota action
a list
, get
ou create
. Se passada para o dicionário de restrições, a cadeia de caracteres ^(list|get|create)$
é equivalente. As restrições passadas para o dicionário de restrições que não correspondem a uma das restrições conhecidas também são tratadas como expressões regulares. As restrições transmitidas em um modelo que não correspondem a uma das restrições conhecidas não são tratadas como expressões regulares.
É possível criar restrições de rota personalizadas com a implementação da interface do IRouteConstraint. A interface do IRouteConstraint
contém Match, que retorna true
quando a restrição é atendida. Caso contrário, retorna false
.
As restrições de rota personalizadas raramente são necessárias. Antes de implementar uma restrição de rota personalizada, considere alternativas, como a associação de modelo.
A pasta restrições do ASP.NET Core fornece bons exemplos de criação de restrições. Por exemplo, GuidRouteConstraint.
Para usar uma IRouteConstraint
personalizada, o tipo de restrição de rota deve ser registrado com o ConstraintMap do aplicativo, no contêiner de serviço. O ConstraintMap
é um dicionário que mapeia as chaves de restrição de rota para implementações de IRouteConstraint
que validam essas restrições. É possível atualizar o ConstraintMap
do aplicativo no Program.cs
como parte de uma chamada AddRouting ou configurando RouteOptions diretamente com builder.Services.Configure<RouteOptions>
. Por exemplo:
builder.Services.AddRouting(options =>
options.ConstraintMap.Add("noZeroes", typeof(NoZeroesRouteConstraint)));
A restrição anterior é aplicada no seguinte código:
[ApiController]
[Route("api/[controller]")]
public class NoZeroesController : ControllerBase
{
[HttpGet("{id:noZeroes}")]
public IActionResult Get(string id) =>
Content(id);
}
A implementação de NoZeroesRouteConstraint
impede que 0
seja usada em um parâmetro de rota:
public class NoZeroesRouteConstraint : IRouteConstraint
{
private static readonly Regex _regex = new(
@"^[1-9]*$",
RegexOptions.CultureInvariant | RegexOptions.IgnoreCase,
TimeSpan.FromMilliseconds(100));
public bool Match(
HttpContext? httpContext, IRouter? route, string routeKey,
RouteValueDictionary values, RouteDirection routeDirection)
{
if (!values.TryGetValue(routeKey, out var routeValue))
{
return false;
}
var routeValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);
if (routeValueString is null)
{
return false;
}
return _regex.IsMatch(routeValueString);
}
}
Aviso
Ao usar System.Text.RegularExpressions para processar entradas não confiáveis, passe um tempo limite. Um usuário mal-intencionado pode fornecer entrada para RegularExpressions
, causando um ataque de negação de serviço. APIs ASP.NET Core Framework que usam RegularExpressions
passam um tempo limite.
O código anterior:
0
no segmento {id}
da rota.O código a seguir é uma abordagem melhor para impedir que um id
que contém um 0
seja processado:
[HttpGet("{id}")]
public IActionResult Get(string id)
{
if (id.Contains('0'))
{
return StatusCode(StatusCodes.Status406NotAcceptable);
}
return Content(id);
}
O código anterior tem as seguintes vantagens em relação à abordagem NoZeroesRouteConstraint
:
0
.Transformadores de parâmetro:
Por exemplo, um transformador de parâmetro slugify
personalizado em padrão de rota blog\{article:slugify}
com Url.Action(new { article = "MyTestArticle" })
gera blog\my-test-article
.
Considere a seguinte implementação IOutboundParameterTransformer
:
public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
public string? TransformOutbound(object? value)
{
if (value is null)
{
return null;
}
return Regex.Replace(
value.ToString()!,
"([a-z])([A-Z])",
"$1-$2",
RegexOptions.CultureInvariant,
TimeSpan.FromMilliseconds(100))
.ToLowerInvariant();
}
}
Para usar um transformador de parâmetro em um padrão de rota, configure-o usando ConstraintMap em Program.cs
:
builder.Services.AddRouting(options =>
options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));
A estrutura do ASP.NET Core usa os transformadores de parâmetro para transformar o URI no qual um ponto de extremidade é resolvido. Por exemplo, os transformadores de parâmetro transformam os valores de rota usado para corresponder a um area
, controller
, action
e page
:
app.MapControllerRoute(
name: "default",
pattern: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");
Com o modelo de rota anterior, a ação SubscriptionManagementController.GetAll
é combinada com o URI /subscription-management/get-all
. Um transformador de parâmetro não altera os valores de rota usados para gerar um link. Por exemplo, Url.Action("GetAll", "SubscriptionManagement")
gera /subscription-management/get-all
.
ASP.NET Core fornece convenções de API para usar transformadores de parâmetro com as rotas geradas:
Esta seção contém uma referência para o algoritmo implementado pela geração de URL. Na prática, os exemplos mais complexos de geração de URL usam controladores ou Razor Pages. Confira o roteamento em controladores para obter informações adicionais.
O processo de geração de URL começa com uma chamada para LinkGenerator.GetPathByAddress ou um método semelhante. O método é fornecido com um endereço, um conjunto de valores de rota e, opcionalmente, informações sobre a solicitação atual de HttpContext
.
A primeira etapa é usar o endereço para resolve um conjunto de pontos de extremidade candidatos usando um IEndpointAddressScheme<TAddress> que corresponda ao tipo do endereço.
Depois que o conjunto de candidatos é encontrado pelo esquema de endereços, os pontos de extremidade são ordenados e processados iterativamente até que uma operação de geração de URL seja bem-sucedida. A geração de URL não verifica se há ambiguidades. O primeiro resultado retornado é o resultado final.
A primeira etapa na solução de problemas de geração de URL é definir o nível de log de Microsoft.AspNetCore.Routing
como TRACE
. LinkGenerator
registra muitos detalhes sobre o processamento, o que pode ser útil para solucionar problemas.
Confira Referência de geração de URL para obter detalhes sobre a geração de URL.
Os endereços são o conceito na geração de URL usado para vincular uma chamada ao gerador de links para um conjunto de pontos de extremidade candidatos.
Os endereços são um conceito extensível que vem com duas implementações por padrão:
string
) como o endereço: IUrlHelper
, Auxiliares de Marca, Auxiliares HTML, Resultados da Ação etc.A função do esquema de endereços é fazer a associação entre o endereço e os pontos de extremidade correspondentes por critérios arbitrários:
Na solicitação atual, o roteamento acessa os valores de rota da solicitação atual HttpContext.Request.RouteValues
. Os valores associados à solicitação atual são chamados de valores ambientes. Para maior clareza, a documentação se refere aos valores de rota transmitidos para os métodos como valores explícitos.
O exemplo a seguir mostra valores ambientes e valores explícitos. Fornece os valores ambientes da solicitação atual e os valores explícitos:
public class WidgetController : ControllerBase
{
private readonly LinkGenerator _linkGenerator;
public WidgetController(LinkGenerator linkGenerator) =>
_linkGenerator = linkGenerator;
public IActionResult Index()
{
var indexPath = _linkGenerator.GetPathByAction(
HttpContext, values: new { id = 17 })!;
return Content(indexPath);
}
// ...
O código anterior:
/Widget/Index/17
O código a seguir fornece apenas valores explícitos e nenhum valor ambiente:
var subscribePath = _linkGenerator.GetPathByAction(
"Subscribe", "Home", new { id = 17 })!;
O método anterior retorna /Home/Subscribe/17
O código a seguir no WidgetController
retorna /Widget/Subscribe/17
:
var subscribePath = _linkGenerator.GetPathByAction(
HttpContext, "Subscribe", null, new { id = 17 });
O código a seguir fornece o controlador dos valores ambientes na solicitação atual e dos valores explícitos:
public class GadgetController : ControllerBase
{
public IActionResult Index() =>
Content(Url.Action("Edit", new { id = 17 })!);
}
No código anterior:
/Gadget/Edit/17
é retornado.action
e os valores route
especificados.O código a seguir fornece os valores ambientes da solicitação atual e os valores explícitos:
public class IndexModel : PageModel
{
public void OnGet()
{
var editUrl = Url.Page("./Edit", new { id = 17 });
// ...
}
}
O código anterior define url
como /Edit/17
, quando a página Editar Razor contém a seguinte diretiva de página:
@page "{id:int}"
Se a página Editar não contiver o modelo de rota "{id:int}"
, url
será /Edit?id=17
.
O comportamento do MVC IUrlHelper adiciona uma camada de complexidade, além das regras descritas aqui:
IUrlHelper
sempre fornece os valores de rota da solicitação atual como valores ambientes.action
e controller
como valores explícitos, a menos que sejam substituídos pelo desenvolvedor.page
como valor explícito, a menos que seja substituído. IUrlHelper.Page
sempre substitui o valor de rota atual handler
por null
como um valor explícito, a menos que seja substituído.Os usuários geralmente ficam surpresos com os detalhes comportamentais dos valores ambientes, pois o MVC não parece seguir suas próprias regras. Por motivos de histórico e compatibilidade, determinados valores de rota, como action
, controller
, page
e handler
, têm seu próprio comportamento de caso especial.
A funcionalidade equivalente fornecida por LinkGenerator.GetPathByAction
e LinkGenerator.GetPathByPage
duplica essas anomalias de IUrlHelper
para compatibilidade.
Depois que o conjunto de pontos de extremidade candidatos for encontrado, o algoritmo de geração de URL:
A primeira etapa nesse processo é chamada de invalidação de valor de rota. A invalidação de valor de rota é o processo pelo qual o roteamento decide quais valores de rota dos valores ambientes devem ser usados e quais devem ser ignorados. Cada valor ambiente é considerado e combinado com os valores explícitos ou ignorado.
A melhor maneira de pensar sobre a função dos valores de ambiente é que eles tentam salvar a digitação dos desenvolvedores de aplicativos, em alguns casos comuns. Tradicionalmente, os cenários em que os valores ambientes são úteis estão relacionados ao MVC:
As chamadas para LinkGenerator
ou IUrlHelper
que retornam null
geralmente são causadas por não entender a invalidação de valor de rota. Solucione problemas de invalidação de valor de rota especificando explicitamente mais valores de rota para ver se isso resolve o problema.
A invalidação de valor de rota funciona supondo que o esquema de URL do aplicativo é hierárquico, com uma hierarquia formada da esquerda para a direita. Considere o modelo de rota de controlador básico {controller}/{action}/{id?}
para ter uma noção intuitiva de como isso funciona na prática. Uma alteração em um valor invalida todos os valores de rota que são exibidos à direita. Isso reflete a suposição sobre a hierarquia. Se o aplicativo tiver um valor ambiente para id
e a operação especificar um valor diferente para o controller
:
id
não será reutilizado porque {controller}
está à esquerda de {id?}
.Alguns exemplos que demonstram esse princípio:
id
, o valor ambiente para id
será ignorado. Os valores ambientes para controller
e action
podem ser usados.action
, qualquer valor ambiente para action
será ignorado. Os valores ambientes para controller
podem ser usados. Se o valor explícito para action
for diferente do valor ambiente para action
, o valor id
não será usado. Se o valor explícito para action
for igual ao valor ambiente para action
, o valor id
poderá ser usado.controller
, qualquer valor ambiente para controller
será ignorado. Se o valor explícito para controller
for diferente do valor ambiente para controller
, os valores action
e id
não serão usados. Se o valor explícito para controller
for igual ao valor ambiente para controller
, os valores action
e id
poderão ser usados.Esse processo é ainda mais complicado devido à existência de rotas de atributo e rotas convencionais dedicadas. As rotas convencionais do controlador, como {controller}/{action}/{id?}
, especificam uma hierarquia usando parâmetros de rota. Para rotas convencionais dedicadas e rotas de atributo para os controladores e Razor Pages:
Para esses casos, a geração de URL define o conceito de valores necessários. Os pontos de extremidade criados por controladores e Razor Pages têm os valores necessários especificados que permitem que a invalidação do valor de rota funcione.
O algoritmo de invalidação de valor de rota em detalhes:
Neste ponto, a operação de geração de URL está pronta para avaliar as restrições de rota. O conjunto de valores aceitos é combinado com os valores padrão do parâmetro, que são fornecidos às restrições. Se todas as restrições forem aprovadas, a operação continuará.
Em seguida, os valores aceitos podem ser usados para expandir o modelo de rota. O modelo de rota é processado:
Valores fornecidos explicitamente, que não correspondem a um segmento da rota, são adicionados à cadeia de consulta. A tabela a seguir mostra o resultado do uso do modelo de rota {controller}/{action}/{id?}
.
Valores de ambiente | Valores explícitos | Resultado |
---|---|---|
controlador = "Home" | ação = "About" | /Home/About |
controlador = "Home" | controlador = "Order", ação = "About" | /Order/About |
controlador = "Home", cor = "Vermelho" | ação = "About" | /Home/About |
controlador = "Home" | ação = "About", cor = "Red" | /Home/About?color=Red |
Os parâmetros de rota opcionais devem vir após todos os parâmetros de rota e literais obrigatórios. No código a seguir, os parâmetros id
e name
devem vir após o parâmetro color
:
using Microsoft.AspNetCore.Mvc;
namespace WebApplication1.Controllers;
[Route("api/[controller]")]
public class MyController : ControllerBase
{
// GET /api/my/red/2/joe
// GET /api/my/red/2
// GET /api/my
[HttpGet("{color}/{id:int?}/{name?}")]
public IActionResult GetByIdAndOptionalName(string color, int id = 1, string? name = null)
{
return Ok($"{color} {id} {name ?? ""}");
}
}
O código a seguir mostra um exemplo de um esquema de geração de URL que não é compatível com o roteamento:
app.MapControllerRoute(
"default",
"{culture}/{controller=Home}/{action=Index}/{id?}");
app.MapControllerRoute(
"blog",
"{culture}/{**slug}",
new { controller = "Blog", action = "ReadPost" });
No código anterior, o parâmetro de rota culture
é usado para localização. O ideal é que o parâmetro culture
sempre seja aceito como valor ambiente. No entanto, o parâmetro culture
não é aceito como valor ambiente devido à maneira como os valores necessários funcionam:
"default"
, o parâmetro de rota culture
fica à esquerda de controller
. Portanto, as alterações em controller
não invalidarão culture
."blog"
, considera-se que o parâmetro de rota culture
fica à direita de controller
, que aparece nos valores necessários.A classe LinkParser adiciona suporte para analisar um caminho de URL em um conjunto de valores de rota. O método ParsePathByEndpointName usa um nome de ponto de extremidade e um caminho de URL e retorna um conjunto de valores de rota extraídos do caminho de URL.
No controlador de exemplo a seguir, a ação GetProduct
usa um modelo de rota de api/Products/{id}
e tem um Name de GetProduct
:
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet("{id}", Name = nameof(GetProduct))]
public IActionResult GetProduct(string id)
{
// ...
Na mesma classe do controlador, a ação AddRelatedProduct
espera um caminho de URL, pathToRelatedProduct
, que pode ser fornecido como um parâmetro de cadeia de caracteres de consulta:
[HttpPost("{id}/Related")]
public IActionResult AddRelatedProduct(
string id, string pathToRelatedProduct, [FromServices] LinkParser linkParser)
{
var routeValues = linkParser.ParsePathByEndpointName(
nameof(GetProduct), pathToRelatedProduct);
var relatedProductId = routeValues?["id"];
// ...
No exemplo anterior, a ação AddRelatedProduct
extrai o valor de rota id
do caminho de URL. Por exemplo, com um caminho de URL de /api/Products/1
, o valor relatedProductId
é definido como 1
. Essa abordagem permite que os clientes da API usem os caminhos de URL ao referenciar recursos, sem exigir conhecimento de como essa URL é estruturada.
Os links a seguir fornecem informações sobre como configurar metadados de ponto de extremidade:
[MinimumAgeAuthorize]
RequireHost aplica uma restrição à rota que exige o host especificado. O parâmetro RequireHost
ou [Host] pode ser um:
www.domain.com
, corresponde www.domain.com
a qualquer porta.*.domain.com
, corresponde www.domain.com
, subdomain.domain.com
ou www.subdomain.domain.com
a qualquer porta.*:5000
, corresponde a porta 5000 a qualquer host.www.domain.com:5000
ou *.domain.com:5000
, corresponde ao host e à porta.Vários parâmetros podem ser especificados usando RequireHost
ou [Host]
. A restrição corresponde aos hosts válidos para qualquer um dos parâmetros. Por exemplo, [Host("domain.com", "*.domain.com")]
corresponde a domain.com
, www.domain.com
ou subdomain.domain.com
.
O código a seguir usa RequireHost
para exigir o host especificado na rota:
app.MapGet("/", () => "Contoso").RequireHost("contoso.com");
app.MapGet("/", () => "AdventureWorks").RequireHost("adventure-works.com");
app.MapHealthChecks("/healthz").RequireHost("*:8080");
O código a seguir usa o atributo [Host]
no controlador para exigir qualquer um dos hosts especificados:
[Host("contoso.com", "adventure-works.com")]
public class HostsController : Controller
{
public IActionResult Index() =>
View();
[Host("example.com")]
public IActionResult Example() =>
View();
}
Quando o atributo [Host]
é aplicado ao controlador e ao método de ação:
Aviso
Uma API que dependa do cabeçalho de host, como HttpRequest.Host e RequireHost, está sujeita a possíveis falsificações por clientes.
Para evitar falsificação de host e porta, use uma das seguintes abordagens:
O método de extensão MapGroup ajuda a organizar grupos de pontos de extremidade com um prefixo comum. Isso reduz o código repetitivo e permite personalizar grupos inteiros de pontos de extremidade com uma única chamada a métodos como RequireAuthorization e WithMetadata, que adicionam os metadados de ponto de extremidade.
Por exemplo, o código a seguir cria dois grupos de pontos de extremidade semelhantes:
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 pontos de extremidade corresponderá apenas às solicitações prefixadas com /public/todos
e estará acessível sem autenticação. O segundo grupo de pontos de extremidade corresponderá apenas às solicitações prefixadas com /private/todos
e exigirá autenticação.
A QueryPrivateTodos
fábrica de filtro de ponto de extremidade é uma função local que modifica os TodoDb
parâmetros do manipulador de rota para permitir o acesso e armazenamento de dados privados de tarefas.
Os grupos de rotas também permitem grupos aninhados e padrões de prefixo complexos com parâmetros de rota e restrições. 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 é igual a adicioná-los individualmente a cada ponto de extremidade, antes de adicionar filtros ou metadados extras 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 tenha sido adicionado depois. Como os filtros foram aplicados a grupos diferentes, a ordem em que foram adicionados em relação uns aos outros não importa. A ordem em que os filtros são adicionados importa 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
Quando um aplicativo tem problemas de desempenho, geralmente suspeita-se que o roteamento é o problema. O motivo pelo qual o roteamento é suspeito é que as estruturas como controladores e Razor Pages relatam o tempo gasto dentro da estrutura nas mensagens de log. Quando há uma diferença significativa entre o tempo relatado pelos controladores e o tempo total da solicitação:
O desempenho do roteamento é testado usando milhares de pontos de extremidade. É improvável que um aplicativo típico encontre um problema de desempenho apenas por ser muito grande. A causa raiz mais comum do desempenho lento do roteamento geralmente é um middleware personalizado com comportamento inválido.
O exemplo de código a seguir demonstra uma técnica básica para restringir a fonte de atraso:
var logger = app.Services.GetRequiredService<ILogger<Program>>();
app.Use(async (context, next) =>
{
var stopwatch = Stopwatch.StartNew();
await next(context);
stopwatch.Stop();
logger.LogInformation("Time 1: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});
app.UseRouting();
app.Use(async (context, next) =>
{
var stopwatch = Stopwatch.StartNew();
await next(context);
stopwatch.Stop();
logger.LogInformation("Time 2: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});
app.UseAuthorization();
app.Use(async (context, next) =>
{
var stopwatch = Stopwatch.StartNew();
await next(context);
stopwatch.Stop();
logger.LogInformation("Time 3: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});
app.MapGet("/", () => "Timing Test.");
Para roteamento de tempo:
Essa é uma maneira básica de restringir o atraso quando ele é significativo, por exemplo, mais de 10ms
. Subtrair Time 2
de Time 1
relata o tempo gasto dentro do middleware UseRouting
.
O código a seguir usa uma abordagem mais compacta para o código de tempo anterior:
public sealed class AutoStopwatch : IDisposable
{
private readonly ILogger _logger;
private readonly string _message;
private readonly Stopwatch _stopwatch;
private bool _disposed;
public AutoStopwatch(ILogger logger, string message) =>
(_logger, _message, _stopwatch) = (logger, message, Stopwatch.StartNew());
public void Dispose()
{
if (_disposed)
{
return;
}
_logger.LogInformation("{Message}: {ElapsedMilliseconds}ms",
_message, _stopwatch.ElapsedMilliseconds);
_disposed = true;
}
}
var logger = app.Services.GetRequiredService<ILogger<Program>>();
var timerCount = 0;
app.Use(async (context, next) =>
{
using (new AutoStopwatch(logger, $"Time {++timerCount}"))
{
await next(context);
}
});
app.UseRouting();
app.Use(async (context, next) =>
{
using (new AutoStopwatch(logger, $"Time {++timerCount}"))
{
await next(context);
}
});
app.UseAuthorization();
app.Use(async (context, next) =>
{
using (new AutoStopwatch(logger, $"Time {++timerCount}"))
{
await next(context);
}
});
app.MapGet("/", () => "Timing Test.");
A lista a seguir fornece alguns insights sobre recursos de roteamento relativamente caros, em comparação a modelos de rota básicos:
{x}-{y}-{z}
): Por padrão, o ASP.NET Core usa um algoritmo de roteamento que troca memória por tempo de CPU. O bom resultado disso é que o tempo de correspondência de rotas depende apenas do tamanho do caminho a ser correspondido e não do número de rotas. No entanto, essa abordagem pode ser possivelmente problemática em alguns casos, quando o aplicativo tem um grande número de rotas (milhares) e há uma grande quantidade de prefixos variáveis nas rotas. Por exemplo, se as rotas tiverem parâmetros nos segmentos iniciais da rota, como {parameter}/some/literal
.
É improvável que um aplicativo tenha uma situação em que esse seja um problema, a menos nos seguintes casos:
Microsoft.AspNetCore.Routing.Matching.DfaNode
.Existem várias técnicas e otimizações que podem ser aplicadas às rotas que melhoram muito esse cenário:
{parameter:int}
, {parameter:guid}
, {parameter:regex(\\d+)}
etc., sempre que possível.
MapDynamicControllerRoute
ou MapDynamicPageRoute
.Ao fazer o roteamento corresponder a um ponto de extremidade, ele normalmente permite que o restante do pipeline de middleware seja executado antes de invocar a lógica do ponto de extremidade. Os serviços podem reduzir o uso de recursos filtrando solicitações conhecidas no início do pipeline. Use o método de extensão ShortCircuit para fazer com que o roteamento invoque a lógica do ponto de extremidade imediatamente e, em seguida, encerre a solicitação. Por exemplo, uma determinada rota pode não precisar passar pela autenticação ou middleware CORS. O exemplo a seguir solicitações de curto-circuito que correspondem à rota /short-circuit
:
app.MapGet("/short-circuit", () => "Short circuiting!").ShortCircuit();
O método ShortCircuit(IEndpointConventionBuilder, Nullable<Int32>) pode adotar um código de status opcionalmente.
Use o método MapShortCircuit para configurar o curto-circuito para várias rotas ao mesmo tempo, passando para ele uma matriz de parâmetros de prefixos de URL. Por exemplo, navegadores e bots geralmente investigam servidores para caminhos conhecidos como robots.txt
e favicon.ico
. Se o aplicativo não tiver esses arquivos, uma linha de código poderá configurar as rotas:
app.MapShortCircuit(404, "robots.txt", "favicon.ico");
MapShortCircuit
retorna IEndpointConventionBuilder, de forma que restrições de rota adicionais, como filtragem de host, possam ser adicionadas a ele.
Os métodos ShortCircuit
e MapShortCircuit
não afetam o middleware colocado antes de UseRouting
. Tentar usar esses métodos com pontos de extremidade que também tenham metadados [Authorize]
ou [RequireCors]
fará com que as solicitações falhem com um erro InvalidOperationException
. Esses metadados são aplicados por atributos [Authorize]
ou [EnableCors]
ou por métodos RequireCors ou RequireAuthorization.
Para ver o efeito do middleware de curto-circuito, defina a categoria de registro em log "Microsoft" como "Informação" em appsettings.Development.json
:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
Execute o código a seguir:
var app = WebApplication.Create();
app.UseHttpLogging();
app.MapGet("/", () => "No short-circuiting!");
app.MapGet("/short-circuit", () => "Short circuiting!").ShortCircuit();
app.MapShortCircuit(404, "robots.txt", "favicon.ico");
app.Run();
O exemplo a seguir é dos logs do console produzidos pela execução do ponto de extremidade do /
. Inclui a saída do middleware do registro em log:
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
Executing endpoint 'HTTP: GET /'
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
Executed endpoint 'HTTP: GET /'
info: Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware[2]
Response:
StatusCode: 200
Content-Type: text/plain; charset=utf-8
Date: Wed, 03 May 2023 21:05:59 GMT
Server: Kestrel
Alt-Svc: h3=":5182"; ma=86400
Transfer-Encoding: chunked
O exemplo a seguir é da execução do ponto de extremidade do /short-circuit
. Não tem nada do middleware do registro em log porque o middleware entrou em curto-circuito:
info: Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware[4]
The endpoint 'HTTP: GET /short-circuit' is being executed without running additional middleware.
info: Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware[5]
The endpoint 'HTTP: GET /short-circuit' has been executed without running additional middleware.
Esta seção contém diretrizes para criadores de bibliotecas com base no roteamento. Esses detalhes destinam-se a garantir que os desenvolvedores de aplicativos tenham uma boa experiência usando bibliotecas e estruturas que estendem o roteamento.
Para criar uma estrutura que usa o roteamento para correspondência de URL, comece definindo uma experiência do usuário baseada em UseEndpoints.
CRIE com base em IEndpointRouteBuilder. Isso permite que os usuários componham a estrutura com outros recursos do ASP.NET Core, sem confusão. Cada modelo do ASP.NET Core inclui o roteamento. Suponha que o roteamento esteja presente e seja conhecido para os usuários.
// Your framework
app.MapMyFramework(...);
app.MapHealthChecks("/healthz");
RETORNE um tipo de concreto selado de uma chamada para MapMyFramework(...)
que implemente IEndpointConventionBuilder. A maioria dos métodos Map...
da estrutura segue esse padrão. A interface IEndpointConventionBuilder
:
Declarar seu próprio tipo permite que você adicione sua própria funcionalidade específica da estrutura ao construtor. Não há problema em encapsular um construtor declarado por estrutura e encaminhar chamadas para ele.
// Your framework
app.MapMyFramework(...)
.RequireAuthorization()
.WithMyFrameworkFeature(awesome: true);
app.MapHealthChecks("/healthz");
GRAVE seu próprio EndpointDataSource. EndpointDataSource
é o primitivo de baixo nível para declarar e atualizar uma coleção de pontos de extremidade. EndpointDataSource
é uma API eficiente usada por controladores e Razor Pages. Para obter mais informações, consulte Roteamento de ponto de extremidade dinâmico.
Os testes de roteamento têm um exemplo básico de uma fonte de dados que não está atualizando.
IMPLEMENTEGetGroupedEndpoints. Isso fornece controle total sobre a execução de convenções de grupo e os metadados finais nos pontos de extremidade agrupados. Por exemplo, isso permite que as implementações personalizadas EndpointDataSource
executem os filtros de ponto de extremidade adicionados a grupos.
NÃO tente registrar um EndpointDataSource
por padrão. Exija que os usuários registrem a estrutura no UseEndpoints. A filosofia do roteamento determina que nada está incluído por padrão e esse UseEndpoints
é o local para registrar pontos de extremidade.
DEFINA os tipos de metadados como uma interface.
POSSIBILITE o uso de tipos de metadados como um atributo em classes e métodos.
public interface ICoolMetadata
{
bool IsCool { get; }
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
public bool IsCool => true;
}
As estruturas como controladores e Razor Pages dão suporte à aplicação de atributos de metadados a tipos e métodos. Se você declarar os tipos de metadados:
Declarar um tipo de metadados como uma interface adiciona outra camada de flexibilidade:
POSSIBILITE a substituição de metadados, conforme mostrado no exemplo a seguir:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class SuppressCoolMetadataAttribute : Attribute, ICoolMetadata
{
public bool IsCool => false;
}
[CoolMetadata]
public class MyController : Controller
{
public void MyCool() { }
[SuppressCoolMetadata]
public void Uncool() { }
}
A melhor maneira de seguir estas diretrizes é evitar definir metadados de marcador:
A coleção de metadados é ordenada e permite substituir por prioridade. No caso de controladores, os metadados no método de ação são mais específicos.
TORNE o middleware útil com e sem roteamento:
app.UseAuthorization(new AuthorizationPolicy() { ... });
// Your framework
app.MapMyFramework(...).RequireAuthorization();
Como exemplo dessa diretriz, considere o middleware UseAuthorization
. O middleware de autorização permite que você transmita uma política de fallback. A política de fallback, se especificada, aplica-se a:
Isso torna o middleware de autorização útil fora do contexto de roteamento. O middleware de autorização pode ser usado para programação de middleware tradicional.
Para obter a saída de diagnóstico de roteamento detalhada, defina Logging:LogLevel:Microsoft
como Debug
. No ambiente de desenvolvimento, defina o nível de log em appsettings.Development.json
:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Debug",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
O roteamento é responsável por corresponder solicitações HTTP de entrada e expedir essas solicitações para os pontos de extremidade executáveis do aplicativo. Os pontos de extremidade são as unidades de código executável de manipulação de solicitações do aplicativo. Os pontos de extremidade são definidos no aplicativo e configurados quando o aplicativo é iniciado. O processo de correspondência de ponto de extremidade pode extrair valores da URL da solicitação e fornecer esses valores para processamento de solicitações. Usando as informações de ponto de extremidade do aplicativo, o roteamento também pode gerar URLs que são mapeadas para os pontos de extremidade.
Os aplicativos podem configurar o roteamento usando:
Este artigo aborda os detalhes de baixo nível do roteamento do ASP.NET Core. Para obter informações sobre como configurar o roteamento:
O código a seguir mostra um exemplo básico de roteamento:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
O exemplo anterior inclui um único ponto de extremidade usando o método MapGet:
GET
é enviada para a URL raiz /
: Hello World!
é gravado na resposta HTTP.GET
ou a URL raiz não for /
, nenhuma rota corresponderá e um HTTP 404 será retornado.O roteamento usa um par de middleware registrado por UseRouting e UseEndpoints:
UseRouting
adiciona a correspondência de rotas ao pipeline de middleware. Esse middleware examina o conjunto de pontos de extremidade definidos no aplicativo e seleciona a melhor correspondência com base na solicitação.UseEndpoints
adiciona a execução do ponto de extremidade ao pipeline de middleware. Ele executa o delegado associado ao ponto de extremidade selecionado.Normalmente, os aplicativos não precisam chamar UseRouting
ou UseEndpoints
. WebApplicationBuilder configura um pipeline de middleware, que encapsula o middleware adicionado em Program.cs
com UseRouting
e UseEndpoints
. No entanto, os aplicativos podem alterar a ordem na qual UseRouting
e UseEndpoints
são executados, chamando esses métodos explicitamente. Por exemplo, o código a seguir faz uma chamada explícita para UseRouting
:
app.Use(async (context, next) =>
{
// ...
await next(context);
});
app.UseRouting();
app.MapGet("/", () => "Hello World!");
No código anterior:
app.Use
registra um middleware personalizado, que é executado no início do pipeline.UseRouting
configura o middleware de correspondência de rotas a ser executado após o middleware personalizado.MapGet
é executado na extremidade do pipeline.Se o exemplo anterior não incluísse uma chamada para UseRouting
, o middleware personalizado seria executado após o middleware de correspondência de rotas.
O método MapGet
é usado para definir um ponto de extremidade. Um ponto de extremidade pode ser:
Os pontos de extremidade que podem ser correspondidos e executados pelo aplicativo são configurados no UseEndpoints
. Por exemplo, MapGet, MapPost e métodos semelhantes conectam os delegados de solicitação ao sistema de roteamento. Métodos adicionais podem ser usados para conectar os recursos de estrutura do ASP.NET Core ao sistema de roteamento:
O exemplo a seguir mostra o roteamento com um modelo de rota mais sofisticado:
app.MapGet("/hello/{name:alpha}", (string name) => $"Hello {name}!");
A cadeia de caracteres /hello/{name:alpha}
é um modelo de rota. Um modelo de rota é usado para configurar a forma como o ponto de extremidade é correspondido. Nesse caso, o modelo corresponde a:
/hello/Docs
/hello/
seguido de uma sequência de caracteres alfabéticos. :alpha
aplica uma restrição de rota que corresponde apenas a caracteres alfabéticos. As restrições de rota serão explicadas posteriormente neste artigo.O segundo segmento do caminho de URL, {name:alpha}
:
name
.O exemplo a seguir mostra o roteamento com as verificações de integridade e a autorização:
app.UseAuthentication();
app.UseAuthorization();
app.MapHealthChecks("/healthz").RequireAuthorization();
app.MapGet("/", () => "Hello World!");
O exemplo anterior demonstra como:
A chamada MapHealthChecks adiciona um ponto de extremidade de verificação de integridade. O encadeamento de RequireAuthorization para esta chamada anexa uma política de autorização ao ponto de extremidade.
Chamar UseAuthentication e UseAuthorization adiciona o middleware de autenticação e autorização. Esses programas de middleware são colocados entre UseRouting e UseEndpoints
para que possam:
UseRouting
.No exemplo anterior, há dois pontos de extremidade, mas apenas o ponto de extremidade de verificação de integridade tem uma política de autorização anexada. Se a solicitação corresponder ao ponto de extremidade de verificação de integridade, /healthz
, uma verificação de autorização será executada. Isso demonstra que os pontos de extremidade podem ter dados extras anexados. Esses dados extras são chamados de metadados de ponto de extremidade:
O sistema de roteamento se baseia no pipeline de middleware, adicionando o conceito de ponto de extremidade eficiente. Os pontos de extremidade representam as unidades da funcionalidade do aplicativo que são diferentes umas das outras em termos de roteamento, autorização e qualquer número de sistemas do ASP.NET Core.
Um ponto de extremidade do ASP.NET Core é:
O código a seguir mostra como recuperar e inspecionar o ponto de extremidade correspondente à solicitação atual:
app.Use(async (context, next) =>
{
var currentEndpoint = context.GetEndpoint();
if (currentEndpoint is null)
{
await next(context);
return;
}
Console.WriteLine($"Endpoint: {currentEndpoint.DisplayName}");
if (currentEndpoint is RouteEndpoint routeEndpoint)
{
Console.WriteLine($" - Route Pattern: {routeEndpoint.RoutePattern}");
}
foreach (var endpointMetadata in currentEndpoint.Metadata)
{
Console.WriteLine($" - Metadata: {endpointMetadata}");
}
await next(context);
});
app.MapGet("/", () => "Inspect Endpoint.");
O ponto de extremidade, se selecionado, pode ser recuperado a partir do HttpContext
. Suas propriedades podem ser inspecionadas. Os objetos de ponto de extremidade são imutáveis e não podem ser modificados após a criação. O tipo mais comum de ponto de extremidade é um RouteEndpoint. RouteEndpoint
inclui informações que permitem que ele seja selecionado pelo sistema de roteamento.
No código anterior, app.Use configura um middleware embutido.
O código a seguir mostra que, dependendo de onde app.Use
é chamado no pipeline, pode não haver um ponto de extremidade:
// Location 1: before routing runs, endpoint is always null here.
app.Use(async (context, next) =>
{
Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
await next(context);
});
app.UseRouting();
// Location 2: after routing runs, endpoint will be non-null if routing found a match.
app.Use(async (context, next) =>
{
Console.WriteLine($"2. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
await next(context);
});
// Location 3: runs when this endpoint matches
app.MapGet("/", (HttpContext context) =>
{
Console.WriteLine($"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
return "Hello World!";
}).WithDisplayName("Hello");
app.UseEndpoints(_ => { });
// Location 4: runs after UseEndpoints - will only run if there was no match.
app.Use(async (context, next) =>
{
Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
await next(context);
});
O exemplo anterior adiciona instruções Console.WriteLine
que mostram se um ponto de extremidade foi selecionado ou não. Para maior clareza, o exemplo atribui um nome de exibição ao ponto de extremidade /
fornecido.
O exemplo anterior também inclui chamadas para UseRouting
e UseEndpoints
para controlar exatamente quando esses programas de middleware são executados no pipeline.
Executar esse código com uma URL do /
exibe:
1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello
Executar esse código com qualquer outra URL exibe:
1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)
Essa saída demonstra que:
UseRouting
seja chamado.UseRouting
e UseEndpoints.UseEndpoints
é terminal quando uma correspondência é encontrada. O middleware de terminal será definido posteriormente neste artigo.UseEndpoints
é executado apenas quando nenhuma correspondência é encontrada.O middleware UseRouting
usa o método SetEndpoint para anexar o ponto de extremidade ao contexto atual. É possível substituir o middleware UseRouting
pela lógica personalizada e ainda obter os benefícios de usar pontos de extremidade. Os pontos de extremidade são primitivos de baixo nível, como o middleware, e não são acoplados à implementação de roteamento. A maioria dos aplicativos não precisa substituir UseRouting
pela lógica personalizada.
O middleware UseEndpoints
foi projetado para ser usado em conjunto com o middleware UseRouting
. A lógica principal para executar um ponto de extremidade não é complicada. Use GetEndpoint para recuperar o ponto de extremidade e, em seguida, invoque a propriedade RequestDelegate.
O código a seguir demonstra como o middleware pode influenciar ou reagir ao roteamento:
app.UseHttpMethodOverride();
app.UseRouting();
app.Use(async (context, next) =>
{
if (context.GetEndpoint()?.Metadata.GetMetadata<RequiresAuditAttribute>() is not null)
{
Console.WriteLine($"ACCESS TO SENSITIVE DATA AT: {DateTime.UtcNow}");
}
await next(context);
});
app.MapGet("/", () => "Audit isn't required.");
app.MapGet("/sensitive", () => "Audit required for sensitive data.")
.WithMetadata(new RequiresAuditAttribute());
public class RequiresAuditAttribute : Attribute { }
O exemplo anterior demonstra dois conceitos importantes:
UseRouting
para modificar os dados nos quais o roteamento opera.
UseRouting
e UseEndpoints para processar os resultados do roteamento, antes que o ponto de extremidade seja executado.
UseRouting
e UseEndpoints
: UseAuthorization
e UseCors
.O código anterior mostra um exemplo de um middleware personalizado que permite políticas por ponto de extremidade. O middleware grava um log de auditoria de acesso a dados confidenciais no console. O middleware pode ser configurado para auditar um ponto de extremidade com os metadados RequiresAuditAttribute
. Este exemplo demonstra um padrão de aceitação em que apenas os pontos de extremidade marcados como confidenciais são auditados. É possível definir essa lógica ao contrário, auditando tudo o que não está marcado como seguro, por exemplo. O sistema de metadados do ponto de extremidade é flexível. Essa lógica pode ser projetada da maneira que for adequada para o caso de uso.
O código de exemplo anterior destina-se a demonstrar os conceitos básicos dos pontos de extremidade. O exemplo não se destina ao uso de produção. Uma versão mais completa de um middleware de log de auditoria:
Os metadados da política de auditoria RequiresAuditAttribute
são definidos como um Attribute
para facilitar o uso com estruturas baseadas em classe, como controladores e SignalR. Ao usar a rota para o código:
A melhor prática para tipos de metadados é defini-los como interfaces ou atributos. As interfaces e os atributos permitem a reutilização de código. O sistema de metadados é flexível e não impõe limitações.
O exemplo a seguir demonstra o middleware de terminal e o roteamento:
// Approach 1: Terminal Middleware.
app.Use(async (context, next) =>
{
if (context.Request.Path == "/")
{
await context.Response.WriteAsync("Terminal Middleware.");
return;
}
await next(context);
});
app.UseRouting();
// Approach 2: Routing.
app.MapGet("/Routing", () => "Routing.");
O estilo de middleware mostrado com Approach 1:
é o middleware de terminal. Ele é chamado de middleware de terminal porque faz uma operação correspondente:
Path == "/"
para o middleware e Path == "/Routing"
para o roteamento.next
.Ele é chamado de middleware de terminal porque termina a pesquisa, executa algumas funcionalidades e retorna.
A lista a seguir compara o middleware de terminal com o roteamento:
next
.UseAuthorization
e UseCors
.
UseAuthorization
ou UseCors
exige uma interface manual com o sistema de autorização.Um ponto de extremidade define ambos:
O middleware de terminal pode ser uma ferramenta eficaz, mas pode exigir:
Considere a integração com o roteamento antes de gravar um middleware de terminal.
O middleware de terminal existente que é integrado ao Map ou MapWhen geralmente pode ser transformado em um ponto de extremidade com reconhecimento de roteamento. O MapHealthChecks demonstra o padrão para router-ware:
Map
e forneça o novo pipeline de middleware.Map
usando o método de extensão.O código a seguir mostra o uso do MapHealthChecks:
app.UseAuthentication();
app.UseAuthorization();
app.MapHealthChecks("/healthz").RequireAuthorization();
O exemplo anterior mostra por que retornar o objeto do construtor é importante. Retornar o objeto do construtor permite que o desenvolvedor do aplicativo configure políticas como autorização para o ponto de extremidade. Neste exemplo, o middleware de verificações de integridade não tem integração direta com o sistema de autorização.
O sistema de metadados foi criado em resposta aos problemas encontrados por autores de extensibilidade usando o middleware de terminal. É problemático para cada middleware implementar sua própria integração com o sistema de autorização.
Quando um middleware de roteamento é executado, ele define um Endpoint
e encaminha os valores para um recurso de solicitação no HttpContext a partir da solicitação atual:
HttpRequest.RouteValues
obtém a coleção de valores de rota.O middleware executado após o middleware de roteamento pode inspecionar o ponto de extremidade e realizar uma ação. Por exemplo, um middleware de autorização pode interrogar a coleção de metadados do ponto de extremidade para uma política de autorização. Depois que todos os middlewares no pipeline de processamento da solicitação forem executados, o representante do ponto de extremidade selecionado será invocado.
O sistema de roteamento no roteamento de ponto de extremidade é responsável por todas as decisões de expedição. Como o middleware aplica políticas com base no ponto de extremidade selecionado, é importante que:
Aviso
Para compatibilidade com versões anteriores, quando o delegado do ponto de extremidade do Controlador ou do Razor Pages é executado, as propriedades do RouteContext.RouteData são definidas com os valores apropriados com base no processamento da solicitação executado até o momento.
O tipo RouteContext
será marcado como obsoleto em uma versão futura:
RouteData.Values
para HttpRequest.RouteValues
.RouteData.DataTokens
para recuperar IDataTokensMetadata nos metadados do ponto de extremidade.A correspondência de URL opera em um conjunto configurável de fases. Em cada fase, a saída é um conjunto de correspondências. O conjunto de correspondências pode ser reduzido ainda mais pela próxima fase. A implementação de roteamento não garante uma ordem de processamento para pontos de extremidade correspondentes. Todas as correspondências possíveis são processadas de uma só vez. As fases de correspondência de URL ocorrem na ordem a seguir. ASP.NET Core:
A lista de pontos de extremidade é priorizada de acordo com:
Todos os pontos de extremidade correspondentes são processados em cada fase até que o EndpointSelector seja atingido. O EndpointSelector
é a fase final. Ele escolhe o ponto de extremidade de prioridade mais alta nas correspondências como a melhor correspondência. Se houver outras correspondências com a mesma prioridade que a melhor correspondência, uma exceção de correspondência ambígua será gerada.
A precedência de rota é calculada com base em um modelo de rota mais específico que recebe uma prioridade mais alta. Por exemplo, considere os modelos /hello
e /{message}
:
/hello
./hello
é mais específico e, portanto, tem prioridade mais alta.Em geral, a precedência de rota escolhe a melhor correspondência para os tipos de esquemas de URL usados na prática. Use Order somente quando necessário para evitar uma ambiguidade.
Devido aos tipos de extensibilidade fornecidos pelo roteamento, não é possível que o sistema de roteamento calcule antecipadamente as rotas ambíguas. Considere um exemplo, como os modelos de rota /{message:alpha}
e /{message:int}
:
alpha
corresponde apenas a caracteres alfabéticos.int
corresponde apenas a números.Aviso
A ordem das operações dentro do UseEndpoints não influencia o comportamento do roteamento, com uma exceção. MapControllerRoute e MapAreaRoute atribuem automaticamente um valor de pedido aos pontos de extremidade com base na ordem em que são invocados. Isso simula o comportamento de longo prazo dos controladores, sem que o sistema de roteamento forneça as mesmas garantias que as implementações de roteamento mais antigas.
Roteamento de ponto de extremidade no ASP.NET Core:
A precedência de modelo de rota é um sistema que atribui a cada modelo de rota um valor com base na especificidade. A precedência de modelo de rota:
Por exemplo, considere os modelos /Products/List
e /Products/{id}
. Seria aceitável supor que /Products/List
é uma correspondência melhor do que /Products/{id}
para o caminho de URL /Products/List
. Isso funciona porque o segmento literal /List
é considerado com melhor precedência do que o segmento de parâmetro /{id}
.
Os detalhes de como funciona a precedência são acoplados à forma como os modelos de rota são definidos:
Geração de URL:
O roteamento de ponto de extremidade inclui a API LinkGenerator. LinkGenerator
é um serviço singleton disponível na DI. A API LinkGenerator
pode ser usada fora do contexto de uma solicitação em execução. O Mvc.IUrlHelper e os cenários que dependem do IUrlHelper, como Auxiliares de Marcação, Auxiliares de HTML e Resultados da Ação, usam a API LinkGenerator
internamente para fornecer as funcionalidades de geração de link.
O gerador de link é respaldado pelo conceito de um endereço e esquemas de endereço. Um esquema de endereço é uma maneira de determinar os pontos de extremidade que devem ser considerados para a geração de link. Por exemplo, os cenários de nome de rota e valores de rota com os quais muitos usuários estão familiarizados nos controladores e no Razor Pages são implementados como um esquema de endereço.
O gerador de link pode ser vinculado aos controladores e ao Razor Pages usando os seguintes métodos de extensão:
As sobrecargas desses métodos aceitam argumentos que incluem o HttpContext
. Esses métodos são funcionalmente equivalentes a Url.Action e Url.Page, mas oferecem mais flexibilidade e opções.
Os métodos GetPath*
são mais semelhantes a Url.Action
e Url.Page
, pois geram um URI que contém um caminho absoluto. Os métodos GetUri*
sempre geram um URI absoluto que contém um esquema e um host. Os métodos que aceitam um HttpContext
geram um URI no contexto da solicitação em execução. Os valores de rota de ambiente, o caminho base da URL, o esquema e o host da solicitação em execução são usados, a menos que sejam substituídos.
LinkGenerator é chamado com um endereço. A geração de um URI ocorre em duas etapas:
Os métodos fornecidos pelo LinkGenerator dão suporte a funcionalidades de geração de link padrão para qualquer tipo de endereço. A maneira mais conveniente usar o gerador de link é por meio de métodos de extensão que executam operações para um tipo de endereço específico:
Método de extensão | Descrição |
---|---|
GetPathByAddress | Gera um URI com um caminho absoluto com base nos valores fornecidos. |
GetUriByAddress | Gera um URI absoluto com base nos valores fornecidos. |
Aviso
Preste atenção às seguintes implicações da chamada de métodos LinkGenerator:
Use métodos de extensão de GetUri*
com cuidado em uma configuração de aplicativo que não valide o cabeçalho Host
das solicitações de entrada. Se o cabeçalho Host
das solicitações de entrada não é validado, uma entrada de solicitação não confiável pode ser enviada novamente ao cliente em URIs em uma exibição ou página. Recomendamos que todos os aplicativos de produção configurem seu servidor para validar o cabeçalho Host
com os valores válidos conhecidos.
Use LinkGenerator com cuidado no middleware em combinação com Map
ou MapWhen
. Map*
altera o caminho base da solicitação em execução, o que afeta a saída da geração de link. Todas as APIs de LinkGenerator permitem a especificação de um caminho base. Especifique um caminho base vazio para desfazer o efeito de Map*
na geração de link.
No exemplo a seguir, um middleware usa a API de LinkGenerator para criar um link para um método de ação que lista os produtos da loja. O uso do gerador de link com sua injeção em uma classe e uma chamada a GenerateLink
está disponível para qualquer classe em um aplicativo:
public class ProductsMiddleware
{
private readonly LinkGenerator _linkGenerator;
public ProductsMiddleware(RequestDelegate next, LinkGenerator linkGenerator) =>
_linkGenerator = linkGenerator;
public async Task InvokeAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Plain;
var productsPath = _linkGenerator.GetPathByAction("Products", "Store");
await httpContext.Response.WriteAsync(
$"Go to {productsPath} to see our products.");
}
}
Os tokens entre {}
definem os parâmetros de rota que serão associados, se a rota for correspondida. Mais de um parâmetro de rota pode ser definido em um segmento de rota, mas os parâmetros de rota precisam ser separados por um valor literal. Por exemplo:
{controller=Home}{action=Index}
não é uma rota válida, já que não há valor literal entre {controller}
e {action}
. Os parâmetros de rota devem ter um nome e podem ter atributos adicionais especificados.
Um texto literal diferente dos parâmetros de rota (por exemplo, {id}
) e do separador de caminho /
precisa corresponder ao texto na URL. A correspondência de texto não diferencia maiúsculas de minúsculas e se baseia na representação decodificada do caminho de URLs. Para encontrar a correspondência de um delimitador de parâmetro de rota literal {
ou }
, faça o escape do delimitador repetindo o caractere. Por exemplo {{
ou }}
.
Asterisco *
ou asterisco duplo **
:
blog/{**slug}
: blog/
e tenha qualquer valor depois dele.blog/
é atribuído ao valor de rota de campo de dados dinâmico.Aviso
Um parâmetro catch-all pode corresponder às rotas incorretamente devido a um bug no roteamento. Os aplicativos afetados por esse bug têm as seguintes características:
{**slug}"
Confira os bugs do GitHub 18677 e 16579, por exemplo, casos que atingiram esse bug.
Uma correção de aceitação para esse bug está contida no SDK do .NET Core 3.1.301 e posterior. O código a seguir define um comutador interno que corrige esse bug:
public static void Main(string[] args)
{
AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior",
true);
CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.
Os parâmetros catch-all também podem corresponder à cadeia de caracteres vazia.
O parâmetro catch-all faz o escape dos caracteres corretos quando a rota é usada para gerar uma URL, incluindo os caracteres separadores de caminho /
. Por exemplo, a rota foo/{*path}
com valores de rota { path = "my/path" }
gera foo/my%2Fpath
. Observe o escape da barra invertida. Para fazer a viagem de ida e volta dos caracteres separadores de caminho, use o prefixo do parâmetro da rota **
. A rota foo/{**path}
com { path = "my/path" }
gera foo/my/path
.
Padrões de URL que tentam capturar um nome de arquivo com uma extensão de arquivo opcional apresentam considerações adicionais. Por exemplo, considere o modelo files/{filename}.{ext?}
. Quando existem valores para filename
e ext
, ambos os valores são populados. Se apenas existir um valor para filename
na URL, a rota encontrará uma correspondência, pois o .
à direita é opcional. As URLs a seguir correspondem a essa rota:
/files/myFile.txt
/files/myFile
Os parâmetros de rota podem ter valores padrão, designados pela especificação do valor padrão após o nome do parâmetro separado por um sinal de igual (=
). Por exemplo, {controller=Home}
define Home
como o valor padrão de controller
. O valor padrão é usado se nenhum valor está presente na URL para o parâmetro. Os parâmetros de rota se tornam opcionais com o acréscimo de um ponto de interrogação (?
) ao final do nome do parâmetro. Por exemplo, id?
. A diferença entre valores opcionais e parâmetros de rota padrão é:
Os parâmetros de rota podem ter restrições que precisam corresponder ao valor de rota associado da URL. A adição de :
e do nome da restrição após o nome do parâmetro de rota especifica uma restrição embutida em um parâmetro de rota. Se a restrição exigir argumentos, eles ficarão entre parênteses (...)
após o nome da restrição. Várias restrições embutidas podem ser especificadas por meio do acréscimo de outros :
e do nome da restrição.
O nome da restrição e os argumentos são passados para o serviço IInlineConstraintResolver para criar uma instância de IRouteConstraint a ser usada no processamento de URL. Por exemplo, o modelo de rota blog/{article:minlength(10)}
especifica uma restrição minlength
com o argumento 10
. Para obter mais informações sobre as restrições de rota e uma lista das restrições fornecidas pela estrutura, confira a seção Restrições de rota.
Os parâmetros de rota também podem ter transformadores de parâmetro. Os transformadores de parâmetro transformam o valor de um parâmetro ao gerar links e fazer a correspondência de ações e páginas com URLs. Assim como as restrições, os transformadores de parâmetro podem ser adicionados embutidos a um parâmetro de rota colocando :
e o nome do transformador após o nome do parâmetro de rota. Por exemplo, o modelo de rota blog/{article:slugify}
especifica um transformador slugify
. Para obter mais informações sobre transformadores de parâmetro, confira a seção Transformadores de parâmetro.
A tabela a seguir demonstra modelos de rota de exemplo e seu comportamento:
Modelo de rota | URI de correspondência de exemplo | O URI da solicitação |
---|---|---|
hello |
/hello |
Somente corresponde ao caminho único /hello . |
{Page=Home} |
/ |
Faz a correspondência e define Page como Home . |
{Page=Home} |
/Contact |
Faz a correspondência e define Page como Contact . |
{controller}/{action}/{id?} |
/Products/List |
É mapeado para o controlador Products e a ação List . |
{controller}/{action}/{id?} |
/Products/Details/123 |
É mapeado para o controlador Products e a ação Details , e id definido como 123. |
{controller=Home}/{action=Index}/{id?} |
/ |
É mapeado para o controlador Home e o método Index . id é ignorado. |
{controller=Home}/{action=Index}/{id?} |
/Products |
É mapeado para o controlador Products e o método Index . id é ignorado. |
Em geral, o uso de um modelo é a abordagem mais simples para o roteamento. Restrições e padrões também podem ser especificados fora do modelo de rota.
Os segmentos complexos são processados correspondendo delimitadores literais da direita para a esquerda sem greedy. Por exemplo, [Route("/a{b}c{d}")]
é um segmento complexo.
Os segmentos complexos funcionam de uma maneira específica que deve ser compreendida para usá-los com êxito. O exemplo nesta seção demonstra por que segmentos complexos só funcionam bem quando o texto delimitador não aparece dentro dos valores de parâmetro. Usar um regex e extrair manualmente os valores é necessário para casos mais complexos.
Aviso
Ao usar System.Text.RegularExpressions para processar entradas não confiáveis, passe um tempo limite. Um usuário mal-intencionado pode fornecer entrada para RegularExpressions
, causando um ataque de negação de serviço. APIs ASP.NET Core Framework que usam RegularExpressions
passam um tempo limite.
Este é um resumo das etapas que o roteamento executa com o modelo /a{b}c{d}
e o caminho de URL /abcd
. O |
é usado para ajudar a visualizar como o algoritmo funciona:
c
. Portanto, /abcd
é pesquisado pela direita e localiza /ab|c|d
.d
) agora corresponde ao parâmetro de rota {d}
.a
. Portanto, /ab|c|d
é pesquisado começando de onde paramos e, em seguida, a
é encontrado em /|a|b|c|d
.b
) agora corresponde ao parâmetro de rota {b}
.Este é um exemplo de um caso negativo usando o mesmo modelo /a{b}c{d}
e o caminho de URL /aabcd
. O |
é usado para ajudar a visualizar como o algoritmo funciona. Esse caso não é uma correspondência, que é explicada pelo mesmo algoritmo:
c
. Portanto, /aabcd
é pesquisado pela direita e localiza /aab|c|d
.d
) agora corresponde ao parâmetro de rota {d}
.a
. Portanto, /aab|c|d
é pesquisado começando de onde paramos e, em seguida, a
é encontrado em /a|a|b|c|d
.b
) agora corresponde ao parâmetro de rota {b}
.a
, mas o algoritmo ficou fora do modelo de rota para analisar. Portanto, não é uma correspondência.Como o algoritmo correspondente é sem greedy:
Expressões regulares fornecem muito mais controle sobre o comportamento correspondente.
A correspondência de greedy, também conhecida como correspondência lenta, corresponde à maior cadeia de caracteres possível. O valor sem greedy corresponde à menor cadeia de caracteres possível.
O roteamento com caracteres especiais pode levar a resultados inesperados. Por exemplo, considere um controlador com o seguinte método de ação:
[HttpGet("{id?}/name")]
public async Task<ActionResult<string>> GetName(string id)
{
var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null || todoItem.Name == null)
{
return NotFound();
}
return todoItem.Name;
}
Quando string id
contém os seguintes valores codificados, podem ocorrer resultados inesperados:
ASCII | Encoded |
---|---|
/ |
%2F |
|
+ |
Os parâmetros de rota nem sempre são decodificados por URL. Esse problema poderá ser resolvido no futuro. Para obter mais informações, confira este tópico do GitHub;
As restrições de rota são executadas quando ocorre uma correspondência com a URL de entrada e é criado um token do caminho da URL em valores de rota. Em geral, as restrições da rota inspecionam o valor de rota associado por meio do modelo de rota e tomam uma decisão do tipo "verdadeiro ou falso" sobre se o valor é aceitável. Algumas restrições da rota usam dados fora do valor de rota para considerar se a solicitação pode ser encaminhada. Por exemplo, a HttpMethodRouteConstraint pode aceitar ou rejeitar uma solicitação de acordo com o verbo HTTP. As restrições são usadas em solicitações de roteamento e na geração de link.
Aviso
Não use restrições para a validação de entrada. Se as restrições forem usadas para validação de entrada, a entrada inválida resultará em uma resposta 404
Não Encontrado. A entrada inválida deve produzir uma Solicitação Inválida 400
com uma mensagem de erro apropriada. As restrições de rota são usadas para desfazer a ambiguidade entre rotas semelhantes, não para validar as entradas de uma rota específica.
A tabela a seguir demonstra restrições de rota de exemplo e seu comportamento esperado:
restrição | Exemplo | Correspondências de exemplo | Observações |
---|---|---|---|
int |
{id:int} |
123456789 , -123456789 |
Corresponde a qualquer inteiro |
bool |
{active:bool} |
true , FALSE |
Corresponde a true ou false . Não diferencia maiúsculas de minúsculas |
datetime |
{dob:datetime} |
2016-12-31 , 2016-12-31 7:32pm |
Corresponde a um valor válido DateTime na cultura invariável. Confira o aviso anterior. |
decimal |
{price:decimal} |
49.99 , -1,000.01 |
Corresponde a um valor válido decimal na cultura invariável. Confira o aviso anterior. |
double |
{weight:double} |
1.234 , -1,001.01e8 |
Corresponde a um valor válido double na cultura invariável. Confira o aviso anterior. |
float |
{weight:float} |
1.234 , -1,001.01e8 |
Corresponde a um valor válido float na cultura invariável. Confira o aviso anterior. |
guid |
{id:guid} |
CD2C1638-1638-72D5-1638-DEADBEEF1638 |
Corresponde a um valor Guid válido |
long |
{ticks:long} |
123456789 , -123456789 |
Corresponde a um valor long válido |
minlength(value) |
{username:minlength(4)} |
Rick |
A cadeia de caracteres deve ter, no mínimo, 4 caracteres |
maxlength(value) |
{filename:maxlength(8)} |
MyFile |
A cadeia de caracteres não pode ser maior que 8 caracteres |
length(length) |
{filename:length(12)} |
somefile.txt |
A cadeia de caracteres deve ter exatamente 12 caracteres |
length(min,max) |
{filename:length(8,16)} |
somefile.txt |
A cadeia de caracteres deve ter, pelo menos, 8 e não mais de 16 caracteres |
min(value) |
{age:min(18)} |
19 |
O valor inteiro deve ser, pelo menos, 18 |
max(value) |
{age:max(120)} |
91 |
O valor inteiro não deve ser maior que 120 |
range(min,max) |
{age:range(18,120)} |
91 |
O valor inteiro deve ser, pelo menos, 18, mas não maior que 120 |
alpha |
{name:alpha} |
Rick |
A cadeia de caracteres deve consistir em um ou mais caracteres alfabéticos, a -z e não diferencia maiúsculas de minúsculas. |
regex(expression) |
{ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} |
123-45-6789 |
A cadeia de caracteres deve corresponder à expressão regular. Confira as dicas sobre como definir uma expressão regular. |
required |
{name:required} |
Rick |
Usado para impor que um valor não parâmetro está presente durante a geração de URL |
Aviso
Ao usar System.Text.RegularExpressions para processar entradas não confiáveis, passe um tempo limite. Um usuário mal-intencionado pode fornecer entrada para RegularExpressions
, causando um ataque de negação de serviço. APIs ASP.NET Core Framework que usam RegularExpressions
passam um tempo limite.
Várias restrições delimitadas por dois-pontos podem ser aplicadas a um único parâmetro. Por exemplo, a restrição a seguir restringe um parâmetro para um valor inteiro de 1 ou maior:
[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }
Aviso
As restrições de rota que verificam a URL e são convertidas em um tipo CLR sempre usam a cultura invariável. Por exemplo, conversão para o tipo CLR int
ou DateTime
. Essas restrições consideram que a URL não é localizável. As restrições de rota fornecidas pela estrutura não modificam os valores armazenados nos valores de rota. Todos os valores de rota analisados com base na URL são armazenados como cadeias de caracteres. Por exemplo, a restrição float
tenta converter o valor de rota em um float, mas o valor convertido é usado somente para verificar se ele pode ser convertido em um float.
Aviso
Ao usar System.Text.RegularExpressions para processar entradas não confiáveis, passe um tempo limite. Um usuário mal-intencionado pode fornecer entrada para RegularExpressions
, causando um ataque de negação de serviço. APIs ASP.NET Core Framework que usam RegularExpressions
passam um tempo limite.
As expressões regulares podem ser especificadas como restrições embutidas usando a restrição de rota regex(...)
. Os métodos na família MapControllerRoute também aceitam um literal de objeto das restrições. Se esse formulário for usado, os valores de cadeia de caracteres serão interpretados como expressões regulares.
O código a seguir usa uma restrição regex embutida:
app.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
() => "Inline Regex Constraint Matched");
O código a seguir usa um literal de objeto para especificar uma restrição regex:
app.MapControllerRoute(
name: "people",
pattern: "people/{ssn}",
constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
defaults: new { controller = "People", action = "List" });
A estrutura do ASP.NET Core adiciona RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant
ao construtor de expressão regular. Confira RegexOptions para obter uma descrição desses membros.
As expressões regulares usam delimitadores e tokens semelhantes aos usados pelo roteamento e pela linguagem C#. Os tokens de expressão regular precisam ter escape. Para usar a expressão regular ^\d{3}-\d{2}-\d{4}$
em uma restrição embutida, use uma das seguintes opções:
\
fornecidos na cadeia de caracteres pelos caracteres \\
no arquivo de origem do C# para escapar do caractere de escape da cadeia de caracteres \
.Para fazer o escape dos caracteres de delimitador de parâmetro de roteamento {
, }
, [
, ]
, duplique os caracteres na expressão, por exemplo, {{
, }}
, [[
, ]]
. A tabela a seguir mostra uma expressão regular e a versão com escape:
Expressão regular | Expressão regular com escape |
---|---|
^\d{3}-\d{2}-\d{4}$ |
^\\d{{3}}-\\d{{2}}-\\d{{4}}$ |
^[a-z]{2}$ |
^[[a-z]]{{2}}$ |
As expressões regulares usadas no roteamento geralmente começam com o caractere ^
e correspondem à posição inicial da cadeia de caracteres. As expressões geralmente terminam com o caractere $
e correspondem ao final da cadeia de caracteres. Os caracteres ^
e $
garantem que a expressão regular corresponde a todo o valor do parâmetro de rota. Sem os caracteres ^
e $
, a expressão regular corresponde a qualquer subcadeia de caracteres na cadeia de caracteres, o que geralmente não é o desejado. A tabela a seguir fornece exemplos e explica por que eles encontram ou não uma correspondência:
Expression | String | Corresponder a | Comentar |
---|---|---|---|
[a-z]{2} |
hello | Sim | A subcadeia de caracteres corresponde |
[a-z]{2} |
123abc456 | Sim | A subcadeia de caracteres corresponde |
[a-z]{2} |
mz | Sim | Corresponde à expressão |
[a-z]{2} |
MZ | Sim | Não diferencia maiúsculas de minúsculas |
^[a-z]{2}$ |
hello | Não | Confira ^ e $ acima |
^[a-z]{2}$ |
123abc456 | Não | Confira ^ e $ acima |
Para saber mais sobre a sintaxe de expressões regulares, confira Expressões regulares do .NET Framework.
Para restringir um parâmetro a um conjunto conhecido de valores possíveis, use uma expressão regular. Por exemplo, {action:regex(^(list|get|create)$)}
apenas corresponde o valor da rota action
a list
, get
ou create
. Se passada para o dicionário de restrições, a cadeia de caracteres ^(list|get|create)$
é equivalente. As restrições passadas para o dicionário de restrições que não correspondem a uma das restrições conhecidas também são tratadas como expressões regulares. As restrições transmitidas em um modelo que não correspondem a uma das restrições conhecidas não são tratadas como expressões regulares.
É possível criar restrições de rota personalizadas com a implementação da interface do IRouteConstraint. A interface do IRouteConstraint
contém Match, que retorna true
quando a restrição é atendida. Caso contrário, retorna false
.
As restrições de rota personalizadas raramente são necessárias. Antes de implementar uma restrição de rota personalizada, considere alternativas, como a associação de modelo.
A pasta restrições do ASP.NET Core fornece bons exemplos de criação de restrições. Por exemplo, GuidRouteConstraint.
Para usar uma IRouteConstraint
personalizada, o tipo de restrição de rota deve ser registrado com o ConstraintMap do aplicativo, no contêiner de serviço. O ConstraintMap
é um dicionário que mapeia as chaves de restrição de rota para implementações de IRouteConstraint
que validam essas restrições. É possível atualizar o ConstraintMap
do aplicativo no Program.cs
como parte de uma chamada AddRouting ou configurando RouteOptions diretamente com builder.Services.Configure<RouteOptions>
. Por exemplo:
builder.Services.AddRouting(options =>
options.ConstraintMap.Add("noZeroes", typeof(NoZeroesRouteConstraint)));
A restrição anterior é aplicada no seguinte código:
[ApiController]
[Route("api/[controller]")]
public class NoZeroesController : ControllerBase
{
[HttpGet("{id:noZeroes}")]
public IActionResult Get(string id) =>
Content(id);
}
A implementação de NoZeroesRouteConstraint
impede que 0
seja usada em um parâmetro de rota:
public class NoZeroesRouteConstraint : IRouteConstraint
{
private static readonly Regex _regex = new(
@"^[1-9]*$",
RegexOptions.CultureInvariant | RegexOptions.IgnoreCase,
TimeSpan.FromMilliseconds(100));
public bool Match(
HttpContext? httpContext, IRouter? route, string routeKey,
RouteValueDictionary values, RouteDirection routeDirection)
{
if (!values.TryGetValue(routeKey, out var routeValue))
{
return false;
}
var routeValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);
if (routeValueString is null)
{
return false;
}
return _regex.IsMatch(routeValueString);
}
}
Aviso
Ao usar System.Text.RegularExpressions para processar entradas não confiáveis, passe um tempo limite. Um usuário mal-intencionado pode fornecer entrada para RegularExpressions
, causando um ataque de negação de serviço. APIs ASP.NET Core Framework que usam RegularExpressions
passam um tempo limite.
O código anterior:
0
no segmento {id}
da rota.O código a seguir é uma abordagem melhor para impedir que um id
que contém um 0
seja processado:
[HttpGet("{id}")]
public IActionResult Get(string id)
{
if (id.Contains('0'))
{
return StatusCode(StatusCodes.Status406NotAcceptable);
}
return Content(id);
}
O código anterior tem as seguintes vantagens em relação à abordagem NoZeroesRouteConstraint
:
0
.Transformadores de parâmetro:
Por exemplo, um transformador de parâmetro slugify
personalizado em padrão de rota blog\{article:slugify}
com Url.Action(new { article = "MyTestArticle" })
gera blog\my-test-article
.
Considere a seguinte implementação IOutboundParameterTransformer
:
public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
public string? TransformOutbound(object? value)
{
if (value is null)
{
return null;
}
return Regex.Replace(
value.ToString()!,
"([a-z])([A-Z])",
"$1-$2",
RegexOptions.CultureInvariant,
TimeSpan.FromMilliseconds(100))
.ToLowerInvariant();
}
}
Para usar um transformador de parâmetro em um padrão de rota, configure-o usando ConstraintMap em Program.cs
:
builder.Services.AddRouting(options =>
options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));
A estrutura do ASP.NET Core usa os transformadores de parâmetro para transformar o URI no qual um ponto de extremidade é resolvido. Por exemplo, os transformadores de parâmetro transformam os valores de rota usado para corresponder a um area
, controller
, action
e page
:
app.MapControllerRoute(
name: "default",
pattern: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");
Com o modelo de rota anterior, a ação SubscriptionManagementController.GetAll
é combinada com o URI /subscription-management/get-all
. Um transformador de parâmetro não altera os valores de rota usados para gerar um link. Por exemplo, Url.Action("GetAll", "SubscriptionManagement")
gera /subscription-management/get-all
.
ASP.NET Core fornece convenções de API para usar transformadores de parâmetro com as rotas geradas:
Esta seção contém uma referência para o algoritmo implementado pela geração de URL. Na prática, os exemplos mais complexos de geração de URL usam controladores ou Razor Pages. Confira o roteamento em controladores para obter informações adicionais.
O processo de geração de URL começa com uma chamada para LinkGenerator.GetPathByAddress ou um método semelhante. O método é fornecido com um endereço, um conjunto de valores de rota e, opcionalmente, informações sobre a solicitação atual de HttpContext
.
A primeira etapa é usar o endereço para resolve um conjunto de pontos de extremidade candidatos usando um IEndpointAddressScheme<TAddress> que corresponda ao tipo do endereço.
Depois que o conjunto de candidatos é encontrado pelo esquema de endereços, os pontos de extremidade são ordenados e processados iterativamente até que uma operação de geração de URL seja bem-sucedida. A geração de URL não verifica se há ambiguidades. O primeiro resultado retornado é o resultado final.
A primeira etapa na solução de problemas de geração de URL é definir o nível de log de Microsoft.AspNetCore.Routing
como TRACE
. LinkGenerator
registra muitos detalhes sobre o processamento, o que pode ser útil para solucionar problemas.
Confira Referência de geração de URL para obter detalhes sobre a geração de URL.
Os endereços são o conceito na geração de URL usado para vincular uma chamada ao gerador de links para um conjunto de pontos de extremidade candidatos.
Os endereços são um conceito extensível que vem com duas implementações por padrão:
string
) como o endereço: IUrlHelper
, Auxiliares de Marca, Auxiliares HTML, Resultados da Ação etc.A função do esquema de endereços é fazer a associação entre o endereço e os pontos de extremidade correspondentes por critérios arbitrários:
Na solicitação atual, o roteamento acessa os valores de rota da solicitação atual HttpContext.Request.RouteValues
. Os valores associados à solicitação atual são chamados de valores ambientes. Para maior clareza, a documentação se refere aos valores de rota transmitidos para os métodos como valores explícitos.
O exemplo a seguir mostra valores ambientes e valores explícitos. Fornece os valores ambientes da solicitação atual e os valores explícitos:
public class WidgetController : ControllerBase
{
private readonly LinkGenerator _linkGenerator;
public WidgetController(LinkGenerator linkGenerator) =>
_linkGenerator = linkGenerator;
public IActionResult Index()
{
var indexPath = _linkGenerator.GetPathByAction(
HttpContext, values: new { id = 17 })!;
return Content(indexPath);
}
// ...
O código anterior:
/Widget/Index/17
O código a seguir fornece apenas valores explícitos e nenhum valor ambiente:
var subscribePath = _linkGenerator.GetPathByAction(
"Subscribe", "Home", new { id = 17 })!;
O método anterior retorna /Home/Subscribe/17
O código a seguir no WidgetController
retorna /Widget/Subscribe/17
:
var subscribePath = _linkGenerator.GetPathByAction(
HttpContext, "Subscribe", null, new { id = 17 });
O código a seguir fornece o controlador dos valores ambientes na solicitação atual e dos valores explícitos:
public class GadgetController : ControllerBase
{
public IActionResult Index() =>
Content(Url.Action("Edit", new { id = 17 })!);
}
No código anterior:
/Gadget/Edit/17
é retornado.action
e os valores route
especificados.O código a seguir fornece os valores ambientes da solicitação atual e os valores explícitos:
public class IndexModel : PageModel
{
public void OnGet()
{
var editUrl = Url.Page("./Edit", new { id = 17 });
// ...
}
}
O código anterior define url
como /Edit/17
, quando a página Editar Razor contém a seguinte diretiva de página:
@page "{id:int}"
Se a página Editar não contiver o modelo de rota "{id:int}"
, url
será /Edit?id=17
.
O comportamento do MVC IUrlHelper adiciona uma camada de complexidade, além das regras descritas aqui:
IUrlHelper
sempre fornece os valores de rota da solicitação atual como valores ambientes.action
e controller
como valores explícitos, a menos que sejam substituídos pelo desenvolvedor.page
como valor explícito, a menos que seja substituído. IUrlHelper.Page
sempre substitui o valor de rota atual handler
por null
como um valor explícito, a menos que seja substituído.Os usuários geralmente ficam surpresos com os detalhes comportamentais dos valores ambientes, pois o MVC não parece seguir suas próprias regras. Por motivos de histórico e compatibilidade, determinados valores de rota, como action
, controller
, page
e handler
, têm seu próprio comportamento de caso especial.
A funcionalidade equivalente fornecida por LinkGenerator.GetPathByAction
e LinkGenerator.GetPathByPage
duplica essas anomalias de IUrlHelper
para compatibilidade.
Depois que o conjunto de pontos de extremidade candidatos for encontrado, o algoritmo de geração de URL:
A primeira etapa nesse processo é chamada de invalidação de valor de rota. A invalidação de valor de rota é o processo pelo qual o roteamento decide quais valores de rota dos valores ambientes devem ser usados e quais devem ser ignorados. Cada valor ambiente é considerado e combinado com os valores explícitos ou ignorado.
A melhor maneira de pensar sobre a função dos valores de ambiente é que eles tentam salvar a digitação dos desenvolvedores de aplicativos, em alguns casos comuns. Tradicionalmente, os cenários em que os valores ambientes são úteis estão relacionados ao MVC:
As chamadas para LinkGenerator
ou IUrlHelper
que retornam null
geralmente são causadas por não entender a invalidação de valor de rota. Solucione problemas de invalidação de valor de rota especificando explicitamente mais valores de rota para ver se isso resolve o problema.
A invalidação de valor de rota funciona supondo que o esquema de URL do aplicativo é hierárquico, com uma hierarquia formada da esquerda para a direita. Considere o modelo de rota de controlador básico {controller}/{action}/{id?}
para ter uma noção intuitiva de como isso funciona na prática. Uma alteração em um valor invalida todos os valores de rota que são exibidos à direita. Isso reflete a suposição sobre a hierarquia. Se o aplicativo tiver um valor ambiente para id
e a operação especificar um valor diferente para o controller
:
id
não será reutilizado porque {controller}
está à esquerda de {id?}
.Alguns exemplos que demonstram esse princípio:
id
, o valor ambiente para id
será ignorado. Os valores ambientes para controller
e action
podem ser usados.action
, qualquer valor ambiente para action
será ignorado. Os valores ambientes para controller
podem ser usados. Se o valor explícito para action
for diferente do valor ambiente para action
, o valor id
não será usado. Se o valor explícito para action
for igual ao valor ambiente para action
, o valor id
poderá ser usado.controller
, qualquer valor ambiente para controller
será ignorado. Se o valor explícito para controller
for diferente do valor ambiente para controller
, os valores action
e id
não serão usados. Se o valor explícito para controller
for igual ao valor ambiente para controller
, os valores action
e id
poderão ser usados.Esse processo é ainda mais complicado devido à existência de rotas de atributo e rotas convencionais dedicadas. As rotas convencionais do controlador, como {controller}/{action}/{id?}
, especificam uma hierarquia usando parâmetros de rota. Para rotas convencionais dedicadas e rotas de atributo para os controladores e Razor Pages:
Para esses casos, a geração de URL define o conceito de valores necessários. Os pontos de extremidade criados por controladores e Razor Pages têm os valores necessários especificados que permitem que a invalidação do valor de rota funcione.
O algoritmo de invalidação de valor de rota em detalhes:
Neste ponto, a operação de geração de URL está pronta para avaliar as restrições de rota. O conjunto de valores aceitos é combinado com os valores padrão do parâmetro, que são fornecidos às restrições. Se todas as restrições forem aprovadas, a operação continuará.
Em seguida, os valores aceitos podem ser usados para expandir o modelo de rota. O modelo de rota é processado:
Valores fornecidos explicitamente, que não correspondem a um segmento da rota, são adicionados à cadeia de consulta. A tabela a seguir mostra o resultado do uso do modelo de rota {controller}/{action}/{id?}
.
Valores de ambiente | Valores explícitos | Resultado |
---|---|---|
controlador = "Home" | ação = "About" | /Home/About |
controlador = "Home" | controlador = "Order", ação = "About" | /Order/About |
controlador = "Home", cor = "Vermelho" | ação = "About" | /Home/About |
controlador = "Home" | ação = "About", cor = "Red" | /Home/About?color=Red |
Os parâmetros de rota opcionais devem vir após todos os parâmetros de rota obrigatórios. No código a seguir, os parâmetros id
e name
devem vir após o parâmetro color
:
using Microsoft.AspNetCore.Mvc;
namespace WebApplication1.Controllers;
[Route("api/[controller]")]
public class MyController : ControllerBase
{
// GET /api/my/red/2/joe
// GET /api/my/red/2
// GET /api/my
[HttpGet("{color}/{id:int?}/{name?}")]
public IActionResult GetByIdAndOptionalName(string color, int id = 1, string? name = null)
{
return Ok($"{color} {id} {name ?? ""}");
}
}
O código a seguir mostra um exemplo de um esquema de geração de URL que não é compatível com o roteamento:
app.MapControllerRoute(
"default",
"{culture}/{controller=Home}/{action=Index}/{id?}");
app.MapControllerRoute(
"blog",
"{culture}/{**slug}",
new { controller = "Blog", action = "ReadPost" });
No código anterior, o parâmetro de rota culture
é usado para localização. O ideal é que o parâmetro culture
sempre seja aceito como valor ambiente. No entanto, o parâmetro culture
não é aceito como valor ambiente devido à maneira como os valores necessários funcionam:
"default"
, o parâmetro de rota culture
fica à esquerda de controller
. Portanto, as alterações em controller
não invalidarão culture
."blog"
, considera-se que o parâmetro de rota culture
fica à direita de controller
, que aparece nos valores necessários.A classe LinkParser adiciona suporte para analisar um caminho de URL em um conjunto de valores de rota. O método ParsePathByEndpointName usa um nome de ponto de extremidade e um caminho de URL e retorna um conjunto de valores de rota extraídos do caminho de URL.
No controlador de exemplo a seguir, a ação GetProduct
usa um modelo de rota de api/Products/{id}
e tem um Name de GetProduct
:
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet("{id}", Name = nameof(GetProduct))]
public IActionResult GetProduct(string id)
{
// ...
Na mesma classe do controlador, a ação AddRelatedProduct
espera um caminho de URL, pathToRelatedProduct
, que pode ser fornecido como um parâmetro de cadeia de caracteres de consulta:
[HttpPost("{id}/Related")]
public IActionResult AddRelatedProduct(
string id, string pathToRelatedProduct, [FromServices] LinkParser linkParser)
{
var routeValues = linkParser.ParsePathByEndpointName(
nameof(GetProduct), pathToRelatedProduct);
var relatedProductId = routeValues?["id"];
// ...
No exemplo anterior, a ação AddRelatedProduct
extrai o valor de rota id
do caminho de URL. Por exemplo, com um caminho de URL de /api/Products/1
, o valor relatedProductId
é definido como 1
. Essa abordagem permite que os clientes da API usem os caminhos de URL ao referenciar recursos, sem exigir conhecimento de como essa URL é estruturada.
Os links a seguir fornecem informações sobre como configurar metadados de ponto de extremidade:
[MinimumAgeAuthorize]
RequireHost aplica uma restrição à rota que exige o host especificado. O parâmetro RequireHost
ou [Host] pode ser um:
www.domain.com
, corresponde www.domain.com
a qualquer porta.*.domain.com
, corresponde www.domain.com
, subdomain.domain.com
ou www.subdomain.domain.com
a qualquer porta.*:5000
, corresponde a porta 5000 a qualquer host.www.domain.com:5000
ou *.domain.com:5000
, corresponde ao host e à porta.Vários parâmetros podem ser especificados usando RequireHost
ou [Host]
. A restrição corresponde aos hosts válidos para qualquer um dos parâmetros. Por exemplo, [Host("domain.com", "*.domain.com")]
corresponde a domain.com
, www.domain.com
ou subdomain.domain.com
.
O código a seguir usa RequireHost
para exigir o host especificado na rota:
app.MapGet("/", () => "Contoso").RequireHost("contoso.com");
app.MapGet("/", () => "AdventureWorks").RequireHost("adventure-works.com");
app.MapHealthChecks("/healthz").RequireHost("*:8080");
O código a seguir usa o atributo [Host]
no controlador para exigir qualquer um dos hosts especificados:
[Host("contoso.com", "adventure-works.com")]
public class HostsController : Controller
{
public IActionResult Index() =>
View();
[Host("example.com")]
public IActionResult Example() =>
View();
}
Quando o atributo [Host]
é aplicado ao controlador e ao método de ação:
O método de extensão MapGroup ajuda a organizar grupos de pontos de extremidade com um prefixo comum. Isso reduz o código repetitivo e permite personalizar grupos inteiros de pontos de extremidade com uma única chamada a métodos como RequireAuthorization e WithMetadata, que adicionam os metadados de ponto de extremidade.
Por exemplo, o código a seguir cria dois grupos de pontos de extremidade semelhantes:
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 pontos de extremidade corresponderá apenas às solicitações prefixadas com /public/todos
e estará acessível sem autenticação. O segundo grupo de pontos de extremidade corresponderá apenas às solicitações prefixadas com /private/todos
e exigirá autenticação.
A QueryPrivateTodos
fábrica de filtro de ponto de extremidade é uma função local que modifica os TodoDb
parâmetros do manipulador de rota para permitir o acesso e armazenamento de dados privados de tarefas.
Os grupos de rotas também permitem grupos aninhados e padrões de prefixo complexos com parâmetros de rota e restrições. 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 é igual a adicioná-los individualmente a cada ponto de extremidade, antes de adicionar filtros ou metadados extras 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 tenha sido adicionado depois. Como os filtros foram aplicados a grupos diferentes, a ordem em que foram adicionados em relação uns aos outros não importa. A ordem em que os filtros são adicionados importa 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
Quando um aplicativo tem problemas de desempenho, geralmente suspeita-se que o roteamento é o problema. O motivo pelo qual o roteamento é suspeito é que as estruturas como controladores e Razor Pages relatam o tempo gasto dentro da estrutura nas mensagens de log. Quando há uma diferença significativa entre o tempo relatado pelos controladores e o tempo total da solicitação:
O desempenho do roteamento é testado usando milhares de pontos de extremidade. É improvável que um aplicativo típico encontre um problema de desempenho apenas por ser muito grande. A causa raiz mais comum do desempenho lento do roteamento geralmente é um middleware personalizado com comportamento inválido.
O exemplo de código a seguir demonstra uma técnica básica para restringir a fonte de atraso:
var logger = app.Services.GetRequiredService<ILogger<Program>>();
app.Use(async (context, next) =>
{
var stopwatch = Stopwatch.StartNew();
await next(context);
stopwatch.Stop();
logger.LogInformation("Time 1: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});
app.UseRouting();
app.Use(async (context, next) =>
{
var stopwatch = Stopwatch.StartNew();
await next(context);
stopwatch.Stop();
logger.LogInformation("Time 2: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});
app.UseAuthorization();
app.Use(async (context, next) =>
{
var stopwatch = Stopwatch.StartNew();
await next(context);
stopwatch.Stop();
logger.LogInformation("Time 3: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});
app.MapGet("/", () => "Timing Test.");
Para roteamento de tempo:
Essa é uma maneira básica de restringir o atraso quando ele é significativo, por exemplo, mais de 10ms
. Subtrair Time 2
de Time 1
relata o tempo gasto dentro do middleware UseRouting
.
O código a seguir usa uma abordagem mais compacta para o código de tempo anterior:
public sealed class AutoStopwatch : IDisposable
{
private readonly ILogger _logger;
private readonly string _message;
private readonly Stopwatch _stopwatch;
private bool _disposed;
public AutoStopwatch(ILogger logger, string message) =>
(_logger, _message, _stopwatch) = (logger, message, Stopwatch.StartNew());
public void Dispose()
{
if (_disposed)
{
return;
}
_logger.LogInformation("{Message}: {ElapsedMilliseconds}ms",
_message, _stopwatch.ElapsedMilliseconds);
_disposed = true;
}
}
var logger = app.Services.GetRequiredService<ILogger<Program>>();
var timerCount = 0;
app.Use(async (context, next) =>
{
using (new AutoStopwatch(logger, $"Time {++timerCount}"))
{
await next(context);
}
});
app.UseRouting();
app.Use(async (context, next) =>
{
using (new AutoStopwatch(logger, $"Time {++timerCount}"))
{
await next(context);
}
});
app.UseAuthorization();
app.Use(async (context, next) =>
{
using (new AutoStopwatch(logger, $"Time {++timerCount}"))
{
await next(context);
}
});
app.MapGet("/", () => "Timing Test.");
A lista a seguir fornece alguns insights sobre recursos de roteamento relativamente caros, em comparação a modelos de rota básicos:
{x}-{y}-{z}
): Por padrão, o ASP.NET Core usa um algoritmo de roteamento que troca memória por tempo de CPU. O bom resultado disso é que o tempo de correspondência de rotas depende apenas do tamanho do caminho a ser correspondido e não do número de rotas. No entanto, essa abordagem pode ser possivelmente problemática em alguns casos, quando o aplicativo tem um grande número de rotas (milhares) e há uma grande quantidade de prefixos variáveis nas rotas. Por exemplo, se as rotas tiverem parâmetros nos segmentos iniciais da rota, como {parameter}/some/literal
.
É improvável que um aplicativo tenha uma situação em que esse seja um problema, a menos nos seguintes casos:
Microsoft.AspNetCore.Routing.Matching.DfaNode
.Há várias técnicas e otimizações que podem ser aplicadas a rotas que melhorarão muito esse cenário:
{parameter:int}
, {parameter:guid}
, {parameter:regex(\\d+)}
etc., sempre que possível.
MapDynamicControllerRoute
ou MapDynamicPageRoute
.Esta seção contém diretrizes para criadores de bibliotecas com base no roteamento. Esses detalhes destinam-se a garantir que os desenvolvedores de aplicativos tenham uma boa experiência usando bibliotecas e estruturas que estendem o roteamento.
Para criar uma estrutura que usa o roteamento para correspondência de URL, comece definindo uma experiência do usuário baseada em UseEndpoints.
CRIE com base em IEndpointRouteBuilder. Isso permite que os usuários componham a estrutura com outros recursos do ASP.NET Core, sem confusão. Cada modelo do ASP.NET Core inclui o roteamento. Suponha que o roteamento esteja presente e seja conhecido para os usuários.
// Your framework
app.MapMyFramework(...);
app.MapHealthChecks("/healthz");
RETORNE um tipo de concreto selado de uma chamada para MapMyFramework(...)
que implemente IEndpointConventionBuilder. A maioria dos métodos Map...
da estrutura segue esse padrão. A interface IEndpointConventionBuilder
:
Declarar seu próprio tipo permite que você adicione sua própria funcionalidade específica da estrutura ao construtor. Não há problema em encapsular um construtor declarado por estrutura e encaminhar chamadas para ele.
// Your framework
app.MapMyFramework(...)
.RequireAuthorization()
.WithMyFrameworkFeature(awesome: true);
app.MapHealthChecks("/healthz");
GRAVE seu próprio EndpointDataSource. EndpointDataSource
é o primitivo de baixo nível para declarar e atualizar uma coleção de pontos de extremidade. EndpointDataSource
é uma API eficiente usada por controladores e Razor Pages.
Os testes de roteamento têm um exemplo básico de uma fonte de dados que não está atualizando.
IMPLEMENTEGetGroupedEndpoints. Isso fornece controle total sobre a execução de convenções de grupo e os metadados finais nos pontos de extremidade agrupados. Por exemplo, isso permite que as implementações personalizadas EndpointDataSource
executem os filtros de ponto de extremidade adicionados a grupos.
NÃO tente registrar um EndpointDataSource
por padrão. Exija que os usuários registrem a estrutura no UseEndpoints. A filosofia do roteamento determina que nada está incluído por padrão e esse UseEndpoints
é o local para registrar pontos de extremidade.
DEFINA os tipos de metadados como uma interface.
POSSIBILITE o uso de tipos de metadados como um atributo em classes e métodos.
public interface ICoolMetadata
{
bool IsCool { get; }
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
public bool IsCool => true;
}
As estruturas como controladores e Razor Pages dão suporte à aplicação de atributos de metadados a tipos e métodos. Se você declarar os tipos de metadados:
Declarar um tipo de metadados como uma interface adiciona outra camada de flexibilidade:
POSSIBILITE a substituição de metadados, conforme mostrado no exemplo a seguir:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class SuppressCoolMetadataAttribute : Attribute, ICoolMetadata
{
public bool IsCool => false;
}
[CoolMetadata]
public class MyController : Controller
{
public void MyCool() { }
[SuppressCoolMetadata]
public void Uncool() { }
}
A melhor maneira de seguir estas diretrizes é evitar definir metadados de marcador:
A coleção de metadados é ordenada e permite substituir por prioridade. No caso de controladores, os metadados no método de ação são mais específicos.
TORNE o middleware útil com e sem roteamento:
app.UseAuthorization(new AuthorizationPolicy() { ... });
// Your framework
app.MapMyFramework(...).RequireAuthorization();
Como exemplo dessa diretriz, considere o middleware UseAuthorization
. O middleware de autorização permite que você transmita uma política de fallback. A política de fallback, se especificada, aplica-se a:
Isso torna o middleware de autorização útil fora do contexto de roteamento. O middleware de autorização pode ser usado para programação de middleware tradicional.
Para obter a saída de diagnóstico de roteamento detalhada, defina Logging:LogLevel:Microsoft
como Debug
. No ambiente de desenvolvimento, defina o nível de log em appsettings.Development.json
:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Debug",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
O roteamento é responsável por corresponder solicitações HTTP de entrada e expedir essas solicitações para os pontos de extremidade executáveis do aplicativo. Os pontos de extremidade são as unidades de código executável de manipulação de solicitações do aplicativo. Os pontos de extremidade são definidos no aplicativo e configurados quando o aplicativo é iniciado. O processo de correspondência de ponto de extremidade pode extrair valores da URL da solicitação e fornecer esses valores para processamento de solicitações. Usando as informações de ponto de extremidade do aplicativo, o roteamento também pode gerar URLs que são mapeadas para os pontos de extremidade.
Os aplicativos podem configurar o roteamento usando:
Este artigo aborda os detalhes de baixo nível do roteamento do ASP.NET Core. Para obter informações sobre como configurar o roteamento:
O código a seguir mostra um exemplo básico de roteamento:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
O exemplo anterior inclui um único ponto de extremidade usando o método MapGet:
GET
é enviada para a URL raiz /
: Hello World!
é gravado na resposta HTTP.GET
ou a URL raiz não for /
, nenhuma rota corresponderá e um HTTP 404 será retornado.O roteamento usa um par de middleware registrado por UseRouting e UseEndpoints:
UseRouting
adiciona a correspondência de rotas ao pipeline de middleware. Esse middleware examina o conjunto de pontos de extremidade definidos no aplicativo e seleciona a melhor correspondência com base na solicitação.UseEndpoints
adiciona a execução do ponto de extremidade ao pipeline de middleware. Ele executa o delegado associado ao ponto de extremidade selecionado.Normalmente, os aplicativos não precisam chamar UseRouting
ou UseEndpoints
. WebApplicationBuilder configura um pipeline de middleware, que encapsula o middleware adicionado em Program.cs
com UseRouting
e UseEndpoints
. No entanto, os aplicativos podem alterar a ordem na qual UseRouting
e UseEndpoints
são executados, chamando esses métodos explicitamente. Por exemplo, o código a seguir faz uma chamada explícita para UseRouting
:
app.Use(async (context, next) =>
{
// ...
await next(context);
});
app.UseRouting();
app.MapGet("/", () => "Hello World!");
No código anterior:
app.Use
registra um middleware personalizado, que é executado no início do pipeline.UseRouting
configura o middleware de correspondência de rotas a ser executado após o middleware personalizado.MapGet
é executado na extremidade do pipeline.Se o exemplo anterior não incluísse uma chamada para UseRouting
, o middleware personalizado seria executado após o middleware de correspondência de rotas.
O método MapGet
é usado para definir um ponto de extremidade. Um ponto de extremidade pode ser:
Os pontos de extremidade que podem ser correspondidos e executados pelo aplicativo são configurados no UseEndpoints
. Por exemplo, MapGet, MapPost e métodos semelhantes conectam os delegados de solicitação ao sistema de roteamento. Métodos adicionais podem ser usados para conectar os recursos de estrutura do ASP.NET Core ao sistema de roteamento:
O exemplo a seguir mostra o roteamento com um modelo de rota mais sofisticado:
app.MapGet("/hello/{name:alpha}", (string name) => $"Hello {name}!");
A cadeia de caracteres /hello/{name:alpha}
é um modelo de rota. Um modelo de rota é usado para configurar a forma como o ponto de extremidade é correspondido. Nesse caso, o modelo corresponde a:
/hello/Docs
/hello/
seguido de uma sequência de caracteres alfabéticos. :alpha
aplica uma restrição de rota que corresponde apenas a caracteres alfabéticos. As restrições de rota serão explicadas posteriormente neste artigo.O segundo segmento do caminho de URL, {name:alpha}
:
name
.O exemplo a seguir mostra o roteamento com as verificações de integridade e a autorização:
app.UseAuthentication();
app.UseAuthorization();
app.MapHealthChecks("/healthz").RequireAuthorization();
app.MapGet("/", () => "Hello World!");
O exemplo anterior demonstra como:
A chamada MapHealthChecks adiciona um ponto de extremidade de verificação de integridade. O encadeamento de RequireAuthorization para esta chamada anexa uma política de autorização ao ponto de extremidade.
Chamar UseAuthentication e UseAuthorization adiciona o middleware de autenticação e autorização. Esses programas de middleware são colocados entre UseRouting e UseEndpoints
para que possam:
UseRouting
.No exemplo anterior, há dois pontos de extremidade, mas apenas o ponto de extremidade de verificação de integridade tem uma política de autorização anexada. Se a solicitação corresponder ao ponto de extremidade de verificação de integridade, /healthz
, uma verificação de autorização será executada. Isso demonstra que os pontos de extremidade podem ter dados extras anexados. Esses dados extras são chamados de metadados de ponto de extremidade:
O sistema de roteamento se baseia no pipeline de middleware, adicionando o conceito de ponto de extremidade eficiente. Os pontos de extremidade representam as unidades da funcionalidade do aplicativo que são diferentes umas das outras em termos de roteamento, autorização e qualquer número de sistemas do ASP.NET Core.
Um ponto de extremidade do ASP.NET Core é:
O código a seguir mostra como recuperar e inspecionar o ponto de extremidade correspondente à solicitação atual:
app.Use(async (context, next) =>
{
var currentEndpoint = context.GetEndpoint();
if (currentEndpoint is null)
{
await next(context);
return;
}
Console.WriteLine($"Endpoint: {currentEndpoint.DisplayName}");
if (currentEndpoint is RouteEndpoint routeEndpoint)
{
Console.WriteLine($" - Route Pattern: {routeEndpoint.RoutePattern}");
}
foreach (var endpointMetadata in currentEndpoint.Metadata)
{
Console.WriteLine($" - Metadata: {endpointMetadata}");
}
await next(context);
});
app.MapGet("/", () => "Inspect Endpoint.");
O ponto de extremidade, se selecionado, pode ser recuperado a partir do HttpContext
. Suas propriedades podem ser inspecionadas. Os objetos de ponto de extremidade são imutáveis e não podem ser modificados após a criação. O tipo mais comum de ponto de extremidade é um RouteEndpoint. RouteEndpoint
inclui informações que permitem que ele seja selecionado pelo sistema de roteamento.
No código anterior, app.Use configura um middleware embutido.
O código a seguir mostra que, dependendo de onde app.Use
é chamado no pipeline, pode não haver um ponto de extremidade:
// Location 1: before routing runs, endpoint is always null here.
app.Use(async (context, next) =>
{
Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
await next(context);
});
app.UseRouting();
// Location 2: after routing runs, endpoint will be non-null if routing found a match.
app.Use(async (context, next) =>
{
Console.WriteLine($"2. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
await next(context);
});
// Location 3: runs when this endpoint matches
app.MapGet("/", (HttpContext context) =>
{
Console.WriteLine($"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
return "Hello World!";
}).WithDisplayName("Hello");
app.UseEndpoints(_ => { });
// Location 4: runs after UseEndpoints - will only run if there was no match.
app.Use(async (context, next) =>
{
Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
await next(context);
});
O exemplo anterior adiciona instruções Console.WriteLine
que mostram se um ponto de extremidade foi selecionado ou não. Para maior clareza, o exemplo atribui um nome de exibição ao ponto de extremidade /
fornecido.
O exemplo anterior também inclui chamadas para UseRouting
e UseEndpoints
para controlar exatamente quando esses programas de middleware são executados no pipeline.
Executar esse código com uma URL do /
exibe:
1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello
Executar esse código com qualquer outra URL exibe:
1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)
Essa saída demonstra que:
UseRouting
seja chamado.UseRouting
e UseEndpoints.UseEndpoints
é terminal quando uma correspondência é encontrada. O middleware de terminal será definido posteriormente neste artigo.UseEndpoints
é executado apenas quando nenhuma correspondência é encontrada.O middleware UseRouting
usa o método SetEndpoint para anexar o ponto de extremidade ao contexto atual. É possível substituir o middleware UseRouting
pela lógica personalizada e ainda obter os benefícios de usar pontos de extremidade. Os pontos de extremidade são primitivos de baixo nível, como o middleware, e não são acoplados à implementação de roteamento. A maioria dos aplicativos não precisa substituir UseRouting
pela lógica personalizada.
O middleware UseEndpoints
foi projetado para ser usado em conjunto com o middleware UseRouting
. A lógica principal para executar um ponto de extremidade não é complicada. Use GetEndpoint para recuperar o ponto de extremidade e, em seguida, invoque a propriedade RequestDelegate.
O código a seguir demonstra como o middleware pode influenciar ou reagir ao roteamento:
app.UseHttpMethodOverride();
app.UseRouting();
app.Use(async (context, next) =>
{
if (context.GetEndpoint()?.Metadata.GetMetadata<RequiresAuditAttribute>() is not null)
{
Console.WriteLine($"ACCESS TO SENSITIVE DATA AT: {DateTime.UtcNow}");
}
await next(context);
});
app.MapGet("/", () => "Audit isn't required.");
app.MapGet("/sensitive", () => "Audit required for sensitive data.")
.WithMetadata(new RequiresAuditAttribute());
public class RequiresAuditAttribute : Attribute { }
O exemplo anterior demonstra dois conceitos importantes:
UseRouting
para modificar os dados nos quais o roteamento opera.
UseRouting
e UseEndpoints para processar os resultados do roteamento, antes que o ponto de extremidade seja executado.
UseRouting
e UseEndpoints
: UseAuthorization
e UseCors
.O código anterior mostra um exemplo de um middleware personalizado que permite políticas por ponto de extremidade. O middleware grava um log de auditoria de acesso a dados confidenciais no console. O middleware pode ser configurado para auditar um ponto de extremidade com os metadados RequiresAuditAttribute
. Este exemplo demonstra um padrão de aceitação em que apenas os pontos de extremidade marcados como confidenciais são auditados. É possível definir essa lógica ao contrário, auditando tudo o que não está marcado como seguro, por exemplo. O sistema de metadados do ponto de extremidade é flexível. Essa lógica pode ser projetada da maneira que for adequada para o caso de uso.
O código de exemplo anterior destina-se a demonstrar os conceitos básicos dos pontos de extremidade. O exemplo não se destina ao uso de produção. Uma versão mais completa de um middleware de log de auditoria:
Os metadados da política de auditoria RequiresAuditAttribute
são definidos como um Attribute
para facilitar o uso com estruturas baseadas em classe, como controladores e SignalR. Ao usar a rota para o código:
A melhor prática para tipos de metadados é defini-los como interfaces ou atributos. As interfaces e os atributos permitem a reutilização de código. O sistema de metadados é flexível e não impõe limitações.
O exemplo a seguir demonstra o middleware de terminal e o roteamento:
// Approach 1: Terminal Middleware.
app.Use(async (context, next) =>
{
if (context.Request.Path == "/")
{
await context.Response.WriteAsync("Terminal Middleware.");
return;
}
await next(context);
});
app.UseRouting();
// Approach 2: Routing.
app.MapGet("/Routing", () => "Routing.");
O estilo de middleware mostrado com Approach 1:
é o middleware de terminal. Ele é chamado de middleware de terminal porque faz uma operação correspondente:
Path == "/"
para o middleware e Path == "/Routing"
para o roteamento.next
.Ele é chamado de middleware de terminal porque termina a pesquisa, executa algumas funcionalidades e retorna.
A lista a seguir compara o middleware de terminal com o roteamento:
next
.UseAuthorization
e UseCors
.
UseAuthorization
ou UseCors
exige uma interface manual com o sistema de autorização.Um ponto de extremidade define ambos:
O middleware de terminal pode ser uma ferramenta eficaz, mas pode exigir:
Considere a integração com o roteamento antes de gravar um middleware de terminal.
O middleware de terminal existente que é integrado ao Map ou MapWhen geralmente pode ser transformado em um ponto de extremidade com reconhecimento de roteamento. O MapHealthChecks demonstra o padrão para router-ware:
Map
e forneça o novo pipeline de middleware.Map
usando o método de extensão.O código a seguir mostra o uso do MapHealthChecks:
app.UseAuthentication();
app.UseAuthorization();
app.MapHealthChecks("/healthz").RequireAuthorization();
O exemplo anterior mostra por que retornar o objeto do construtor é importante. Retornar o objeto do construtor permite que o desenvolvedor do aplicativo configure políticas como autorização para o ponto de extremidade. Neste exemplo, o middleware de verificações de integridade não tem integração direta com o sistema de autorização.
O sistema de metadados foi criado em resposta aos problemas encontrados por autores de extensibilidade usando o middleware de terminal. É problemático para cada middleware implementar sua própria integração com o sistema de autorização.
Quando um middleware de roteamento é executado, ele define um Endpoint
e encaminha os valores para um recurso de solicitação no HttpContext a partir da solicitação atual:
HttpRequest.RouteValues
obtém a coleção de valores de rota.O middleware executado após o middleware de roteamento pode inspecionar o ponto de extremidade e realizar uma ação. Por exemplo, um middleware de autorização pode interrogar a coleção de metadados do ponto de extremidade para uma política de autorização. Depois que todos os middlewares no pipeline de processamento da solicitação forem executados, o representante do ponto de extremidade selecionado será invocado.
O sistema de roteamento no roteamento de ponto de extremidade é responsável por todas as decisões de expedição. Como o middleware aplica políticas com base no ponto de extremidade selecionado, é importante que:
Aviso
Para compatibilidade com versões anteriores, quando o delegado do ponto de extremidade do Controlador ou do Razor Pages é executado, as propriedades do RouteContext.RouteData são definidas com os valores apropriados com base no processamento da solicitação executado até o momento.
O tipo RouteContext
será marcado como obsoleto em uma versão futura:
RouteData.Values
para HttpRequest.RouteValues
.RouteData.DataTokens
para recuperar IDataTokensMetadata nos metadados do ponto de extremidade.A correspondência de URL opera em um conjunto configurável de fases. Em cada fase, a saída é um conjunto de correspondências. O conjunto de correspondências pode ser reduzido ainda mais pela próxima fase. A implementação de roteamento não garante uma ordem de processamento para pontos de extremidade correspondentes. Todas as correspondências possíveis são processadas de uma só vez. As fases de correspondência de URL ocorrem na ordem a seguir. ASP.NET Core:
A lista de pontos de extremidade é priorizada de acordo com:
Todos os pontos de extremidade correspondentes são processados em cada fase até que o EndpointSelector seja atingido. O EndpointSelector
é a fase final. Ele escolhe o ponto de extremidade de prioridade mais alta nas correspondências como a melhor correspondência. Se houver outras correspondências com a mesma prioridade que a melhor correspondência, uma exceção de correspondência ambígua será gerada.
A precedência de rota é calculada com base em um modelo de rota mais específico que recebe uma prioridade mais alta. Por exemplo, considere os modelos /hello
e /{message}
:
/hello
./hello
é mais específico e, portanto, tem prioridade mais alta.Em geral, a precedência de rota escolhe a melhor correspondência para os tipos de esquemas de URL usados na prática. Use Order somente quando necessário para evitar uma ambiguidade.
Devido aos tipos de extensibilidade fornecidos pelo roteamento, não é possível que o sistema de roteamento calcule antecipadamente as rotas ambíguas. Considere um exemplo, como os modelos de rota /{message:alpha}
e /{message:int}
:
alpha
corresponde apenas a caracteres alfabéticos.int
corresponde apenas a números.Aviso
A ordem das operações dentro do UseEndpoints não influencia o comportamento do roteamento, com uma exceção. MapControllerRoute e MapAreaRoute atribuem automaticamente um valor de pedido aos pontos de extremidade com base na ordem em que são invocados. Isso simula o comportamento de longo prazo dos controladores, sem que o sistema de roteamento forneça as mesmas garantias que as implementações de roteamento mais antigas.
Roteamento de ponto de extremidade no ASP.NET Core:
A precedência de modelo de rota é um sistema que atribui a cada modelo de rota um valor com base na especificidade. A precedência de modelo de rota:
Por exemplo, considere os modelos /Products/List
e /Products/{id}
. Seria aceitável supor que /Products/List
é uma correspondência melhor do que /Products/{id}
para o caminho de URL /Products/List
. Isso funciona porque o segmento literal /List
é considerado com melhor precedência do que o segmento de parâmetro /{id}
.
Os detalhes de como funciona a precedência são acoplados à forma como os modelos de rota são definidos:
Geração de URL:
O roteamento de ponto de extremidade inclui a API LinkGenerator. LinkGenerator
é um serviço singleton disponível na DI. A API LinkGenerator
pode ser usada fora do contexto de uma solicitação em execução. O Mvc.IUrlHelper e os cenários que dependem do IUrlHelper, como Auxiliares de Marcação, Auxiliares de HTML e Resultados da Ação, usam a API LinkGenerator
internamente para fornecer as funcionalidades de geração de link.
O gerador de link é respaldado pelo conceito de um endereço e esquemas de endereço. Um esquema de endereço é uma maneira de determinar os pontos de extremidade que devem ser considerados para a geração de link. Por exemplo, os cenários de nome de rota e valores de rota com os quais muitos usuários estão familiarizados nos controladores e no Razor Pages são implementados como um esquema de endereço.
O gerador de link pode ser vinculado aos controladores e ao Razor Pages usando os seguintes métodos de extensão:
As sobrecargas desses métodos aceitam argumentos que incluem o HttpContext
. Esses métodos são funcionalmente equivalentes a Url.Action e Url.Page, mas oferecem mais flexibilidade e opções.
Os métodos GetPath*
são mais semelhantes a Url.Action
e Url.Page
, pois geram um URI que contém um caminho absoluto. Os métodos GetUri*
sempre geram um URI absoluto que contém um esquema e um host. Os métodos que aceitam um HttpContext
geram um URI no contexto da solicitação em execução. Os valores de rota de ambiente, o caminho base da URL, o esquema e o host da solicitação em execução são usados, a menos que sejam substituídos.
LinkGenerator é chamado com um endereço. A geração de um URI ocorre em duas etapas:
Os métodos fornecidos pelo LinkGenerator dão suporte a funcionalidades de geração de link padrão para qualquer tipo de endereço. A maneira mais conveniente usar o gerador de link é por meio de métodos de extensão que executam operações para um tipo de endereço específico:
Método de extensão | Descrição |
---|---|
GetPathByAddress | Gera um URI com um caminho absoluto com base nos valores fornecidos. |
GetUriByAddress | Gera um URI absoluto com base nos valores fornecidos. |
Aviso
Preste atenção às seguintes implicações da chamada de métodos LinkGenerator:
Use métodos de extensão de GetUri*
com cuidado em uma configuração de aplicativo que não valide o cabeçalho Host
das solicitações de entrada. Se o cabeçalho Host
das solicitações de entrada não é validado, uma entrada de solicitação não confiável pode ser enviada novamente ao cliente em URIs em uma exibição ou página. Recomendamos que todos os aplicativos de produção configurem seu servidor para validar o cabeçalho Host
com os valores válidos conhecidos.
Use LinkGenerator com cuidado no middleware em combinação com Map
ou MapWhen
. Map*
altera o caminho base da solicitação em execução, o que afeta a saída da geração de link. Todas as APIs de LinkGenerator permitem a especificação de um caminho base. Especifique um caminho base vazio para desfazer o efeito de Map*
na geração de link.
No exemplo a seguir, um middleware usa a API de LinkGenerator para criar um link para um método de ação que lista os produtos da loja. O uso do gerador de link com sua injeção em uma classe e uma chamada a GenerateLink
está disponível para qualquer classe em um aplicativo:
public class ProductsMiddleware
{
private readonly LinkGenerator _linkGenerator;
public ProductsMiddleware(RequestDelegate next, LinkGenerator linkGenerator) =>
_linkGenerator = linkGenerator;
public async Task InvokeAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Plain;
var productsPath = _linkGenerator.GetPathByAction("Products", "Store");
await httpContext.Response.WriteAsync(
$"Go to {productsPath} to see our products.");
}
}
Os tokens entre {}
definem os parâmetros de rota que serão associados, se a rota for correspondida. Mais de um parâmetro de rota pode ser definido em um segmento de rota, mas os parâmetros de rota precisam ser separados por um valor literal. Por exemplo:
{controller=Home}{action=Index}
não é uma rota válida, já que não há valor literal entre {controller}
e {action}
. Os parâmetros de rota devem ter um nome e podem ter atributos adicionais especificados.
Um texto literal diferente dos parâmetros de rota (por exemplo, {id}
) e do separador de caminho /
precisa corresponder ao texto na URL. A correspondência de texto não diferencia maiúsculas de minúsculas e se baseia na representação decodificada do caminho de URLs. Para encontrar a correspondência de um delimitador de parâmetro de rota literal {
ou }
, faça o escape do delimitador repetindo o caractere. Por exemplo {{
ou }}
.
Asterisco *
ou asterisco duplo **
:
blog/{**slug}
: blog/
e tenha qualquer valor depois dele.blog/
é atribuído ao valor de rota de campo de dados dinâmico.Aviso
Um parâmetro catch-all pode corresponder às rotas incorretamente devido a um bug no roteamento. Os aplicativos afetados por esse bug têm as seguintes características:
{**slug}"
Confira os bugs do GitHub 18677 e 16579, por exemplo, casos que atingiram esse bug.
Uma correção de aceitação para esse bug está contida no SDK do .NET Core 3.1.301 e posterior. O código a seguir define um comutador interno que corrige esse bug:
public static void Main(string[] args)
{
AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior",
true);
CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.
Os parâmetros catch-all também podem corresponder à cadeia de caracteres vazia.
O parâmetro catch-all faz o escape dos caracteres corretos quando a rota é usada para gerar uma URL, incluindo os caracteres separadores de caminho /
. Por exemplo, a rota foo/{*path}
com valores de rota { path = "my/path" }
gera foo/my%2Fpath
. Observe o escape da barra invertida. Para fazer a viagem de ida e volta dos caracteres separadores de caminho, use o prefixo do parâmetro da rota **
. A rota foo/{**path}
com { path = "my/path" }
gera foo/my/path
.
Padrões de URL que tentam capturar um nome de arquivo com uma extensão de arquivo opcional apresentam considerações adicionais. Por exemplo, considere o modelo files/{filename}.{ext?}
. Quando existem valores para filename
e ext
, ambos os valores são populados. Se apenas existir um valor para filename
na URL, a rota encontrará uma correspondência, pois o .
à direita é opcional. As URLs a seguir correspondem a essa rota:
/files/myFile.txt
/files/myFile
Os parâmetros de rota podem ter valores padrão, designados pela especificação do valor padrão após o nome do parâmetro separado por um sinal de igual (=
). Por exemplo, {controller=Home}
define Home
como o valor padrão de controller
. O valor padrão é usado se nenhum valor está presente na URL para o parâmetro. Os parâmetros de rota se tornam opcionais com o acréscimo de um ponto de interrogação (?
) ao final do nome do parâmetro. Por exemplo, id?
. A diferença entre valores opcionais e parâmetros de rota padrão é:
Os parâmetros de rota podem ter restrições que precisam corresponder ao valor de rota associado da URL. A adição de :
e do nome da restrição após o nome do parâmetro de rota especifica uma restrição embutida em um parâmetro de rota. Se a restrição exigir argumentos, eles ficarão entre parênteses (...)
após o nome da restrição. Várias restrições embutidas podem ser especificadas por meio do acréscimo de outros :
e do nome da restrição.
O nome da restrição e os argumentos são passados para o serviço IInlineConstraintResolver para criar uma instância de IRouteConstraint a ser usada no processamento de URL. Por exemplo, o modelo de rota blog/{article:minlength(10)}
especifica uma restrição minlength
com o argumento 10
. Para obter mais informações sobre as restrições de rota e uma lista das restrições fornecidas pela estrutura, confira a seção Restrições de rota.
Os parâmetros de rota também podem ter transformadores de parâmetro. Os transformadores de parâmetro transformam o valor de um parâmetro ao gerar links e fazer a correspondência de ações e páginas com URLs. Assim como as restrições, os transformadores de parâmetro podem ser adicionados embutidos a um parâmetro de rota colocando :
e o nome do transformador após o nome do parâmetro de rota. Por exemplo, o modelo de rota blog/{article:slugify}
especifica um transformador slugify
. Para obter mais informações sobre transformadores de parâmetro, confira a seção Transformadores de parâmetro.
A tabela a seguir demonstra modelos de rota de exemplo e seu comportamento:
Modelo de rota | URI de correspondência de exemplo | O URI da solicitação |
---|---|---|
hello |
/hello |
Somente corresponde ao caminho único /hello . |
{Page=Home} |
/ |
Faz a correspondência e define Page como Home . |
{Page=Home} |
/Contact |
Faz a correspondência e define Page como Contact . |
{controller}/{action}/{id?} |
/Products/List |
É mapeado para o controlador Products e a ação List . |
{controller}/{action}/{id?} |
/Products/Details/123 |
É mapeado para o controlador Products e a ação Details , e id definido como 123. |
{controller=Home}/{action=Index}/{id?} |
/ |
É mapeado para o controlador Home e o método Index . id é ignorado. |
{controller=Home}/{action=Index}/{id?} |
/Products |
É mapeado para o controlador Products e o método Index . id é ignorado. |
Em geral, o uso de um modelo é a abordagem mais simples para o roteamento. Restrições e padrões também podem ser especificados fora do modelo de rota.
Os segmentos complexos são processados correspondendo delimitadores literais da direita para a esquerda sem greedy. Por exemplo, [Route("/a{b}c{d}")]
é um segmento complexo.
Os segmentos complexos funcionam de uma maneira específica que deve ser compreendida para usá-los com êxito. O exemplo nesta seção demonstra por que segmentos complexos só funcionam bem quando o texto delimitador não aparece dentro dos valores de parâmetro. Usar um regex e extrair manualmente os valores é necessário para casos mais complexos.
Aviso
Ao usar System.Text.RegularExpressions para processar entradas não confiáveis, passe um tempo limite. Um usuário mal-intencionado pode fornecer entrada para RegularExpressions
, causando um ataque de negação de serviço. APIs ASP.NET Core Framework que usam RegularExpressions
passam um tempo limite.
Este é um resumo das etapas que o roteamento executa com o modelo /a{b}c{d}
e o caminho de URL /abcd
. O |
é usado para ajudar a visualizar como o algoritmo funciona:
c
. Portanto, /abcd
é pesquisado pela direita e localiza /ab|c|d
.d
) agora corresponde ao parâmetro de rota {d}
.a
. Portanto, /ab|c|d
é pesquisado começando de onde paramos e, em seguida, a
é encontrado em /|a|b|c|d
.b
) agora corresponde ao parâmetro de rota {b}
.Este é um exemplo de um caso negativo usando o mesmo modelo /a{b}c{d}
e o caminho de URL /aabcd
. O |
é usado para ajudar a visualizar como o algoritmo funciona. Esse caso não é uma correspondência, que é explicada pelo mesmo algoritmo:
c
. Portanto, /aabcd
é pesquisado pela direita e localiza /aab|c|d
.d
) agora corresponde ao parâmetro de rota {d}
.a
. Portanto, /aab|c|d
é pesquisado começando de onde paramos e, em seguida, a
é encontrado em /a|a|b|c|d
.b
) agora corresponde ao parâmetro de rota {b}
.a
, mas o algoritmo ficou fora do modelo de rota para analisar. Portanto, não é uma correspondência.Como o algoritmo correspondente é sem greedy:
Expressões regulares fornecem muito mais controle sobre o comportamento correspondente.
A correspondência de greedy, também conhecida como correspondência lenta, corresponde à maior cadeia de caracteres possível. O valor sem greedy corresponde à menor cadeia de caracteres possível.
O roteamento com caracteres especiais pode levar a resultados inesperados. Por exemplo, considere um controlador com o seguinte método de ação:
[HttpGet("{id?}/name")]
public async Task<ActionResult<string>> GetName(string id)
{
var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null || todoItem.Name == null)
{
return NotFound();
}
return todoItem.Name;
}
Quando string id
contém os seguintes valores codificados, podem ocorrer resultados inesperados:
ASCII | Encoded |
---|---|
/ |
%2F |
|
+ |
Os parâmetros de rota nem sempre são decodificados por URL. Esse problema poderá ser resolvido no futuro. Para obter mais informações, confira este tópico do GitHub;
As restrições de rota são executadas quando ocorre uma correspondência com a URL de entrada e é criado um token do caminho da URL em valores de rota. Em geral, as restrições da rota inspecionam o valor de rota associado por meio do modelo de rota e tomam uma decisão do tipo "verdadeiro ou falso" sobre se o valor é aceitável. Algumas restrições da rota usam dados fora do valor de rota para considerar se a solicitação pode ser encaminhada. Por exemplo, a HttpMethodRouteConstraint pode aceitar ou rejeitar uma solicitação de acordo com o verbo HTTP. As restrições são usadas em solicitações de roteamento e na geração de link.
Aviso
Não use restrições para a validação de entrada. Se as restrições forem usadas para validação de entrada, a entrada inválida resultará em uma resposta 404
Não Encontrado. A entrada inválida deve produzir uma Solicitação Inválida 400
com uma mensagem de erro apropriada. As restrições de rota são usadas para desfazer a ambiguidade entre rotas semelhantes, não para validar as entradas de uma rota específica.
A tabela a seguir demonstra restrições de rota de exemplo e seu comportamento esperado:
restrição | Exemplo | Correspondências de exemplo | Observações |
---|---|---|---|
int |
{id:int} |
123456789 , -123456789 |
Corresponde a qualquer inteiro |
bool |
{active:bool} |
true , FALSE |
Corresponde a true ou false . Não diferencia maiúsculas de minúsculas |
datetime |
{dob:datetime} |
2016-12-31 , 2016-12-31 7:32pm |
Corresponde a um valor válido DateTime na cultura invariável. Confira o aviso anterior. |
decimal |
{price:decimal} |
49.99 , -1,000.01 |
Corresponde a um valor válido decimal na cultura invariável. Confira o aviso anterior. |
double |
{weight:double} |
1.234 , -1,001.01e8 |
Corresponde a um valor válido double na cultura invariável. Confira o aviso anterior. |
float |
{weight:float} |
1.234 , -1,001.01e8 |
Corresponde a um valor válido float na cultura invariável. Confira o aviso anterior. |
guid |
{id:guid} |
CD2C1638-1638-72D5-1638-DEADBEEF1638 |
Corresponde a um valor Guid válido |
long |
{ticks:long} |
123456789 , -123456789 |
Corresponde a um valor long válido |
minlength(value) |
{username:minlength(4)} |
Rick |
A cadeia de caracteres deve ter, no mínimo, 4 caracteres |
maxlength(value) |
{filename:maxlength(8)} |
MyFile |
A cadeia de caracteres não pode ser maior que 8 caracteres |
length(length) |
{filename:length(12)} |
somefile.txt |
A cadeia de caracteres deve ter exatamente 12 caracteres |
length(min,max) |
{filename:length(8,16)} |
somefile.txt |
A cadeia de caracteres deve ter, pelo menos, 8 e não mais de 16 caracteres |
min(value) |
{age:min(18)} |
19 |
O valor inteiro deve ser, pelo menos, 18 |
max(value) |
{age:max(120)} |
91 |
O valor inteiro não deve ser maior que 120 |
range(min,max) |
{age:range(18,120)} |
91 |
O valor inteiro deve ser, pelo menos, 18, mas não maior que 120 |
alpha |
{name:alpha} |
Rick |
A cadeia de caracteres deve consistir em um ou mais caracteres alfabéticos, a -z e não diferencia maiúsculas de minúsculas. |
regex(expression) |
{ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} |
123-45-6789 |
A cadeia de caracteres deve corresponder à expressão regular. Confira as dicas sobre como definir uma expressão regular. |
required |
{name:required} |
Rick |
Usado para impor que um valor não parâmetro está presente durante a geração de URL |
Aviso
Ao usar System.Text.RegularExpressions para processar entradas não confiáveis, passe um tempo limite. Um usuário mal-intencionado pode fornecer entrada para RegularExpressions
, causando um ataque de negação de serviço. APIs ASP.NET Core Framework que usam RegularExpressions
passam um tempo limite.
Várias restrições delimitadas por dois-pontos podem ser aplicadas a um único parâmetro. Por exemplo, a restrição a seguir restringe um parâmetro para um valor inteiro de 1 ou maior:
[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }
Aviso
As restrições de rota que verificam a URL e são convertidas em um tipo CLR sempre usam a cultura invariável. Por exemplo, conversão para o tipo CLR int
ou DateTime
. Essas restrições consideram que a URL não é localizável. As restrições de rota fornecidas pela estrutura não modificam os valores armazenados nos valores de rota. Todos os valores de rota analisados com base na URL são armazenados como cadeias de caracteres. Por exemplo, a restrição float
tenta converter o valor de rota em um float, mas o valor convertido é usado somente para verificar se ele pode ser convertido em um float.
Aviso
Ao usar System.Text.RegularExpressions para processar entradas não confiáveis, passe um tempo limite. Um usuário mal-intencionado pode fornecer entrada para RegularExpressions
, causando um ataque de negação de serviço. APIs ASP.NET Core Framework que usam RegularExpressions
passam um tempo limite.
As expressões regulares podem ser especificadas como restrições embutidas usando a restrição de rota regex(...)
. Os métodos na família MapControllerRoute também aceitam um literal de objeto das restrições. Se esse formulário for usado, os valores de cadeia de caracteres serão interpretados como expressões regulares.
O código a seguir usa uma restrição regex embutida:
app.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
() => "Inline Regex Constraint Matched");
O código a seguir usa um literal de objeto para especificar uma restrição regex:
app.MapControllerRoute(
name: "people",
pattern: "people/{ssn}",
constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
defaults: new { controller = "People", action = "List" });
A estrutura do ASP.NET Core adiciona RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant
ao construtor de expressão regular. Confira RegexOptions para obter uma descrição desses membros.
As expressões regulares usam delimitadores e tokens semelhantes aos usados pelo roteamento e pela linguagem C#. Os tokens de expressão regular precisam ter escape. Para usar a expressão regular ^\d{3}-\d{2}-\d{4}$
em uma restrição embutida, use uma das seguintes opções:
\
fornecidos na cadeia de caracteres pelos caracteres \\
no arquivo de origem do C# para escapar do caractere de escape da cadeia de caracteres \
.Para fazer o escape dos caracteres de delimitador de parâmetro de roteamento {
, }
, [
, ]
, duplique os caracteres na expressão, por exemplo, {{
, }}
, [[
, ]]
. A tabela a seguir mostra uma expressão regular e a versão com escape:
Expressão regular | Expressão regular com escape |
---|---|
^\d{3}-\d{2}-\d{4}$ |
^\\d{{3}}-\\d{{2}}-\\d{{4}}$ |
^[a-z]{2}$ |
^[[a-z]]{{2}}$ |
As expressões regulares usadas no roteamento geralmente começam com o caractere ^
e correspondem à posição inicial da cadeia de caracteres. As expressões geralmente terminam com o caractere $
e correspondem ao final da cadeia de caracteres. Os caracteres ^
e $
garantem que a expressão regular corresponde a todo o valor do parâmetro de rota. Sem os caracteres ^
e $
, a expressão regular corresponde a qualquer subcadeia de caracteres na cadeia de caracteres, o que geralmente não é o desejado. A tabela a seguir fornece exemplos e explica por que eles encontram ou não uma correspondência:
Expression | String | Corresponder a | Comentar |
---|---|---|---|
[a-z]{2} |
hello | Sim | A subcadeia de caracteres corresponde |
[a-z]{2} |
123abc456 | Sim | A subcadeia de caracteres corresponde |
[a-z]{2} |
mz | Sim | Corresponde à expressão |
[a-z]{2} |
MZ | Sim | Não diferencia maiúsculas de minúsculas |
^[a-z]{2}$ |
hello | Não | Confira ^ e $ acima |
^[a-z]{2}$ |
123abc456 | Não | Confira ^ e $ acima |
Para saber mais sobre a sintaxe de expressões regulares, confira Expressões regulares do .NET Framework.
Para restringir um parâmetro a um conjunto conhecido de valores possíveis, use uma expressão regular. Por exemplo, {action:regex(^(list|get|create)$)}
apenas corresponde o valor da rota action
a list
, get
ou create
. Se passada para o dicionário de restrições, a cadeia de caracteres ^(list|get|create)$
é equivalente. As restrições passadas para o dicionário de restrições que não correspondem a uma das restrições conhecidas também são tratadas como expressões regulares. As restrições transmitidas em um modelo que não correspondem a uma das restrições conhecidas não são tratadas como expressões regulares.
É possível criar restrições de rota personalizadas com a implementação da interface do IRouteConstraint. A interface do IRouteConstraint
contém Match, que retorna true
quando a restrição é atendida. Caso contrário, retorna false
.
As restrições de rota personalizadas raramente são necessárias. Antes de implementar uma restrição de rota personalizada, considere alternativas, como a associação de modelo.
A pasta restrições do ASP.NET Core fornece bons exemplos de criação de restrições. Por exemplo, GuidRouteConstraint.
Para usar uma IRouteConstraint
personalizada, o tipo de restrição de rota deve ser registrado com o ConstraintMap do aplicativo, no contêiner de serviço. O ConstraintMap
é um dicionário que mapeia as chaves de restrição de rota para implementações de IRouteConstraint
que validam essas restrições. É possível atualizar o ConstraintMap
do aplicativo no Program.cs
como parte de uma chamada AddRouting ou configurando RouteOptions diretamente com builder.Services.Configure<RouteOptions>
. Por exemplo:
builder.Services.AddRouting(options =>
options.ConstraintMap.Add("noZeroes", typeof(NoZeroesRouteConstraint)));
A restrição anterior é aplicada no seguinte código:
[ApiController]
[Route("api/[controller]")]
public class NoZeroesController : ControllerBase
{
[HttpGet("{id:noZeroes}")]
public IActionResult Get(string id) =>
Content(id);
}
A implementação de NoZeroesRouteConstraint
impede que 0
seja usada em um parâmetro de rota:
public class NoZeroesRouteConstraint : IRouteConstraint
{
private static readonly Regex _regex = new(
@"^[1-9]*$",
RegexOptions.CultureInvariant | RegexOptions.IgnoreCase,
TimeSpan.FromMilliseconds(100));
public bool Match(
HttpContext? httpContext, IRouter? route, string routeKey,
RouteValueDictionary values, RouteDirection routeDirection)
{
if (!values.TryGetValue(routeKey, out var routeValue))
{
return false;
}
var routeValueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);
if (routeValueString is null)
{
return false;
}
return _regex.IsMatch(routeValueString);
}
}
Aviso
Ao usar System.Text.RegularExpressions para processar entradas não confiáveis, passe um tempo limite. Um usuário mal-intencionado pode fornecer entrada para RegularExpressions
, causando um ataque de negação de serviço. APIs ASP.NET Core Framework que usam RegularExpressions
passam um tempo limite.
O código anterior:
0
no segmento {id}
da rota.O código a seguir é uma abordagem melhor para impedir que um id
que contém um 0
seja processado:
[HttpGet("{id}")]
public IActionResult Get(string id)
{
if (id.Contains('0'))
{
return StatusCode(StatusCodes.Status406NotAcceptable);
}
return Content(id);
}
O código anterior tem as seguintes vantagens em relação à abordagem NoZeroesRouteConstraint
:
0
.Transformadores de parâmetro:
Por exemplo, um transformador de parâmetro slugify
personalizado em padrão de rota blog\{article:slugify}
com Url.Action(new { article = "MyTestArticle" })
gera blog\my-test-article
.
Considere a seguinte implementação IOutboundParameterTransformer
:
public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
public string? TransformOutbound(object? value)
{
if (value is null)
{
return null;
}
return Regex.Replace(
value.ToString()!,
"([a-z])([A-Z])",
"$1-$2",
RegexOptions.CultureInvariant,
TimeSpan.FromMilliseconds(100))
.ToLowerInvariant();
}
}
Para usar um transformador de parâmetro em um padrão de rota, configure-o usando ConstraintMap em Program.cs
:
builder.Services.AddRouting(options =>
options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer));
A estrutura do ASP.NET Core usa os transformadores de parâmetro para transformar o URI no qual um ponto de extremidade é resolvido. Por exemplo, os transformadores de parâmetro transformam os valores de rota usado para corresponder a um area
, controller
, action
e page
:
app.MapControllerRoute(
name: "default",
pattern: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");
Com o modelo de rota anterior, a ação SubscriptionManagementController.GetAll
é combinada com o URI /subscription-management/get-all
. Um transformador de parâmetro não altera os valores de rota usados para gerar um link. Por exemplo, Url.Action("GetAll", "SubscriptionManagement")
gera /subscription-management/get-all
.
ASP.NET Core fornece convenções de API para usar transformadores de parâmetro com as rotas geradas:
Esta seção contém uma referência para o algoritmo implementado pela geração de URL. Na prática, os exemplos mais complexos de geração de URL usam controladores ou Razor Pages. Confira o roteamento em controladores para obter informações adicionais.
O processo de geração de URL começa com uma chamada para LinkGenerator.GetPathByAddress ou um método semelhante. O método é fornecido com um endereço, um conjunto de valores de rota e, opcionalmente, informações sobre a solicitação atual de HttpContext
.
A primeira etapa é usar o endereço para resolve um conjunto de pontos de extremidade candidatos usando um IEndpointAddressScheme<TAddress> que corresponda ao tipo do endereço.
Depois que o conjunto de candidatos é encontrado pelo esquema de endereços, os pontos de extremidade são ordenados e processados iterativamente até que uma operação de geração de URL seja bem-sucedida. A geração de URL não verifica se há ambiguidades. O primeiro resultado retornado é o resultado final.
A primeira etapa na solução de problemas de geração de URL é definir o nível de log de Microsoft.AspNetCore.Routing
como TRACE
. LinkGenerator
registra muitos detalhes sobre o processamento, o que pode ser útil para solucionar problemas.
Confira Referência de geração de URL para obter detalhes sobre a geração de URL.
Os endereços são o conceito na geração de URL usado para vincular uma chamada ao gerador de links para um conjunto de pontos de extremidade candidatos.
Os endereços são um conceito extensível que vem com duas implementações por padrão:
string
) como o endereço: IUrlHelper
, Auxiliares de Marca, Auxiliares HTML, Resultados da Ação etc.A função do esquema de endereços é fazer a associação entre o endereço e os pontos de extremidade correspondentes por critérios arbitrários:
Na solicitação atual, o roteamento acessa os valores de rota da solicitação atual HttpContext.Request.RouteValues
. Os valores associados à solicitação atual são chamados de valores ambientes. Para maior clareza, a documentação se refere aos valores de rota transmitidos para os métodos como valores explícitos.
O exemplo a seguir mostra valores ambientes e valores explícitos. Fornece os valores ambientes da solicitação atual e os valores explícitos:
public class WidgetController : ControllerBase
{
private readonly LinkGenerator _linkGenerator;
public WidgetController(LinkGenerator linkGenerator) =>
_linkGenerator = linkGenerator;
public IActionResult Index()
{
var indexPath = _linkGenerator.GetPathByAction(
HttpContext, values: new { id = 17 })!;
return Content(indexPath);
}
// ...
O código anterior:
/Widget/Index/17
O código a seguir fornece apenas valores explícitos e nenhum valor ambiente:
var subscribePath = _linkGenerator.GetPathByAction(
"Subscribe", "Home", new { id = 17 })!;
O método anterior retorna /Home/Subscribe/17
O código a seguir no WidgetController
retorna /Widget/Subscribe/17
:
var subscribePath = _linkGenerator.GetPathByAction(
HttpContext, "Subscribe", null, new { id = 17 });
O código a seguir fornece o controlador dos valores ambientes na solicitação atual e dos valores explícitos:
public class GadgetController : ControllerBase
{
public IActionResult Index() =>
Content(Url.Action("Edit", new { id = 17 })!);
}
No código anterior:
/Gadget/Edit/17
é retornado.action
e os valores route
especificados.O código a seguir fornece os valores ambientes da solicitação atual e os valores explícitos:
public class IndexModel : PageModel
{
public void OnGet()
{
var editUrl = Url.Page("./Edit", new { id = 17 });
// ...
}
}
O código anterior define url
como /Edit/17
, quando a página Editar Razor contém a seguinte diretiva de página:
@page "{id:int}"
Se a página Editar não contiver o modelo de rota "{id:int}"
, url
será /Edit?id=17
.
O comportamento do MVC IUrlHelper adiciona uma camada de complexidade, além das regras descritas aqui:
IUrlHelper
sempre fornece os valores de rota da solicitação atual como valores ambientes.action
e controller
como valores explícitos, a menos que sejam substituídos pelo desenvolvedor.page
como valor explícito, a menos que seja substituído. IUrlHelper.Page
sempre substitui o valor de rota atual handler
por null
como um valor explícito, a menos que seja substituído.Os usuários geralmente ficam surpresos com os detalhes comportamentais dos valores ambientes, pois o MVC não parece seguir suas próprias regras. Por motivos de histórico e compatibilidade, determinados valores de rota, como action
, controller
, page
e handler
, têm seu próprio comportamento de caso especial.
A funcionalidade equivalente fornecida por LinkGenerator.GetPathByAction
e LinkGenerator.GetPathByPage
duplica essas anomalias de IUrlHelper
para compatibilidade.
Depois que o conjunto de pontos de extremidade candidatos for encontrado, o algoritmo de geração de URL:
A primeira etapa nesse processo é chamada de invalidação de valor de rota. A invalidação de valor de rota é o processo pelo qual o roteamento decide quais valores de rota dos valores ambientes devem ser usados e quais devem ser ignorados. Cada valor ambiente é considerado e combinado com os valores explícitos ou ignorado.
A melhor maneira de pensar sobre a função dos valores de ambiente é que eles tentam salvar a digitação dos desenvolvedores de aplicativos, em alguns casos comuns. Tradicionalmente, os cenários em que os valores ambientes são úteis estão relacionados ao MVC:
As chamadas para LinkGenerator
ou IUrlHelper
que retornam null
geralmente são causadas por não entender a invalidação de valor de rota. Solucione problemas de invalidação de valor de rota especificando explicitamente mais valores de rota para ver se isso resolve o problema.
A invalidação de valor de rota funciona supondo que o esquema de URL do aplicativo é hierárquico, com uma hierarquia formada da esquerda para a direita. Considere o modelo de rota de controlador básico {controller}/{action}/{id?}
para ter uma noção intuitiva de como isso funciona na prática. Uma alteração em um valor invalida todos os valores de rota que são exibidos à direita. Isso reflete a suposição sobre a hierarquia. Se o aplicativo tiver um valor ambiente para id
e a operação especificar um valor diferente para o controller
:
id
não será reutilizado porque {controller}
está à esquerda de {id?}
.Alguns exemplos que demonstram esse princípio:
id
, o valor ambiente para id
será ignorado. Os valores ambientes para controller
e action
podem ser usados.action
, qualquer valor ambiente para action
será ignorado. Os valores ambientes para controller
podem ser usados. Se o valor explícito para action
for diferente do valor ambiente para action
, o valor id
não será usado. Se o valor explícito para action
for igual ao valor ambiente para action
, o valor id
poderá ser usado.controller
, qualquer valor ambiente para controller
será ignorado. Se o valor explícito para controller
for diferente do valor ambiente para controller
, os valores action
e id
não serão usados. Se o valor explícito para controller
for igual ao valor ambiente para controller
, os valores action
e id
poderão ser usados.Esse processo é ainda mais complicado devido à existência de rotas de atributo e rotas convencionais dedicadas. As rotas convencionais do controlador, como {controller}/{action}/{id?}
, especificam uma hierarquia usando parâmetros de rota. Para rotas convencionais dedicadas e rotas de atributo para os controladores e Razor Pages:
Para esses casos, a geração de URL define o conceito de valores necessários. Os pontos de extremidade criados por controladores e Razor Pages têm os valores necessários especificados que permitem que a invalidação do valor de rota funcione.
O algoritmo de invalidação de valor de rota em detalhes:
Neste ponto, a operação de geração de URL está pronta para avaliar as restrições de rota. O conjunto de valores aceitos é combinado com os valores padrão do parâmetro, que são fornecidos às restrições. Se todas as restrições forem aprovadas, a operação continuará.
Em seguida, os valores aceitos podem ser usados para expandir o modelo de rota. O modelo de rota é processado:
Valores fornecidos explicitamente, que não correspondem a um segmento da rota, são adicionados à cadeia de consulta. A tabela a seguir mostra o resultado do uso do modelo de rota {controller}/{action}/{id?}
.
Valores de ambiente | Valores explícitos | Resultado |
---|---|---|
controlador = "Home" | ação = "About" | /Home/About |
controlador = "Home" | controlador = "Order", ação = "About" | /Order/About |
controlador = "Home", cor = "Vermelho" | ação = "About" | /Home/About |
controlador = "Home" | ação = "About", cor = "Red" | /Home/About?color=Red |
O código a seguir mostra um exemplo de um esquema de geração de URL que não é compatível com o roteamento:
app.MapControllerRoute(
"default",
"{culture}/{controller=Home}/{action=Index}/{id?}");
app.MapControllerRoute(
"blog",
"{culture}/{**slug}",
new { controller = "Blog", action = "ReadPost" });
No código anterior, o parâmetro de rota culture
é usado para localização. O ideal é que o parâmetro culture
sempre seja aceito como valor ambiente. No entanto, o parâmetro culture
não é aceito como valor ambiente devido à maneira como os valores necessários funcionam:
"default"
, o parâmetro de rota culture
fica à esquerda de controller
. Portanto, as alterações em controller
não invalidarão culture
."blog"
, considera-se que o parâmetro de rota culture
fica à direita de controller
, que aparece nos valores necessários.A classe LinkParser adiciona suporte para analisar um caminho de URL em um conjunto de valores de rota. O método ParsePathByEndpointName usa um nome de ponto de extremidade e um caminho de URL e retorna um conjunto de valores de rota extraídos do caminho de URL.
No controlador de exemplo a seguir, a ação GetProduct
usa um modelo de rota de api/Products/{id}
e tem um Name de GetProduct
:
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet("{id}", Name = nameof(GetProduct))]
public IActionResult GetProduct(string id)
{
// ...
Na mesma classe do controlador, a ação AddRelatedProduct
espera um caminho de URL, pathToRelatedProduct
, que pode ser fornecido como um parâmetro de cadeia de caracteres de consulta:
[HttpPost("{id}/Related")]
public IActionResult AddRelatedProduct(
string id, string pathToRelatedProduct, [FromServices] LinkParser linkParser)
{
var routeValues = linkParser.ParsePathByEndpointName(
nameof(GetProduct), pathToRelatedProduct);
var relatedProductId = routeValues?["id"];
// ...
No exemplo anterior, a ação AddRelatedProduct
extrai o valor de rota id
do caminho de URL. Por exemplo, com um caminho de URL de /api/Products/1
, o valor relatedProductId
é definido como 1
. Essa abordagem permite que os clientes da API usem os caminhos de URL ao referenciar recursos, sem exigir conhecimento de como essa URL é estruturada.
Os links a seguir fornecem informações sobre como configurar metadados de ponto de extremidade:
[MinimumAgeAuthorize]
RequireHost aplica uma restrição à rota que exige o host especificado. O parâmetro RequireHost
ou [Host] pode ser um:
www.domain.com
, corresponde www.domain.com
a qualquer porta.*.domain.com
, corresponde www.domain.com
, subdomain.domain.com
ou www.subdomain.domain.com
a qualquer porta.*:5000
, corresponde a porta 5000 a qualquer host.www.domain.com:5000
ou *.domain.com:5000
, corresponde ao host e à porta.Vários parâmetros podem ser especificados usando RequireHost
ou [Host]
. A restrição corresponde aos hosts válidos para qualquer um dos parâmetros. Por exemplo, [Host("domain.com", "*.domain.com")]
corresponde a domain.com
, www.domain.com
ou subdomain.domain.com
.
O código a seguir usa RequireHost
para exigir o host especificado na rota:
app.MapGet("/", () => "Contoso").RequireHost("contoso.com");
app.MapGet("/", () => "AdventureWorks").RequireHost("adventure-works.com");
app.MapHealthChecks("/healthz").RequireHost("*:8080");
O código a seguir usa o atributo [Host]
no controlador para exigir qualquer um dos hosts especificados:
[Host("contoso.com", "adventure-works.com")]
public class HostsController : Controller
{
public IActionResult Index() =>
View();
[Host("example.com")]
public IActionResult Example() =>
View();
}
Quando o atributo [Host]
é aplicado ao controlador e ao método de ação:
Quando um aplicativo tem problemas de desempenho, geralmente suspeita-se que o roteamento é o problema. O motivo pelo qual o roteamento é suspeito é que as estruturas como controladores e Razor Pages relatam o tempo gasto dentro da estrutura nas mensagens de log. Quando há uma diferença significativa entre o tempo relatado pelos controladores e o tempo total da solicitação:
O desempenho do roteamento é testado usando milhares de pontos de extremidade. É improvável que um aplicativo típico encontre um problema de desempenho apenas por ser muito grande. A causa raiz mais comum do desempenho lento do roteamento geralmente é um middleware personalizado com comportamento inválido.
O exemplo de código a seguir demonstra uma técnica básica para restringir a fonte de atraso:
var logger = app.Services.GetRequiredService<ILogger<Program>>();
app.Use(async (context, next) =>
{
var stopwatch = Stopwatch.StartNew();
await next(context);
stopwatch.Stop();
logger.LogInformation("Time 1: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});
app.UseRouting();
app.Use(async (context, next) =>
{
var stopwatch = Stopwatch.StartNew();
await next(context);
stopwatch.Stop();
logger.LogInformation("Time 2: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});
app.UseAuthorization();
app.Use(async (context, next) =>
{
var stopwatch = Stopwatch.StartNew();
await next(context);
stopwatch.Stop();
logger.LogInformation("Time 3: {ElapsedMilliseconds}ms", stopwatch.ElapsedMilliseconds);
});
app.MapGet("/", () => "Timing Test.");
Para roteamento de tempo:
Essa é uma maneira básica de restringir o atraso quando ele é significativo, por exemplo, mais de 10ms
. Subtrair Time 2
de Time 1
relata o tempo gasto dentro do middleware UseRouting
.
O código a seguir usa uma abordagem mais compacta para o código de tempo anterior:
public sealed class AutoStopwatch : IDisposable
{
private readonly ILogger _logger;
private readonly string _message;
private readonly Stopwatch _stopwatch;
private bool _disposed;
public AutoStopwatch(ILogger logger, string message) =>
(_logger, _message, _stopwatch) = (logger, message, Stopwatch.StartNew());
public void Dispose()
{
if (_disposed)
{
return;
}
_logger.LogInformation("{Message}: {ElapsedMilliseconds}ms",
_message, _stopwatch.ElapsedMilliseconds);
_disposed = true;
}
}
var logger = app.Services.GetRequiredService<ILogger<Program>>();
var timerCount = 0;
app.Use(async (context, next) =>
{
using (new AutoStopwatch(logger, $"Time {++timerCount}"))
{
await next(context);
}
});
app.UseRouting();
app.Use(async (context, next) =>
{
using (new AutoStopwatch(logger, $"Time {++timerCount}"))
{
await next(context);
}
});
app.UseAuthorization();
app.Use(async (context, next) =>
{
using (new AutoStopwatch(logger, $"Time {++timerCount}"))
{
await next(context);
}
});
app.MapGet("/", () => "Timing Test.");
A lista a seguir fornece alguns insights sobre recursos de roteamento relativamente caros, em comparação a modelos de rota básicos:
{x}-{y}-{z}
): Por padrão, o ASP.NET Core usa um algoritmo de roteamento que troca memória por tempo de CPU. O bom resultado disso é que o tempo de correspondência de rotas depende apenas do tamanho do caminho a ser correspondido e não do número de rotas. No entanto, essa abordagem pode ser possivelmente problemática em alguns casos, quando o aplicativo tem um grande número de rotas (milhares) e há uma grande quantidade de prefixos variáveis nas rotas. Por exemplo, se as rotas tiverem parâmetros nos segmentos iniciais da rota, como {parameter}/some/literal
.
É improvável que um aplicativo tenha uma situação em que esse seja um problema, a menos nos seguintes casos:
Microsoft.AspNetCore.Routing.Matching.DfaNode
.Há várias técnicas e otimizações que podem ser aplicadas a rotas que melhorarão muito esse cenário:
{parameter:int}
, {parameter:guid}
, {parameter:regex(\\d+)}
etc., sempre que possível.
MapDynamicControllerRoute
ou MapDynamicPageRoute
.Esta seção contém diretrizes para criadores de bibliotecas com base no roteamento. Esses detalhes destinam-se a garantir que os desenvolvedores de aplicativos tenham uma boa experiência usando bibliotecas e estruturas que estendem o roteamento.
Para criar uma estrutura que usa o roteamento para correspondência de URL, comece definindo uma experiência do usuário baseada em UseEndpoints.
CRIE com base em IEndpointRouteBuilder. Isso permite que os usuários componham a estrutura com outros recursos do ASP.NET Core, sem confusão. Cada modelo do ASP.NET Core inclui o roteamento. Suponha que o roteamento esteja presente e seja conhecido para os usuários.
// Your framework
app.MapMyFramework(...);
app.MapHealthChecks("/healthz");
RETORNE um tipo de concreto selado de uma chamada para MapMyFramework(...)
que implemente IEndpointConventionBuilder. A maioria dos métodos Map...
da estrutura segue esse padrão. A interface IEndpointConventionBuilder
:
Declarar seu próprio tipo permite que você adicione sua própria funcionalidade específica da estrutura ao construtor. Não há problema em encapsular um construtor declarado por estrutura e encaminhar chamadas para ele.
// Your framework
app.MapMyFramework(...)
.RequireAuthorization()
.WithMyFrameworkFeature(awesome: true);
app.MapHealthChecks("/healthz");
GRAVE seu próprio EndpointDataSource. EndpointDataSource
é o primitivo de baixo nível para declarar e atualizar uma coleção de pontos de extremidade. EndpointDataSource
é uma API eficiente usada por controladores e Razor Pages.
Os testes de roteamento têm um exemplo básico de uma fonte de dados que não está atualizando.
NÃO tente registrar um EndpointDataSource
por padrão. Exija que os usuários registrem a estrutura no UseEndpoints. A filosofia do roteamento determina que nada está incluído por padrão e esse UseEndpoints
é o local para registrar pontos de extremidade.
DEFINA os tipos de metadados como uma interface.
POSSIBILITE o uso de tipos de metadados como um atributo em classes e métodos.
public interface ICoolMetadata
{
bool IsCool { get; }
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
public bool IsCool => true;
}
As estruturas como controladores e Razor Pages dão suporte à aplicação de atributos de metadados a tipos e métodos. Se você declarar os tipos de metadados:
Declarar um tipo de metadados como uma interface adiciona outra camada de flexibilidade:
POSSIBILITE a substituição de metadados, conforme mostrado no exemplo a seguir:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class SuppressCoolMetadataAttribute : Attribute, ICoolMetadata
{
public bool IsCool => false;
}
[CoolMetadata]
public class MyController : Controller
{
public void MyCool() { }
[SuppressCoolMetadata]
public void Uncool() { }
}
A melhor maneira de seguir estas diretrizes é evitar definir metadados de marcador:
A coleção de metadados é ordenada e permite substituir por prioridade. No caso de controladores, os metadados no método de ação são mais específicos.
TORNE o middleware útil com e sem roteamento:
app.UseAuthorization(new AuthorizationPolicy() { ... });
// Your framework
app.MapMyFramework(...).RequireAuthorization();
Como exemplo dessa diretriz, considere o middleware UseAuthorization
. O middleware de autorização permite que você transmita uma política de fallback. A política de fallback, se especificada, aplica-se a:
Isso torna o middleware de autorização útil fora do contexto de roteamento. O middleware de autorização pode ser usado para programação de middleware tradicional.
Para obter a saída de diagnóstico de roteamento detalhada, defina Logging:LogLevel:Microsoft
como Debug
. No ambiente de desenvolvimento, defina o nível de log em appsettings.Development.json
:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Debug",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
O roteamento é responsável por corresponder solicitações HTTP de entrada e expedir essas solicitações para os pontos de extremidade executáveis do aplicativo. Os pontos de extremidade são as unidades de código executável de manipulação de solicitações do aplicativo. Os pontos de extremidade são definidos no aplicativo e configurados quando o aplicativo é iniciado. O processo de correspondência de ponto de extremidade pode extrair valores da URL da solicitação e fornecer esses valores para processamento de solicitações. Usando as informações de ponto de extremidade do aplicativo, o roteamento também pode gerar URLs que são mapeadas para os pontos de extremidade.
Os aplicativos podem configurar o roteamento usando:
Este documento aborda os detalhes de baixo nível do roteamento do ASP.NET Core. Para obter informações sobre como configurar o roteamento:
O sistema de roteamento de ponto de extremidade descrito neste documento aplica-se ao ASP.NET Core 3.0 e posteriores. Para obter informações sobre o sistema de roteamento anterior com base em IRouter, selecione a versão do ASP.NET Core 2.1 usando uma das seguintes abordagens:
Exibir ou baixar código de exemplo (como baixar)
Os exemplos de download deste documento são habilitados por uma classe específica Startup
. Para executar um exemplo específico, modifique Program.cs
para chamar a classe desejada Startup
.
Todos os modelos do ASP.NET Core incluem o roteamento no código gerado. O roteamento é registrado no pipeline de middleware no Startup.Configure
.
O código a seguir mostra um exemplo básico de roteamento:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});
});
}
O roteamento usa um par de middleware registrado por UseRouting e UseEndpoints:
UseRouting
adiciona a correspondência de rotas ao pipeline de middleware. Esse middleware examina o conjunto de pontos de extremidade definidos no aplicativo e seleciona a melhor correspondência com base na solicitação.UseEndpoints
adiciona a execução do ponto de extremidade ao pipeline de middleware. Ele executa o delegado associado ao ponto de extremidade selecionado.O exemplo anterior inclui um único ponto de extremidade da rota para o código usando o método MapGet:
GET
é enviada para a URL raiz /
: Hello World!
é gravado na resposta HTTP. Por padrão, a URL raiz /
é https://localhost:5001/
.GET
ou a URL raiz não for /
, nenhuma rota corresponderá e um HTTP 404 será retornado.O método MapGet
é usado para definir um ponto de extremidade. Um ponto de extremidade pode ser:
Os pontos de extremidade que podem ser correspondidos e executados pelo aplicativo são configurados no UseEndpoints
. Por exemplo, MapGet, MapPost e métodos semelhantes conectam os delegados de solicitação ao sistema de roteamento. Métodos adicionais podem ser usados para conectar os recursos de estrutura do ASP.NET Core ao sistema de roteamento:
O exemplo a seguir mostra o roteamento com um modelo de rota mais sofisticado:
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/hello/{name:alpha}", async context =>
{
var name = context.Request.RouteValues["name"];
await context.Response.WriteAsync($"Hello {name}!");
});
});
A cadeia de caracteres /hello/{name:alpha}
é um modelo de rota. É usado para configurar a forma como o ponto de extremidade é correspondido. Nesse caso, o modelo corresponde a:
/hello/Ryan
/hello/
seguido de uma sequência de caracteres alfabéticos. :alpha
aplica uma restrição de rota que corresponde apenas a caracteres alfabéticos. As restrições de rota serão explicadas posteriormente neste documento.O segundo segmento do caminho de URL, {name:alpha}
:
name
.O sistema de roteamento de ponto de extremidade descrito neste documento é novo a partir do ASP.NET Core 3.0. No entanto, todas as versões do ASP.NET Core são compatíveis com o mesmo conjunto de recursos de modelo de rota e restrições de rota.
O exemplo a seguir mostra o roteamento com as verificações de integridade e a autorização:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// Matches request to an endpoint.
app.UseRouting();
// Endpoint aware middleware.
// Middleware can use metadata from the matched endpoint.
app.UseAuthentication();
app.UseAuthorization();
// Execute the matched endpoint.
app.UseEndpoints(endpoints =>
{
// Configure the Health Check endpoint and require an authorized user.
endpoints.MapHealthChecks("/healthz").RequireAuthorization();
// Configure another endpoint, no authorization requirements.
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});
});
}
Se você quiser ver os comentários de código traduzidos para idiomas diferentes do inglês, informe-nos neste problema de discussão do GitHub.
O exemplo anterior demonstra como:
A chamada MapHealthChecks adiciona um ponto de extremidade de verificação de integridade. O encadeamento de RequireAuthorization para esta chamada anexa uma política de autorização ao ponto de extremidade.
Chamar UseAuthentication e UseAuthorization adiciona o middleware de autenticação e autorização. Esses programas de middleware são colocados entre UseRouting e UseEndpoints
para que possam:
UseRouting
.No exemplo anterior, há dois pontos de extremidade, mas apenas o ponto de extremidade de verificação de integridade tem uma política de autorização anexada. Se a solicitação corresponder ao ponto de extremidade de verificação de integridade, /healthz
, uma verificação de autorização será executada. Isso demonstra que os pontos de extremidade podem ter dados extras anexados. Esses dados extras são chamados de metadados de ponto de extremidade:
O sistema de roteamento se baseia no pipeline de middleware, adicionando o conceito de ponto de extremidade eficiente. Os pontos de extremidade representam as unidades da funcionalidade do aplicativo que são diferentes umas das outras em termos de roteamento, autorização e qualquer número de sistemas do ASP.NET Core.
Um ponto de extremidade do ASP.NET Core é:
O código a seguir mostra como recuperar e inspecionar o ponto de extremidade correspondente à solicitação atual:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.Use(next => context =>
{
var endpoint = context.GetEndpoint();
if (endpoint is null)
{
return Task.CompletedTask;
}
Console.WriteLine($"Endpoint: {endpoint.DisplayName}");
if (endpoint is RouteEndpoint routeEndpoint)
{
Console.WriteLine("Endpoint has route pattern: " +
routeEndpoint.RoutePattern.RawText);
}
foreach (var metadata in endpoint.Metadata)
{
Console.WriteLine($"Endpoint has metadata: {metadata}");
}
return Task.CompletedTask;
});
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});
});
}
O ponto de extremidade, se selecionado, pode ser recuperado a partir do HttpContext
. Suas propriedades podem ser inspecionadas. Os objetos de ponto de extremidade são imutáveis e não podem ser modificados após a criação. O tipo mais comum de ponto de extremidade é um RouteEndpoint. RouteEndpoint
inclui informações que permitem que ele seja selecionado pelo sistema de roteamento.
No código anterior, app.Use configura um middleware embutido.
O código a seguir mostra que, dependendo de onde app.Use
é chamado no pipeline, pode não haver um ponto de extremidade:
// Location 1: before routing runs, endpoint is always null here
app.Use(next => context =>
{
Console.WriteLine($"1. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
return next(context);
});
app.UseRouting();
// Location 2: after routing runs, endpoint will be non-null if routing found a match
app.Use(next => context =>
{
Console.WriteLine($"2. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
return next(context);
});
app.UseEndpoints(endpoints =>
{
// Location 3: runs when this endpoint matches
endpoints.MapGet("/", context =>
{
Console.WriteLine(
$"3. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
return Task.CompletedTask;
}).WithDisplayName("Hello");
});
// Location 4: runs after UseEndpoints - will only run if there was no match
app.Use(next => context =>
{
Console.WriteLine($"4. Endpoint: {context.GetEndpoint()?.DisplayName ?? "(null)"}");
return next(context);
});
O exemplo anterior adiciona instruções Console.WriteLine
que mostram se um ponto de extremidade foi selecionado ou não. Para maior clareza, o exemplo atribui um nome de exibição ao ponto de extremidade /
fornecido.
Executar esse código com uma URL do /
exibe:
1. Endpoint: (null)
2. Endpoint: Hello
3. Endpoint: Hello
Executar esse código com qualquer outra URL exibe:
1. Endpoint: (null)
2. Endpoint: (null)
4. Endpoint: (null)
Essa saída demonstra que:
UseRouting
seja chamado.UseRouting
e UseEndpoints.UseEndpoints
é terminal quando uma correspondência é encontrada. O middleware de terminal será definido posteriormente neste documento.UseEndpoints
é executado apenas quando nenhuma correspondência é encontrada.O middleware UseRouting
usa o método SetEndpoint para anexar o ponto de extremidade ao contexto atual. É possível substituir o middleware UseRouting
pela lógica personalizada e ainda obter os benefícios de usar pontos de extremidade. Os pontos de extremidade são primitivos de baixo nível, como o middleware, e não são acoplados à implementação de roteamento. A maioria dos aplicativos não precisa substituir UseRouting
pela lógica personalizada.
O middleware UseEndpoints
foi projetado para ser usado em conjunto com o middleware UseRouting
. A lógica principal para executar um ponto de extremidade não é complicada. Use GetEndpoint para recuperar o ponto de extremidade e, em seguida, invoque a propriedade RequestDelegate.
O código a seguir demonstra como o middleware pode influenciar ou reagir ao roteamento:
public class IntegratedMiddlewareStartup
{
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// Location 1: Before routing runs. Can influence request before routing runs.
app.UseHttpMethodOverride();
app.UseRouting();
// Location 2: After routing runs. Middleware can match based on metadata.
app.Use(next => context =>
{
var endpoint = context.GetEndpoint();
if (endpoint?.Metadata.GetMetadata<AuditPolicyAttribute>()?.NeedsAudit
== true)
{
Console.WriteLine($"ACCESS TO SENSITIVE DATA AT: {DateTime.UtcNow}");
}
return next(context);
});
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello world!");
});
// Using metadata to configure the audit policy.
endpoints.MapGet("/sensitive", async context =>
{
await context.Response.WriteAsync("sensitive data");
})
.WithMetadata(new AuditPolicyAttribute(needsAudit: true));
});
}
}
public class AuditPolicyAttribute : Attribute
{
public AuditPolicyAttribute(bool needsAudit)
{
NeedsAudit = needsAudit;
}
public bool NeedsAudit { get; }
}
O exemplo anterior demonstra dois conceitos importantes:
UseRouting
para modificar os dados nos quais o roteamento opera.
UseRouting
e UseEndpoints para processar os resultados do roteamento, antes que o ponto de extremidade seja executado.
UseRouting
e UseEndpoints
: UseAuthorization
e UseCors
.O código anterior mostra um exemplo de um middleware personalizado que permite políticas por ponto de extremidade. O middleware grava um log de auditoria de acesso a dados confidenciais no console. O middleware pode ser configurado para auditar um ponto de extremidade com os metadados AuditPolicyAttribute
. Este exemplo demonstra um padrão de aceitação em que apenas os pontos de extremidade marcados como confidenciais são auditados. É possível definir essa lógica ao contrário, auditando tudo o que não está marcado como seguro, por exemplo. O sistema de metadados do ponto de extremidade é flexível. Essa lógica pode ser projetada da maneira que for adequada para o caso de uso.
O código de exemplo anterior destina-se a demonstrar os conceitos básicos dos pontos de extremidade. O exemplo não se destina ao uso de produção. Uma versão mais completa de um middleware de log de auditoria:
Os metadados da política de auditoria AuditPolicyAttribute
são definidos como um Attribute
para facilitar o uso com estruturas baseadas em classe, como controladores e SignalR. Ao usar a rota para o código:
A melhor prática para tipos de metadados é defini-los como interfaces ou atributos. As interfaces e os atributos permitem a reutilização de código. O sistema de metadados é flexível e não impõe limitações.
O exemplo de código a seguir contrasta o uso de middleware com o uso de roteamento:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// Approach 1: Writing a terminal middleware.
app.Use(next => async context =>
{
if (context.Request.Path == "/")
{
await context.Response.WriteAsync("Hello terminal middleware!");
return;
}
await next(context);
});
app.UseRouting();
app.UseEndpoints(endpoints =>
{
// Approach 2: Using routing.
endpoints.MapGet("/Movie", async context =>
{
await context.Response.WriteAsync("Hello routing!");
});
});
}
O estilo de middleware mostrado com Approach 1:
é o middleware de terminal. Ele é chamado de middleware de terminal porque faz uma operação correspondente:
Path == "/"
para o middleware e Path == "/Movie"
para o roteamento.next
.Ele é chamado de middleware de terminal porque termina a pesquisa, executa algumas funcionalidades e retorna.
Comparação entre middleware de terminal e roteamento:
next
.UseAuthorization
e UseCors
.
UseAuthorization
ou UseCors
exige uma interface manual com o sistema de autorização.Um ponto de extremidade define ambos:
O middleware de terminal pode ser uma ferramenta eficaz, mas pode exigir:
Considere a integração com o roteamento antes de gravar um middleware de terminal.
O middleware de terminal existente que é integrado ao Map ou MapWhen geralmente pode ser transformado em um ponto de extremidade com reconhecimento de roteamento. O MapHealthChecks demonstra o padrão para router-ware:
Map
e forneça o novo pipeline de middleware.Map
usando o método de extensão.O código a seguir mostra o uso do MapHealthChecks:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// Matches request to an endpoint.
app.UseRouting();
// Endpoint aware middleware.
// Middleware can use metadata from the matched endpoint.
app.UseAuthentication();
app.UseAuthorization();
// Execute the matched endpoint.
app.UseEndpoints(endpoints =>
{
// Configure the Health Check endpoint and require an authorized user.
endpoints.MapHealthChecks("/healthz").RequireAuthorization();
// Configure another endpoint, no authorization requirements.
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello World!");
});
});
}
O exemplo anterior mostra por que retornar o objeto do construtor é importante. Retornar o objeto do construtor permite que o desenvolvedor do aplicativo configure políticas como autorização para o ponto de extremidade. Neste exemplo, o middleware de verificações de integridade não tem integração direta com o sistema de autorização.
O sistema de metadados foi criado em resposta aos problemas encontrados por autores de extensibilidade usando o middleware de terminal. É problemático para cada middleware implementar sua própria integração com o sistema de autorização.
Quando um middleware de roteamento é executado, ele define um Endpoint
e encaminha os valores para um recurso de solicitação no HttpContext a partir da solicitação atual:
HttpRequest.RouteValues
obtém a coleção de valores de rota.O middleware executado após o middleware de roteamento pode inspecionar o ponto de extremidade e realizar uma ação. Por exemplo, um middleware de autorização pode interrogar a coleção de metadados do ponto de extremidade para uma política de autorização. Depois que todos os middlewares no pipeline de processamento da solicitação forem executados, o representante do ponto de extremidade selecionado será invocado.
O sistema de roteamento no roteamento de ponto de extremidade é responsável por todas as decisões de expedição. Como o middleware aplica políticas com base no ponto de extremidade selecionado, é importante que:
Aviso
Para compatibilidade com versões anteriores, quando o delegado do ponto de extremidade do Controlador ou do Razor Pages é executado, as propriedades do RouteContext.RouteData são definidas com os valores apropriados com base no processamento da solicitação executado até o momento.
O tipo RouteContext
será marcado como obsoleto em uma versão futura:
RouteData.Values
para HttpRequest.RouteValues
.RouteData.DataTokens
para recuperar IDataTokensMetadata dos metadados do ponto de extremidade.A correspondência de URL opera em um conjunto configurável de fases. Em cada fase, a saída é um conjunto de correspondências. O conjunto de correspondências pode ser reduzido ainda mais pela próxima fase. A implementação de roteamento não garante uma ordem de processamento para pontos de extremidade correspondentes. Todas as correspondências possíveis são processadas de uma só vez. As fases de correspondência de URL ocorrem na ordem a seguir. ASP.NET Core:
A lista de pontos de extremidade é priorizada de acordo com:
Todos os pontos de extremidade correspondentes são processados em cada fase até que o EndpointSelector seja atingido. O EndpointSelector
é a fase final. Ele escolhe o ponto de extremidade de prioridade mais alta nas correspondências como a melhor correspondência. Se houver outras correspondências com a mesma prioridade que a melhor correspondência, uma exceção de correspondência ambígua será gerada.
A precedência de rota é calculada com base em um modelo de rota mais específico que recebe uma prioridade mais alta. Por exemplo, considere os modelos /hello
e /{message}
:
/hello
./hello
é mais específico e, portanto, tem prioridade mais alta.Em geral, a precedência de rota escolhe a melhor correspondência para os tipos de esquemas de URL usados na prática. Use Order somente quando necessário para evitar uma ambiguidade.
Devido aos tipos de extensibilidade fornecidos pelo roteamento, não é possível que o sistema de roteamento calcule antecipadamente as rotas ambíguas. Considere um exemplo, como os modelos de rota /{message:alpha}
e /{message:int}
:
alpha
corresponde apenas a caracteres alfabéticos.int
corresponde apenas a números.Aviso
A ordem das operações dentro do UseEndpoints não influencia o comportamento do roteamento, com uma exceção. MapControllerRoute e MapAreaRoute atribuem automaticamente um valor de pedido aos pontos de extremidade com base na ordem em que são invocados. Isso simula o comportamento de longo prazo dos controladores, sem que o sistema de roteamento forneça as mesmas garantias que as implementações de roteamento mais antigas.
Na implementação herdada do roteamento, é possível implementar a extensibilidade de roteamento que tem uma dependência na ordem em que as rotas são processadas. Roteamento de ponto de extremidade no ASP.NET Core 3.0 e posterior:
A precedência de modelo de rota é um sistema que atribui a cada modelo de rota um valor com base na especificidade. A precedência de modelo de rota:
Por exemplo, considere os modelos /Products/List
e /Products/{id}
. Seria aceitável supor que /Products/List
é uma correspondência melhor do que /Products/{id}
para o caminho de URL /Products/List
. Isso funciona porque o segmento literal /List
é considerado com melhor precedência do que o segmento de parâmetro /{id}
.
Os detalhes de como funciona a precedência são acoplados à forma como os modelos de rota são definidos:
Confira o código-fonte no GitHub para obter uma referência de valores exatos.
Geração de URL:
O roteamento de ponto de extremidade inclui a API LinkGenerator. LinkGenerator
é um serviço singleton disponível na DI. A API LinkGenerator
pode ser usada fora do contexto de uma solicitação em execução. O Mvc.IUrlHelper e os cenários que dependem do IUrlHelper, como Auxiliares de Marcação, Auxiliares de HTML e Resultados da Ação, usam a API LinkGenerator
internamente para fornecer as funcionalidades de geração de link.
O gerador de link é respaldado pelo conceito de um endereço e esquemas de endereço. Um esquema de endereço é uma maneira de determinar os pontos de extremidade que devem ser considerados para a geração de link. Por exemplo, os cenários de nome de rota e valores de rota com os quais muitos usuários estão familiarizados nos controladores e no Razor Pages são implementados como um esquema de endereço.
O gerador de link pode ser vinculado aos controladores e ao Razor Pages usando os seguintes métodos de extensão:
As sobrecargas desses métodos aceitam argumentos que incluem o HttpContext
. Esses métodos são funcionalmente equivalentes a Url.Action e Url.Page, mas oferecem mais flexibilidade e opções.
Os métodos GetPath*
são mais semelhantes a Url.Action
e Url.Page
, pois geram um URI que contém um caminho absoluto. Os métodos GetUri*
sempre geram um URI absoluto que contém um esquema e um host. Os métodos que aceitam um HttpContext
geram um URI no contexto da solicitação em execução. Os valores de rota de ambiente, o caminho base da URL, o esquema e o host da solicitação em execução são usados, a menos que sejam substituídos.
LinkGenerator é chamado com um endereço. A geração de um URI ocorre em duas etapas:
Os métodos fornecidos pelo LinkGenerator dão suporte a funcionalidades de geração de link padrão para qualquer tipo de endereço. A maneira mais conveniente usar o gerador de link é por meio de métodos de extensão que executam operações para um tipo de endereço específico:
Método de extensão | Descrição |
---|---|
GetPathByAddress | Gera um URI com um caminho absoluto com base nos valores fornecidos. |
GetUriByAddress | Gera um URI absoluto com base nos valores fornecidos. |
Aviso
Preste atenção às seguintes implicações da chamada de métodos LinkGenerator:
Use métodos de extensão de GetUri*
com cuidado em uma configuração de aplicativo que não valide o cabeçalho Host
das solicitações de entrada. Se o cabeçalho Host
das solicitações de entrada não é validado, uma entrada de solicitação não confiável pode ser enviada novamente ao cliente em URIs em uma exibição ou página. Recomendamos que todos os aplicativos de produção configurem seu servidor para validar o cabeçalho Host
com os valores válidos conhecidos.
Use LinkGenerator com cuidado no middleware em combinação com Map
ou MapWhen
. Map*
altera o caminho base da solicitação em execução, o que afeta a saída da geração de link. Todas as APIs de LinkGenerator permitem a especificação de um caminho base. Especifique um caminho base vazio para desfazer o efeito de Map*
na geração de link.
No exemplo a seguir, um middleware usa a API de LinkGenerator para criar um link para um método de ação que lista os produtos da loja. O uso do gerador de link com sua injeção em uma classe e uma chamada a GenerateLink
está disponível para qualquer classe em um aplicativo:
public class ProductsLinkMiddleware
{
private readonly LinkGenerator _linkGenerator;
public ProductsLinkMiddleware(RequestDelegate next, LinkGenerator linkGenerator)
{
_linkGenerator = linkGenerator;
}
public async Task InvokeAsync(HttpContext httpContext)
{
var url = _linkGenerator.GetPathByAction("ListProducts", "Store");
httpContext.Response.ContentType = "text/plain";
await httpContext.Response.WriteAsync($"Go to {url} to see our products.");
}
}
Os tokens entre {}
definem os parâmetros de rota que serão associados, se a rota for correspondida. Mais de um parâmetro de rota pode ser definido em um segmento de rota, mas os parâmetros de rota precisam ser separados por um valor literal. Por exemplo, {controller=Home}{action=Index}
não é uma rota válida, já que não há nenhum valor literal entre {controller}
e {action}
. Os parâmetros de rota devem ter um nome e podem ter atributos adicionais especificados.
Um texto literal diferente dos parâmetros de rota (por exemplo, {id}
) e do separador de caminho /
precisa corresponder ao texto na URL. A correspondência de texto não diferencia maiúsculas de minúsculas e se baseia na representação decodificada do caminho de URLs. Para encontrar a correspondência de um delimitador de parâmetro de rota literal {
ou }
, faça o escape do delimitador repetindo o caractere. Por exemplo {{
ou }}
.
Asterisco *
ou asterisco duplo **
:
blog/{**slug}
: /blog
e tenha qualquer valor depois dele./blog
é atribuído ao valor de rota de campo de dados dinâmico.Aviso
Um parâmetro catch-all pode corresponder às rotas incorretamente devido a um bug no roteamento. Os aplicativos afetados por esse bug têm as seguintes características:
{**slug}"
Confira os bugs do GitHub 18677 e 16579, por exemplo, casos que atingiram esse bug.
Uma correção de aceitação para esse bug está contida no SDK do .NET Core 3.1.301 e posterior. O código a seguir define um comutador interno que corrige esse bug:
public static void Main(string[] args)
{
AppContext.SetSwitch("Microsoft.AspNetCore.Routing.UseCorrectCatchAllBehavior",
true);
CreateHostBuilder(args).Build().Run();
}
// Remaining code removed for brevity.
Os parâmetros catch-all também podem corresponder à cadeia de caracteres vazia.
O parâmetro catch-all faz o escape dos caracteres corretos quando a rota é usada para gerar uma URL, incluindo os caracteres separadores de caminho /
. Por exemplo, a rota foo/{*path}
com valores de rota { path = "my/path" }
gera foo/my%2Fpath
. Observe o escape da barra invertida. Para fazer a viagem de ida e volta dos caracteres separadores de caminho, use o prefixo do parâmetro da rota **
. A rota foo/{**path}
com { path = "my/path" }
gera foo/my/path
.
Padrões de URL que tentam capturar um nome de arquivo com uma extensão de arquivo opcional apresentam considerações adicionais. Por exemplo, considere o modelo files/{filename}.{ext?}
. Quando existem valores para filename
e ext
, ambos os valores são populados. Se apenas existir um valor para filename
na URL, a rota encontrará uma correspondência, pois o .
à direita é opcional. As URLs a seguir correspondem a essa rota:
/files/myFile.txt
/files/myFile
Os parâmetros de rota podem ter valores padrão, designados pela especificação do valor padrão após o nome do parâmetro separado por um sinal de igual (=
). Por exemplo, {controller=Home}
define Home
como o valor padrão de controller
. O valor padrão é usado se nenhum valor está presente na URL para o parâmetro. Os parâmetros de rota se tornam opcionais com o acréscimo de um ponto de interrogação (?
) ao final do nome do parâmetro. Por exemplo, id?
. A diferença entre valores opcionais e parâmetros de rota padrão é:
Os parâmetros de rota podem ter restrições que precisam corresponder ao valor de rota associado da URL. A adição de :
e do nome da restrição após o nome do parâmetro de rota especifica uma restrição embutida em um parâmetro de rota. Se a restrição exigir argumentos, eles ficarão entre parênteses (...)
após o nome da restrição. Várias restrições embutidas podem ser especificadas por meio do acréscimo de outros :
e do nome da restrição.
O nome da restrição e os argumentos são passados para o serviço IInlineConstraintResolver para criar uma instância de IRouteConstraint a ser usada no processamento de URL. Por exemplo, o modelo de rota blog/{article:minlength(10)}
especifica uma restrição minlength
com o argumento 10
. Para obter mais informações sobre as restrições de rota e uma lista das restrições fornecidas pela estrutura, confira a seção Referência de restrição de rota.
Os parâmetros de rota também podem ter transformadores de parâmetro. Os transformadores de parâmetro transformam o valor de um parâmetro ao gerar links e fazer a correspondência de ações e páginas com URLs. Assim como as restrições, os transformadores de parâmetro podem ser adicionados embutidos a um parâmetro de rota colocando :
e o nome do transformador após o nome do parâmetro de rota. Por exemplo, o modelo de rota blog/{article:slugify}
especifica um transformador slugify
. Para obter mais informações sobre transformadores de parâmetro, confira a seção Referência de transformador de parâmetro.
A tabela a seguir demonstra modelos de rota de exemplo e seu comportamento:
Modelo de rota | URI de correspondência de exemplo | O URI da solicitação |
---|---|---|
hello |
/hello |
Somente corresponde ao caminho único /hello . |
{Page=Home} |
/ |
Faz a correspondência e define Page como Home . |
{Page=Home} |
/Contact |
Faz a correspondência e define Page como Contact . |
{controller}/{action}/{id?} |
/Products/List |
É mapeado para o controlador Products e a ação List . |
{controller}/{action}/{id?} |
/Products/Details/123 |
É mapeado para o controlador Products e a ação Details , e id definido como 123. |
{controller=Home}/{action=Index}/{id?} |
/ |
É mapeado para o controlador Home e o método Index . id é ignorado. |
{controller=Home}/{action=Index}/{id?} |
/Products |
É mapeado para o controlador Products e o método Index . id é ignorado. |
Em geral, o uso de um modelo é a abordagem mais simples para o roteamento. Restrições e padrões também podem ser especificados fora do modelo de rota.
Os segmentos complexos são processados correspondendo delimitadores literais da direita para a esquerda sem greedy. Por exemplo, [Route("/a{b}c{d}")]
é um segmento complexo.
Os segmentos complexos funcionam de uma maneira específica que deve ser compreendida para usá-los com êxito. O exemplo nesta seção demonstra por que segmentos complexos só funcionam bem quando o texto delimitador não aparece dentro dos valores de parâmetro. Usar um regex e extrair manualmente os valores é necessário para casos mais complexos.
Aviso
Ao usar System.Text.RegularExpressions para processar entradas não confiáveis, passe um tempo limite. Um usuário mal-intencionado pode fornecer entrada para RegularExpressions
, causando um ataque de negação de serviço. APIs ASP.NET Core Framework que usam RegularExpressions
passam um tempo limite.
Este é um resumo das etapas que o roteamento executa com o modelo /a{b}c{d}
e o caminho de URL /abcd
. O |
é usado para ajudar a visualizar como o algoritmo funciona:
c
. Portanto, /abcd
é pesquisado pela direita e localiza /ab|c|d
.d
) agora corresponde ao parâmetro de rota {d}
.a
. Portanto, /ab|c|d
é pesquisado começando de onde paramos e, em seguida, a
é encontrado em /|a|b|c|d
.b
) agora corresponde ao parâmetro de rota {b}
.Este é um exemplo de um caso negativo usando o mesmo modelo /a{b}c{d}
e o caminho de URL /aabcd
. O |
é usado para ajudar a visualizar como o algoritmo funciona. Esse caso não é uma correspondência, que é explicada pelo mesmo algoritmo:
c
. Portanto, /aabcd
é pesquisado pela direita e localiza /aab|c|d
.d
) agora corresponde ao parâmetro de rota {d}
.a
. Portanto, /aab|c|d
é pesquisado começando de onde paramos e, em seguida, a
é encontrado em /a|a|b|c|d
.b
) agora corresponde ao parâmetro de rota {b}
.a
, mas o algoritmo ficou fora do modelo de rota para analisar. Portanto, não é uma correspondência.Como o algoritmo correspondente é sem greedy:
Expressões regulares fornecem muito mais controle sobre o comportamento correspondente.
A correspondência de greedy, também conhecida como correspondência lenta, corresponde à maior cadeia de caracteres possível. O valor sem greedy corresponde à menor cadeia de caracteres possível.
As restrições de rota são executadas quando ocorre uma correspondência com a URL de entrada e é criado um token do caminho da URL em valores de rota. Em geral, as restrições da rota inspecionam o valor de rota associado por meio do modelo de rota e tomam uma decisão do tipo "verdadeiro ou falso" sobre se o valor é aceitável. Algumas restrições da rota usam dados fora do valor de rota para considerar se a solicitação pode ser encaminhada. Por exemplo, a HttpMethodRouteConstraint pode aceitar ou rejeitar uma solicitação de acordo com o verbo HTTP. As restrições são usadas em solicitações de roteamento e na geração de link.
Aviso
Não use restrições para a validação de entrada. Se as restrições forem usadas para validação de entrada, a entrada inválida resultará em uma resposta 404
Não Encontrado. A entrada inválida deve produzir uma Solicitação Inválida 400
com uma mensagem de erro apropriada. As restrições de rota são usadas para desfazer a ambiguidade entre rotas semelhantes, não para validar as entradas de uma rota específica.
A tabela a seguir demonstra restrições de rota de exemplo e seu comportamento esperado:
restrição | Exemplo | Correspondências de exemplo | Observações |
---|---|---|---|
int |
{id:int} |
123456789 , -123456789 |
Corresponde a qualquer inteiro |
bool |
{active:bool} |
true , FALSE |
Corresponde a true ou false . Não diferencia maiúsculas de minúsculas |
datetime |
{dob:datetime} |
2016-12-31 , 2016-12-31 7:32pm |
Corresponde a um valor válido DateTime na cultura invariável. Confira o aviso anterior. |
decimal |
{price:decimal} |
49.99 , -1,000.01 |
Corresponde a um valor válido decimal na cultura invariável. Confira o aviso anterior. |
double |
{weight:double} |
1.234 , -1,001.01e8 |
Corresponde a um valor válido double na cultura invariável. Confira o aviso anterior. |
float |
{weight:float} |
1.234 , -1,001.01e8 |
Corresponde a um valor válido float na cultura invariável. Confira o aviso anterior. |
guid |
{id:guid} |
CD2C1638-1638-72D5-1638-DEADBEEF1638 |
Corresponde a um valor Guid válido |
long |
{ticks:long} |
123456789 , -123456789 |
Corresponde a um valor long válido |
minlength(value) |
{username:minlength(4)} |
Rick |
A cadeia de caracteres deve ter, no mínimo, 4 caracteres |
maxlength(value) |
{filename:maxlength(8)} |
MyFile |
A cadeia de caracteres não pode ser maior que 8 caracteres |
length(length) |
{filename:length(12)} |
somefile.txt |
A cadeia de caracteres deve ter exatamente 12 caracteres |
length(min,max) |
{filename:length(8,16)} |
somefile.txt |
A cadeia de caracteres deve ter, pelo menos, 8 e não mais de 16 caracteres |
min(value) |
{age:min(18)} |
19 |
O valor inteiro deve ser, pelo menos, 18 |
max(value) |
{age:max(120)} |
91 |
O valor inteiro não deve ser maior que 120 |
range(min,max) |
{age:range(18,120)} |
91 |
O valor inteiro deve ser, pelo menos, 18, mas não maior que 120 |
alpha |
{name:alpha} |
Rick |
A cadeia de caracteres deve consistir em um ou mais caracteres alfabéticos, a -z e não diferencia maiúsculas de minúsculas. |
regex(expression) |
{ssn:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)} |
123-45-6789 |
A cadeia de caracteres deve corresponder à expressão regular. Confira as dicas sobre como definir uma expressão regular. |
required |
{name:required} |
Rick |
Usado para impor que um valor não parâmetro está presente durante a geração de URL |
Aviso
Ao usar System.Text.RegularExpressions para processar entradas não confiáveis, passe um tempo limite. Um usuário mal-intencionado pode fornecer entrada para RegularExpressions
, causando um ataque de negação de serviço. APIs ASP.NET Core Framework que usam RegularExpressions
passam um tempo limite.
Várias restrições delimitadas por dois-pontos podem ser aplicadas a um único parâmetro. Por exemplo, a restrição a seguir restringe um parâmetro para um valor inteiro de 1 ou maior:
[Route("users/{id:int:min(1)}")]
public User GetUserById(int id) { }
Aviso
As restrições de rota que verificam a URL e são convertidas em um tipo CLR sempre usam a cultura invariável. Por exemplo, conversão para o tipo CLR int
ou DateTime
. Essas restrições consideram que a URL não é localizável. As restrições de rota fornecidas pela estrutura não modificam os valores armazenados nos valores de rota. Todos os valores de rota analisados com base na URL são armazenados como cadeias de caracteres. Por exemplo, a restrição float
tenta converter o valor de rota em um float, mas o valor convertido é usado somente para verificar se ele pode ser convertido em um float.
Aviso
Ao usar System.Text.RegularExpressions para processar entradas não confiáveis, passe um tempo limite. Um usuário mal-intencionado pode fornecer entrada para RegularExpressions
, causando um ataque de negação de serviço. APIs ASP.NET Core Framework que usam RegularExpressions
passam um tempo limite.
As expressões regulares podem ser especificadas como restrições embutidas usando a restrição de rota regex(...)
. Os métodos na família MapControllerRoute também aceitam um literal de objeto das restrições. Se esse formulário for usado, os valores de cadeia de caracteres serão interpretados como expressões regulares.
O código a seguir usa uma restrição regex embutida:
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("{message:regex(^\\d{{3}}-\\d{{2}}-\\d{{4}}$)}",
context =>
{
return context.Response.WriteAsync("inline-constraint match");
});
});
O código a seguir usa um literal de objeto para especificar uma restrição regex:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "people",
pattern: "People/{ssn}",
constraints: new { ssn = "^\\d{3}-\\d{2}-\\d{4}$", },
defaults: new { controller = "People", action = "List", });
});
A estrutura do ASP.NET Core adiciona RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.CultureInvariant
ao construtor de expressão regular. Confira RegexOptions para obter uma descrição desses membros.
As expressões regulares usam delimitadores e tokens semelhantes aos usados pelo roteamento e pela linguagem C#. Os tokens de expressão regular precisam ter escape. Para usar a expressão regular ^\d{3}-\d{2}-\d{4}$
em uma restrição embutida, use uma das seguintes opções:
\
fornecidos na cadeia de caracteres pelos caracteres \\
no arquivo de origem do C# para escapar do caractere de escape da cadeia de caracteres \
.Para fazer o escape dos caracteres de delimitador de parâmetro de roteamento {
, }
, [
, ]
, duplique os caracteres na expressão, por exemplo, {{
, }}
, [[
, ]]
. A tabela a seguir mostra uma expressão regular e a versão com escape:
Expressão regular | Expressão regular com escape |
---|---|
^\d{3}-\d{2}-\d{4}$ |
^\\d{{3}}-\\d{{2}}-\\d{{4}}$ |
^[a-z]{2}$ |
^[[a-z]]{{2}}$ |
As expressões regulares usadas no roteamento geralmente começam com o caractere ^
e correspondem à posição inicial da cadeia de caracteres. As expressões geralmente terminam com o caractere $
e correspondem ao final da cadeia de caracteres. Os caracteres ^
e $
garantem que a expressão regular corresponde a todo o valor do parâmetro de rota. Sem os caracteres ^
e $
, a expressão regular corresponde a qualquer subcadeia de caracteres na cadeia de caracteres, o que geralmente não é o desejado. A tabela a seguir fornece exemplos e explica por que eles encontram ou não uma correspondência:
Expression | String | Corresponder a | Comentar |
---|---|---|---|
[a-z]{2} |
hello | Sim | A subcadeia de caracteres corresponde |
[a-z]{2} |
123abc456 | Sim | A subcadeia de caracteres corresponde |
[a-z]{2} |
mz | Sim | Corresponde à expressão |
[a-z]{2} |
MZ | Sim | Não diferencia maiúsculas de minúsculas |
^[a-z]{2}$ |
hello | Não | Confira ^ e $ acima |
^[a-z]{2}$ |
123abc456 | Não | Confira ^ e $ acima |
Para saber mais sobre a sintaxe de expressões regulares, confira Expressões regulares do .NET Framework.
Para restringir um parâmetro a um conjunto conhecido de valores possíveis, use uma expressão regular. Por exemplo, {action:regex(^(list|get|create)$)}
apenas corresponde o valor da rota action
a list
, get
ou create
. Se passada para o dicionário de restrições, a cadeia de caracteres ^(list|get|create)$
é equivalente. As restrições passadas para o dicionário de restrições que não correspondem a uma das restrições conhecidas também são tratadas como expressões regulares. As restrições transmitidas em um modelo que não correspondem a uma das restrições conhecidas não são tratadas como expressões regulares.
É possível criar restrições de rota personalizadas com a implementação da interface do IRouteConstraint. A interface do IRouteConstraint
contém Match, que retorna true
quando a restrição é atendida. Caso contrário, retorna false
.
As restrições de rota personalizadas raramente são necessárias. Antes de implementar uma restrição de rota personalizada, considere alternativas, como a associação de modelo.
A pasta restrições do ASP.NET Core fornece bons exemplos de criação de restrições. Por exemplo, GuidRouteConstraint.
Para usar uma IRouteConstraint
personalizada, o tipo de restrição de rota deve ser registrado com o ConstraintMap do aplicativo, no contêiner de serviço. O ConstraintMap
é um dicionário que mapeia as chaves de restrição de rota para implementações de IRouteConstraint
que validam essas restrições. É possível atualizar o ConstraintMap
do aplicativo no Startup.ConfigureServices
como parte de uma chamada services.AddRouting ou configurando RouteOptions diretamente com services.Configure<RouteOptions>
. Por exemplo:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddRouting(options =>
{
options.ConstraintMap.Add("customName", typeof(MyCustomConstraint));
});
}
A restrição anterior é aplicada no seguinte código:
[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
// GET /api/test/3
[HttpGet("{id:customName}")]
public IActionResult Get(string id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
// GET /api/test/my/3
[HttpGet("my/{id:customName}")]
public IActionResult Get(int id)
{
return ControllerContext.MyDisplayRouteInfo(id);
}
}
MyDisplayRouteInfo é fornecido pelo pacote NuGet Rick.Docs.Samples.RouteInfo e exibe as informações de rota.
A implementação de MyCustomConstraint
impede que 0
seja aplicada a um parâmetro de rota:
class MyCustomConstraint : IRouteConstraint
{
private Regex _regex;
public MyCustomConstraint()
{
_regex = new Regex(@"^[1-9]*$",
RegexOptions.CultureInvariant | RegexOptions.IgnoreCase,
TimeSpan.FromMilliseconds(100));
}
public bool Match(HttpContext httpContext, IRouter route, string routeKey,
RouteValueDictionary values, RouteDirection routeDirection)
{
if (values.TryGetValue(routeKey, out object value))
{
var parameterValueString = Convert.ToString(value,
CultureInfo.InvariantCulture);
if (parameterValueString == null)
{
return false;
}
return _regex.IsMatch(parameterValueString);
}
return false;
}
}
Aviso
Ao usar System.Text.RegularExpressions para processar entradas não confiáveis, passe um tempo limite. Um usuário mal-intencionado pode fornecer entrada para RegularExpressions
, causando um ataque de negação de serviço. APIs ASP.NET Core Framework que usam RegularExpressions
passam um tempo limite.
O código anterior:
0
no segmento {id}
da rota.O código a seguir é uma abordagem melhor para impedir que um id
que contém um 0
seja processado:
[HttpGet("{id}")]
public IActionResult Get(string id)
{
if (id.Contains('0'))
{
return StatusCode(StatusCodes.Status406NotAcceptable);
}
return ControllerContext.MyDisplayRouteInfo(id);
}
O código anterior tem as seguintes vantagens em relação à abordagem MyCustomConstraint
:
0
.Transformadores de parâmetro:
Por exemplo, um transformador de parâmetro slugify
personalizado em padrão de rota blog\{article:slugify}
com Url.Action(new { article = "MyTestArticle" })
gera blog\my-test-article
.
Considere a seguinte implementação IOutboundParameterTransformer
:
public class SlugifyParameterTransformer : IOutboundParameterTransformer
{
public string TransformOutbound(object value)
{
if (value == null) { return null; }
return Regex.Replace(value.ToString(),
"([a-z])([A-Z])",
"$1-$2",
RegexOptions.CultureInvariant,
TimeSpan.FromMilliseconds(100)).ToLowerInvariant();
}
}
Para usar um transformador de parâmetro em um padrão de rota, configure-o usando ConstraintMap em Startup.ConfigureServices
:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddRouting(options =>
{
options.ConstraintMap["slugify"] = typeof(SlugifyParameterTransformer);
});
}
A estrutura do ASP.NET Core usa os transformadores de parâmetro para transformar o URI no qual um ponto de extremidade é resolvido. Por exemplo, os transformadores de parâmetro transformam os valores de rota usado para corresponder a um area
, controller
, action
e page
.
routes.MapControllerRoute(
name: "default",
template: "{controller:slugify=Home}/{action:slugify=Index}/{id?}");
Com o modelo de rota anterior, a ação SubscriptionManagementController.GetAll
é combinada com o URI /subscription-management/get-all
. Um transformador de parâmetro não altera os valores de rota usados para gerar um link. Por exemplo, Url.Action("GetAll", "SubscriptionManagement")
gera /subscription-management/get-all
.
ASP.NET Core fornece convenções de API para usar transformadores de parâmetro com as rotas geradas:
Esta seção contém uma referência para o algoritmo implementado pela geração de URL. Na prática, os exemplos mais complexos de geração de URL usam controladores ou Razor Pages. Confira o roteamento em controladores para obter informações adicionais.
O processo de geração de URL começa com uma chamada para LinkGenerator.GetPathByAddress ou um método semelhante. O método é fornecido com um endereço, um conjunto de valores de rota e, opcionalmente, informações sobre a solicitação atual de HttpContext
.
A primeira etapa é usar o endereço para resolve um conjunto de pontos de extremidade candidatos usando um IEndpointAddressScheme<TAddress>
que corresponda ao tipo do endereço.
Depois que o conjunto de candidatos é encontrado pelo esquema de endereços, os pontos de extremidade são ordenados e processados iterativamente até que uma operação de geração de URL seja bem-sucedida. A geração de URL não verifica se há ambiguidades. O primeiro resultado retornado é o resultado final.
A primeira etapa na solução de problemas de geração de URL é definir o nível de log de Microsoft.AspNetCore.Routing
como TRACE
. LinkGenerator
registra muitos detalhes sobre o processamento, o que pode ser útil para solucionar problemas.
Confira Referência de geração de URL para obter detalhes sobre a geração de URL.
Os endereços são o conceito na geração de URL usado para vincular uma chamada ao gerador de links para um conjunto de pontos de extremidade candidatos.
Os endereços são um conceito extensível que vem com duas implementações por padrão:
string
) como o endereço: IUrlHelper
, Auxiliares de Marca, Auxiliares HTML, Resultados da Ação etc.A função do esquema de endereços é fazer a associação entre o endereço e os pontos de extremidade correspondentes por critérios arbitrários:
Na solicitação atual, o roteamento acessa os valores de rota da solicitação atual HttpContext.Request.RouteValues
. Os valores associados à solicitação atual são chamados de valores ambientes. Para maior clareza, a documentação se refere aos valores de rota transmitidos para os métodos como valores explícitos.
O exemplo a seguir mostra valores ambientes e valores explícitos. Fornece os valores ambientes da solicitação atual e os valores explícitos: { id = 17, }
:
public class WidgetController : Controller
{
private readonly LinkGenerator _linkGenerator;
public WidgetController(LinkGenerator linkGenerator)
{
_linkGenerator = linkGenerator;
}
public IActionResult Index()
{
var url = _linkGenerator.GetPathByAction(HttpContext,
null, null,
new { id = 17, });
return Content(url);
}
O código anterior:
/Widget/Index/17
O código a seguir não fornece valores ambientes e valores explícitos: { controller = "Home", action = "Subscribe", id = 17, }
:
public IActionResult Index2()
{
var url = _linkGenerator.GetPathByAction("Subscribe", "Home",
new { id = 17, });
return Content(url);
}
O método anterior retorna /Home/Subscribe/17
O código a seguir no WidgetController
retorna /Widget/Subscribe/17
:
var url = _linkGenerator.GetPathByAction("Subscribe", null,
new { id = 17, });
O código a seguir fornece o controlador dos valores ambientes na solicitação atual e dos valores explícitos: { action = "Edit", id = 17, }
:
public class GadgetController : Controller
{
public IActionResult Index()
{
var url = Url.Action("Edit", new { id = 17, });
return Content(url);
}
No código anterior:
/Gadget/Edit/17
é retornado.action
e os valores route
especificados.O código a seguir fornece os valores ambientes da solicitação atual e os valores explícitos: { page = "./Edit, id = 17, }
:
public class IndexModel : PageModel
{
public void OnGet()
{
var url = Url.Page("./Edit", new { id = 17, });
ViewData["URL"] = url;
}
}
O código anterior define url
como /Edit/17
, quando a página Editar Razor contém a seguinte diretiva de página:
@page "{id:int}"
Se a página Editar não contiver o modelo de rota "{id:int}"
, url
será /Edit?id=17
.
O comportamento do MVC IUrlHelper adiciona uma camada de complexidade, além das regras descritas aqui:
IUrlHelper
sempre fornece os valores de rota da solicitação atual como valores ambientes.action
e controller
como valores explícitos, a menos que sejam substituídos pelo desenvolvedor.page
como valor explícito, a menos que seja substituído. IUrlHelper.Page
sempre substitui o valor de rota atual handler
por null
como um valor explícito, a menos que seja substituído.Os usuários geralmente ficam surpresos com os detalhes comportamentais dos valores ambientes, pois o MVC não parece seguir suas próprias regras. Por motivos de histórico e compatibilidade, determinados valores de rota, como action
, controller
, page
e handler
, têm seu próprio comportamento de caso especial.
A funcionalidade equivalente fornecida por LinkGenerator.GetPathByAction
e LinkGenerator.GetPathByPage
duplica essas anomalias de IUrlHelper
para compatibilidade.
Depois que o conjunto de pontos de extremidade candidatos for encontrado, o algoritmo de geração de URL:
A primeira etapa nesse processo é chamada de invalidação de valor de rota. A invalidação de valor de rota é o processo pelo qual o roteamento decide quais valores de rota dos valores ambientes devem ser usados e quais devem ser ignorados. Cada valor ambiente é considerado e combinado com os valores explícitos ou ignorado.
A melhor maneira de pensar sobre a função dos valores de ambiente é que eles tentam salvar a digitação dos desenvolvedores de aplicativos, em alguns casos comuns. Tradicionalmente, os cenários em que os valores ambientes são úteis estão relacionados ao MVC:
As chamadas para LinkGenerator
ou IUrlHelper
que retornam null
geralmente são causadas por não entender a invalidação de valor de rota. Solucione problemas de invalidação de valor de rota especificando explicitamente mais valores de rota para ver se isso resolve o problema.
A invalidação de valor de rota funciona supondo que o esquema de URL do aplicativo é hierárquico, com uma hierarquia formada da esquerda para a direita. Considere o modelo de rota de controlador básico {controller}/{action}/{id?}
para ter uma noção intuitiva de como isso funciona na prática. Uma alteração em um valor invalida todos os valores de rota que são exibidos à direita. Isso reflete a suposição sobre a hierarquia. Se o aplicativo tiver um valor ambiente para id
e a operação especificar um valor diferente para o controller
:
id
não será reutilizado porque {controller}
está à esquerda de {id?}
.Alguns exemplos que demonstram esse princípio:
id
, o valor ambiente para id
será ignorado. Os valores ambientes para controller
e action
podem ser usados.action
, qualquer valor ambiente para action
será ignorado. Os valores ambientes para controller
podem ser usados. Se o valor explícito para action
for diferente do valor ambiente para action
, o valor id
não será usado. Se o valor explícito para action
for igual ao valor ambiente para action
, o valor id
poderá ser usado.controller
, qualquer valor ambiente para controller
será ignorado. Se o valor explícito para controller
for diferente do valor ambiente para controller
, os valores action
e id
não serão usados. Se o valor explícito para controller
for igual ao valor ambiente para controller
, os valores action
e id
poderão ser usados.Esse processo é ainda mais complicado devido à existência de rotas de atributo e rotas convencionais dedicadas. As rotas convencionais do controlador, como {controller}/{action}/{id?}
, especificam uma hierarquia usando parâmetros de rota. Para rotas convencionais dedicadas e rotas de atributo para os controladores e Razor Pages:
Para esses casos, a geração de URL define o conceito de valores necessários. Os pontos de extremidade criados por controladores e Razor Pages têm os valores necessários especificados que permitem que a invalidação do valor de rota funcione.
O algoritmo de invalidação de valor de rota em detalhes:
Neste ponto, a operação de geração de URL está pronta para avaliar as restrições de rota. O conjunto de valores aceitos é combinado com os valores padrão do parâmetro, que são fornecidos às restrições. Se todas as restrições forem aprovadas, a operação continuará.
Em seguida, os valores aceitos podem ser usados para expandir o modelo de rota. O modelo de rota é processado:
Valores fornecidos explicitamente, que não correspondem a um segmento da rota, são adicionados à cadeia de consulta. A tabela a seguir mostra o resultado do uso do modelo de rota {controller}/{action}/{id?}
.
Valores de ambiente | Valores explícitos | Resultado |
---|---|---|
controlador = "Home" | ação = "About" | /Home/About |
controlador = "Home" | controlador = "Order", ação = "About" | /Order/About |
controlador = "Home", cor = "Vermelho" | ação = "About" | /Home/About |
controlador = "Home" | ação = "About", cor = "Red" | /Home/About?color=Red |
A partir do ASP.NET Core 3.0, alguns esquemas de geração de URL usados nas versões anteriores do ASP.NET Core não funcionam bem com a geração de URL. A equipe do ASP.NET Core planeja adicionar recursos para atender a essas necessidades em uma versão futura. Por enquanto, a melhor solução é usar o roteamento herdado.
O código a seguir mostra um exemplo de um esquema de geração de URL que não é compatível com o roteamento.
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute("default",
"{culture}/{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllerRoute("blog", "{culture}/{**slug}",
new { controller = "Blog", action = "ReadPost", });
});
No código anterior, o parâmetro de rota culture
é usado para localização. O ideal é que o parâmetro culture
sempre seja aceito como valor ambiente. No entanto, o parâmetro culture
não é aceito como valor ambiente devido à maneira como os valores necessários funcionam:
"default"
, o parâmetro de rota culture
fica à esquerda de controller
. Portanto, as alterações em controller
não invalidarão culture
."blog"
, considera-se que o parâmetro de rota culture
fica à direita de controller
, que aparece nos valores necessários.Os links a seguir fornecem informações sobre como configurar metadados de ponto de extremidade:
[MinimumAgeAuthorize]
RequireHost aplica uma restrição à rota que exige o host especificado. O parâmetro RequireHost
ou [Host] pode ser:
www.domain.com
, corresponde www.domain.com
a qualquer porta.*.domain.com
, corresponde www.domain.com
, subdomain.domain.com
ou www.subdomain.domain.com
a qualquer porta.*:5000
, corresponde a porta 5000 a qualquer host.www.domain.com:5000
ou *.domain.com:5000
, corresponde ao host e à porta.Vários parâmetros podem ser especificados usando RequireHost
ou [Host]
. A restrição corresponde aos hosts válidos para qualquer um dos parâmetros. Por exemplo, [Host("domain.com", "*.domain.com")]
corresponde a domain.com
, www.domain.com
ou subdomain.domain.com
.
O código a seguir usa RequireHost
para exigir o host especificado na rota:
public void Configure(IApplicationBuilder app)
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", context => context.Response.WriteAsync("Hi Contoso!"))
.RequireHost("contoso.com");
endpoints.MapGet("/", context => context.Response.WriteAsync("AdventureWorks!"))
.RequireHost("adventure-works.com");
endpoints.MapHealthChecks("/healthz").RequireHost("*:8080");
});
}
O código a seguir usa o atributo [Host]
no controlador para exigir qualquer um dos hosts especificados:
[Host("contoso.com", "adventure-works.com")]
public class ProductController : Controller
{
public IActionResult Index()
{
return ControllerContext.MyDisplayRouteInfo();
}
[Host("example.com:8080")]
public IActionResult Privacy()
{
return ControllerContext.MyDisplayRouteInfo();
}
}
Quando o atributo [Host]
é aplicado ao controlador e ao método de ação:
A maior parte do roteamento foi atualizada no ASP.NET Core 3.0 para aumentar o desempenho.
Quando um aplicativo tem problemas de desempenho, geralmente suspeita-se que o roteamento é o problema. O motivo pelo qual o roteamento é suspeito é que as estruturas como controladores e Razor Pages relatam o tempo gasto dentro da estrutura nas mensagens de log. Quando há uma diferença significativa entre o tempo relatado pelos controladores e o tempo total da solicitação:
O desempenho do roteamento é testado usando milhares de pontos de extremidade. É improvável que um aplicativo típico encontre um problema de desempenho apenas por ser muito grande. A causa raiz mais comum do desempenho lento do roteamento geralmente é um middleware personalizado com comportamento inválido.
O exemplo de código a seguir demonstra uma técnica básica para restringir a fonte de atraso:
public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
app.Use(next => async context =>
{
var sw = Stopwatch.StartNew();
await next(context);
sw.Stop();
logger.LogInformation("Time 1: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds);
});
app.UseRouting();
app.Use(next => async context =>
{
var sw = Stopwatch.StartNew();
await next(context);
sw.Stop();
logger.LogInformation("Time 2: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds);
});
app.UseAuthorization();
app.Use(next => async context =>
{
var sw = Stopwatch.StartNew();
await next(context);
sw.Stop();
logger.LogInformation("Time 3: {ElapsedMilliseconds}ms", sw.ElapsedMilliseconds);
});
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Timing test.");
});
});
}
Para roteamento de tempo:
Essa é uma maneira básica de restringir o atraso quando ele é significativo, por exemplo, mais de 10ms
. Subtrair Time 2
de Time 1
relata o tempo gasto dentro do middleware UseRouting
.
O código a seguir usa uma abordagem mais compacta para o código de tempo anterior:
public sealed class MyStopwatch : IDisposable
{
ILogger<Startup> _logger;
string _message;
Stopwatch _sw;
public MyStopwatch(ILogger<Startup> logger, string message)
{
_logger = logger;
_message = message;
_sw = Stopwatch.StartNew();
}
private bool disposed = false;
public void Dispose()
{
if (!disposed)
{
_logger.LogInformation("{Message }: {ElapsedMilliseconds}ms",
_message, _sw.ElapsedMilliseconds);
disposed = true;
}
}
}
public void Configure(IApplicationBuilder app, ILogger<Startup> logger)
{
int count = 0;
app.Use(next => async context =>
{
using (new MyStopwatch(logger, $"Time {++count}"))
{
await next(context);
}
});
app.UseRouting();
app.Use(next => async context =>
{
using (new MyStopwatch(logger, $"Time {++count}"))
{
await next(context);
}
});
app.UseAuthorization();
app.Use(next => async context =>
{
using (new MyStopwatch(logger, $"Time {++count}"))
{
await next(context);
}
});
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Timing test.");
});
});
}
A lista a seguir fornece alguns insights sobre recursos de roteamento relativamente caros, em comparação a modelos de rota básicos:
{x}-{y}-{z}
): Esta seção contém diretrizes para criadores de bibliotecas com base no roteamento. Esses detalhes destinam-se a garantir que os desenvolvedores de aplicativos tenham uma boa experiência usando bibliotecas e estruturas que estendem o roteamento.
Para criar uma estrutura que usa o roteamento para correspondência de URL, comece definindo uma experiência do usuário baseada em UseEndpoints.
CRIE com base em IEndpointRouteBuilder. Isso permite que os usuários componham a estrutura com outros recursos do ASP.NET Core, sem confusão. Cada modelo do ASP.NET Core inclui o roteamento. Suponha que o roteamento esteja presente e seja conhecido para os usuários.
app.UseEndpoints(endpoints =>
{
// Your framework
endpoints.MapMyFramework(...);
endpoints.MapHealthChecks("/healthz");
});
RETORNE um tipo de concreto selado de uma chamada para MapMyFramework(...)
que implemente IEndpointConventionBuilder. A maioria dos métodos Map...
da estrutura segue esse padrão. A interface IEndpointConventionBuilder
:
Declarar seu próprio tipo permite que você adicione sua própria funcionalidade específica da estrutura ao construtor. Não há problema em encapsular um construtor declarado por estrutura e encaminhar chamadas para ele.
app.UseEndpoints(endpoints =>
{
// Your framework
endpoints.MapMyFramework(...).RequireAuthorization()
.WithMyFrameworkFeature(awesome: true);
endpoints.MapHealthChecks("/healthz");
});
GRAVE seu próprio EndpointDataSource. EndpointDataSource
é o primitivo de baixo nível para declarar e atualizar uma coleção de pontos de extremidade. EndpointDataSource
é uma API eficiente usada por controladores e Razor Pages.
Os testes de roteamento têm um exemplo básico de uma fonte de dados que não está atualizando.
NÃO tente registrar um EndpointDataSource
por padrão. Exija que os usuários registrem a estrutura no UseEndpoints. A filosofia do roteamento determina que nada está incluído por padrão e esse UseEndpoints
é o local para registrar pontos de extremidade.
DEFINA os tipos de metadados como uma interface.
POSSIBILITE o uso de tipos de metadados como um atributo em classes e métodos.
public interface ICoolMetadata
{
bool IsCool { get; }
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
public bool IsCool => true;
}
As estruturas como controladores e Razor Pages dão suporte à aplicação de atributos de metadados a tipos e métodos. Se você declarar os tipos de metadados:
Declarar um tipo de metadados como uma interface adiciona outra camada de flexibilidade:
POSSIBILITE a substituição de metadados, conforme mostrado no exemplo a seguir:
public interface ICoolMetadata
{
bool IsCool { get; }
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CoolMetadataAttribute : Attribute, ICoolMetadata
{
public bool IsCool => true;
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class SuppressCoolMetadataAttribute : Attribute, ICoolMetadata
{
public bool IsCool => false;
}
[CoolMetadata]
public class MyController : Controller
{
public void MyCool() { }
[SuppressCoolMetadata]
public void Uncool() { }
}
A melhor maneira de seguir estas diretrizes é evitar definir metadados de marcador:
A coleção de metadados é ordenada e permite substituir por prioridade. No caso de controladores, os metadados no método de ação são mais específicos.
TORNE o middleware útil com e sem roteamento.
app.UseRouting();
app.UseAuthorization(new AuthorizationPolicy() { ... });
app.UseEndpoints(endpoints =>
{
// Your framework
endpoints.MapMyFramework(...).RequireAuthorization();
});
Como exemplo dessa diretriz, considere o middleware UseAuthorization
. O middleware de autorização permite que você transmita uma política de fallback. A política de fallback, se especificada, aplica-se a:
Isso torna o middleware de autorização útil fora do contexto de roteamento. O middleware de autorização pode ser usado para programação de middleware tradicional.
Para obter a saída de diagnóstico de roteamento detalhada, defina Logging:LogLevel:Microsoft
como Debug
. No ambiente de desenvolvimento, defina o nível de log em appsettings.Development.json
:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Debug",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
Comentários do ASP.NET Core
O ASP.NET Core é um projeto código aberto. Selecione um link para fornecer comentários:
Treinamento
Módulo
Usar páginas, roteamento e layouts para melhorar a navegação no Blazor - Training
Saiba como otimizar a navegação no aplicativo, usar parâmetros da URL e criar layouts reutilizáveis em um aplicativo Web do Blazor.