Sisk

Маршрутизация

Эта страница переведена с английского автоматически. Читать оригинал

The Router is the first step in building the server. It is responsible for housing Route objects, which are endpoints that map URLs and their methods to actions executed by the server. Each action is responsible for receiving a request and delivering a response to the client.

The routes are pairs of path expressions (“path pattern”) and the HTTP method that they can listen to. When a request is made to the server, it will attempt to find a route that matches the received request, then it will call the action of that route and deliver the resulting response to the client.

There are multiple ways to define routes in Sisk: they can be static, dynamic or auto-scanned, defined by attributes, or directly in the Router object.

C#
Router mainRouter = new Router();

// maps the GET / route into the following action
// отображает маршрут GET / в следующее действие
mainRouter.MapGet("/", request => {
    return new HttpResponse("Hello, world!");
});

To understand what a route is capable of doing, we need to understand what a request is capable of doing. An HttpRequest will contain everything you need. Sisk also includes some extra features that speed up the overral development.

For every action received by the server, a delegate of type RouteAction will be called. This delegate contains an parameter holding an HttpRequest with all the necessary information about the request received by the server. The resulting object from this delegate must be an HttpResponse or an object that maps to it through implicit response types.

Сопоставление маршрутов #

When a request is received by the HTTP server, Sisk searches for a route that satisfies the expression of the path received by the request. The expression is always tested between the route and the request path, without considering the query string.

The first route whose path and method match the request handles it. When no route is matched with that request, the Router.NotFoundErrorHandler response is returned to the client. When the path pattern is matched, but the HTTP method is mismatched, the Router.MethodNotAllowedErrorHandler response is sent back to the client. The order in which routes are tested is described in matching precedence.

Sisk checks for the possibility of route collisions between static routes. When the server starts, Sisk will look for static routes whose path patterns and methods overlap, and throws an exception if any is found. In the same check, Sisk throws an ArgumentException if two or more routes share the same Route.Name. Both checks are controlled by Router.CheckForRouteCollisions, which is enabled by default.

Приоритет сопоставления #

Every route is either static or dynamic, as indicated by Route.IsStatic:

  • Static routes strictly follow their path pattern, such as /users/<id>. Routes created with Map* methods, Route instances and route attributes are static.
  • Dynamic routes interpret their path in their own way and may match paths that cannot be described by a path pattern. RegexRoute, PrefixRoute (used by file servers) and custom matching routes are dynamic.

When the server starts, the router reorders its routes once so that every static route is tested before every dynamic route. Within each group, routes keep their registration order. This means that a static route always takes precedence over a dynamic route, regardless of the order in which they were defined:

C#
// dynamic: serves any GET path from the "wwwroot" directory
// динамический: обслуживает любой GET‑путь из каталога "wwwroot"
mainRouter.MapFileSystem("/", "wwwroot");

// static: handles GET /api/status, even though it was defined after the file server
// статический: обрабатывает GET /api/status, хотя был определён после файлового сервера
mainRouter.MapGet("/api/status", request => new HttpResponse("ok"));

In the example above, the file server acts as a fallback for any GET request that is not handled by a static route.

The two groups also differ in how they are validated and transformed:

Static routesDynamic routes
Порядок сопоставленияСначала, в порядке регистрацииПосле всех статических маршрутов, в порядке регистрации
Проверка столкновений маршрутовДаНет
Применяется Router.PrefixДаНет

Предупреждение

Dynamic routes are not checked for collisions, since Sisk cannot know which paths they match. When two dynamic routes can match the same request, the one defined first wins. For example, a file server mapped to / defined before a file server mapped to /static makes the second one unreachable.

Динамические маршруты не проверяются на столкновения, поскольку Sisk не может знать, какие пути они соответствуют. Если два динамических маршрута могут соответствовать одному запросу, выигрывает тот, который определён первым. Например, файловый сервер, смонтированный на /, определённый до файлового сервера, смонтированного на /static, делает второй недоступным.

Важно

In versions before 1.7, routes were tested only in their registration order, and regex routes could take precedence over static routes defined after them. If your application relies on a regex route handling a path that is also matched by a static route, move that logic into the static route or narrow the static path pattern.

В версиях до 1.7 маршруты проверялись только в порядке регистрации, и маршруты‑регулярки могли иметь приоритет над статическими маршрутами, определёнными после них. Если ваше приложение зависит от маршрута‑регулярки, обрабатывающего путь, который также совпадает со статическим маршрутом, перенесите эту логику в статический маршрут или сузьте шаблон статического пути.

Создание маршрутов с использованием шаблонов пути #

For new applications, prefer the Map* methods. They keep the HTTP method visible at the call site and match the current Router API. The older SetRoute methods still exist as compatibility wrappers, but new examples should use Map, MapGet, MapPost, MapPut, MapDelete, MapPatch, MapAny, MapOptions, or MapHead.

C#
// Map* methods are the usual way to define method-specific routes.
// Методы Map* — обычный способ определения маршрутов, специфичных для метода.
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(); // empty 200 ok
});

// Map can also receive a Route instance when you need route options.
mainRouter.Map(Route.Get("/image.png", (request) =>
{
    var imageStream = File.OpenRead("image.png");
    
    return new HttpResponse()
    {
        // the StreamContent inner
        // stream is disposed after sending
        // the response.
        // внутренний StreamContent
        // поток освобождается после отправки
        // ответа.
        Content = new StreamContent(imageStream)
    };
}));

// multiple parameters
// несколько параметров
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}!");
});

The RouteParameters property of HttpRequest contains all the information about the path variables of the received request.

Every path received by the server is normalized before the path pattern test is executed, following these rules:

  • All empty segments are removed from the path, eg: ////foo//bar becomes /foo/bar. // Все пустые сегменты удаляются из пути, например: ////foo//bar становится /foo/bar.
  • Path matching is case-sensitive, unless Router.MatchRoutesIgnoreCase is set to true. // Сопоставление пути чувствительно к регистру, если только [Router.MatchRoutesIgnoreCase] не установлен в true.

The Query and RouteParameters properties of HttpRequest return a StringValueCollection object, where each indexed property returns a non-null StringValue, which can be used as an option/monad to convert its raw value into a managed object.

The example below reads the route parameter “id” and obtains a Guid from it. If the parameter is not a valid Guid, an exception is thrown, and a 500 error is returned to the client if the server is not handling Router.CallbackErrorHandler.

C#
mainRouter.MapGet("/user/<id>", (request) =>
{
    Guid id = request.RouteParameters["id"].GetGuid();
    return new HttpResponse($"User id: {id}");
});

Примечание

Paths have their trailing / ignored in both request and route path, that is, if you try to access a route defined as /index/page you’ll be able to access using /index/page/ too.

You can also force URLs to terminate with / by enabling HttpServerConfiguration.ForceTrailingSlash.

Путь игнорирует завершающий / как в запросе, так и в шаблоне маршрута, то есть если вы попытаетесь обратиться к маршруту, определённому как /index/page, вы сможете обратиться к нему и как к /index/page/.

Вы также можете принудительно добавлять завершающий / к URL, включив [HttpServerConfiguration.ForceTrailingSlash].

Создание маршрутов с использованием экземпляров классов #

You can also define routes dynamically using reflection with the attribute RouteAttribute. This way, the instance of a class in which its methods implement this attribute will have their routes defined in the target router.

For a method to be defined as a route, it must be marked with a RouteAttribute, such as the attribute itself or a RouteGetAttribute. The method can be static, instance, public, or private. Use MapInstance when you want to map instance and static route methods from an object. Use MapType when you want to map only static route methods from a type.

Controller/MyController.csC#
public class MyController
{
    // will match GET /
    // будет соответствовать GET /
    [RouteGet]
    HttpResponse Index(HttpRequest request)
    {
        HttpResponse res = new HttpResponse();
        res.Content = new StringContent("Index!");
        return res;
    }
    
    // static methods works too
    // статические методы тоже работают
    [RouteGet("/hello")]
    static HttpResponse Hello(HttpRequest request)
    {
        HttpResponse res = new HttpResponse();
        res.Content = new StringContent("Hello world!");
        return res;
    }
}

The line below will define both the Index and Hello methods of MyController as routes, as both are marked as routes, and an instance of the class has been provided, not its type. If its type had been provided instead of an instance, only the static methods would be defined.

C#
var myController = new MyController();
mainRouter.MapInstance(myController);

To map only static route methods from a type, use:

C#
mainRouter.MapType<MyController>();

Since Sisk version 0.16, it is possible to enable AutoScan, which will search for user-defined classes that implement RouterModule and will automatically associate it with the router. This is not supported with AOT compilation.

C#
mainRouter.AutoScanModules<ApiController>();

// The above instruction will search for all types which implements ApiController but not the type itself. The two optional parameters indicate how the method will search for these types. The first argument implies the Assembly where the types will be searched and the second indicates the way in which the types will be defined. // Данная инструкция будет искать все типы, реализующие ApiController, но не сам тип. Два необязательных параметра указывают, как метод будет искать эти типы. Первый аргумент подразумевает сборку, в которой будет происходить поиск типов, а второй указывает способ определения этих типов.

Маршруты с регулярными выражениями #

Instead of using the default HTTP path matching methods, you can define a route which path is interpreted as a regular expression with the RegexRoute class. Regex routes are dynamic routes.

C#
mainRouter.Map(new RegexRoute(RouteMethod.Get, @"\/[a-z]+\/", request =>
{
    return new HttpResponse("hello, world");
}));

You can also capture groups from the regex pattern into the HttpRequest.RouteParameters contents:

Controller/MyController.csC#
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}");
    }
}

Примечание

The regular expression is not anchored automatically: a pattern such as /uploads also matches /files/uploads/a. Use ^ and $ to match the whole path.

Регулярное выражение не привязывается автоматически: шаблон вроде /uploads также совпадает с /files/uploads/a. Используйте ^ и $, чтобы сопоставить весь путь.

Важно

Since Sisk 1.7, the Route.UseRegex property can no longer be set. It is obsolete and only reports whether the route is a RegexRoute. The UseRegex property of route attributes, such as [Route(RouteMethod.Get, "...", UseRegex = true)], and the [RegexRoute] attribute keep working.

Начиная с Sisk 1.7 свойство Route.UseRegex больше нельзя задавать. Оно устарело и лишь сообщает, является ли маршрут RegexRoute. Свойство UseRegex атрибутов маршрутов, например [Route(RouteMethod.Get, "...", UseRegex = true)], и атрибут [RegexRoute] продолжают работать.

To migrate code that created regex routes with Route.UseRegex, replace the route with a RegexRoute:

C#
// before 1.7
// до 1.7
mainRouter.Map(new Route(RouteMethod.Get, @"^/items/(?<id>\d+)$", action) { UseRegex = true });

// 1.7 and later
// 1.7 и позже
mainRouter.Map(new RegexRoute(RouteMethod.Get, @"^/items/(?<id>\d+)$", action));

Префиксные маршруты #

A PrefixRoute matches its path and every path nested under it, comparing whole path segments. It is the route used by Router.MapFileSystem. Prefix routes are dynamic routes.

C#
// matches /docs, /docs/ and /docs/any/nested/path, but not /docsfoo
// соответствует /docs, /docs/ и /docs/any/nested/path, но не /docsfoo
mainRouter.Map(new PrefixRoute(RouteMethod.Get, "/docs", null, request =>
{
    return new HttpResponse($"Requested {request.Path}");
}, null));

Пользовательское сопоставление маршрутов #

You can create your own matching rules by inheriting Route and overriding Route.Match. The method receives the request path and the router that is matching the route, and returns a RouteMatch with the result and, optionally, the captured route parameters.

The router validates the HTTP method only after Match succeeds, so Match should only test the path. This keeps the 405 Method Not Allowed and OPTIONS responses working for custom routes.

Routing/ExtensionRoute.csC#
// matches any path ending with the given file extension, such as /reports/2024.csv
// соответствует любому пути, заканчивающемуся указанным расширением файла, например /reports/2024.csv
public sealed class ExtensionRoute : Route
{
    public ExtensionRoute(string extension, RouteAction action)
        : base(RouteMethod.Get, extension, action)
    {
    }

    // this route does not follow a path pattern
    // этот маршрут не следует шаблону пути
    public override bool IsStatic => false;

    // the path is not a path pattern, so it cannot be redirected to its trailing-slash form
    // путь не является шаблоном, поэтому его нельзя перенаправить в форму с завершающим слешем
    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);
    }
}
C#
mainRouter.Map(new ExtensionRoute(".csv", request =>
{
    string file = request.RouteParameters["file"].GetString();
    return new HttpResponse($"Exporting {file} as CSV");
}));

When overriding Match, keep in mind:

  • Override Route.IsStatic to return false whenever the route does not strictly follow a path pattern. Otherwise, the router treats its Path as a path pattern when checking collisions and applying Router.Prefix, which can produce false collisions or invalid paths. // Переопределяйте [Route.IsStatic], возвращая false, когда маршрут не строго следует шаблону пути. Иначе роутер будет рассматривать Path как шаблон при проверке столкновений и применении [Router.Prefix], что может вызвать ложные столкновения или неверные пути.
  • Override Route.AllowRewrites to return false when the route path cannot be redirected to its trailing-slash form by HttpServerConfiguration.ForceTrailingSlash. // Переопределяйте [Route.AllowRewrites], возвращая false, когда путь маршрута нельзя перенаправить в форму с завершающим слешем через [HttpServerConfiguration.ForceTrailingSlash].
  • Match is called for every request that reaches the route, possibly from multiple threads at once. Avoid allocations when the route does not match and keep any cached state thread-safe. Return RouteMatch.NotMatched for unsuccessful matches. // Match вызывается для каждого запроса, попадающего в маршрут, возможно из нескольких потоков одновременно. Избегайте выделений памяти, когда маршрут не совпадает, и держите кэшированное состояние потокобезопасным. Возвращайте [RouteMatch.NotMatched] для неуспешных совпадений.
  • Respect Router.MatchRoutesIgnoreCase through the router parameter. // Учитывайте [Router.MatchRoutesIgnoreCase] через параметр router.

Добавление префикса к маршрутам #

You can prefix all routes in a class or module with the RoutePrefix attribute and set the prefix as a string.

See the example below using the BREAD architecture (Browse, Read, Edit, Add and Delete):

Controller/Api/UsersController.csC#
[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()
    {
        ...
    }
}

In the above example, the HttpResponse parameter is omitted in favor of being used through the global context HttpContext.Current. Read more in the section that follows.

Маршруты без параметра запроса #

Routes can be defined without the HttpRequest parameter and still be possible to obtain the request and its components in the request context. Let’s consider an abstraction ControllerBase that serves as a foundation for all controllers of an API, and that abstraction provides the Request property to obtain the HttpRequest currently.

Controller/ControllerBase.csC#
public abstract class ControllerBase
{
    // gets the request from the current thread
    // получает запрос из текущего потока
    public HttpRequest Request { get => HttpContext.Current.Request; }
    
    // the line below, when called, gets the database from the current HTTP session,
    // or creates a new one if it doesn't exist
    // строка ниже, при вызове, получает базу данных из текущей HTTP‑сессии,
    // или создаёт новую, если её нет
    public DbContext Database { get => HttpContext.Current.RequestBag.GetOrAdd<DbContext>(); }
}

And for all it’s descendants to be able to use the route syntax without the request parameter:

Controller/UsersController.csC#
[RoutePrefix("/api/users")]
public class UsersController : ControllerBase
{    
    [RoutePost]
    public async Task<HttpResponse> Create()
    {
        // reads the JSON data from the current request
        UserCreationDto? user = await Request.GetJsonContentAsync<UserCreationDto>();
        ...
        Database.Users.Add(user);
        
        return new HttpResponse(201);
    }
}

More details about the current context and dependency injection can be found in the dependency injection tutorial.

Маршруты любого метода #

You can define a route to be matched only by its path and skip the HTTP method. This can be useful for you to do method validation inside the route callback.

C#
// will match / on any HTTP method
// будет соответствовать / для любого HTTP‑метода
mainRouter.MapAny("/", callbackFunction);

RouteMethod.Any includes GET, POST, PUT, PATCH, DELETE, QUERY, HEAD and OPTIONS.

Маршруты метода QUERY #

Sisk supports the HTTP QUERY method, a safe and idempotent method that carries the query in the request body. Use Router.MapQuery, Route.Query, RouteMethod.Query or the RouteQuery attribute to define these routes:

C#
mainRouter.MapQuery("/search", request =>
{
    string filter = request.Body;
    return new HttpResponse($"Searching for {filter}");
});
Controller/SearchController.csC#
public class SearchController
{
    [RouteQuery("/products")]
    static HttpResponse SearchProducts(HttpRequest request)
    {
        return new HttpResponse($"Searching for {request.Body}");
    }
}

The Accept-Query header, which advertises the media types accepted in QUERY requests, is available through HttpHeaderCollection.AcceptQuery and HttpKnownHeaderNames.AcceptQuery.

Маршруты любого пути #

Any path routes test for any path received by the HTTP server, subject to the route method being tested. If the route method is RouteMethod.Any and the route uses Route.AnyPath in its path expression, this route will listen to all requests from the HTTP server, and no other routes can be defined.

C#
// the following route will match all POST requests
// следующий маршрут будет соответствовать всем POST‑запросам
mainRouter.Map(RouteMethod.Post, Route.AnyPath, callbackFunction);

Сопоставление маршрутов без учёта регистра #

By default, the interpretation of routes with requests are case-sensitive. To make it ignore case, enable this option:

C#
mainRouter.MatchRoutesIgnoreCase = true;

This will also enable the option RegexOptions.IgnoreCase for regex routes and case-insensitive comparison for prefix routes.

Обработчик обратного вызова Not Found (404) #

You can create a custom callback for when a request doesn’t match any known routes.

C#
mainRouter.NotFoundErrorHandler = () =>
{
    return new HttpResponse(404)
    {
        // Since v0.14
        // с версии 0.14
        Content = new HtmlContent("<h1>Not found</h1>")
        // older versions
        // более старые версии
        Content = new StringContent("<h1>Not found</h1>", Encoding.UTF8, "text/html")
    };
};

Обработчик обратного вызова Method not allowed (405) #

You can also create a custom callback for when a request matches it’s path, but doens’t match the method.

C#
mainRouter.MethodNotAllowedErrorHandler = (context) =>
{
    return new HttpResponse(405)
    {
        Content = new StringContent($"Method not allowed for this route.")
    };
};

Обработка ошибок #

Exceptions can be thrown within a request lifecycle, which spans from the pre-execution request handler, through the router action, to the post-execution request handlers and value handlers. These exceptions are managed by the mechanism:

  • If HttpServerConfiguration.ThrowExceptions is true, exceptions will be thrown normally and will not be caught by Sisk, and the HTTP server may be interrupted if the exception is not caught. // Если [HttpServerConfiguration.ThrowExceptions] установлен в true, исключения будут выбрасываться обычным образом и не будут перехвачены Sisk, и HTTP‑сервер может быть прерван, если исключение не будет поймано.
  • If HttpServerConfiguration.ThrowExceptions is false, exceptions will be caught and handled by Sisk. After that, if Router.CallbackErrorHandler is defined, it will be called with the caught exception and the request context, and it will not be forwarded to the standard error output. If Router.CallbackErrorHandler is not defined, the exception will be forwarded to the standard error output, and the client will receive an HTTP 500 error response. If the standard error output is not defined, the error will be silently ignored. // Если [HttpServerConfiguration.ThrowExceptions] установлен в false, исключения будут перехвачены и обработаны Sisk. После этого, если определён Router.CallbackErrorHandler, он будет вызван с перехваченным исключением и контекстом запроса, и не будет передан в стандартный поток ошибок. Если Router.CallbackErrorHandler не определён, исключение будет передано в стандартный поток ошибок, и клиент получит ответ HTTP 500. Если стандартный поток ошибок не определён, ошибка будет тихо проигнорирована.

Note: within Router.CallbackErrorHandler, you can set the log mode for errors, access log, both, or none, and alter the default log writing behavior:

C#
router.CallbackErrorHandler = (ex, ctx) =>
{
    ctx.LogMode = LogOutput.Both; // override log mode to log the error in both access and error logs
}

Внутренний обработчик ошибок #

Route callbacks can throw errors during server execution. If not handled correctly, the overall functioning of the HTTP server can be terminated. The router has a callback for when a route callback fails and prevents service interruption.

This method is only reacheable when ThrowExceptions is set to false.

C#
mainRouter.CallbackErrorHandler = (ex, context) =>
{
    return new HttpResponse(500)
    {
        Content = new StringContent($"Error: {ex.Message}")
    };
};

Sisk распространяется с открытым исходным кодом по лицензии MIT.

Начните вводить, чтобы искать по документации и справочнику API.