Enrutamiento
Esta página fue traducida automáticamente del inglés. Leer el original
El Router es el primer paso para construir el servidor. Es responsable de albergar objetos Route, que son puntos finales que asignan URLs y sus métodos a acciones ejecutadas por el servidor. Cada acción se encarga de recibir una solicitud y entregar una respuesta al cliente.
Las rutas son pares de expresiones de ruta (“patrón de ruta”) y el método HTTP al que pueden escuchar. Cuando se realiza una solicitud al servidor, éste intentará encontrar una ruta que coincida con la solicitud recibida, luego llamará a la acción de esa ruta y entregará la respuesta resultante al cliente.
Existen múltiples formas de definir rutas en Sisk: pueden ser estáticas, dinámicas o auto‑exploradas, definidas por atributos, o directamente en el objeto Router.
Router mainRouter = new Router();
// asigna la ruta GET / a la siguiente acción
mainRouter.MapGet("/", request => {
return new HttpResponse("Hello, world!");
});Para entender lo que una ruta es capaz de hacer, necesitamos entender lo que una solicitud es capaz de hacer. Un HttpRequest contendrá todo lo que necesitas. Sisk también incluye algunas características extra que aceleran el desarrollo en general.
Para cada acción recibida por el servidor, se llamará a un delegado del tipo RouteAction. Este delegado contiene un parámetro que lleva un HttpRequest con toda la información necesaria sobre la solicitud recibida por el servidor. El objeto resultante de este delegado debe ser un HttpResponse o un objeto que se mapee a él mediante tipos de respuesta implícitos.
Matching routes #
Cuando una solicitud es recibida por el servidor HTTP, Sisk busca una ruta que satisfaga la expresión del camino recibido por la solicitud. La expresión siempre se prueba entre la ruta y el camino de la solicitud, sin considerar la cadena de consulta.
La primera ruta cuyo camino y método coincidan con la solicitud la maneja. Cuando no se encuentra ninguna ruta que coincida con esa solicitud, se devuelve al cliente la respuesta de Router.NotFoundErrorHandler. Cuando el patrón de ruta coincide, pero el método HTTP no, se envía al cliente la respuesta de Router.MethodNotAllowedErrorHandler. El orden en que se prueban las rutas se describe en matching precedence.
Sisk verifica la posibilidad de colisiones de rutas entre rutas estáticas. Cuando el servidor se inicia, Sisk buscará rutas estáticas cuyos patrones de ruta y métodos se solapen, y lanzará una excepción si se encuentra alguna. En la misma comprobación, Sisk lanza una ArgumentException si dos o más rutas comparten el mismo Route.Name. Ambas comprobaciones están controladas por Router.CheckForRouteCollisions, que está habilitada por defecto.
Matching precedence #
Cada ruta es estática o dinámica, según lo indique Route.IsStatic:
- Rutas estáticas siguen estrictamente su patrón de ruta, como
/users/<id>. Las rutas creadas con los métodosMap*, instancias deRoutey atributos de ruta son estáticas. - Rutas dinámicas interpretan su camino a su manera y pueden coincidir con caminos que no pueden describirse mediante un patrón de ruta. RegexRoute, PrefixRoute (usado por file servers) y rutas de coincidencia personalizadas son dinámicas.
Cuando el servidor se inicia, el router reordena sus rutas una vez de modo que cada ruta estática se pruebe antes que cualquier ruta dinámica. Dentro de cada grupo, las rutas conservan su orden de registro. Esto significa que una ruta estática siempre tiene precedencia sobre una ruta dinámica, sin importar el orden en que fueron definidas:
// dinámico: sirve cualquier ruta GET del directorio "wwwroot"
mainRouter.MapFileSystem("/", "wwwroot");
// estático: maneja GET /api/status, aunque se definió después del servidor de archivos
mainRouter.MapGet("/api/status", request => new HttpResponse("ok"));En el ejemplo anterior, el servidor de archivos actúa como respaldo para cualquier solicitud GET que no sea manejada por una ruta estática.
Los dos grupos también difieren en cómo se validan y transforman:
| Rutas estáticas | Rutas dinámicas | |
|---|---|---|
| Orden de coincidencia | Primero, en orden de registro | Después de todas las rutas estáticas, en orden de registro |
| Comprobación de colisión de rutas | Sí | No |
| Se aplica Router.Prefix | Sí | No |
Advertencia
Las rutas dinámicas no se comprueban para colisiones, ya que Sisk no puede saber qué caminos coinciden. Cuando dos rutas dinámicas pueden coincidir con la misma solicitud, gana la que se definió primero. Por ejemplo, un servidor de archivos mapeado a / definido antes que un servidor de archivos mapeado a /static hace que el segundo sea inaccesible.
Importante
En versiones anteriores a la 1.7, las rutas se probaban solo en su orden de registro, y las rutas regex podían tener precedencia sobre rutas estáticas definidas después de ellas. Si tu aplicación depende de que una ruta regex maneje un camino que también coincide con una ruta estática, mueve esa lógica a la ruta estática o estrecha el patrón de ruta estática.
Creating routes using path patterns #
Para aplicaciones nuevas, prefiere los métodos Map*. Mantienen visible el método HTTP en el sitio de llamada y coinciden con la API actual del Router. Los métodos más antiguos SetRoute siguen existiendo como envoltorios de compatibilidad, pero los ejemplos nuevos deben usar Map, MapGet, MapPost, MapPut, MapDelete, MapPatch, MapAny, MapOptions o MapHead.
// Los métodos Map* son la forma habitual de definir rutas 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 vacío
});
// Map también puede recibir una instancia de Route cuando necesitas opciones de ruta.
mainRouter.Map(Route.Get("/image.png", (request) =>
{
var imageStream = File.OpenRead("image.png");
return new HttpResponse()
{
// el contenido interno de StreamContent
// el stream se libera después de enviar
// la respuesta.
Content = new StreamContent(imageStream)
};
}));
// múltiples 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}!");
});La propiedad RouteParameters de HttpRequest contiene toda la información sobre las variables de ruta del request recibido.
Cada camino recibido por el servidor se normaliza antes de ejecutar la prueba del patrón de ruta, siguiendo estas reglas:
- Todos los segmentos vacíos se eliminan del camino, por ejemplo:
////foo//barse convierte en/foo/bar. - La coincidencia de rutas es sensible a mayúsculas/minúsculas, a menos que Router.MatchRoutesIgnoreCase esté establecido en
true.
Las propiedades Query y RouteParameters de HttpRequest devuelven un objeto StringValueCollection, donde cada propiedad indexada devuelve un StringValue no nulo, que puede usarse como una opción/monada para convertir su valor bruto en un objeto gestionado.
El ejemplo a continuación lee el parámetro de ruta “id” y obtiene un Guid a partir de él. Si el parámetro no es un Guid válido, se lanza una excepción, y se devuelve un error 500 al cliente si el servidor no está manejando Router.CallbackErrorHandler.
mainRouter.MapGet("/user/<id>", (request) =>
{
Guid id = request.RouteParameters["id"].GetGuid();
return new HttpResponse($"User id: {id}");
});Nota
Los caminos ignoran su / final tanto en la solicitud como en la ruta, es decir, si intentas acceder a una ruta definida como /index/page también podrás acceder usando /index/page/.
También puedes forzar que las URLs terminen con / habilitando HttpServerConfiguration.ForceTrailingSlash.
Creating routes using class instances #
También puedes definir rutas dinámicamente usando reflexión con el atributo RouteAttribute. De esta forma, la instancia de una clase cuyas métodos implementen este atributo tendrá sus rutas definidas en el router objetivo.
Para que un método sea definido como ruta, debe estar marcado con un RouteAttribute, como el propio atributo o un RouteGetAttribute. El método puede ser estático, de instancia, público o privado. Usa MapInstance cuando quieras mapear métodos de ruta de instancia y estáticos de un objeto. Usa MapType cuando quieras mapear solo métodos de ruta estáticos de un tipo.
public class MyController
{
// coincidirá con GET /
[RouteGet]
HttpResponse Index(HttpRequest request)
{
HttpResponse res = new HttpResponse();
res.Content = new StringContent("Index!");
return res;
}
// los métodos estáticos también funcionan
[RouteGet("/hello")]
static HttpResponse Hello(HttpRequest request)
{
HttpResponse res = new HttpResponse();
res.Content = new StringContent("Hello world!");
return res;
}
}La línea siguiente definirá tanto los métodos Index como Hello de MyController como rutas, ya que ambos están marcados como rutas, y se ha proporcionado una instancia de la clase, no su tipo. Si se hubiera proporcionado su tipo en lugar de una instancia, solo se definirían los métodos estáticos.
var myController = new MyController();
mainRouter.MapInstance(myController);Para mapear solo los métodos de ruta estáticos de un tipo, usa:
mainRouter.MapType<MyController>();Desde la versión 0.16 de Sisk, es posible habilitar AutoScan, que buscará clases definidas por el usuario que implementen RouterModule y las asociará automáticamente con el router. Esto no es compatible con compilación AOT.
mainRouter.AutoScanModules<ApiController>();La instrucción anterior buscará todos los tipos que implementen ApiController pero no el propio tipo. Los dos parámetros opcionales indican cómo el método buscará esos tipos. El primer argumento implica el ensamblado donde se buscarán los tipos y el segundo indica la forma en que los tipos serán definidos.
Regex routes #
En lugar de usar los métodos de coincidencia de ruta HTTP por defecto, puedes definir una ruta cuyo camino se interprete como una expresión regular con la clase RegexRoute. Las rutas regex son rutas dinámicas.
mainRouter.Map(new RegexRoute(RouteMethod.Get, @"\/[a-z]+\/", request =>
{
return new HttpResponse("hello, world");
}));También puedes capturar grupos del patrón regex en el contenido 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
La expresión regular no se ancla automáticamente: un patrón como /uploads también coincide con /files/uploads/a. Usa ^ y $ para coincidir con todo el camino.
Importante
Desde Sisk 1.7, la propiedad Route.UseRegex ya no puede establecerse. Está obsoleta y solo indica si la ruta es una RegexRoute. La propiedad UseRegex de los atributos de ruta, como [Route(RouteMethod.Get, "...", UseRegex = true)], y el atributo [RegexRoute] siguen funcionando.
Para migrar código que creaba rutas regex con Route.UseRegex, reemplaza la ruta por una RegexRoute:
// antes de 1.7
mainRouter.Map(new Route(RouteMethod.Get, @"^/items/(?<id>\d+)$", action) { UseRegex = true });
// 1.7 y posteriores
mainRouter.Map(new RegexRoute(RouteMethod.Get, @"^/items/(?<id>\d+)$", action));Prefix routes #
Una PrefixRoute coincide con su camino y con cualquier camino anidado bajo él, comparando segmentos de camino completos. Es la ruta usada por Router.MapFileSystem. Las rutas prefijo son rutas dinámicas.
// coincide con /docs, /docs/ y /docs/any/nested/path, pero no con /docsfoo
mainRouter.Map(new PrefixRoute(RouteMethod.Get, "/docs", null, request =>
{
return new HttpResponse($"Requested {request.Path}");
}, null));Custom route matching #
Puedes crear tus propias reglas de coincidencia heredando de Route y sobrescribiendo Route.Match. El método recibe el camino de la solicitud y el router que está realizando la coincidencia, y devuelve un RouteMatch con el resultado y, opcionalmente, los parámetros de ruta capturados.
El router valida el método HTTP solo después de que Match tenga éxito, por lo que Match solo debe probar el camino. Esto mantiene funcionando las respuestas de 405 Method Not Allowed y OPTIONS para rutas personalizadas.
// coincide con cualquier camino que termine con la extensión dada, como /reports/2024.csv
public sealed class ExtensionRoute : Route
{
public ExtensionRoute(string extension, RouteAction action)
: base(RouteMethod.Get, extension, action)
{
}
// esta ruta no sigue un patrón de ruta
public override bool IsStatic => false;
// el camino no es un patrón de ruta, por lo que no puede redirigirse a su forma con 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");
}));Al sobrescribir Match, ten en cuenta:
- Sobrescribe Route.IsStatic para devolver
falsesiempre que la ruta no siga estrictamente un patrón de ruta. De lo contrario, el router trata suPathcomo un patrón de ruta al comprobar colisiones y aplicar Router.Prefix, lo que puede producir colisiones falsas o caminos inválidos. - Sobrescribe Route.AllowRewrites para devolver
falsecuando el camino de la ruta no pueda redirigirse a su forma con barra final mediante HttpServerConfiguration.ForceTrailingSlash. Matchse llama para cada solicitud que llega a la ruta, posiblemente desde varios hilos simultáneamente. Evita asignaciones cuando la ruta no coincide y mantén cualquier estado en caché seguro para hilos. Devuelve RouteMatch.NotMatched para coincidencias fallidas.- Respeta Router.MatchRoutesIgnoreCase a través del parámetro
router.
Prefixing routes #
Puedes anteponer todas las rutas en una clase o módulo con el atributo RoutePrefix y establecer el prefijo como una cadena.
Mira el ejemplo siguiente usando la arquitectura 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()
{
...
}
}En el ejemplo anterior, el parámetro HttpResponse se omite a favor de usarlo a través del contexto global HttpContext.Current. Lee más en la sección que sigue.
Routes without request parameter #
Las rutas pueden definirse sin el parámetro HttpRequest y aún así es posible obtener la solicitud y sus componentes en el contexto de la solicitud. Consideremos una abstracción ControllerBase que sirve como base para todos los controladores de una API, y que dicha abstracción proporciona la propiedad Request para obtener el HttpRequest actual.
public abstract class ControllerBase
{
// obtiene la solicitud del hilo actual
public HttpRequest Request { get => HttpContext.Current.Request; }
// la línea siguiente, cuando se llama, obtiene la base de datos de la sesión HTTP actual,
// o crea una nueva si no existe
public DbContext Database { get => HttpContext.Current.RequestBag.GetOrAdd<DbContext>(); }
}Y para que todos sus descendientes puedan usar la sintaxis de ruta sin el parámetro de solicitud:
[RoutePrefix("/api/users")]
public class UsersController : ControllerBase
{
[RoutePost]
public async Task<HttpResponse> Create()
{
// lee los datos JSON de la solicitud actual
UserCreationDto? user = await Request.GetJsonContentAsync<UserCreationDto>();
...
Database.Users.Add(user);
return new HttpResponse(201);
}
}Más detalles sobre el contexto actual y la inyección de dependencias pueden encontrarse en el tutorial de dependency injection.
Any method routes #
Puedes definir una ruta que se coincida solo por su camino y omitir el método HTTP. Esto puede ser útil para validar el método dentro del callback de la ruta.
// coincidirá con / en cualquier método HTTP
mainRouter.MapAny("/", callbackFunction);RouteMethod.Any incluye GET, POST, PUT, PATCH, DELETE, QUERY, HEAD y OPTIONS.
QUERY method routes #
Sisk soporta el método HTTP QUERY, un método seguro e idempotente que lleva la consulta en el cuerpo de la solicitud. Usa Router.MapQuery, Route.Query, RouteMethod.Query o el atributo RouteQuery para definir estas rutas:
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}");
}
}El encabezado Accept-Query, que anuncia los tipos de medio aceptados en solicitudes QUERY, está disponible a través de HttpHeaderCollection.AcceptQuery y HttpKnownHeaderNames.AcceptQuery.
Any path routes #
Las rutas de cualquier camino prueban cualquier camino recibido por el servidor HTTP, sujeto al método de la ruta que se está probando. Si el método de la ruta es RouteMethod.Any y la ruta usa Route.AnyPath en su expresión de camino, esta ruta escuchará todas las solicitudes del servidor HTTP, y no se podrán definir otras rutas.
// la siguiente ruta coincidirá con todas las solicitudes POST
mainRouter.Map(RouteMethod.Post, Route.AnyPath, callbackFunction);Ignore case route matching #
Por defecto, la interpretación de rutas con solicitudes es sensible a mayúsculas/minúsculas. Para hacer que ignore mayúsculas, habilita esta opción:
mainRouter.MatchRoutesIgnoreCase = true;Esto también habilitará la opción RegexOptions.IgnoreCase para rutas regex y la comparación sin distinción de mayúsculas para rutas prefijo.
Not Found (404) callback handler #
Puedes crear un callback personalizado para cuando una solicitud no coincida con ninguna ruta conocida.
mainRouter.NotFoundErrorHandler = () =>
{
return new HttpResponse(404)
{
// Desde v0.14
Content = new HtmlContent("<h1>Not found</h1>")
// versiones anteriores
Content = new StringContent("<h1>Not found</h1>", Encoding.UTF8, "text/html")
};
};Method not allowed (405) callback handler #
También puedes crear un callback personalizado para cuando una solicitud coincida con su camino, pero no con el método.
mainRouter.MethodNotAllowedErrorHandler = (context) =>
{
return new HttpResponse(405)
{
Content = new StringContent($"Method not allowed for this route.")
};
};Error Handling #
Las excepciones pueden lanzarse dentro del ciclo de vida de una solicitud, que abarca desde el manejador de solicitud previo a la ejecución, pasando por la acción del router, hasta los manejadores de solicitud posteriores y los manejadores de valores. Estas excepciones son gestionadas por el mecanismo:
- Si HttpServerConfiguration.ThrowExceptions es
true, las excepciones se lanzarán normalmente y no serán capturadas por Sisk, y el servidor HTTP puede interrumpirse si la excepción no se captura. - Si HttpServerConfiguration.ThrowExceptions es
false, las excepciones serán capturadas y manejadas por Sisk. Después de eso, siRouter.CallbackErrorHandlerestá definido, se llamará con la excepción capturada y el contexto de la solicitud, y no se reenviará a la salida de error estándar. SiRouter.CallbackErrorHandlerno está definido, la excepción se reenviará a la salida de error estándar, y el cliente recibirá una respuesta HTTP 500. Si la salida de error estándar no está definida, el error se ignorará silenciosamente.
Nota: dentro de Router.CallbackErrorHandler, puedes establecer el modo de registro para errores, registro de acceso, ambos o ninguno, y alterar el comportamiento predeterminado de escritura de logs:
router.CallbackErrorHandler = (ex, ctx) =>
{
ctx.LogMode = LogOutput.Both; // sobrescribe el modo de registro para registrar el error tanto en el log de acceso como en el de errores
}Internal error handler #
Los callbacks de ruta pueden lanzar errores durante la ejecución del servidor. Si no se manejan correctamente, el funcionamiento general del servidor HTTP puede terminar. El router tiene un callback para cuando un callback de ruta falla y evita la interrupción del servicio.
Este método solo es accesible cuando ThrowExceptions está configurado en false.
mainRouter.CallbackErrorHandler = (ex, context) =>
{
return new HttpResponse(500)
{
Content = new StringContent($"Error: {ex.Message}")
};
};