Roteamento
Esta página foi traduzida automaticamente do inglês. Ler o original
O Router é o primeiro passo na construção do servidor. Ele é responsável por armazenar objetos Route, que são pontos de extremidade que mapeiam URLs e seus métodos para ações executadas pelo servidor. Cada ação é responsável por receber uma requisição e entregar uma resposta ao cliente.
As rotas são pares de expressões de caminho (“padrão de caminho”) e o método HTTP que podem escutar. Quando uma requisição é feita ao servidor, ele tentará encontrar uma rota que corresponda à requisição recebida, então chamará a ação dessa rota e entregará a resposta resultante ao cliente.
Existem várias maneiras de definir rotas no Sisk: elas podem ser estáticas, dinâmicas ou auto‑escanadas, definidas por atributos, ou diretamente no objeto Router.
Router mainRouter = new Router();
// mapeia a rota GET / para a ação a seguir
mainRouter.MapGet("/", request => {
return new HttpResponse("Hello, world!");
});Para entender o que uma rota pode fazer, precisamos entender o que uma requisição pode fazer. Um HttpRequest conterá tudo o que você precisa. O Sisk também inclui alguns recursos extras que aceleram o desenvolvimento geral.
Para cada ação recebida pelo servidor, um delegate do tipo RouteAction será chamado. Esse delegate contém um parâmetro que mantém um HttpRequest com todas as informações necessárias sobre a requisição recebida pelo servidor. O objeto resultante desse delegate deve ser um HttpResponse ou um objeto que mapeie para ele através de implicit response types.
Correspondência de rotas #
Quando uma requisição é recebida pelo servidor HTTP, o Sisk procura uma rota que satisfaça a expressão do caminho recebido pela requisição. A expressão é sempre testada entre a rota e o caminho da requisição, sem considerar a string de consulta.
A primeira rota cujo caminho e método correspondam à requisição a trata. Quando nenhuma rota corresponde a essa requisição, a resposta de Router.NotFoundErrorHandler é retornada ao cliente. Quando o padrão de caminho corresponde, mas o método HTTP não, a resposta de Router.MethodNotAllowedErrorHandler é enviada de volta ao cliente. A ordem em que as rotas são testadas é descrita em precedência de correspondência.
O Sisk verifica a possibilidade de colisões de rotas entre rotas estáticas. Quando o servidor inicia, o Sisk procura rotas estáticas cujos padrões de caminho e métodos se sobrepõem, e lança uma exceção se alguma for encontrada. Na mesma verificação, o Sisk lança um ArgumentException se duas ou mais rotas compartilharem o mesmo Route.Name. Ambas as verificações são controladas por Router.CheckForRouteCollisions, que está habilitado por padrão.
Precedência de correspondência #
Cada rota é estática ou dinâmica, conforme indicado por Route.IsStatic:
- Rotas estáticas seguem estritamente seu padrão de caminho, como
/users/<id>. Rotas criadas com métodosMap*, instânciasRoutee atributos de rota são estáticas. - Rotas dinâmicas interpretam seu caminho à sua maneira e podem corresponder a caminhos que não podem ser descritos por um padrão de caminho. RegexRoute, PrefixRoute (usado por file servers) e custom matching routes são dinâmicas.
Quando o servidor inicia, o router reordena suas rotas uma única vez de modo que toda rota estática seja testada antes de toda rota dinâmica. Dentro de cada grupo, as rotas mantêm a ordem de registro. Isso significa que uma rota estática sempre tem precedência sobre uma rota dinâmica, independentemente da ordem em que foram definidas:
// dinâmico: serve qualquer caminho GET do diretório "wwwroot"
mainRouter.MapFileSystem("/", "wwwroot");
// estático: trata GET /api/status, mesmo que tenha sido definido após o servidor de arquivos
mainRouter.MapGet("/api/status", request => new HttpResponse("ok"));No exemplo acima, o servidor de arquivos atua como fallback para qualquer requisição GET que não seja tratada por uma rota estática.
Os dois grupos também diferem em como são validados e transformados:
| Rotas estáticas | Rotas dinâmicas | |
|---|---|---|
| Ordem de correspondência | Primeira, na ordem de registro | Após todas as rotas estáticas, na ordem de registro |
| Checagem de colisão de rotas | Sim | Não |
| Router.Prefix é aplicado | Sim | Não |
Aviso
Rotas dinâmicas não são verificadas quanto a colisões, pois o Sisk não pode saber quais caminhos elas correspondem. Quando duas rotas dinâmicas podem corresponder à mesma requisição, a que foi definida primeiro vence. Por exemplo, um servidor de arquivos mapeado para / definido antes de um servidor de arquivos mapeado para /static torna o segundo inacessível.
Importante
Em versões anteriores à 1.7, as rotas eram testadas apenas na ordem de registro, e rotas regex podiam ter precedência sobre rotas estáticas definidas depois delas. Se sua aplicação depende de uma rota regex que trata um caminho também correspondido por uma rota estática, mova essa lógica para a rota estática ou restrinja o padrão de caminho estático.
Criando rotas usando padrões de caminho #
Para novas aplicações, prefira os métodos Map*. Eles mantêm o método HTTP visível no ponto de chamada e correspondem à API atual do Router. Os métodos mais antigos SetRoute ainda existem como wrappers de compatibilidade, mas novos exemplos devem usar Map, MapGet, MapPost, MapPut, MapDelete, MapPatch, MapAny, MapOptions ou MapHead.
// Métodos Map* são a forma usual de definir rotas específicas por método.
mainRouter.MapGet("/hey/<name>", (request) =>
{
string name = request.RouteParameters["name"].GetString();
return new HttpResponse($"Hello, {name}");
});
mainRouter.MapPost("/form", (request) =>
{
var formData = request.GetFormContent();
return new HttpResponse(); // 200 ok vazio
});
// Map também pode receber uma instância de Route quando você precisa de opções de rota.
mainRouter.Map(Route.Get("/image.png", (request) =>
{
var imageStream = File.OpenRead("image.png");
return new HttpResponse()
{
// o interior do StreamContent
// o stream é descartado após o envio
// da resposta.
Content = new StreamContent(imageStream)
};
}));
// múltiplos parâmetros
mainRouter.MapGet("/hey/<name>/surname/<surname>", (request) =>
{
string name = request.RouteParameters["name"].GetString();
string surname = request.RouteParameters["surname"].GetString();
return new HttpResponse($"Hello, {name} {surname}!");
});A propriedade RouteParameters de HttpRequest contém todas as informações sobre as variáveis de caminho da requisição recebida.
Todo caminho recebido pelo servidor é normalizado antes da execução do teste de padrão de caminho, seguindo estas regras:
- Todos os segmentos vazios são removidos do caminho, por exemplo:
////foo//bartorna‑se/foo/bar. - A correspondência de caminho é sensível a maiúsculas/minúsculas, a menos que Router.MatchRoutesIgnoreCase esteja definido como
true.
As propriedades Query e RouteParameters de HttpRequest retornam um objeto StringValueCollection, onde cada propriedade indexada devolve um StringValue não nulo, que pode ser usado como uma opção/monad para converter seu valor bruto em um objeto gerenciado.
mainRouter.MapGet("/user/<id>", (request) =>
{
Guid id = request.RouteParameters["id"].GetGuid();
return new HttpResponse($"User id: {id}");
});Nota
Caminhos têm sua barra final / ignorada tanto na requisição quanto no caminho da rota; ou seja, se você tentar acessar uma rota definida como /index/page também poderá acessá‑la usando /index/page/.
Você também pode forçar URLs a terminarem com / habilitando HttpServerConfiguration.ForceTrailingSlash.
Criando rotas usando instâncias de classe #
Você também pode definir rotas dinamicamente usando reflexão com o atributo RouteAttribute. Dessa forma, a instância de uma classe cujos métodos implementam esse atributo terá suas rotas definidas no router de destino.
Para que um método seja definido como rota, ele deve ser marcado com um RouteAttribute, como o próprio atributo ou um RouteGetAttribute. O método pode ser estático, de instância, público ou privado. Use MapInstance quando quiser mapear métodos de rota de instância e estáticos de um objeto. Use MapType quando quiser mapear apenas métodos de rota estáticos de um tipo.
public class MyController
{
// corresponderá ao GET /
[RouteGet]
HttpResponse Index(HttpRequest request)
{
HttpResponse res = new HttpResponse();
res.Content = new StringContent("Index!");
return res;
}
// métodos estáticos também funcionam
[RouteGet("/hello")]
static HttpResponse Hello(HttpRequest request)
{
HttpResponse res = new HttpResponse();
res.Content = new StringContent("Hello world!");
return res;
}
}A linha abaixo definirá tanto os métodos Index quanto Hello de MyController como rotas, pois ambos estão marcados como rotas, e uma instância da classe foi fornecida, não seu tipo. Se seu tipo tivesse sido fornecido em vez de uma instância, apenas os métodos estáticos seriam definidos.
var myController = new MyController();
mainRouter.MapInstance(myController);Para mapear apenas métodos de rota estáticos de um tipo, use:
mainRouter.MapType<MyController>();Desde a versão 0.16 do Sisk, é possível habilitar AutoScan, que buscará classes definidas pelo usuário que implementam RouterModule e as associará automaticamente ao router. Isso não é suportado com compilação AOT.
mainRouter.AutoScanModules<ApiController>();A instrução acima buscará todos os tipos que implementam ApiController, mas não o próprio tipo. Os dois parâmetros opcionais indicam como o método buscará esses tipos. O primeiro argumento indica o Assembly onde os tipos serão buscados e o segundo indica a forma como os tipos serão definidos.
Rotas Regex #
Em vez de usar os métodos padrão de correspondência de caminho HTTP, você pode definir uma rota cujo caminho é interpretado como expressão regular com a classe RegexRoute. Rotas regex são rotas dinâmicas.
mainRouter.Map(new RegexRoute(RouteMethod.Get, @"\/[a-z]+\/", request =>
{
return new HttpResponse("hello, world");
}));Você também pode capturar grupos do padrão regex dentro do conteúdo de HttpRequest.RouteParameters:
public class MyController
{
[RegexRoute(RouteMethod.Get, @"/uploads/(?<filename>.*\.(jpeg|jpg|png))")]
static HttpResponse RegexRoute(HttpRequest request)
{
string filename = request.RouteParameters["filename"].GetString();
return new HttpResponse().WithContent($"Acessing file {filename}");
}
}Nota
A expressão regular não é ancorada automaticamente: um padrão como /uploads também corresponde a /files/uploads/a. Use ^ e $ para corresponder ao caminho inteiro.
Importante
Desde o Sisk 1.7, a propriedade Route.UseRegex não pode mais ser definida. Ela está obsoleta e apenas indica se a rota é uma RegexRoute. A propriedade UseRegex de atributos de rota, como [Route(RouteMethod.Get, "...", UseRegex = true)], e o atributo [RegexRoute] continuam funcionando.
Para migrar código que criava rotas regex com Route.UseRegex, substitua a rota por um RegexRoute:
// antes da 1.7
mainRouter.Map(new Route(RouteMethod.Get, @"^/items/(?<id>\d+)$", action) { UseRegex = true });
// 1.7 e posteriores
mainRouter.Map(new RegexRoute(RouteMethod.Get, @"^/items/(?<id>\d+)$", action));Rotas Prefix #
Uma PrefixRoute corresponde ao seu caminho e a todos os caminhos aninhados sob ele, comparando segmentos de caminho completos. É a rota usada por Router.MapFileSystem. Rotas prefix são rotas dinâmicas.
// corresponde a /docs, /docs/ e /docs/any/nested/path, mas não a /docsfoo
mainRouter.Map(new PrefixRoute(RouteMethod.Get, "/docs", null, request =>
{
return new HttpResponse($"Requested {request.Path}");
}, null));Correspondência de rotas personalizada #
Você pode criar suas próprias regras de correspondência herdando Route e sobrescrevendo Route.Match. O método recebe o caminho da requisição e o router que está correspondendo a rota, e retorna um RouteMatch com o resultado e, opcionalmente, os parâmetros de rota capturados.
O router valida o método HTTP somente após o Match ter sucesso, portanto Match deve testar apenas o caminho. Isso mantém as respostas 405 Method Not Allowed e OPTIONS funcionando para rotas personalizadas.
// corresponde a qualquer caminho que termina com a extensão de arquivo fornecida, como /reports/2024.csv
public sealed class ExtensionRoute : Route
{
public ExtensionRoute(string extension, RouteAction action)
: base(RouteMethod.Get, extension, action)
{
}
// esta rota não segue um padrão de caminho
public override bool IsStatic => false;
// o caminho não é um padrão de caminho, portanto não pode ser redirecionado para sua forma com barra final
public override bool AllowRewrites => false;
public override RouteMatch Match(string requestPath, Router router)
{
StringComparison comparison = router.MatchRoutesIgnoreCase
? StringComparison.OrdinalIgnoreCase
: StringComparison.Ordinal;
if (!requestPath.EndsWith(Path, comparison))
return RouteMatch.NotMatched;
var parameters = new NameValueCollection
{
["file"] = requestPath[..^Path.Length]
};
return new RouteMatch(true, parameters);
}
}mainRouter.Map(new ExtensionRoute(".csv", request =>
{
string file = request.RouteParameters["file"].GetString();
return new HttpResponse($"Exporting {file} as CSV");
}));Ao sobrescrever Match, tenha em mente:
- Sobrescreva Route.IsStatic para retornar
falsesempre que a rota não seguir estritamente um padrão de caminho. Caso contrário, o router trata seuPathcomo um padrão de caminho ao verificar colisões e ao aplicar Router.Prefix, o que pode gerar colisões falsas ou caminhos inválidos. - Sobrescreva Route.AllowRewrites para retornar
falsequando o caminho da rota não puder ser redirecionado para sua forma com barra final por HttpServerConfiguration.ForceTrailingSlash. Matché chamado para cada requisição que chega à rota, possivelmente de múltiplas threads simultaneamente. Evite alocações quando a rota não corresponder e mantenha qualquer estado em cache thread‑safe. Retorne RouteMatch.NotMatched para correspondências malsucedidas.- Respeite Router.MatchRoutesIgnoreCase através do parâmetro
router.
Prefixando rotas #
Você pode prefixar todas as rotas em uma classe ou módulo com o atributo RoutePrefix e definir o prefixo como uma string.
Veja o exemplo abaixo usando a arquitetura BREAD (Browse, Read, Edit, Add and Delete):
[RoutePrefix("/api/users")]
public class UsersController
{
// GET /api/users
[RouteGet]
public async Task<HttpResponse> Browse()
{
...
}
// GET /api/users/<id>
[RouteGet("/<id>")]
public async Task<HttpResponse> Read()
{
...
}
// PATCH /api/users/<id>
[RoutePatch("/<id>")]
public async Task<HttpResponse> Edit()
{
...
}
// POST /api/users
[RoutePost]
public async Task<HttpResponse> Add()
{
...
}
// DELETE /api/users/<id>
[RouteDelete("/<id>")]
public async Task<HttpResponse> Delete()
{
...
}
}No exemplo acima, o parâmetro HttpResponse é omitido em favor de ser usado através do contexto global HttpContext.Current. Leia mais na seção que se segue.
Rotas sem parâmetro de requisição #
Rotas podem ser definidas sem o parâmetro HttpRequest e ainda assim ser possível obter a requisição e seus componentes no contexto da requisição. Vamos considerar uma abstração ControllerBase que serve como base para todos os controladores de uma API, e que fornece a propriedade Request para obter o HttpRequest atual.
public abstract class ControllerBase
{
// obtém a requisição da thread atual
public HttpRequest Request { get => HttpContext.Current.Request; }
// a linha abaixo, quando chamada, obtém o banco de dados da sessão HTTP atual,
// ou cria um novo se não existir
public DbContext Database { get => HttpContext.Current.RequestBag.GetOrAdd<DbContext>(); }
}E para que todos os seus descendentes possam usar a sintaxe de rota sem o parâmetro de requisição:
[RoutePrefix("/api/users")]
public class UsersController : ControllerBase
{
[RoutePost]
public async Task<HttpResponse> Create()
{
// lê os dados JSON da requisição atual
UserCreationDto? user = await Request.GetJsonContentAsync<UserCreationDto>();
...
Database.Users.Add(user);
return new HttpResponse(201);
}
}Mais detalhes sobre o contexto atual e injeção de dependência podem ser encontrados no tutorial de dependency injection.
Rotas de qualquer método #
Você pode definir uma rota para ser correspondida apenas pelo seu caminho e ignorar o método HTTP. Isso pode ser útil para validar o método dentro do callback da rota.
// corresponderá a / em qualquer método HTTP
mainRouter.MapAny("/", callbackFunction);RouteMethod.Any inclui GET, POST, PUT, PATCH, DELETE, QUERY, HEAD e OPTIONS.
Rotas do método QUERY #
O Sisk suporta o método HTTP QUERY, um método seguro e idempotente que transporta a consulta no corpo da requisição. Use Router.MapQuery, Route.Query, RouteMethod.Query ou o atributo RouteQuery para definir essas rotas:
mainRouter.MapQuery("/search", request =>
{
string filter = request.Body;
return new HttpResponse($"Searching for {filter}");
});public class SearchController
{
[RouteQuery("/products")]
static HttpResponse SearchProducts(HttpRequest request)
{
return new HttpResponse($"Searching for {request.Body}");
}
}O cabeçalho Accept-Query, que anuncia os tipos de mídia aceitos em requisições QUERY, está disponível através de HttpHeaderCollection.AcceptQuery e HttpKnownHeaderNames.AcceptQuery.
Rotas de qualquer caminho #
Rotas de qualquer caminho testam qualquer caminho recebido pelo servidor HTTP, sujeito ao método da rota ser testado. Se o método da rota for RouteMethod.Any e a rota usar Route.AnyPath em sua expressão de caminho, essa rota ouvirá todas as requisições do servidor HTTP, e nenhuma outra rota poderá ser definida.
// a rota a seguir corresponderá a todas as requisições POST
mainRouter.Map(RouteMethod.Post, Route.AnyPath, callbackFunction);Ignorando diferenciação de maiúsculas/minúsculas na correspondência de rotas #
Por padrão, a interpretação das rotas com as requisições diferencia maiúsculas de minúsculas. Para fazer com que ignore isso, habilite esta opção:
mainRouter.MatchRoutesIgnoreCase = true;Isso também habilitará a opção RegexOptions.IgnoreCase para rotas regex e comparação sem diferenciação de maiúsculas/minúsculas para rotas prefix.
Manipulador de callback Not Found (404) #
Você pode criar um callback customizado para quando uma requisição não corresponder a nenhuma rota conhecida.
mainRouter.NotFoundErrorHandler = () =>
{
return new HttpResponse(404)
{
// Since v0.14
Content = new HtmlContent("<h1>Not found</h1>")
// older versions
Content = new StringContent("<h1>Not found</h1>", Encoding.UTF8, "text/html")
};
};Manipulador de callback Method not allowed (405) #
Você também pode criar um callback customizado para quando uma requisição corresponde ao seu caminho, mas não ao método.
mainRouter.MethodNotAllowedErrorHandler = (context) =>
{
return new HttpResponse(405)
{
Content = new StringContent($"Method not allowed for this route.")
};
};Tratamento de Erros #
Exceções podem ser lançadas dentro do ciclo de vida de uma requisição, que vai do manipulador de requisição pré‑execução, passando pela ação do router, até os manipuladores de requisição pós‑execução e manipuladores de valor. Essas exceções são gerenciadas pelo mecanismo:
- Se HttpServerConfiguration.ThrowExceptions for
true, as exceções serão lançadas normalmente e não serão capturadas pelo Sisk, e o servidor HTTP pode ser interrompido se a exceção não for tratada. - Se HttpServerConfiguration.ThrowExceptions for
false, as exceções serão capturadas e tratadas pelo Sisk. Depois disso, seRouter.CallbackErrorHandlerestiver definido, ele será chamado com a exceção capturada e o contexto da requisição, e não será encaminhado para a saída de erro padrão. SeRouter.CallbackErrorHandlernão estiver definido, a exceção será encaminhada para a saída de erro padrão, e o cliente receberá uma resposta HTTP 500. Se a saída de erro padrão não estiver definida, o erro será ignorado silenciosamente.
Nota: dentro de Router.CallbackErrorHandler, você pode definir o modo de log para erros, log de acesso, ambos ou nenhum, e alterar o comportamento padrão de escrita de logs:
router.CallbackErrorHandler = (ex, ctx) =>
{
ctx.LogMode = LogOutput.Both; // sobrescreve o modo de log para registrar o erro tanto no log de acesso quanto no de erro
}Manipulador interno de erro #
Callbacks de rotas podem lançar erros durante a execução do servidor. Se não forem tratados corretamente, o funcionamento geral do servidor HTTP pode ser interrompido. O router possui um callback para quando um callback de rota falha e impede a interrupção do serviço.
Esse método só é acessível quando ThrowExceptions está definido como false.
mainRouter.CallbackErrorHandler = (ex, context) =>
{
return new HttpResponse(500)
{
Content = new StringContent($"Error: {ex.Message}")
};
};