Roteamento no ASP.NET Core

De Ryan Nowak, Kirk Larkin e Rick Anderson

Observação

Esta não é a versão mais recente deste artigo. Para informações sobre a versão vigente, confira a Versão do .NET 8 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 informações sobre a versão vigente, confira a Versão do .NET 8 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:

Conceitos básicos sobre 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:

  • Quando uma solicitação HTTP GET é enviada para a URL raiz /:
    • O delegado de solicitação é executado.
    • Hello World! é gravado na resposta HTTP.
  • Se o método de solicitação não for 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:

  • A chamada para app.Use registra um middleware personalizado, que é executado no início do pipeline.
  • A chamada para UseRouting configura o middleware de correspondência de rotas a ser executado após o middleware personalizado.
  • O ponto de extremidade registrado com 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.

Pontos de extremidade

O método MapGet é usado para definir um ponto de extremidade. Um ponto de extremidade pode ser:

  • Selecionado, correspondendo a URL e o método HTTP.
  • Executado, processando o delegado.

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:

  • Uma URL como /hello/Docs
  • Qualquer caminho de URL que comece com /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}:

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:

  • O middleware de autorização pode ser usado com o roteamento.
  • Os pontos de extremidade podem ser usados para configurar o comportamento de autorização.

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:

  • Ver qual ponto de extremidade foi selecionado por UseRouting.
  • Aplicar uma política de autorização antes que UseEndpoints seja expedido para o ponto de extremidade.

Metadados de ponto de extremidade

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:

  • Os metadados podem ser processados pelo middleware com reconhecimento de roteamento.
  • Os metadados podem ser de qualquer tipo de .NET.

Conceitos de roteamento

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.

Definição de ponto de extremidade 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:

  • O ponto de extremidade é sempre nulo antes que UseRouting seja chamado.
  • Se uma correspondência for encontrada, o ponto de extremidade não será nulo entre UseRouting e UseEndpoints.
  • O middleware UseEndpoints é terminal quando uma correspondência é encontrada. O middleware de terminal será definido posteriormente neste artigo.
  • O middleware após 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:

  • O middleware pode ser executado antes do UseRouting para modificar os dados nos quais o roteamento opera.
  • O middleware pode ser executado entre UseRouting e UseEndpoints para processar os resultados do roteamento, antes que o ponto de extremidade seja executado.
    • Middleware que é executado entre UseRouting e UseEndpoints:
      • Geralmente inspeciona os metadados para entender os pontos de extremidade.
      • Geralmente toma as decisões de segurança, conforme feito por UseAuthorization e UseCors.
    • A combinação de middleware e metadados permite configurar políticas por ponto de extremidade.

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:

  • Registra um arquivo ou banco de dados.
  • Inclui detalhes como o usuário, endereço IP, nome do ponto de extremidade confidencial e muito mais.

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:

  • Os metadados são anexados a uma API do construtor.
  • As estruturas baseadas em classe incluem todos os atributos no método e na classe correspondentes ao criar os pontos de extremidade.

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.

Comparar o middleware de terminal com o roteamento

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:

  • A operação correspondente no exemplo anterior é Path == "/" para o middleware e Path == "/Routing" para o roteamento.
  • Quando uma correspondência é bem-sucedida, ela executa algumas funcionalidades e retorna, em vez de invocar o middleware 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:

  • Ambas as abordagens permitem terminar o pipeline de processamento:
    • O middleware termina o pipeline retornando, em vez de invocar next.
    • Os pontos de extremidade são sempre terminais.
  • O middleware de terminal permite posicionar o middleware em um local arbitrário no pipeline:
    • Os pontos de extremidade são executados na posição do UseEndpoints.
  • O middleware de terminal permite que o código arbitrário determine quando o middleware corresponde ao seguinte:
    • O código de correspondência de rotas personalizado pode ser detalhado e difícil de ser gravado corretamente.
    • O roteamento fornece soluções simples para aplicativos típicos. A maioria dos aplicativos não exige o código de correspondência de rotas personalizado.
  • Os pontos de extremidade fazem interface com o middleware, como UseAuthorization e UseCors.
    • Usar um middleware de terminal com UseAuthorization ou UseCors exige uma interface manual com o sistema de autorização.

Um ponto de extremidade define ambos:

  • Um delegado para processar as solicitações.
  • Uma coleção de metadados arbitrários. Os metadados são usados para implementar interesses paralelos com base em políticas e na configuração anexada a cada ponto de extremidade.

O middleware de terminal pode ser uma ferramenta eficaz, mas pode exigir:

  • Um valor significativo de codificação e teste.
  • Integração manual com outros sistemas para atingir o nível desejado de flexibilidade.

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:

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.

Correspondência de URL

  • É o processo pelo qual o roteamento corresponde uma solicitação de entrada com um ponto de extremidade.
  • É baseado em dados nos cabeçalhos e caminho de URL.
  • Pode ser estendido para considerar quaisquer dados na solicitaçã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:

  • Chamar HttpContext.GetEndpoint obtém o ponto de extremidade.
  • 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:

  • Qualquer decisão que possa afetar a expedição ou a aplicação de políticas de segurança seja tomada dentro do sistema de roteamento.

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:

  • Migrar RouteData.Values para HttpRequest.RouteValues.
  • Migrar 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:

  1. Processa o caminho de URL em relação ao conjunto de pontos de extremidade e os modelos de rota, coletando todas as correspondências.
  2. Usa a lista anterior e remove as correspondências que falham com restrições de rota aplicadas.
  3. Usa a lista anterior e remove as correspondências que falham no conjunto de instâncias MatcherPolicy.
  4. Usa o EndpointSelector para tomar uma decisão final na lista anterior.

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

  • Ambos correspondem ao caminho de URL /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}:

  • A restrição alpha corresponde apenas a caracteres alfabéticos.
  • A restrição int corresponde apenas a números.
  • Esses modelos têm a mesma precedência de rota, mas não há uma única URL que corresponda a ambos.
  • Se o sistema de roteamento relatasse um erro de ambiguidade na inicialização, ele bloquearia esse caso de uso válido.

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:

  • Não tem o conceito de rotas.
  • Não fornece garantias de ordenação. Todos os pontos de extremidade são processados de uma só vez.

Precedência de modelo de rota e ordem de seleção de ponto de extremidade

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:

  • Evita a necessidade de ajustar a ordem dos pontos de extremidade em casos comuns.
  • Tenta corresponder às expectativas de bom senso do comportamento de roteamento.

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:

  • Os modelos com mais segmentos são considerados mais específicos.
  • Um segmento com texto literal é considerado mais específico do que um segmento de parâmetro.
  • Um segmento de parâmetro com uma restrição é considerado mais específico do que um sem restrição.
  • Um segmento complexo é considerado tão específico quanto um segmento de parâmetro com uma restrição.
  • Os parâmetros catch-all são os menos específicos. Confira catch-all na seção Modelos de rota para obter informações importantes sobre rotas catch-all.

Conceitos de geração de URL

Geração de URL:

  • É o processo pelo qual o roteamento pode criar um caminho de URL de acordo com um conjunto de valores de rota.
  • Permite uma separação lógica entre os pontos de extremidade e as URLs que os acessam.

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:

  1. Um endereço é associado a uma lista de pontos de extremidade que correspondem ao endereço.
  2. O RoutePattern de cada ponto de extremidade é avaliado até que seja encontrado um padrão de rota correspondente aos valores fornecidos. A saída resultante é combinada com as outras partes de URI fornecidas ao gerador de link e é retornada.

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.

Exemplo de middleware

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

Modelos de rota

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 **:

  • Pode ser usado como prefixo para um parâmetro de rota a ser associado ao restante do URI.
  • São chamados de parâmetros catch-all. Por exemplo, blog/{**slug}:
    • Corresponde a qualquer URI que comece com blog/ e tenha qualquer valor depois dele.
    • O valor a seguir 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:

  • Uma rota catch-all, por exemplo, {**slug}"
  • A rota catch-all não corresponde às solicitações que deveria corresponder.
  • Remover outras rotas faz com que a rota catch-all comece a funcionar.

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 é:

  • Um parâmetro de rota com um valor padrão sempre produz um valor.
  • Um parâmetro opcional só tem um valor quando um valor é fornecido pela URL de solicitaçã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.

Segmentos complexos

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:

  • O primeiro literal, da direita para a esquerda, é c. Portanto, /abcd é pesquisado pela direita e localiza /ab|c|d.
  • Tudo à direita (d) agora corresponde ao parâmetro de rota {d}.
  • O próximo literal, da direita para a esquerda, é a. Portanto, /ab|c|d é pesquisado começando de onde paramos e, em seguida, a é encontrado em /|a|b|c|d.
  • O valor à direita (b) agora corresponde ao parâmetro de rota {b}.
  • Não há texto restante nem modelo de rota restante. Portanto, esta é uma correspondência.

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:

  • O primeiro literal, da direita para a esquerda, é c. Portanto, /aabcd é pesquisado pela direita e localiza /aab|c|d.
  • Tudo à direita (d) agora corresponde ao parâmetro de rota {d}.
  • O próximo literal, da direita para a esquerda, é a. Portanto, /aab|c|d é pesquisado começando de onde paramos e, em seguida, a é encontrado em /a|a|b|c|d.
  • O valor à direita (b) agora corresponde ao parâmetro de rota {b}.
  • Neste ponto, há texto restante a, mas o algoritmo ficou fora do modelo de rota para analisar. Portanto, não é uma correspondência.

Como o algoritmo correspondente é sem greedy:

  • Ele corresponde à menor quantidade de texto possível em cada etapa.
  • Qualquer caso em que o valor delimitador apareça dentro dos valores de parâmetro resulta em não correspondência.

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.

Roteamento com caracteres especiais

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;

Restrições de rota

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.

Expressões regulares em restrições

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:

  • Substitua os caracteres \ fornecidos na cadeia de caracteres pelos caracteres \\ no arquivo de origem do C# para escapar do caractere de escape da cadeia de caracteres \.
  • Literais de cadeia de caracteres textuais.

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.

Restrições de rota personalizadas

É 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:

  • Impede 0 no segmento {id} da rota.
  • É mostrado para fornecer um exemplo básico de implementação de uma restrição personalizada. Não deve ser usado em um aplicativo de produção.

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:

  • Não requer uma restrição personalizada.
  • Retorna um erro mais descritivo quando o parâmetro de rota inclui 0.

Transformadores de parâmetro

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:

Referência de geração de URL

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.

Solução de problemas de geração de URL com log

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.

Endereços

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:

  • Usando o nome do ponto de extremidade (string) como o endereço:
    • Fornece funcionalidade semelhante ao nome da rota do MVC.
    • Usa o tipo de metadados IEndpointNameMetadata.
    • Resolve a cadeia de caracteres fornecida em relação aos metadados de todos os pontos de extremidade registrados.
    • Gera uma exceção na inicialização, se vários pontos de extremidade usarem o mesmo nome.
    • Recomendado para uso geral fora dos controladores e Razor Pages.
  • Usando os valores de rota (RouteValuesAddress) como o endereço:
    • Fornece uma funcionalidade semelhante à geração de URL herdada dos controladores e Razor Pages.
    • Muito difícil de estender e depurar.
    • Fornece a implementação usada por 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:

  • O esquema de nome do ponto de extremidade executa uma pesquisa de dicionário básica.
  • O esquema de valores de rota tem um subconjunto de conjunto mais complexo.

Valores ambientes e valores explícitos

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:

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.
  • Url obtém o IUrlHelper.
  • Action gera uma URL com um caminho absoluto para um método de ação. A URL contém o nome 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.
  • IUrlHelper.Action sempre copia os valores de rota atuais action e controller como valores explícitos, a menos que sejam substituídos pelo desenvolvedor.
  • IUrlHelper.Page sempre copia o valor de rota atual 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.

Processo de geração de URL

Depois que o conjunto de pontos de extremidade candidatos for encontrado, o algoritmo de geração de URL:

  • Processa os pontos de extremidade iterativamente.
  • Retorna o primeiro resultado bem-sucedido.

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:

  • Ao vincular-se a outra ação no mesmo controlador, o nome do controlador não precisa ser especificado.
  • Ao vincular-se a outro controlador na mesma área, o nome da área não precisa ser especificado.
  • Ao vincular-se ao mesmo método de ação, os valores de rota não precisam ser especificados.
  • Ao vincular-se a outra parte do aplicativo, não convém carregar valores de rota que não têm significado nessa parte do aplicativo.

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:

  • Se os valores explícitos contiverem um valor para id, o valor ambiente para id será ignorado. Os valores ambientes para controller e action podem ser usados.
  • Se os valores explícitos contiverem um valor para 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.
  • Se os valores explícitos contiverem um valor para 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:

  • Existe uma hierarquia de valores de rota.
  • Eles não são exibidos no modelo.

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:

  • Os nomes de valor necessários são combinados com os parâmetros de rota e processados da esquerda para a direita.
  • Para cada parâmetro, o valor ambiente e o valor explícito são comparados:
    • Se o valor ambiente e o valor explícito forem iguais, o processo continuará.
    • Se o valor ambiente estiver presente e o valor explícito não, o valor ambiente será usado ao gerar a URL.
    • Se o valor ambiente não estiver presente e o valor explícito sim, rejeite o valor ambiente e todos os valores ambientes subsequentes.
    • Se o valor ambiente e o valor explícito estiverem presentes e os dois valores forem diferentes, rejeite o valor ambiente e todos os valores ambientes subsequentes.

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:

  • Da esquerda para a direita.
  • O valor aceito de cada parâmetro é substituído.
  • Com os seguintes casos especiais:
    • Se faltar um valor nos valores aceitos e o parâmetro tiver um valor padrão, o valor padrão será usado.
    • Se faltar um valor nos valores aceitos e o parâmetro for opcional, o processamento continuará.
    • Se qualquer parâmetro de rota à direita de um parâmetro opcional ausente tiver um valor, a operação falhará.
    • Os parâmetros com valor padrão contíguos e parâmetros opcionais são recolhidos sempre que possível.

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

Ordem de parâmetro de rota opcional

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

Problemas com a invalidação de valor de rota

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:

  • No modelo de rota "default", o parâmetro de rota culture fica à esquerda de controller. Portanto, as alterações em controller não invalidarão culture.
  • No modelo de rota "blog", considera-se que o parâmetro de rota culture fica à direita de controller, que aparece nos valores necessários.

Analisar caminhos de URL com LinkParser

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.

Configurar metadados de ponto de extremidade

Os links a seguir fornecem informações sobre como configurar metadados de ponto de extremidade:

Correspondência de host em rotas com RequireHost

RequireHost aplica uma restrição à rota que exige o host especificado. O parâmetro RequireHostou [Host] pode ser um:

  • Host: www.domain.com, corresponde www.domain.com a qualquer porta.
  • Host com curinga: *.domain.com, corresponde www.domain.com, subdomain.domain.com ou www.subdomain.domain.com a qualquer porta.
  • Porta: *:5000, corresponde a porta 5000 a qualquer host.
  • Host e porta: 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 atributo na ação será usado.
  • O atributo do controlador será ignorado.

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:

Grupos de rotas

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.

O QueryPrivateTodosalocador de filtros de ponto de extremidade é uma função local que modifica os parâmetros TodoDb do manipulador de rotas para permitir o acesso e o armazenamento de dados todo privados.

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

Diretrizes de desempenho para roteamento

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:

  • Os desenvolvedores eliminam o código do aplicativo como a origem do problema.
  • É comum supor que o roteamento é a causa.

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:

  • Intercale cada middleware com uma cópia do middleware de tempo mostrado no código anterior.
  • Adicione um identificador exclusivo para correlacionar os dados de tempo com o código.

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.");

Recursos de roteamento possivelmente caros

A lista a seguir fornece alguns insights sobre recursos de roteamento relativamente caros, em comparação a modelos de rota básicos:

  • Expressões regulares: é possível gravar expressões regulares complexas ou que tenham um tempo de execução prolongada com um pequeno valor de entrada.
  • Segmentos complexos ({x}-{y}-{z}):
    • São significativamente mais caros do que analisar um segmento de caminho de URL regular.
    • Resulta na alocação de muito mais subcadeias de caracteres.
  • Acesso a dados síncronos: muitos aplicativos complexos têm acesso ao banco de dados como parte do roteamento. Use pontos de extensibilidade como MatcherPolicy e EndpointSelectorContext, que são assíncronos.

Diretrizes para tabelas de rotas grandes

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:

  • Há um grande número de rotas no aplicativo usando esse padrão.
  • Há um grande número de rotas no aplicativo.

Problema ao determinar se um aplicativo está em execução na tabela de rotas grande

  • Há dois sintomas a procurar:
    • O aplicativo está lento para iniciar na primeira solicitação.
      • Observe que isso é necessário, mas não é suficiente. Há muitos outros problemas não relacionados à rota que podem causar uma inicialização lenta do aplicativo. Verifique a condição abaixo para determinar com precisão se o aplicativo está nessa situação.
    • O aplicativo consome muita memória durante a inicialização e um despejo de memória mostra um grande número de instâncias Microsoft.AspNetCore.Routing.Matching.DfaNode.

Como resolver esse problema

Existem várias técnicas e otimizações que podem ser aplicadas às rotas que melhoram muito esse cenário:

  • Aplique restrições de rota aos parâmetros, por exemplo {parameter:int}, {parameter:guid}, {parameter:regex(\\d+)} etc., sempre que possível.
    • Isso permite que o algoritmo de roteamento otimize internamente as estruturas usadas para correspondência e reduza consideravelmente a memória usada.
    • Na grande maioria dos casos, isso será suficiente para voltar a um comportamento aceitável.
  • Altere as rotas para mover parâmetros para os segmentos posteriores no modelo.
    • Isso reduz o número de "caminhos" possíveis para corresponder a um ponto de extremidade, considerando um caminho.
  • Use uma rota dinâmica e execute o mapeamento para um controlador/página dinamicamente.
    • Isso pode ser feito usando MapDynamicControllerRoute ou MapDynamicPageRoute.

Middleware de curto-circuito após o roteamento

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.

Diretrizes para criadores de bibliotecas

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.

Definir pontos de extremidade

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:

  • Permite que os metadados sejam compostos.
  • É direcionada por uma variedade de métodos de extensão.

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.

Como criar o middleware integrado ao roteamento

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:

  • Torne-os acessíveis como atributos.
  • A maioria dos usuários está familiarizada com a aplicação de atributos.

Declarar um tipo de metadados como uma interface adiciona outra camada de flexibilidade:

  • As interfaces podem ser formadas.
  • Os desenvolvedores podem declarar seus próprios tipos que combinam várias políticas.

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:

  • Não procure apenas a presença de um tipo de metadados.
  • Defina uma propriedade nos metadados e verifique a propriedade.

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:

  • Pontos de extremidade sem uma política especificada.
  • Solicitações que não correspondem a um ponto de extremidade.

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.

Depurar diagnóstico

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

Recursos adicionais

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:

Conceitos básicos sobre 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:

  • Quando uma solicitação HTTP GET é enviada para a URL raiz /:
    • O delegado de solicitação é executado.
    • Hello World! é gravado na resposta HTTP.
  • Se o método de solicitação não for 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:

  • A chamada para app.Use registra um middleware personalizado, que é executado no início do pipeline.
  • A chamada para UseRouting configura o middleware de correspondência de rotas a ser executado após o middleware personalizado.
  • O ponto de extremidade registrado com 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.

Pontos de extremidade

O método MapGet é usado para definir um ponto de extremidade. Um ponto de extremidade pode ser:

  • Selecionado, correspondendo a URL e o método HTTP.
  • Executado, processando o delegado.

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:

  • Uma URL como /hello/Docs
  • Qualquer caminho de URL que comece com /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}:

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:

  • O middleware de autorização pode ser usado com o roteamento.
  • Os pontos de extremidade podem ser usados para configurar o comportamento de autorização.

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:

  • Ver qual ponto de extremidade foi selecionado por UseRouting.
  • Aplicar uma política de autorização antes que UseEndpoints seja expedido para o ponto de extremidade.

Metadados de ponto de extremidade

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:

  • Os metadados podem ser processados pelo middleware com reconhecimento de roteamento.
  • Os metadados podem ser de qualquer tipo de .NET.

Conceitos de roteamento

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.

Definição de ponto de extremidade 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:

  • O ponto de extremidade é sempre nulo antes que UseRouting seja chamado.
  • Se uma correspondência for encontrada, o ponto de extremidade não será nulo entre UseRouting e UseEndpoints.
  • O middleware UseEndpoints é terminal quando uma correspondência é encontrada. O middleware de terminal será definido posteriormente neste artigo.
  • O middleware após 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:

  • O middleware pode ser executado antes do UseRouting para modificar os dados nos quais o roteamento opera.
  • O middleware pode ser executado entre UseRouting e UseEndpoints para processar os resultados do roteamento, antes que o ponto de extremidade seja executado.
    • Middleware que é executado entre UseRouting e UseEndpoints:
      • Geralmente inspeciona os metadados para entender os pontos de extremidade.
      • Geralmente toma as decisões de segurança, conforme feito por UseAuthorization e UseCors.
    • A combinação de middleware e metadados permite configurar políticas por ponto de extremidade.

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:

  • Registra um arquivo ou banco de dados.
  • Inclui detalhes como o usuário, endereço IP, nome do ponto de extremidade confidencial e muito mais.

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:

  • Os metadados são anexados a uma API do construtor.
  • As estruturas baseadas em classe incluem todos os atributos no método e na classe correspondentes ao criar os pontos de extremidade.

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.

Comparar o middleware de terminal com o roteamento

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:

  • A operação correspondente no exemplo anterior é Path == "/" para o middleware e Path == "/Routing" para o roteamento.
  • Quando uma correspondência é bem-sucedida, ela executa algumas funcionalidades e retorna, em vez de invocar o middleware 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:

  • Ambas as abordagens permitem terminar o pipeline de processamento:
    • O middleware termina o pipeline retornando, em vez de invocar next.
    • Os pontos de extremidade são sempre terminais.
  • O middleware de terminal permite posicionar o middleware em um local arbitrário no pipeline:
    • Os pontos de extremidade são executados na posição do UseEndpoints.
  • O middleware de terminal permite que o código arbitrário determine quando o middleware corresponde ao seguinte:
    • O código de correspondência de rotas personalizado pode ser detalhado e difícil de ser gravado corretamente.
    • O roteamento fornece soluções simples para aplicativos típicos. A maioria dos aplicativos não exige o código de correspondência de rotas personalizado.
  • Os pontos de extremidade fazem interface com o middleware, como UseAuthorization e UseCors.
    • Usar um middleware de terminal com UseAuthorization ou UseCors exige uma interface manual com o sistema de autorização.

Um ponto de extremidade define ambos:

  • Um delegado para processar as solicitações.
  • Uma coleção de metadados arbitrários. Os metadados são usados para implementar interesses paralelos com base em políticas e na configuração anexada a cada ponto de extremidade.

O middleware de terminal pode ser uma ferramenta eficaz, mas pode exigir:

  • Um valor significativo de codificação e teste.
  • Integração manual com outros sistemas para atingir o nível desejado de flexibilidade.

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:

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.

Correspondência de URL

  • É o processo pelo qual o roteamento corresponde uma solicitação de entrada com um ponto de extremidade.
  • É baseado em dados nos cabeçalhos e caminho de URL.
  • Pode ser estendido para considerar quaisquer dados na solicitaçã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:

  • Chamar HttpContext.GetEndpoint obtém o ponto de extremidade.
  • 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:

  • Qualquer decisão que possa afetar a expedição ou a aplicação de políticas de segurança seja tomada dentro do sistema de roteamento.

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:

  • Migrar RouteData.Values para HttpRequest.RouteValues.
  • Migrar 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:

  1. Processa o caminho de URL em relação ao conjunto de pontos de extremidade e os modelos de rota, coletando todas as correspondências.
  2. Usa a lista anterior e remove as correspondências que falham com restrições de rota aplicadas.
  3. Usa a lista anterior e remove as correspondências que falham no conjunto de instâncias MatcherPolicy.
  4. Usa o EndpointSelector para tomar uma decisão final na lista anterior.

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

  • Ambos correspondem ao caminho de URL /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}:

  • A restrição alpha corresponde apenas a caracteres alfabéticos.
  • A restrição int corresponde apenas a números.
  • Esses modelos têm a mesma precedência de rota, mas não há uma única URL que corresponda a ambos.
  • Se o sistema de roteamento relatasse um erro de ambiguidade na inicialização, ele bloquearia esse caso de uso válido.

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:

  • Não tem o conceito de rotas.
  • Não fornece garantias de ordenação. Todos os pontos de extremidade são processados de uma só vez.

Precedência de modelo de rota e ordem de seleção de ponto de extremidade

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:

  • Evita a necessidade de ajustar a ordem dos pontos de extremidade em casos comuns.
  • Tenta corresponder às expectativas de bom senso do comportamento de roteamento.

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:

  • Os modelos com mais segmentos são considerados mais específicos.
  • Um segmento com texto literal é considerado mais específico do que um segmento de parâmetro.
  • Um segmento de parâmetro com uma restrição é considerado mais específico do que um sem restrição.
  • Um segmento complexo é considerado tão específico quanto um segmento de parâmetro com uma restrição.
  • Os parâmetros catch-all são os menos específicos. Confira catch-all na seção Modelos de rota para obter informações importantes sobre rotas catch-all.

Conceitos de geração de URL

Geração de URL:

  • É o processo pelo qual o roteamento pode criar um caminho de URL de acordo com um conjunto de valores de rota.
  • Permite uma separação lógica entre os pontos de extremidade e as URLs que os acessam.

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:

  1. Um endereço é associado a uma lista de pontos de extremidade que correspondem ao endereço.
  2. O RoutePattern de cada ponto de extremidade é avaliado até que seja encontrado um padrão de rota correspondente aos valores fornecidos. A saída resultante é combinada com as outras partes de URI fornecidas ao gerador de link e é retornada.

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.

Exemplo de middleware

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

Modelos de rota

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 **:

  • Pode ser usado como prefixo para um parâmetro de rota a ser associado ao restante do URI.
  • São chamados de parâmetros catch-all. Por exemplo, blog/{**slug}:
    • Corresponde a qualquer URI que comece com blog/ e tenha qualquer valor depois dele.
    • O valor a seguir 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:

  • Uma rota catch-all, por exemplo, {**slug}"
  • A rota catch-all não corresponde às solicitações que deveria corresponder.
  • Remover outras rotas faz com que a rota catch-all comece a funcionar.

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 é:

  • Um parâmetro de rota com um valor padrão sempre produz um valor.
  • Um parâmetro opcional só tem um valor quando um valor é fornecido pela URL de solicitaçã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.

Segmentos complexos

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:

  • O primeiro literal, da direita para a esquerda, é c. Portanto, /abcd é pesquisado pela direita e localiza /ab|c|d.
  • Tudo à direita (d) agora corresponde ao parâmetro de rota {d}.
  • O próximo literal, da direita para a esquerda, é a. Portanto, /ab|c|d é pesquisado começando de onde paramos e, em seguida, a é encontrado em /|a|b|c|d.
  • O valor à direita (b) agora corresponde ao parâmetro de rota {b}.
  • Não há texto restante nem modelo de rota restante. Portanto, esta é uma correspondência.

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:

  • O primeiro literal, da direita para a esquerda, é c. Portanto, /aabcd é pesquisado pela direita e localiza /aab|c|d.
  • Tudo à direita (d) agora corresponde ao parâmetro de rota {d}.
  • O próximo literal, da direita para a esquerda, é a. Portanto, /aab|c|d é pesquisado começando de onde paramos e, em seguida, a é encontrado em /a|a|b|c|d.
  • O valor à direita (b) agora corresponde ao parâmetro de rota {b}.
  • Neste ponto, há texto restante a, mas o algoritmo ficou fora do modelo de rota para analisar. Portanto, não é uma correspondência.

Como o algoritmo correspondente é sem greedy:

  • Ele corresponde à menor quantidade de texto possível em cada etapa.
  • Qualquer caso em que o valor delimitador apareça dentro dos valores de parâmetro resulta em não correspondência.

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.

Roteamento com caracteres especiais

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;

Restrições de rota

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.

Expressões regulares em restrições

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:

  • Substitua os caracteres \ fornecidos na cadeia de caracteres pelos caracteres \\ no arquivo de origem do C# para escapar do caractere de escape da cadeia de caracteres \.
  • Literais de cadeia de caracteres textuais.

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.

Restrições de rota personalizadas

É 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:

  • Impede 0 no segmento {id} da rota.
  • É mostrado para fornecer um exemplo básico de implementação de uma restrição personalizada. Não deve ser usado em um aplicativo de produção.

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:

  • Não requer uma restrição personalizada.
  • Retorna um erro mais descritivo quando o parâmetro de rota inclui 0.

Transformadores de parâmetro

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:

Referência de geração de URL

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.

Solução de problemas de geração de URL com log

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.

Endereços

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:

  • Usando o nome do ponto de extremidade (string) como o endereço:
    • Fornece funcionalidade semelhante ao nome da rota do MVC.
    • Usa o tipo de metadados IEndpointNameMetadata.
    • Resolve a cadeia de caracteres fornecida em relação aos metadados de todos os pontos de extremidade registrados.
    • Gera uma exceção na inicialização, se vários pontos de extremidade usarem o mesmo nome.
    • Recomendado para uso geral fora dos controladores e Razor Pages.
  • Usando os valores de rota (RouteValuesAddress) como o endereço:
    • Fornece uma funcionalidade semelhante à geração de URL herdada dos controladores e Razor Pages.
    • Muito difícil de estender e depurar.
    • Fornece a implementação usada por 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:

  • O esquema de nome do ponto de extremidade executa uma pesquisa de dicionário básica.
  • O esquema de valores de rota tem um subconjunto de conjunto mais complexo.

Valores ambientes e valores explícitos

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:

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.
  • Url obtém o IUrlHelper.
  • Action gera uma URL com um caminho absoluto para um método de ação. A URL contém o nome 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.
  • IUrlHelper.Action sempre copia os valores de rota atuais action e controller como valores explícitos, a menos que sejam substituídos pelo desenvolvedor.
  • IUrlHelper.Page sempre copia o valor de rota atual 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.

Processo de geração de URL

Depois que o conjunto de pontos de extremidade candidatos for encontrado, o algoritmo de geração de URL:

  • Processa os pontos de extremidade iterativamente.
  • Retorna o primeiro resultado bem-sucedido.

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:

  • Ao vincular-se a outra ação no mesmo controlador, o nome do controlador não precisa ser especificado.
  • Ao vincular-se a outro controlador na mesma área, o nome da área não precisa ser especificado.
  • Ao vincular-se ao mesmo método de ação, os valores de rota não precisam ser especificados.
  • Ao vincular-se a outra parte do aplicativo, não convém carregar valores de rota que não têm significado nessa parte do aplicativo.

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:

  • Se os valores explícitos contiverem um valor para id, o valor ambiente para id será ignorado. Os valores ambientes para controller e action podem ser usados.
  • Se os valores explícitos contiverem um valor para 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.
  • Se os valores explícitos contiverem um valor para 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:

  • Existe uma hierarquia de valores de rota.
  • Eles não são exibidos no modelo.

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:

  • Os nomes de valor necessários são combinados com os parâmetros de rota e processados da esquerda para a direita.
  • Para cada parâmetro, o valor ambiente e o valor explícito são comparados:
    • Se o valor ambiente e o valor explícito forem iguais, o processo continuará.
    • Se o valor ambiente estiver presente e o valor explícito não, o valor ambiente será usado ao gerar a URL.
    • Se o valor ambiente não estiver presente e o valor explícito sim, rejeite o valor ambiente e todos os valores ambientes subsequentes.
    • Se o valor ambiente e o valor explícito estiverem presentes e os dois valores forem diferentes, rejeite o valor ambiente e todos os valores ambientes subsequentes.

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:

  • Da esquerda para a direita.
  • O valor aceito de cada parâmetro é substituído.
  • Com os seguintes casos especiais:
    • Se faltar um valor nos valores aceitos e o parâmetro tiver um valor padrão, o valor padrão será usado.
    • Se faltar um valor nos valores aceitos e o parâmetro for opcional, o processamento continuará.
    • Se qualquer parâmetro de rota à direita de um parâmetro opcional ausente tiver um valor, a operação falhará.
    • Os parâmetros com valor padrão contíguos e parâmetros opcionais são recolhidos sempre que possível.

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

Ordem de parâmetro de rota opcional

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

Problemas com a invalidação de valor de rota

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:

  • No modelo de rota "default", o parâmetro de rota culture fica à esquerda de controller. Portanto, as alterações em controller não invalidarão culture.
  • No modelo de rota "blog", considera-se que o parâmetro de rota culture fica à direita de controller, que aparece nos valores necessários.

Analisar caminhos de URL com LinkParser

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.

Configurar metadados de ponto de extremidade

Os links a seguir fornecem informações sobre como configurar metadados de ponto de extremidade:

Correspondência de host em rotas com RequireHost

RequireHost aplica uma restrição à rota que exige o host especificado. O parâmetro RequireHostou [Host] pode ser um:

  • Host: www.domain.com, corresponde www.domain.com a qualquer porta.
  • Host com curinga: *.domain.com, corresponde www.domain.com, subdomain.domain.com ou www.subdomain.domain.com a qualquer porta.
  • Porta: *:5000, corresponde a porta 5000 a qualquer host.
  • Host e porta: 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 atributo na ação será usado.
  • O atributo do controlador será ignorado.

Grupos de rotas

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.

O QueryPrivateTodosalocador de filtros de ponto de extremidade é uma função local que modifica os parâmetros TodoDb do manipulador de rotas para permitir o acesso e o armazenamento de dados todo privados.

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

Diretrizes de desempenho para roteamento

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:

  • Os desenvolvedores eliminam o código do aplicativo como a origem do problema.
  • É comum supor que o roteamento é a causa.

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:

  • Intercale cada middleware com uma cópia do middleware de tempo mostrado no código anterior.
  • Adicione um identificador exclusivo para correlacionar os dados de tempo com o código.

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.");

Recursos de roteamento possivelmente caros

A lista a seguir fornece alguns insights sobre recursos de roteamento relativamente caros, em comparação a modelos de rota básicos:

  • Expressões regulares: é possível gravar expressões regulares complexas ou que tenham um tempo de execução prolongada com um pequeno valor de entrada.
  • Segmentos complexos ({x}-{y}-{z}):
    • São significativamente mais caros do que analisar um segmento de caminho de URL regular.
    • Resulta na alocação de muito mais subcadeias de caracteres.
  • Acesso a dados síncronos: muitos aplicativos complexos têm acesso ao banco de dados como parte do roteamento. Use pontos de extensibilidade como MatcherPolicy e EndpointSelectorContext, que são assíncronos.

Diretrizes para tabelas de rotas grandes

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:

  • Há um grande número de rotas no aplicativo usando esse padrão.
  • Há um grande número de rotas no aplicativo.

Problema ao determinar se um aplicativo está em execução na tabela de rotas grande

  • Há dois sintomas a procurar:
    • O aplicativo está lento para iniciar na primeira solicitação.
      • Observe que isso é necessário, mas não é suficiente. Há muitos outros problemas não relacionados à rota que podem causar uma inicialização lenta do aplicativo. Verifique a condição abaixo para determinar com precisão se o aplicativo está nessa situação.
    • O aplicativo consome muita memória durante a inicialização e um despejo de memória mostra um grande número de instâncias Microsoft.AspNetCore.Routing.Matching.DfaNode.

Como resolver esse problema

Há várias técnicas e otimizações que podem ser aplicadas a rotas que melhorarão muito esse cenário:

  • Aplique restrições de rota aos parâmetros, por exemplo {parameter:int}, {parameter:guid}, {parameter:regex(\\d+)} etc., sempre que possível.
    • Isso permite que o algoritmo de roteamento otimize internamente as estruturas usadas para correspondência e reduza consideravelmente a memória usada.
    • Na grande maioria dos casos, isso será suficiente para voltar a um comportamento aceitável.
  • Altere as rotas para mover parâmetros para os segmentos posteriores no modelo.
    • Isso reduz o número de "caminhos" possíveis para corresponder a um ponto de extremidade, considerando um caminho.
  • Use uma rota dinâmica e execute o mapeamento para um controlador/página dinamicamente.
    • Isso pode ser feito usando MapDynamicControllerRoute ou MapDynamicPageRoute.

Diretrizes para criadores de bibliotecas

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.

Definir pontos de extremidade

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:

  • Permite que os metadados sejam compostos.
  • É direcionada por uma variedade de métodos de extensão.

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.

Como criar o middleware integrado ao roteamento

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:

  • Torne-os acessíveis como atributos.
  • A maioria dos usuários está familiarizada com a aplicação de atributos.

Declarar um tipo de metadados como uma interface adiciona outra camada de flexibilidade:

  • As interfaces podem ser formadas.
  • Os desenvolvedores podem declarar seus próprios tipos que combinam várias políticas.

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:

  • Não procure apenas a presença de um tipo de metadados.
  • Defina uma propriedade nos metadados e verifique a propriedade.

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:

  • Pontos de extremidade sem uma política especificada.
  • Solicitações que não correspondem a um ponto de extremidade.

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.

Depurar diagnóstico

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

Recursos adicionais

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:

Conceitos básicos sobre 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:

  • Quando uma solicitação HTTP GET é enviada para a URL raiz /:
    • O delegado de solicitação é executado.
    • Hello World! é gravado na resposta HTTP.
  • Se o método de solicitação não for 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:

  • A chamada para app.Use registra um middleware personalizado, que é executado no início do pipeline.
  • A chamada para UseRouting configura o middleware de correspondência de rotas a ser executado após o middleware personalizado.
  • O ponto de extremidade registrado com 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.

Pontos de extremidade

O método MapGet é usado para definir um ponto de extremidade. Um ponto de extremidade pode ser:

  • Selecionado, correspondendo a URL e o método HTTP.
  • Executado, processando o delegado.

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:

  • Uma URL como /hello/Docs
  • Qualquer caminho de URL que comece com /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}:

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:

  • O middleware de autorização pode ser usado com o roteamento.
  • Os pontos de extremidade podem ser usados para configurar o comportamento de autorização.

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:

  • Ver qual ponto de extremidade foi selecionado por UseRouting.
  • Aplicar uma política de autorização antes que UseEndpoints seja expedido para o ponto de extremidade.

Metadados de ponto de extremidade

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:

  • Os metadados podem ser processados pelo middleware com reconhecimento de roteamento.
  • Os metadados podem ser de qualquer tipo de .NET.

Conceitos de roteamento

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.

Definição de ponto de extremidade 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:

  • O ponto de extremidade é sempre nulo antes que UseRouting seja chamado.
  • Se uma correspondência for encontrada, o ponto de extremidade não será nulo entre UseRouting e UseEndpoints.
  • O middleware UseEndpoints é terminal quando uma correspondência é encontrada. O middleware de terminal será definido posteriormente neste artigo.
  • O middleware após 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:

  • O middleware pode ser executado antes do UseRouting para modificar os dados nos quais o roteamento opera.
  • O middleware pode ser executado entre UseRouting e UseEndpoints para processar os resultados do roteamento, antes que o ponto de extremidade seja executado.
    • Middleware que é executado entre UseRouting e UseEndpoints:
      • Geralmente inspeciona os metadados para entender os pontos de extremidade.
      • Geralmente toma as decisões de segurança, conforme feito por UseAuthorization e UseCors.
    • A combinação de middleware e metadados permite configurar políticas por ponto de extremidade.

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:

  • Registra um arquivo ou banco de dados.
  • Inclui detalhes como o usuário, endereço IP, nome do ponto de extremidade confidencial e muito mais.

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:

  • Os metadados são anexados a uma API do construtor.
  • As estruturas baseadas em classe incluem todos os atributos no método e na classe correspondentes ao criar os pontos de extremidade.

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.

Comparar o middleware de terminal com o roteamento

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:

  • A operação correspondente no exemplo anterior é Path == "/" para o middleware e Path == "/Routing" para o roteamento.
  • Quando uma correspondência é bem-sucedida, ela executa algumas funcionalidades e retorna, em vez de invocar o middleware 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:

  • Ambas as abordagens permitem terminar o pipeline de processamento:
    • O middleware termina o pipeline retornando, em vez de invocar next.
    • Os pontos de extremidade são sempre terminais.
  • O middleware de terminal permite posicionar o middleware em um local arbitrário no pipeline:
    • Os pontos de extremidade são executados na posição do UseEndpoints.
  • O middleware de terminal permite que o código arbitrário determine quando o middleware corresponde ao seguinte:
    • O código de correspondência de rotas personalizado pode ser detalhado e difícil de ser gravado corretamente.
    • O roteamento fornece soluções simples para aplicativos típicos. A maioria dos aplicativos não exige o código de correspondência de rotas personalizado.
  • Os pontos de extremidade fazem interface com o middleware, como UseAuthorization e UseCors.
    • Usar um middleware de terminal com UseAuthorization ou UseCors exige uma interface manual com o sistema de autorização.

Um ponto de extremidade define ambos:

  • Um delegado para processar as solicitações.
  • Uma coleção de metadados arbitrários. Os metadados são usados para implementar interesses paralelos com base em políticas e na configuração anexada a cada ponto de extremidade.

O middleware de terminal pode ser uma ferramenta eficaz, mas pode exigir:

  • Um valor significativo de codificação e teste.
  • Integração manual com outros sistemas para atingir o nível desejado de flexibilidade.

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:

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.

Correspondência de URL

  • É o processo pelo qual o roteamento corresponde uma solicitação de entrada com um ponto de extremidade.
  • É baseado em dados nos cabeçalhos e caminho de URL.
  • Pode ser estendido para considerar quaisquer dados na solicitaçã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:

  • Chamar HttpContext.GetEndpoint obtém o ponto de extremidade.
  • 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:

  • Qualquer decisão que possa afetar a expedição ou a aplicação de políticas de segurança seja tomada dentro do sistema de roteamento.

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:

  • Migrar RouteData.Values para HttpRequest.RouteValues.
  • Migrar 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:

  1. Processa o caminho de URL em relação ao conjunto de pontos de extremidade e os modelos de rota, coletando todas as correspondências.
  2. Usa a lista anterior e remove as correspondências que falham com restrições de rota aplicadas.
  3. Usa a lista anterior e remove as correspondências que falham no conjunto de instâncias MatcherPolicy.
  4. Usa o EndpointSelector para tomar uma decisão final na lista anterior.

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

  • Ambos correspondem ao caminho de URL /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}:

  • A restrição alpha corresponde apenas a caracteres alfabéticos.
  • A restrição int corresponde apenas a números.
  • Esses modelos têm a mesma precedência de rota, mas não há uma única URL que corresponda a ambos.
  • Se o sistema de roteamento relatasse um erro de ambiguidade na inicialização, ele bloquearia esse caso de uso válido.

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:

  • Não tem o conceito de rotas.
  • Não fornece garantias de ordenação. Todos os pontos de extremidade são processados de uma só vez.

Precedência de modelo de rota e ordem de seleção de ponto de extremidade

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:

  • Evita a necessidade de ajustar a ordem dos pontos de extremidade em casos comuns.
  • Tenta corresponder às expectativas de bom senso do comportamento de roteamento.

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:

  • Os modelos com mais segmentos são considerados mais específicos.
  • Um segmento com texto literal é considerado mais específico do que um segmento de parâmetro.
  • Um segmento de parâmetro com uma restrição é considerado mais específico do que um sem restrição.
  • Um segmento complexo é considerado tão específico quanto um segmento de parâmetro com uma restrição.
  • Os parâmetros catch-all são os menos específicos. Confira catch-all na seção Modelos de rota para obter informações importantes sobre rotas catch-all.

Conceitos de geração de URL

Geração de URL:

  • É o processo pelo qual o roteamento pode criar um caminho de URL de acordo com um conjunto de valores de rota.
  • Permite uma separação lógica entre os pontos de extremidade e as URLs que os acessam.

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:

  1. Um endereço é associado a uma lista de pontos de extremidade que correspondem ao endereço.
  2. O RoutePattern de cada ponto de extremidade é avaliado até que seja encontrado um padrão de rota correspondente aos valores fornecidos. A saída resultante é combinada com as outras partes de URI fornecidas ao gerador de link e é retornada.

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.

Exemplo de middleware

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

Modelos de rota

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 **:

  • Pode ser usado como prefixo para um parâmetro de rota a ser associado ao restante do URI.
  • São chamados de parâmetros catch-all. Por exemplo, blog/{**slug}:
    • Corresponde a qualquer URI que comece com blog/ e tenha qualquer valor depois dele.
    • O valor a seguir 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:

  • Uma rota catch-all, por exemplo, {**slug}"
  • A rota catch-all não corresponde às solicitações que deveria corresponder.
  • Remover outras rotas faz com que a rota catch-all comece a funcionar.

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 é:

  • Um parâmetro de rota com um valor padrão sempre produz um valor.
  • Um parâmetro opcional só tem um valor quando um valor é fornecido pela URL de solicitaçã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.

Segmentos complexos

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:

  • O primeiro literal, da direita para a esquerda, é c. Portanto, /abcd é pesquisado pela direita e localiza /ab|c|d.
  • Tudo à direita (d) agora corresponde ao parâmetro de rota {d}.
  • O próximo literal, da direita para a esquerda, é a. Portanto, /ab|c|d é pesquisado começando de onde paramos e, em seguida, a é encontrado em /|a|b|c|d.
  • O valor à direita (b) agora corresponde ao parâmetro de rota {b}.
  • Não há texto restante nem modelo de rota restante. Portanto, esta é uma correspondência.

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:

  • O primeiro literal, da direita para a esquerda, é c. Portanto, /aabcd é pesquisado pela direita e localiza /aab|c|d.
  • Tudo à direita (d) agora corresponde ao parâmetro de rota {d}.
  • O próximo literal, da direita para a esquerda, é a. Portanto, /aab|c|d é pesquisado começando de onde paramos e, em seguida, a é encontrado em /a|a|b|c|d.
  • O valor à direita (b) agora corresponde ao parâmetro de rota {b}.
  • Neste ponto, há texto restante a, mas o algoritmo ficou fora do modelo de rota para analisar. Portanto, não é uma correspondência.

Como o algoritmo correspondente é sem greedy:

  • Ele corresponde à menor quantidade de texto possível em cada etapa.
  • Qualquer caso em que o valor delimitador apareça dentro dos valores de parâmetro resulta em não correspondência.

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.

Roteamento com caracteres especiais

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;

Restrições de rota

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.

Expressões regulares em restrições

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:

  • Substitua os caracteres \ fornecidos na cadeia de caracteres pelos caracteres \\ no arquivo de origem do C# para escapar do caractere de escape da cadeia de caracteres \.
  • Literais de cadeia de caracteres textuais.

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.

Restrições de rota personalizadas

É 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:

  • Impede 0 no segmento {id} da rota.
  • É mostrado para fornecer um exemplo básico de implementação de uma restrição personalizada. Não deve ser usado em um aplicativo de produção.

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:

  • Não requer uma restrição personalizada.
  • Retorna um erro mais descritivo quando o parâmetro de rota inclui 0.

Transformadores de parâmetro

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:

Referência de geração de URL

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.

Solução de problemas de geração de URL com log

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.

Endereços

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:

  • Usando o nome do ponto de extremidade (string) como o endereço:
    • Fornece funcionalidade semelhante ao nome da rota do MVC.
    • Usa o tipo de metadados IEndpointNameMetadata.
    • Resolve a cadeia de caracteres fornecida em relação aos metadados de todos os pontos de extremidade registrados.
    • Gera uma exceção na inicialização, se vários pontos de extremidade usarem o mesmo nome.
    • Recomendado para uso geral fora dos controladores e Razor Pages.
  • Usando os valores de rota (RouteValuesAddress) como o endereço:
    • Fornece uma funcionalidade semelhante à geração de URL herdada dos controladores e Razor Pages.
    • Muito difícil de estender e depurar.
    • Fornece a implementação usada por 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:

  • O esquema de nome do ponto de extremidade executa uma pesquisa de dicionário básica.
  • O esquema de valores de rota tem um subconjunto de conjunto mais complexo.

Valores ambientes e valores explícitos

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:

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.
  • Url obtém o IUrlHelper.
  • Action gera uma URL com um caminho absoluto para um método de ação. A URL contém o nome 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.
  • IUrlHelper.Action sempre copia os valores de rota atuais action e controller como valores explícitos, a menos que sejam substituídos pelo desenvolvedor.
  • IUrlHelper.Page sempre copia o valor de rota atual 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.

Processo de geração de URL

Depois que o conjunto de pontos de extremidade candidatos for encontrado, o algoritmo de geração de URL:

  • Processa os pontos de extremidade iterativamente.
  • Retorna o primeiro resultado bem-sucedido.

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:

  • Ao vincular-se a outra ação no mesmo controlador, o nome do controlador não precisa ser especificado.
  • Ao vincular-se a outro controlador na mesma área, o nome da área não precisa ser especificado.
  • Ao vincular-se ao mesmo método de ação, os valores de rota não precisam ser especificados.
  • Ao vincular-se a outra parte do aplicativo, não convém carregar valores de rota que não têm significado nessa parte do aplicativo.

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:

  • Se os valores explícitos contiverem um valor para id, o valor ambiente para id será ignorado. Os valores ambientes para controller e action podem ser usados.
  • Se os valores explícitos contiverem um valor para 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.
  • Se os valores explícitos contiverem um valor para 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:

  • Existe uma hierarquia de valores de rota.
  • Eles não são exibidos no modelo.

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:

  • Os nomes de valor necessários são combinados com os parâmetros de rota e processados da esquerda para a direita.
  • Para cada parâmetro, o valor ambiente e o valor explícito são comparados:
    • Se o valor ambiente e o valor explícito forem iguais, o processo continuará.
    • Se o valor ambiente estiver presente e o valor explícito não, o valor ambiente será usado ao gerar a URL.
    • Se o valor ambiente não estiver presente e o valor explícito sim, rejeite o valor ambiente e todos os valores ambientes subsequentes.
    • Se o valor ambiente e o valor explícito estiverem presentes e os dois valores forem diferentes, rejeite o valor ambiente e todos os valores ambientes subsequentes.

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:

  • Da esquerda para a direita.
  • O valor aceito de cada parâmetro é substituído.
  • Com os seguintes casos especiais:
    • Se faltar um valor nos valores aceitos e o parâmetro tiver um valor padrão, o valor padrão será usado.
    • Se faltar um valor nos valores aceitos e o parâmetro for opcional, o processamento continuará.
    • Se qualquer parâmetro de rota à direita de um parâmetro opcional ausente tiver um valor, a operação falhará.
    • Os parâmetros com valor padrão contíguos e parâmetros opcionais são recolhidos sempre que possível.

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

Problemas com a invalidação de valor de rota

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:

  • No modelo de rota "default", o parâmetro de rota culture fica à esquerda de controller. Portanto, as alterações em controller não invalidarão culture.
  • No modelo de rota "blog", considera-se que o parâmetro de rota culture fica à direita de controller, que aparece nos valores necessários.

Analisar caminhos de URL com LinkParser

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.

Configurar metadados de ponto de extremidade

Os links a seguir fornecem informações sobre como configurar metadados de ponto de extremidade:

Correspondência de host em rotas com RequireHost

RequireHost aplica uma restrição à rota que exige o host especificado. O parâmetro RequireHostou [Host] pode ser um:

  • Host: www.domain.com, corresponde www.domain.com a qualquer porta.
  • Host com curinga: *.domain.com, corresponde www.domain.com, subdomain.domain.com ou www.subdomain.domain.com a qualquer porta.
  • Porta: *:5000, corresponde a porta 5000 a qualquer host.
  • Host e porta: 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 atributo na ação será usado.
  • O atributo do controlador será ignorado.

Diretrizes de desempenho para roteamento

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:

  • Os desenvolvedores eliminam o código do aplicativo como a origem do problema.
  • É comum supor que o roteamento é a causa.

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:

  • Intercale cada middleware com uma cópia do middleware de tempo mostrado no código anterior.
  • Adicione um identificador exclusivo para correlacionar os dados de tempo com o código.

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.");

Recursos de roteamento possivelmente caros

A lista a seguir fornece alguns insights sobre recursos de roteamento relativamente caros, em comparação a modelos de rota básicos:

  • Expressões regulares: é possível gravar expressões regulares complexas ou que tenham um tempo de execução prolongada com um pequeno valor de entrada.
  • Segmentos complexos ({x}-{y}-{z}):
    • São significativamente mais caros do que analisar um segmento de caminho de URL regular.
    • Resulta na alocação de muito mais subcadeias de caracteres.
  • Acesso a dados síncronos: muitos aplicativos complexos têm acesso ao banco de dados como parte do roteamento. Use pontos de extensibilidade como MatcherPolicy e EndpointSelectorContext, que são assíncronos.

Diretrizes para tabelas de rotas grandes

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:

  • Há um grande número de rotas no aplicativo usando esse padrão.
  • Há um grande número de rotas no aplicativo.

Problema ao determinar se um aplicativo está em execução na tabela de rotas grande

  • Há dois sintomas a procurar:
    • O aplicativo está lento para iniciar na primeira solicitação.
      • Observe que isso é necessário, mas não é suficiente. Há muitos outros problemas não relacionados à rota que podem causar uma inicialização lenta do aplicativo. Verifique a condição abaixo para determinar com precisão se o aplicativo está nessa situação.
    • O aplicativo consome muita memória durante a inicialização e um despejo de memória mostra um grande número de instâncias Microsoft.AspNetCore.Routing.Matching.DfaNode.

Como resolver esse problema

Há várias técnicas e otimizações que podem ser aplicadas a rotas que melhorarão muito esse cenário:

  • Aplique restrições de rota aos parâmetros, por exemplo {parameter:int}, {parameter:guid}, {parameter:regex(\\d+)} etc., sempre que possível.
    • Isso permite que o algoritmo de roteamento otimize internamente as estruturas usadas para correspondência e reduza consideravelmente a memória usada.
    • Na grande maioria dos casos, isso será suficiente para voltar a um comportamento aceitável.
  • Altere as rotas para mover parâmetros para os segmentos posteriores no modelo.
    • Isso reduz o número de "caminhos" possíveis para corresponder a um ponto de extremidade, considerando um caminho.
  • Use uma rota dinâmica e execute o mapeamento para um controlador/página dinamicamente.
    • Isso pode ser feito usando MapDynamicControllerRoute ou MapDynamicPageRoute.

Diretrizes para criadores de bibliotecas

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.

Definir pontos de extremidade

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:

  • Permite que os metadados sejam compostos.
  • É direcionada por uma variedade de métodos de extensão.

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.

Como criar o middleware integrado ao roteamento

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:

  • Torne-os acessíveis como atributos.
  • A maioria dos usuários está familiarizada com a aplicação de atributos.

Declarar um tipo de metadados como uma interface adiciona outra camada de flexibilidade:

  • As interfaces podem ser formadas.
  • Os desenvolvedores podem declarar seus próprios tipos que combinam várias políticas.

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:

  • Não procure apenas a presença de um tipo de metadados.
  • Defina uma propriedade nos metadados e verifique a propriedade.

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:

  • Pontos de extremidade sem uma política especificada.
  • Solicitações que não correspondem a um ponto de extremidade.

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.

Depurar diagnóstico

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

Recursos adicionais

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.

Conceitos básicos sobre roteamento

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:

  • Quando uma solicitação HTTP GET é enviada para a URL raiz /:
    • O delegado de solicitação mostrado é executado.
    • Hello World! é gravado na resposta HTTP. Por padrão, a URL raiz / é https://localhost:5001/.
  • Se o método de solicitação não for GET ou a URL raiz não for /, nenhuma rota corresponderá e um HTTP 404 será retornado.

Ponto de extremidade

O método MapGet é usado para definir um ponto de extremidade. Um ponto de extremidade pode ser:

  • Selecionado, correspondendo a URL e o método HTTP.
  • Executado, processando o delegado.

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:

  • Uma URL como /hello/Ryan
  • Qualquer caminho de URL que comece com /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}:

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:

  • O middleware de autorização pode ser usado com o roteamento.
  • Os pontos de extremidade podem ser usados para configurar o comportamento de autorização.

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:

  • Ver qual ponto de extremidade foi selecionado por UseRouting.
  • Aplicar uma política de autorização antes que UseEndpoints seja expedido para o ponto de extremidade.

Metadados de ponto de extremidade

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:

  • Os metadados podem ser processados pelo middleware com reconhecimento de roteamento.
  • Os metadados podem ser de qualquer tipo de .NET.

Conceitos de roteamento

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.

Definição de ponto de extremidade 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:

  • O ponto de extremidade é sempre nulo antes que UseRouting seja chamado.
  • Se uma correspondência for encontrada, o ponto de extremidade não será nulo entre UseRouting e UseEndpoints.
  • O middleware UseEndpoints é terminal quando uma correspondência é encontrada. O middleware de terminal será definido posteriormente neste documento.
  • O middleware após 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:

  • O middleware pode ser executado antes do UseRouting para modificar os dados nos quais o roteamento opera.
  • O middleware pode ser executado entre UseRouting e UseEndpoints para processar os resultados do roteamento, antes que o ponto de extremidade seja executado.
    • Middleware que é executado entre UseRouting e UseEndpoints:
      • Geralmente inspeciona os metadados para entender os pontos de extremidade.
      • Geralmente toma as decisões de segurança, conforme feito por UseAuthorization e UseCors.
    • A combinação de middleware e metadados permite configurar políticas por ponto de extremidade.

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:

  • Registra um arquivo ou banco de dados.
  • Inclui detalhes como o usuário, endereço IP, nome do ponto de extremidade confidencial e muito mais.

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:

  • Os metadados são anexados a uma API do construtor.
  • As estruturas baseadas em classe incluem todos os atributos no método e na classe correspondentes ao criar os pontos de extremidade.

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.

Comparação entre middleware de terminal e roteamento

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:

  • A operação correspondente no exemplo anterior é Path == "/" para o middleware e Path == "/Movie" para o roteamento.
  • Quando uma correspondência é bem-sucedida, ela executa algumas funcionalidades e retorna, em vez de invocar o middleware next.

Ele é chamado de middleware de terminal porque termina a pesquisa, executa algumas funcionalidades e retorna.

Comparação entre middleware de terminal e roteamento:

  • Ambas as abordagens permitem terminar o pipeline de processamento:
    • O middleware termina o pipeline retornando, em vez de invocar next.
    • Os pontos de extremidade são sempre terminais.
  • O middleware de terminal permite posicionar o middleware em um local arbitrário no pipeline:
    • Os pontos de extremidade são executados na posição do UseEndpoints.
  • O middleware de terminal permite que o código arbitrário determine quando o middleware corresponde ao seguinte:
    • O código de correspondência de rotas personalizado pode ser detalhado e difícil de ser gravado corretamente.
    • O roteamento fornece soluções simples para aplicativos típicos. A maioria dos aplicativos não exige o código de correspondência de rotas personalizado.
  • Os pontos de extremidade fazem interface com o middleware, como UseAuthorization e UseCors.
    • Usar um middleware de terminal com UseAuthorization ou UseCors exige uma interface manual com o sistema de autorização.

Um ponto de extremidade define ambos:

  • Um delegado para processar as solicitações.
  • Uma coleção de metadados arbitrários. Os metadados são usados para implementar interesses paralelos com base em políticas e na configuração anexada a cada ponto de extremidade.

O middleware de terminal pode ser uma ferramenta eficaz, mas pode exigir:

  • Um valor significativo de codificação e teste.
  • Integração manual com outros sistemas para atingir o nível desejado de flexibilidade.

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:

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.

Correspondência de URL

  • É o processo pelo qual o roteamento corresponde uma solicitação de entrada com um ponto de extremidade.
  • É baseado em dados nos cabeçalhos e caminho de URL.
  • Pode ser estendido para considerar quaisquer dados na solicitaçã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:

  • Chamar HttpContext.GetEndpoint obtém o ponto de extremidade.
  • 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:

  • Qualquer decisão que possa afetar a expedição ou a aplicação de políticas de segurança seja tomada dentro do sistema de roteamento.

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:

  • Migrar RouteData.Values para HttpRequest.RouteValues.
  • Migre 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:

  1. Processa o caminho de URL em relação ao conjunto de pontos de extremidade e os modelos de rota, coletando todas as correspondências.
  2. Usa a lista anterior e remove as correspondências que falham com restrições de rota aplicadas.
  3. Usa a lista anterior e remove as correspondências que falham no conjunto de instâncias MatcherPolicy.
  4. Usa o EndpointSelector para tomar uma decisão final na lista anterior.

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

  • Ambos correspondem ao caminho de URL /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}:

  • A restrição alpha corresponde apenas a caracteres alfabéticos.
  • A restrição int corresponde apenas a números.
  • Esses modelos têm a mesma precedência de rota, mas não há uma única URL que corresponda a ambos.
  • Se o sistema de roteamento relatasse um erro de ambiguidade na inicialização, ele bloquearia esse caso de uso válido.

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:

  • Não tem um conceito de rotas.
  • Não fornece garantias de ordenação. Todos os pontos de extremidade são processados de uma só vez.

Precedência de modelo de rota e ordem de seleção de ponto de extremidade

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:

  • Evita a necessidade de ajustar a ordem dos pontos de extremidade em casos comuns.
  • Tenta corresponder às expectativas de bom senso do comportamento de roteamento.

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:

  • Os modelos com mais segmentos são considerados mais específicos.
  • Um segmento com texto literal é considerado mais específico do que um segmento de parâmetro.
  • Um segmento de parâmetro com uma restrição é considerado mais específico do que um sem restrição.
  • Um segmento complexo é considerado tão específico quanto um segmento de parâmetro com uma restrição.
  • Os parâmetros catch-all são os menos específicos. Confira catch-all na Referência de modelos de rota para obter informações importantes sobre rotas catch-all.

Confira o código-fonte no GitHub para obter uma referência de valores exatos.

Conceitos de geração de URL

Geração de URL:

  • É o processo pelo qual o roteamento pode criar um caminho de URL de acordo com um conjunto de valores de rota.
  • Permite uma separação lógica entre os pontos de extremidade e as URLs que os acessam.

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:

  1. Um endereço é associado a uma lista de pontos de extremidade que correspondem ao endereço.
  2. O RoutePattern de cada ponto de extremidade é avaliado até que seja encontrado um padrão de rota correspondente aos valores fornecidos. A saída resultante é combinada com as outras partes de URI fornecidas ao gerador de link e é retornada.

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.

Exemplo de middleware

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

Referência de modelo de rota

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 **:

  • Pode ser usado como prefixo para um parâmetro de rota a ser associado ao restante do URI.
  • São chamados de parâmetros catch-all. Por exemplo, blog/{**slug}:
    • Corresponde a qualquer URI que comece com /blog e tenha qualquer valor depois dele.
    • O valor a seguir /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:

  • Uma rota catch-all, por exemplo, {**slug}"
  • A rota catch-all não corresponde às solicitações que deveria corresponder.
  • Remover outras rotas faz com que a rota catch-all comece a funcionar.

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 é:

  • Um parâmetro de rota com um valor padrão sempre produz um valor.
  • Um parâmetro opcional só tem um valor quando um valor é fornecido pela URL de solicitaçã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.

Segmentos complexos

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:

  • O primeiro literal, da direita para a esquerda, é c. Portanto, /abcd é pesquisado pela direita e localiza /ab|c|d.
  • Tudo à direita (d) agora corresponde ao parâmetro de rota {d}.
  • O próximo literal, da direita para a esquerda, é a. Portanto, /ab|c|d é pesquisado começando de onde paramos e, em seguida, a é encontrado em /|a|b|c|d.
  • O valor à direita (b) agora corresponde ao parâmetro de rota {b}.
  • Não há texto restante nem modelo de rota restante. Portanto, esta é uma correspondência.

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:

  • O primeiro literal, da direita para a esquerda, é c. Portanto, /aabcd é pesquisado pela direita e localiza /aab|c|d.
  • Tudo à direita (d) agora corresponde ao parâmetro de rota {d}.
  • O próximo literal, da direita para a esquerda, é a. Portanto, /aab|c|d é pesquisado começando de onde paramos e, em seguida, a é encontrado em /a|a|b|c|d.
  • O valor à direita (b) agora corresponde ao parâmetro de rota {b}.
  • Neste ponto, há texto restante a, mas o algoritmo ficou fora do modelo de rota para analisar. Portanto, não é uma correspondência.

Como o algoritmo correspondente é sem greedy:

  • Ele corresponde à menor quantidade de texto possível em cada etapa.
  • Qualquer caso em que o valor delimitador apareça dentro dos valores de parâmetro resulta em não correspondência.

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.

Referência de restrição de rota

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.

Expressões regulares em restrições

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:

  • Substitua os caracteres \ fornecidos na cadeia de caracteres pelos caracteres \\ no arquivo de origem do C# para escapar do caractere de escape da cadeia de caracteres \.
  • Literais de cadeia de caracteres textuais.

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.

Restrições de rota personalizadas

É 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:

  • Impede 0 no segmento {id} da rota.
  • É mostrado para fornecer um exemplo básico de implementação de uma restrição personalizada. Não deve ser usado em um aplicativo de produção.

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:

  • Não requer uma restrição personalizada.
  • Retorna um erro mais descritivo quando o parâmetro de rota inclui 0.

Referência de parâmetro de transformador

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:

Referência de geração de URL

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.

Solução de problemas de geração de URL com log

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.

Endereços

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:

  • Usando o nome do ponto de extremidade (string) como o endereço:
    • Fornece funcionalidade semelhante ao nome da rota do MVC.
    • Usa o tipo de metadados IEndpointNameMetadata.
    • Resolve a cadeia de caracteres fornecida em relação aos metadados de todos os pontos de extremidade registrados.
    • Gera uma exceção na inicialização, se vários pontos de extremidade usarem o mesmo nome.
    • Recomendado para uso geral fora dos controladores e Razor Pages.
  • Usando os valores de rota (RouteValuesAddress) como o endereço:
    • Fornece uma funcionalidade semelhante à geração de URL herdada dos controladores e Razor Pages.
    • Muito difícil de estender e depurar.
    • Fornece a implementação usada por 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:

  • O esquema de nome do ponto de extremidade executa uma pesquisa de dicionário básica.
  • O esquema de valores de rota tem um subconjunto de conjunto mais complexo.

Valores ambientes e valores explícitos

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:

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.
  • Url obtém o IUrlHelper.
  • Action gera uma URL com um caminho absoluto para um método de ação. A URL contém o nome 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.
  • IUrlHelper.Action sempre copia os valores de rota atuais action e controller como valores explícitos, a menos que sejam substituídos pelo desenvolvedor.
  • IUrlHelper.Page sempre copia o valor de rota atual 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.

Processo de geração de URL

Depois que o conjunto de pontos de extremidade candidatos for encontrado, o algoritmo de geração de URL:

  • Processa os pontos de extremidade iterativamente.
  • Retorna o primeiro resultado bem-sucedido.

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:

  • Ao vincular-se a outra ação no mesmo controlador, o nome do controlador não precisa ser especificado.
  • Ao vincular-se a outro controlador na mesma área, o nome da área não precisa ser especificado.
  • Ao vincular-se ao mesmo método de ação, os valores de rota não precisam ser especificados.
  • Ao vincular-se a outra parte do aplicativo, não convém carregar valores de rota que não têm significado nessa parte do aplicativo.

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:

  • Se os valores explícitos contiverem um valor para id, o valor ambiente para id será ignorado. Os valores ambientes para controller e action podem ser usados.
  • Se os valores explícitos contiverem um valor para 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.
  • Se os valores explícitos contiverem um valor para 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:

  • Existe uma hierarquia de valores de rota.
  • Eles não são exibidos no modelo.

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:

  • Os nomes de valor necessários são combinados com os parâmetros de rota e processados da esquerda para a direita.
  • Para cada parâmetro, o valor ambiente e o valor explícito são comparados:
    • Se o valor ambiente e o valor explícito forem iguais, o processo continuará.
    • Se o valor ambiente estiver presente e o valor explícito não, o valor ambiente será usado ao gerar a URL.
    • Se o valor ambiente não estiver presente e o valor explícito sim, rejeite o valor ambiente e todos os valores ambientes subsequentes.
    • Se o valor ambiente e o valor explícito estiverem presentes e os dois valores forem diferentes, rejeite o valor ambiente e todos os valores ambientes subsequentes.

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:

  • Da esquerda para a direita.
  • O valor aceito de cada parâmetro é substituído.
  • Com os seguintes casos especiais:
    • Se faltar um valor nos valores aceitos e o parâmetro tiver um valor padrão, o valor padrão será usado.
    • Se faltar um valor nos valores aceitos e o parâmetro for opcional, o processamento continuará.
    • Se qualquer parâmetro de rota à direita de um parâmetro opcional ausente tiver um valor, a operação falhará.
    • Os parâmetros com valor padrão contíguos e parâmetros opcionais são recolhidos sempre que possível.

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

Problemas com a invalidação de valor de rota

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:

  • No modelo de rota "default", o parâmetro de rota culture fica à esquerda de controller. Portanto, as alterações em controller não invalidarão culture.
  • No modelo de rota "blog", considera-se que o parâmetro de rota culture fica à direita de controller, que aparece nos valores necessários.

Como configurar metadados de ponto de extremidade

Os links a seguir fornecem informações sobre como configurar metadados de ponto de extremidade:

Correspondência de host em rotas com RequireHost

RequireHost aplica uma restrição à rota que exige o host especificado. O parâmetro RequireHostou [Host] pode ser:

  • Host: www.domain.com, corresponde www.domain.com a qualquer porta.
  • Host com curinga: *.domain.com, corresponde www.domain.com, subdomain.domain.com ou www.subdomain.domain.com a qualquer porta.
  • Porta: *:5000, corresponde a porta 5000 a qualquer host.
  • Host e porta: 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:

  • O atributo na ação será usado.
  • O atributo do controlador será ignorado.

Diretrizes de desempenho para roteamento

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:

  • Os desenvolvedores eliminam o código do aplicativo como a origem do problema.
  • É comum supor que o roteamento é a causa.

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:

  • Intercale cada middleware com uma cópia do middleware de tempo mostrado no código anterior.
  • Adicione um identificador exclusivo para correlacionar os dados de tempo com o código.

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

Recursos de roteamento possivelmente caros

A lista a seguir fornece alguns insights sobre recursos de roteamento relativamente caros, em comparação a modelos de rota básicos:

  • Expressões regulares: é possível gravar expressões regulares complexas ou que tenham um tempo de execução prolongada com um pequeno valor de entrada.
  • Segmentos complexos ({x}-{y}-{z}):
    • São significativamente mais caros do que analisar um segmento de caminho de URL regular.
    • Resulta na alocação de muito mais subcadeias de caracteres.
    • A lógica de segmento complexa não foi atualizada na atualização de desempenho de roteamento do ASP.NET Core 3.0.
  • Acesso a dados síncronos: muitos aplicativos complexos têm acesso ao banco de dados como parte do roteamento. O roteamento do ASP.NET Core 2.2 e anterior pode não fornecer os pontos de extensibilidade certos para dar suporte ao roteamento de acesso ao banco de dados. Por exemplo, IRouteConstraint e IActionConstraint são síncronos. Os pontos de extensibilidade como MatcherPolicy e EndpointSelectorContext são assíncronos.

Diretrizes para criadores de bibliotecas

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.

Definir pontos de extremidade

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:

  • Permite a capacidade de composição de metadados.
  • É direcionada por uma variedade de métodos de extensão.

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.

Como criar o middleware integrado ao roteamento

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:

  • Torne-os acessíveis como atributos.
  • A maioria dos usuários está familiarizada com a aplicação de atributos.

Declarar um tipo de metadados como uma interface adiciona outra camada de flexibilidade:

  • As interfaces podem ser formadas.
  • Os desenvolvedores podem declarar seus próprios tipos que combinam várias políticas.

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:

  • Não procure apenas a presença de um tipo de metadados.
  • Defina uma propriedade nos metadados e verifique a propriedade.

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:

  • Pontos de extremidade sem uma política especificada.
  • Solicitações que não correspondem a um ponto de extremidade.

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.

Depurar diagnóstico

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