# Sisk Framework documentation > Sisk is a lightweight, agnostic and robust .NET web development framework. This file contains the complete Sisk documentation (Русский). The API reference is not included; read https://docs.sisk-framework.org/api/index.md for the type index. --- # Getting started Source: https://docs.sisk-framework.org/ru/docs/getting-started.html Welcome to the Sisk documentation! Sisk is an open-source lightweight HTTP framework for .NET. You can use it to build a standalone web service, embed an HTTP module inside an existing application, or run a service behind a reverse proxy with only the configuration you need. Sisk's values include code transparency, modularity, performance, and scalability. It can handle different application styles, including RESTful APIs, JSON-RPC services, WebSockets, Server-Sent Events, and static file serving. It's main features includes: | Resource | Description | | ------- | --------- | | [Routing](https://docs.sisk-framework.org/ru/docs/fundamentals/routing.md) | Маршрутизатор путей, поддерживающий префиксы, пользовательские методы, переменные пути, конвертеры значений и многое другое. | | [Request Handlers](https://docs.sisk-framework.org/ru/docs/fundamentals/request-handlers.md) | Также известные как *middlewares*, предоставляют интерфейс для создания собственных обработчиков запросов, работающих до или после действия. | | [Compression](https://docs.sisk-framework.org/ru/docs/fundamentals/responses.md#gzip-deflate-and-brotli-compression) | Легко сжимайте содержимое ответов с помощью Sisk. | | [Web sockets](https://docs.sisk-framework.org/ru/docs/features/websockets.md) | Предоставляет маршруты, принимающие полноценные веб‑сокеты для чтения и записи клиенту. | | [Server-sent events](https://docs.sisk-framework.org/ru/docs/features/server-sent-events.md) | Обеспечивает отправку серверных событий клиентам, поддерживающим протокол SSE. | | [Logging](https://docs.sisk-framework.org/ru/docs/features/logging.md) | Упрощённое логирование. Записывайте ошибки, доступ, определяйте ротацию логов по размеру, несколько потоков вывода для одного лога и многое другое. | | [Multi-host](https://docs.sisk-framework.org/ru/docs/advanced/multi-host-setup.md) | Позволяет иметь HTTP‑сервер для нескольких портов, каждый из которых со своим маршрутизатором и приложением. | | [Server handlers](https://docs.sisk-framework.org/ru/docs/advanced/http-server-handlers.md) | Расширяйте собственную реализацию HTTP‑сервера. Настраивайте с помощью расширений, улучшений и новых функций. | ## First steps Sisk can run in any .NET environment. In this guide, we will teach you how to create a Sisk application using .NET. If you haven't installed it yet, please download the SDK from [here](https://dotnet.microsoft.com/en-us/download/dotnet/7.0). In this tutorial, we will cover how to create a project structure, receive a request, obtain a URL parameter, and send a response. This guide will focus on building a simple server using C#. You can also use your favorite programming language. > [!NOTE] > Возможно, вам будет интересен проект quickstart. Смотрите [this repository](https://github.com/sisk-http/quickstart) для получения дополнительной информации. ## Creating a Project Let's name our project "My Sisk Application." Once you have .NET set up, you can create your project with the following command: ```bash dotnet new console -n my-sisk-application ``` Next, navigate to your project directory and install Sisk using the .NET utility tool: ```bash cd my-sisk-application dotnet add package Sisk.HttpServer ``` You can find additional ways to install Sisk in your project [here](https://www.nuget.org/packages/Sisk.HttpServer/). Now, let's create an instance of our HTTP server. For this example, we will configure it to listen on port 5000. ## Building the HTTP Server Sisk allows you to build your application step by step manually, as it routes to the HttpServer object. However, this may not be very convenient for most projects. Therefore, we can use the builder method, which makes it easier to get our app up and running. ```csharp {title="Program.cs"} class Program { static async Task Main(string[] args) { using var app = HttpServer.CreateBuilder() .UseListeningPort("http://localhost:5000/") .Build(); app.Router.MapGet("/", request => { return new HttpResponse() { Status = 200, Content = new StringContent("Hello, world!") }; }); await app.StartAsync(); } } ``` It's important to understand each vital component of Sisk. Later in this document, you will learn more about how Sisk works. ## Manual (advanced) setup You can learn how each Sisk mechanism works in [this section](https://docs.sisk-framework.org/ru/docs/advanced/manual-setup.md) of the documentation, which explains the behavior and relationships between the HttpServer, Router, ListeningPort, and other components. --- # Установка Source: https://docs.sisk-framework.org/ru/docs/installing.html Вы можете установить Sisk через NuGet, dotnet CLI или [another options](https://www.nuget.org/packages/Sisk.HttpServer/). Вы можете легко настроить свою среду Sisk, выполнив эту команду в консоли разработчика: ```sh dotnet add package Sisk.HttpServer ``` Эта команда установит последнюю версию Sisk в ваш проект. Sisk 1.7 поддерживает .NET 8, .NET 9 и .NET 10. Пакеты [Cadente engine](https://docs.sisk-framework.org/ru/docs/cadente.md) поддерживают только .NET 9 и .NET 10. --- # Поддержка Native AOT Source: https://docs.sisk-framework.org/ru/docs/native-aot.html [.NET Native AOT](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/) позволяет публиковать родные приложения .NET, которые являются самодостаточными и не требуют установки среды выполнения .NET на целевом хосте. Кроме того, Native AOT предоставляет такие преимущества, как: - Значительно меньшие приложения - Значительно более быстрая инициализация - Низкое потребление памяти Sisk Framework, благодаря своей явной природе, позволяет использовать Native AOT для几乎 всех своих функций без необходимости переработки исходного кода для адаптации его к Native AOT. ## Не поддерживаемые функции Однако Sisk использует рефлексию, хотя и минимальную, для некоторых функций. Функции, упомянутые ниже, могут быть частично доступны или полностью недоступны во время выполнения родного кода: - [Автоматическое сканирование модулей](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.AutoScanModules.md) маршрутизатора: этот ресурс сканирует типы, встроенные в выполняемую сборку, и регистрирует типы, которые являются [модулями маршрутизатора](https://docs.sisk-framework.org/ru/docs/fundamentals/routing.md). Этот ресурс требует типов, которые могут быть исключены во время обрезки сборки. Все остальные функции совместимы с AOT в Sisk. Обычно можно найти один или другой метод, который выдает предупреждение AOT, но тот же метод, если он не упоминается здесь, имеет перегруженную версию, которая указывает на передачу типа, параметра или информации о типе, что помогает компилятору AOT компилировать объект. --- # Развёртывание вашего приложения Sisk Source: https://docs.sisk-framework.org/ru/docs/deploying.html Процесс развёртывания приложения Sisk состоит в публикации вашего проекта в продакшн. Хотя процесс относительно прост, стоит обратить внимание на детали, которые могут быть критичными для безопасности и стабильности инфраструктуры развертывания. Идеально, если вы готовы развернуть приложение в облаке, после проведения всех возможных тестов, чтобы приложение было готово к работе. ## Публикация вашего приложения Публикация вашего приложения или сервиса Sisk — это генерация бинарных файлов, готовых и оптимизированных для продакшна. В этом примере мы скомпилируем бинарники для продакшна, чтобы запустить их на машине, где установлен .NET Runtime. Вам понадобится установленный .NET SDK на вашей машине для сборки приложения и .NET Runtime на целевом сервере для его запуска. Как установить .NET Runtime на ваш Linux‑сервер, вы можете узнать [здесь](https://learn.microsoft.com/en-us/dotnet/core/install/linux), [Windows](https://learn.microsoft.com/en-us/dotnet/core/install/windows?tabs=net70) и [Mac OS](https://learn.microsoft.com/en-us/dotnet/core/install/macos). В папке, где находится ваш проект, откройте терминал и выполните команду публикации .NET: ```shell $ dotnet publish -r linux-x64 -c Release ``` Это сгенерирует ваши бинарные файлы в `bin/Release/publish/linux-x64`. > [!NOTE] > Если ваше приложение использует [portable configuration file](https://docs.sisk-framework.org/ru/docs/extensions/service-providers.md), скопируйте `service-config.json` на сервер вместе со всеми бинарными файлами, сгенерированными `dotnet publish`. > Вы можете оставить файл предварительно сконфигурированным, с переменными окружения, портами прослушивания и хостами, а также дополнительными настройками сервера. Следующий шаг — перенести эти файлы на сервер, где будет размещено ваше приложение. После этого дайте исполняемые права вашему бинарному файлу. В данном случае предположим, что имя нашего проекта — «my-app»: ```shell $ cd /home/htdocs $ chmod +x my-app $ ./my-app ``` После запуска приложения проверьте, появляются ли сообщения об ошибках. Если их нет, значит приложение работает. На данном этапе, скорее всего, будет невозможно получить доступ к вашему приложению из внешней сети, так как правила доступа, такие как брандмауэр, ещё не настроены. Мы рассмотрим это в следующих шагах. У вас должен быть адрес виртуального хоста, на котором ваше приложение слушает запросы. Он задаётся вручную в приложении и зависит от того, как вы инстанцируете сервис Sisk. Если вы **не** используете portable configuration file, найдите его там, где вы определяете экземпляр `HttpServer`: ```cs HttpServer server = HttpServer.Emit(5000, out HttpServerConfiguration config, out var host, out var router); // sisk должен слушать http://localhost:5000/ ``` Ассоциация `ListeningHost` вручную: ```cs config.ListeningHosts.Add(new ListeningHost("https://localhost:5000/", router)); ``` Или, если вы используете portable configuration file, в вашем `service-config.json`: ```json { "Server": { }, "ListeningHost": { "Ports": [ "http://localhost:5000/" ] } } ``` Исходя из этого, мы можем создать обратный прокси, который будет слушать ваш сервис и делать трафик доступным в открытой сети. ## Прокси вашего приложения Проксирование вашего сервиса означает, что вы не будете напрямую выставлять сервис Sisk во внешнюю сеть. Эта практика очень распространена при развертывании серверов, потому что: - Позволяет привязать SSL‑сертификат к вашему приложению; - Создаёт правила доступа до обращения к сервису и предотвращает перегрузки; - Позволяет контролировать полосу пропускания и лимиты запросов; - Разделяет балансировщики нагрузки для вашего приложения; - Предотвращает повреждения безопасности в случае сбоя инфраструктуры. Вы можете обслуживать приложение через обратный прокси, такой как [Nginx](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/linux-nginx?view=aspnetcore-7.0&tabs=linux-ubuntu#install-nginx) или [Apache](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/linux-apache?view=aspnetcore-7.0), либо использовать туннель http‑over‑dns, например [Cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/install-and-setup/tunnel-guide/). Также не забудьте правильно обрабатывать заголовки переадресации вашего прокси, чтобы получать информацию о клиенте, такую как IP‑адрес и хост, через [forwarding resolvers](https://docs.sisk-framework.org/ru/docs/advanced/forwarding-resolvers.md). Следующий шаг после создания туннеля, настройки брандмауэра и запуска приложения — создать сервис для вашего приложения. > [!NOTE] > Использовать SSL‑сертификаты напрямую в сервисе Sisk на системах, отличных от Windows, невозможно. Это связано с реализацией `HttpListener`, который является центральным модулем управления HTTP‑очередью в Sisk, а реализация различается от ОС к ОС. Вы можете использовать SSL в сервисе Sisk, если [привяжете сертификат к виртуальному хосту через IIS](https://learn.microsoft.com/en-us/iis/manage/configuring-security/how-to-set-up-ssl-on-iis). Для остальных систем настоятельно рекомендуется использовать обратный прокси. ## Создание службы Создание службы сделает ваше приложение всегда доступным, даже после перезапуска сервера или некорректного сбоя. В этом простом руководстве мы используем материал из предыдущего урока в качестве примера, чтобы ваш сервис всегда оставался активным. 1. Перейдите в папку, где находятся файлы конфигурации службы: ```sh cd /etc/systemd/system ``` 2. Создайте файл `my-app.service` и включите в него следующее содержимое: ```ini {title="my-app.service"} [Unit] Description= [Service] # установить пользователя, который будет запускать службу User= # путь ExecStart не относителен к WorkingDirectory. # указать полный путь к исполняемому файлу WorkingDirectory=/home/htdocs ExecStart=/home/htdocs/my-app # настроить службу на постоянный перезапуск при сбое Restart=always RestartSec=3 [Install] WantedBy=multi-user.target ``` 3. Перезапустите модуль менеджера служб: ```sh $ sudo systemctl daemon-reload ``` 4. Запустите только что созданную службу по имени файла и проверьте, работает ли она: ```sh $ sudo systemctl start my-app $ sudo systemctl status my-app ``` 5. Теперь, если ваше приложение запущено («Active: active»), включите службу, чтобы она запускалась после перезагрузки системы: ```sh $ sudo systemctl enable my-app ``` Теперь вы готовы представить ваше приложение Sisk всем. --- # Работа с SSL Source: https://docs.sisk-framework.org/ru/docs/ssl.html Работа с SSL в процессе разработки может быть необходима при работе в контекстах, требующих безопасности, таких как большинство сценариев веб‑разработки. Sisk работает поверх HttpListener, который не поддерживает нативный HTTPS, только HTTP. Однако существуют обходные пути, позволяющие использовать SSL в Sisk. См. ниже: ## Через Sisk.Cadente.CoreEngine - Доступно на: Linux, macOS, Windows - Сложность: легко Можно использовать экспериментальный движок [**Cadente**](https://docs.sisk-framework.org/ru/docs/cadente.md) в проектах Sisk без необходимости дополнительной настройки компьютера или проекта. Вам потребуется установить пакет `Sisk.Cadente.CoreEngine` в ваш проект, чтобы иметь возможность использовать сервер Cadente в сервере Sisk. Для настройки SSL вы можете использовать методы `UseSsl` и `UseEngine` билдера: ```csharp using var http = HttpServer.CreateBuilder() .UseEngine() .UseSsl(CertificateHelper.CreateTrustedDevelopmentCertificate("localhost")) ``` > Примечание: этот пакет всё ещё находится в экспериментальной фазе. ## Через IIS в Windows - Доступно на: Windows - Сложность: средняя Если вы используете Windows, вы можете воспользоваться IIS для включения SSL на вашем HTTP‑сервере. Чтобы это работало, рекомендуется предварительно пройти [этот учебник](https://docs.sisk-framework.org/ru/docs/registering-namespace.md), если вы хотите, чтобы ваше приложение слушало хост, отличный от «localhost». Для этого необходимо установить IIS через компоненты Windows. IIS доступен бесплатно пользователям Windows и Windows Server. Чтобы настроить SSL в вашем приложении, подготовьте SSL‑сертификат, даже если он самоподписан. Затем вы можете ознакомиться с [инструкцией по настройке SSL в IIS 7 и выше](https://learn.microsoft.com/en-us/iis/manage/configuring-security/how-to-set-up-ssl-on-iis). ## Через mitmproxy - Доступно на: Linux, macOS, Windows - Сложность: легко **mitmproxy** — это инструмент перехватывающего прокси, позволяющий разработчикам и специалистам по безопасности инспектировать, изменять и записывать HTTP и HTTPS трафик между клиентом (например, веб‑браузером) и сервером. Вы можете использовать утилиту **mitmdump** для запуска обратного SSL‑прокси между вашим клиентом и приложением Sisk. 1. Сначала установите [mitmproxy](https://mitmproxy.org/) на ваш компьютер. 2. Запустите ваше приложение Sisk. Для этого примера будем использовать порт 8000 как небезопасный HTTP‑порт. 3. Запустите сервер mitmproxy, чтобы он слушал защищённый порт 8001: ```sh mitmdump --mode reverse:http://localhost:8000/ -p 8001 ``` И всё готово! Вы уже можете получить доступ к вашему приложению по адресу `https://localhost:8001/`. Ваше приложение не обязано быть запущено, чтобы вы могли запустить `mitmdump`. В качестве альтернативы вы можете добавить ссылку на [mitmproxy helper](https://github.com/sisk-http/core/tree/main/extensions/Sisk.Helpers.mitmproxy) в ваш проект. Это всё равно требует, чтобы mitmproxy был установлен на вашем компьютере. ## Пакет Sisk.SslProxy (прекращённый) > [!CAUTION] > > Пакет Sisk.SslProxy прекращён и помечен как устаревший в NuGet. Он больше не компилируется с текущим API Cadente. Не используйте его: вместо этого используйте нативную поддержку SSL в [движке Cadente](#through-the-siskcadentecoreengine). --- # Cadente Source: https://docs.sisk-framework.org/ru/docs/cadente.html Cadente — экспериментальная управляемая реализация HTTP/1.1‑слушателя для Sisk. Она заменяет стандартный `System.Net.HttpListener`, предоставляя больший контроль и гибкость, особенно на платформах, отличных от Windows. ## Обзор По умолчанию Sisk использует `HttpListener` (из `System.Net`) в качестве базового HTTP‑движка. Хотя `HttpListener` стабилен и производителен в Windows (где он работает через драйвер ядра HTTP.sys), его реализация в Linux и macOS управляемая и исторически имела ограничения, такие как отсутствие нативной поддержки SSL (требуется обратный прокси, например Nginx) и различия в производительности. Cadente стремится решить эти проблемы, предоставляя полностью управляемый HTTP/1.1‑сервер, написанный на C#. Его ключевые цели: - **Нативная поддержка SSL:** Работает на всех платформах без внешних прокси и сложных настроек. - **Кроссплатформенная согласованность:** Идентичное поведение в Windows, Linux и macOS. - **Производительность:** Спроектирован как высокопроизводительная альтернатива управляемому `HttpListener`. - **Независимость:** Отделён от `System.Net.HttpListener`, защищая Sisk от возможных будущих устареваний или прекращения поддержки этого компонента в .NET. > [!WARNING] > **Экспериментальный статус** > > Cadente находится в экспериментальной (Beta) стадии. Пока не рекомендуется для критически важных продакшн‑окружений. API и поведение могут измениться. ## Установка Cadente доступен как отдельный пакет. Чтобы использовать его с Sisk, необходимо установить пакет `Sisk.Cadente.CoreEngine`. ```bash dotnet add package Sisk.Cadente.CoreEngine ``` `Sisk.Cadente` и `Sisk.Cadente.CoreEngine` нацелены на .NET 9 и .NET 10. Их нельзя использовать в проектах, таргетирующих .NET 8, хотя `Sisk.HttpServer` поддерживает эту версию. ## Использование с Sisk Чтобы задействовать Cadente в качестве HTTP‑движка вашего приложения Sisk, необходимо сконфигурировать `HttpServer` для использования `CadenteHttpServerEngine` вместо движка по умолчанию. `CadenteHttpServerEngine` адаптирует `HttpHost` из Cadente к абстракции `HttpServerEngine`, требуемой Sisk. ```csharp using Sisk.Core.Http; using Sisk.Cadente.CoreEngine; using var host = HttpServer.CreateBuilder() .UseEngine() .UseSsl(certificate: CertificateHelper.CreateTrustedDevelopmentCertificate("localhost")) .Build(); await host.StartAsync(); ``` Когда настроенный хост прослушивает с SSL, [HttpServer.ListeningPrefixes](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.ListeningPrefixes.md) возвращает его адреса со схемой `https`. Если клиент отправит обычный HTTP на порт, сконфигурированный для SSL, Cadente ответит редиректом `301 Moved Permanently` на `https`‑адрес того же хоста и пути. ### Расширенная конфигурация Можно настроить базовый объект `HttpHost`, передав действие настройки в конструктор `CadenteHttpServerEngine`. Это удобно для задания тайм‑аутов и других низкоуровневых параметров. ```csharp using var engine = new CadenteHttpServerEngine(host => { // Настройка тайм‑аутов чтения/записи клиента host.TimeoutManager.ClientReadTimeout = TimeSpan.FromSeconds(30); host.TimeoutManager.ClientWriteTimeout = TimeSpan.FromSeconds(30); }); ``` ## Автономное использование Хотя Cadente в первую очередь предназначен для Sisk, его можно использовать как самостоятельный HTTP‑сервер (аналог `HttpListener`). ```csharp using Sisk.Cadente; var host = new HttpHost(15000) { Handler = new MyHostHandler() }; host.Start(); Thread.Sleep(-1); class MyHostHandler : HttpHostHandler { public override async Task OnContextCreatedAsync(HttpHost host, HttpHostContext context) { context.Response.StatusCode = 200; using var stream = await context.Response.GetResponseStreamAsync(chunked: true); using var writer = new StreamWriter(stream); await writer.WriteLineAsync("Hello, world!"); } } ``` Когда тело ответа небольшое и уже находится в памяти, `WriteInlineContent` и `WriteInlineContentAsync` записывают заголовки и тело за одну операцию. Ответ должен содержать заголовок `Content-Length`: ```csharp byte[] body = "Hello, world!"u8.ToArray(); context.Response.StatusCode = 200; context.Response.Headers.Set(new HttpHeader("Content-Length", body.Length.ToString())); await context.Response.WriteInlineContentAsync(body); ``` ## Проверка запросов Cadente проверяет корректность запросов до того, как они попадают в обработчик. Начиная с версии 1.7, он: - поддерживает конвейеризацию запросов на соединениях keep‑alive; - отклоняет методы запросов, не являющиеся валидными HTTP‑токенами, и цели запросов, содержащие байты за пределами видимого диапазона ASCII (`%x21-7E`); - отклоняет запросы, у которых `Transfer-Encoding` отличается от единственного значения `chunked`, а также запросы с конфликтующими заголовками `Content-Length`; - отвечает `431 Request Header Fields Too Large` и закрывает соединение, если строка запроса и заголовки превышают 8 KB. Невалидные запросы получают ошибку, соединение закрывается, и обработчик не вызывается. --- # Настройка резервирования пространств имён в Windows Source: https://docs.sisk-framework.org/ru/docs/registering-namespace.html > [!NOTE] > Эта конфигурация является необязательной и требуется только тогда, когда вы хотите, чтобы Sisk прослушивал хосты, отличные от "localhost" в Windows, используя движок HttpListener. Sisk работает с сетевым интерфейсом HttpListener, который привязывает виртуальный хост к системе для прослушивания запросов. В Windows такое привязывание несколько ограничено: в качестве допустимого хоста можно привязать только localhost. При попытке прослушивать другой хост сервер выдаёт ошибку доступа. В этом руководстве объясняется, как предоставить разрешение на прослушивание любого желаемого хоста в системе. ```bat {title="Namespace Setup.bat"} @echo off :: вставьте префикс здесь, без пробелов и кавычек SET PREFIX= SET DOMAIN=%ComputerName%\%USERNAME% netsh http add urlacl url=%PREFIX% user=%DOMAIN% pause ``` Где в `PREFIX` указывается префикс ("Listening Host->Port"), на котором ваш сервер будет прослушивать. Он должен быть отформатирован с указанием схемы URL, хоста, порта и завершающего слеша, пример: ```bat {title="Namespace Setup.bat"} SET PREFIX=http://my-application.example.test/ ``` Чтобы вы могли прослушивать в вашем приложении через: ```csharp {title="Program.cs"} class Program { static async Task Main(string[] args) { using var app = HttpServer.CreateBuilder() .UseListeningPort("http://my-application.example.test/") .Build(); app.Router.MapGet("/", request => { return new HttpResponse() { Status = 200, Content = new StringContent("Hello, world!") }; }); await app.StartAsync(); } } ``` --- # Журналы изменений Source: https://docs.sisk-framework.org/ru/docs/changelogs.html Каждое изменение, внесённое в Sisk, фиксируется в журнале изменений. Вы можете просмотреть журналы изменений для всех версий Sisk [здесь](https://github.com/sisk-http/archive/tree/master/changelogs). ## Переход с 1.6 на 1.7 Sisk 1.7.0 поставляется с `Sisk.HttpServer`, `Sisk.Cadente`, `Sisk.Cadente.CoreEngine` и `Sisk.IniConfiguration` версии 1.7.0. `Sisk.Documenting`, `Sisk.Documenting.Html` и `Sisk.Monitoring` поставляются как 1.7.0-rc3, а `Sisk.ModelContextProtocol` — как 1.7.0-rc1. Прочитайте [полный журнал изменений 1.7.0](https://github.com/sisk-http/archive/blob/master/changelogs/core/v1.7/v1.7.0.md) для всех изменений. ### Несовместимые изменения - **`Route.UseRegex` больше нельзя задавать.** Свойство устарело и только сообщает, является ли маршрут `RegexRoute`. Создайте [RegexRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RegexRoute.md) вместо него. Атрибуты маршрутов с `UseRegex = true` и атрибут `[RegexRoute]` продолжают работать. См. [regex routes](https://docs.sisk-framework.org/ru/docs/fundamentals/routing.md#regex-routes). ```cs // до router.Map(new Route(RouteMethod.Get, @"^/items/(?\d+)$", action) { UseRegex = true }); // после router.Map(new RegexRoute(RouteMethod.Get, @"^/items/(?\d+)$", action)); ``` - **Тела запросов по умолчанию ограничены 100 МБ.** Значение по умолчанию [HttpServerConfiguration.MaximumContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.MaximumContentLength.md) изменилось с `0` (неограниченно) на 100 МБ. Ограничение также применяется к потокам запросов и сообщениям WebSocket. Установите `0`, чтобы восстановить прежнее поведение. См. [getting the request body](https://docs.sisk-framework.org/ru/docs/fundamentals/requests.md#getting-the-request-body). ```cs using var app = HttpServer.CreateBuilder() .UseConfiguration(config => config.MaximumContentLength = 0) .Build(); ``` - **Статические маршруты проверяются до динамических маршрутов.** Маршруты с регулярными выражениями, префиксные маршруты (включая файловые серверы) и пользовательские маршруты проверяются после каждого статического маршрута, независимо от порядка их регистрации. См. [matching precedence](https://docs.sisk-framework.org/ru/docs/fundamentals/routing.md#matching-precedence). - **Повторяющиеся имена маршрутов вызывают исключение при запуске.** Два или более маршрутов с одинаковым `Name` вызывают `ArgumentException` при старте сервера, если включена [Router.CheckForRouteCollisions](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CheckForRouteCollisions.md) (по умолчанию). - **Маршруты файлового сервера являются префиксными маршрутами.** `Router.MapFileSystem` и `HttpFileServer.CreateServingRoute` создают [PrefixRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.PrefixRoute.md) вместо маршрута с регулярным выражением и больше не зависят от `ForceTrailingSlash`. - **`RouteMethod.Any` включает `QUERY`.** Маршруты, сопоставленные с `MapAny`, также отвечают на запросы `QUERY`. Числовое значение `RouteMethod.Any` изменилось, поэтому пересоберите код, который сохраняет или сравнивает его. - **Предварительные CORS‑запросы учитывают `UseCors`.** Автоматический ответ `OPTIONS` больше не отправляет заголовки CORS для маршрутов с `UseCors = false`, а маршруты, явно сопоставленные для `OPTIONS`, обрабатывают запрос сами. См. [preflight requests](https://docs.sisk-framework.org/ru/docs/features/cors.md#preflight-requests). - **Cadente отклоняет некорректные запросы.** Неправильные методы запросов и цели, неподдерживаемые значения `Transfer-Encoding` и конфликтующие заголовки `Content-Length` отклоняются, а блоки заголовков более 8 KB получают `431 Request Header Fields Too Large`. См. [Cadente request validation](https://docs.sisk-framework.org/ru/docs/cadente.md#request-validation). ### Прекращённые пакеты `Sisk.BasicAuth`, `Sisk.SslProxy` и `Sisk.ServiceProvider` больше не поддерживаются и помечены как устаревшие в NuGet, без заменяющего пакета. Удалите их из ваших проектов: - Замените `Sisk.BasicAuth` собственным [обработчиком запросов](https://docs.sisk-framework.org/ru/docs/fundamentals/request-handlers.md), который проверяет заголовок `Authorization`. - Замените `Sisk.SslProxy` нативной поддержкой SSL движка [Cadente](https://docs.sisk-framework.org/ru/docs/cadente.md). - Замените `Sisk.ServiceProvider` [портируемой конфигурацией](https://docs.sisk-framework.org/ru/docs/extensions/service-providers.md), встроенной в `Sisk.HttpServer` через `UsePortableConfiguration`. ### Новые возможности - [Пользовательское сопоставление маршрутов](https://docs.sisk-framework.org/ru/docs/fundamentals/routing.md#custom-route-matching) через `Route.Match`, `RouteMatch`, `Route.IsStatic` и `Route.AllowRewrites`, а также новые [префиксные маршруты](https://docs.sisk-framework.org/ru/docs/fundamentals/routing.md#prefix-routes). - Поддержка HTTP‑метода [QUERY](https://docs.sisk-framework.org/ru/docs/fundamentals/routing.md#query-method-routes) с `MapQuery`, `Route.Query`, `[RouteQuery]` и заголовком `Accept-Query`. - [Атомарные значения контекста](https://docs.sisk-framework.org/ru/docs/fundamentals/requests.md#atomic-context-values) через `HttpContext.Interlocked`. - Термин доступа `%{$key}` [access log term](https://docs.sisk-framework.org/ru/docs/features/logging.md#access-log-formatting), который выводит значения из request bag, и [безопасные помощники записи файлов](https://docs.sisk-framework.org/ru/docs/features/logging.md#writing-files-safely) `LogStream.SafeWriteToFile` и `LogStream.SafeAppendToFile`. - В Cadente: конвейерные keep-alive запросы, `WriteInlineContent` и `WriteInlineContentAsync`, а также перенаправление на HTTPS для обычных HTTP‑запросов, полученных на SSL‑порт. - В расширениях: примеры параметров и `LlmsApiExporter.FooterText` в [Sisk.Documenting](https://docs.sisk-framework.org/ru/docs/extensions/api-documentation.md), навыки и ревизия протокола `2026-07-28` в [Sisk.ModelContextProtocol](https://docs.sisk-framework.org/ru/docs/extensions/mcp.md), а также телеметрия HTTP‑трафика, задержек, маршрутов и исключений в панели Sisk.Monitoring. ### Другие изменения поведения - Движок `HttpListener` распределяет каждый запрос в пул потоков, поэтому медленный запрос больше не задерживает следующую принятые соединение. Его `ContextAbortedToken` возвращает `CancellationToken.None` вместо выброса исключения. - Исключения, выброшенные обработчиками сервера в конце запроса и при записи логов, записываются в журнал ошибок вместо того, чтобы «выскочить» в HTTP‑движок. См. [exceptions thrown by handlers](https://docs.sisk-framework.org/ru/docs/advanced/http-server-handlers.md#exceptions-thrown-by-handlers). - Ответы CORS отправляют `Vary: Origin`, когда разрешённый источник зависит от запроса. - Термин доступа `%ri` использует клиентский адрес, определённый в начале запроса. - `Sisk.HttpServer` нацелен на .NET 8, .NET 9 и .NET 10. `Sisk.Cadente` и `Sisk.Cadente.CoreEngine` нацелены на .NET 9 и .NET 10 и публикуют свои отладочные символы как пакеты `.snupkg`. --- # Часто задаваемые вопросы Source: https://docs.sisk-framework.org/ru/docs/faq.html Часто задаваемые вопросы о Sisk. ## Является ли Sisk открытым исходным кодом? Полностью. Всё исходный код, используемый Sisk, публикуется и регулярно обновляется на [GitHub](https://github.com/sisk-http). ## Принимаются ли вклады? Пока они совместимы с [философией Sisk](/), все вклады очень приветствуются! Вклады не обязательно должны быть только кодом! Вы можете внести вклад в документацию, тесты, переводы, пожертвования и публикации, например. ## Является ли Sisk финансируемым? Нет. Никакая организация или проект в настоящее время не спонсирует Sisk. ## Можно ли использовать Sisk в производстве? Определенно. Проект разрабатывается более трех лет и прошел интенсивное тестирование в коммерческих приложениях, которые находятся в производстве с тех пор. Sisk используется в важных коммерческих проектах в качестве основной инфраструктуры. Руководство по [развертыванию](https://docs.sisk-framework.org/ru/docs/deploying.md) в разных системах и средах написано и доступно. ## Имеет ли Sisk аутентификацию, мониторинг и базу данных? Нет. Sisk не имеет ни одного из этих. Это фреймворк для разработки веб-приложений HTTP, но это все еще минимальный фреймворк, который предоставляет все необходимое для работы вашего приложения. Вы можете реализовать все услуги, которые вам нужны, используя любую библиотеку третьих лиц, которую вы предпочитаете. Sisk был создан, чтобы быть агностическим, гибким и работать с чем угодно. ## Почему мне следует использовать Sisk вместо <фреймворка>? Я не знаю. Вы мне скажите. Sisk был создан, чтобы заполнить общий сценарий для веб-приложений HTTP в .NET. Установленные проекты, такие как ASP.NET, решают различные проблемы, но с разными предубеждениями. В отличие от более крупных фреймворков, Sisk требует от пользователя знать, что он делает и строит. Основные понятия веб-разработки и протокола HTTP необходимы для работы с Sisk. Sisk ближе к Express Node.js, чем к ASP.NET Core. Это высокоуровневая абстракция, которая позволяет создавать приложения с логикой HTTP, которую вы хотите. ## Что мне нужно, чтобы выучить Sisk? Вам нужно знать основы: - Веб-разработки (HTTP, Restful и т. д.) - .NET Всего лишь это. Имея представление о этих двух темах, вы можете посвятить несколько часов разработке продвинутого приложения с Sisk. ## Можно ли разрабатывать коммерческие приложения с Sisk? Определенно. Sisk был создан под лицензией MIT, что означает, что вы можете использовать Sisk в любом коммерческом проекте, коммерчески или некоммерчески, без необходимости в проприетарной лицензии. Мы просим только, чтобы где-то в вашем приложении был уведомление об открытых исходных проектах, используемых в вашем проекте, и что Sisk есть там. --- # Маршрутизация Source: https://docs.sisk-framework.org/ru/docs/fundamentals/routing.html The [Router](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.md) is the first step in building the server. It is responsible for housing [Route](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.md) 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. ```cs 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](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) 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](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteAction.md) will be called. This delegate contains an parameter holding an [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) with all the necessary information about the request received by the server. The resulting object from this delegate must be an [HttpResponse](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpResponse.md) or an object that maps to it through [implicit response types](https://docs.sisk-framework.org/ru/docs/fundamentals/responses.md#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](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.NotFoundErrorHandler.md) response is returned to the client. When the path pattern is matched, but the HTTP method is mismatched, the [Router.MethodNotAllowedErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MethodNotAllowedErrorHandler.md) response is sent back to the client. The order in which routes are tested is described in [matching precedence](#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](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.Name.md). Both checks are controlled by [Router.CheckForRouteCollisions](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CheckForRouteCollisions.md), which is enabled by default. ### Приоритет сопоставления Every route is either **static** or **dynamic**, as indicated by [Route.IsStatic](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.IsStatic.md): - **Static routes** strictly follow their path pattern, such as `/users/`. 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](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RegexRoute.md), [PrefixRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.PrefixRoute.md) (used by [file servers](https://docs.sisk-framework.org/ru/docs/features/file-server.md)) and [custom matching routes](#custom-route-matching) 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: ```cs // 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 routes | Dynamic routes | |---|---|---| | Порядок сопоставления | Сначала, в порядке регистрации | После всех статических маршрутов, в порядке регистрации | | Проверка столкновений маршрутов | Да | Нет | | Применяется [Router.Prefix](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.Prefix.md) | Да | Нет | > [!WARNING] > 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`, делает второй недоступным. > [!IMPORTANT] > 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`. ```cs // Map* methods are the usual way to define method-specific routes. // Методы Map* — обычный способ определения маршрутов, специфичных для метода. mainRouter.MapGet("/hey/", (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//surname/", (request) => { string name = request.RouteParameters["name"].GetString(); string surname = request.RouteParameters["surname"].GetString(); return new HttpResponse($"Hello, {name} {surname}!"); }); ``` The [RouteParameters](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RouteParameters.md) 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](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MatchRoutesIgnoreCase.md) is set to `true`. // Сопоставление пути **чувствительно к регистру**, если только [Router.MatchRoutesIgnoreCase] не установлен в `true`. The [Query](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Query.md) and [RouteParameters](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RouteParameters.md) properties of [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) return a [StringValueCollection](https://docs.sisk-framework.org/api/Sisk.Core.Entity.StringValueCollection.md) object, where each indexed property returns a non-null [StringValue](https://docs.sisk-framework.org/api/Sisk.Core.Entity.StringValue.md), 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](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md). ```cs mainRouter.MapGet("/user/", (request) => { Guid id = request.RouteParameters["id"].GetGuid(); return new HttpResponse($"User id: {id}"); }); ``` > [!NOTE] > 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](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ForceTrailingSlash.md). > > Путь игнорирует завершающий `/` как в запросе, так и в шаблоне маршрута, то есть если вы попытаетесь обратиться к маршруту, определённому как `/index/page`, вы сможете обратиться к нему и как к `/index/page/`. > > Вы также можете принудительно добавлять завершающий `/` к URL, включив [HttpServerConfiguration.ForceTrailingSlash]. ### Создание маршрутов с использованием экземпляров классов You can also define routes dynamically using reflection with the attribute [RouteAttribute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteAttribute.md). 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](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteAttribute.md), such as the attribute itself or a [RouteGetAttribute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteGetAttribute.md). 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. ```cs {title="Controller/MyController.cs"} 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. ```cs var myController = new MyController(); mainRouter.MapInstance(myController); ``` To map only static route methods from a type, use: ```cs mainRouter.MapType(); ``` 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. ```cs mainRouter.AutoScanModules(); ``` // 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](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RegexRoute.md) class. Regex routes are [dynamic routes](#matching-precedence). ```cs 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](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RouteParameters.md) contents: ```cs {title="Controller/MyController.cs"} public class MyController { [RegexRoute(RouteMethod.Get, @"/uploads/(?.*\.(jpeg|jpg|png))")] static HttpResponse RegexRoute(HttpRequest request) { string filename = request.RouteParameters["filename"].GetString(); return new HttpResponse().WithContent($"Acessing file {filename}"); } } ``` > [!NOTE] > 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`. Используйте `^` и `$`, чтобы сопоставить весь путь. > [!IMPORTANT] > 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](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RegexRoute.md). 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](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RegexRoute.md). Свойство `UseRegex` атрибутов маршрутов, например `[Route(RouteMethod.Get, "...", UseRegex = true)]`, и атрибут `[RegexRoute]` продолжают работать. To migrate code that created regex routes with `Route.UseRegex`, replace the route with a `RegexRoute`: ```cs // before 1.7 // до 1.7 mainRouter.Map(new Route(RouteMethod.Get, @"^/items/(?\d+)$", action) { UseRegex = true }); // 1.7 and later // 1.7 и позже mainRouter.Map(new RegexRoute(RouteMethod.Get, @"^/items/(?\d+)$", action)); ``` ## Префиксные маршруты A [PrefixRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.PrefixRoute.md) matches its path and every path nested under it, comparing whole path segments. It is the route used by [Router.MapFileSystem](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MapFileSystem.md). Prefix routes are [dynamic routes](#matching-precedence). ```cs // 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](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.md) and overriding [Route.Match](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.Match.md). The method receives the request path and the router that is matching the route, and returns a [RouteMatch](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMatch.md) 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](#method-not-allowed-405-callback-handler) and `OPTIONS` responses working for custom routes. ```cs {title="Routing/ExtensionRoute.cs"} // 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); } } ``` ```cs 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](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.IsStatic.md) 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](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.Prefix.md), which can produce false collisions or invalid paths. // Переопределяйте [Route.IsStatic], возвращая `false`, когда маршрут не строго следует шаблону пути. Иначе роутер будет рассматривать `Path` как шаблон при проверке столкновений и применении [Router.Prefix], что может вызвать ложные столкновения или неверные пути. - Override [Route.AllowRewrites](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.AllowRewrites.md) to return `false` when the route path cannot be redirected to its trailing-slash form by [HttpServerConfiguration.ForceTrailingSlash](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ForceTrailingSlash.md). // Переопределяйте [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](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMatch.NotMatched.md) for unsuccessful matches. // `Match` вызывается для каждого запроса, попадающего в маршрут, возможно из нескольких потоков одновременно. Избегайте выделений памяти, когда маршрут не совпадает, и держите кэшированное состояние потокобезопасным. Возвращайте [RouteMatch.NotMatched] для неуспешных совпадений. - Respect [Router.MatchRoutesIgnoreCase](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MatchRoutesIgnoreCase.md) through the `router` parameter. // Учитывайте [Router.MatchRoutesIgnoreCase] через параметр `router`. ## Добавление префикса к маршрутам You can prefix all routes in a class or module with the [RoutePrefix](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RoutePrefixAttribute.md) attribute and set the prefix as a string. See the example below using the BREAD architecture (Browse, Read, Edit, Add and Delete): ```cs {title="Controller/Api/UsersController.cs"} [RoutePrefix("/api/users")] public class UsersController { // GET /api/users [RouteGet] public async Task Browse() { ... } // GET /api/users/ [RouteGet("/")] public async Task Read() { ... } // PATCH /api/users/ [RoutePatch("/")] public async Task Edit() { ... } // POST /api/users [RoutePost] public async Task Add() { ... } // DELETE /api/users/ [RouteDelete("/")] public async Task Delete() { ... } } ``` In the above example, the HttpResponse parameter is omitted in favor of being used through the global context [HttpContext.Current](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.Current.md). Read more in the section that follows. ## Маршруты без параметра запроса Routes can be defined without the [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) 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](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) currently. ```cs {title="Controller/ControllerBase.cs"} 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(); } } ``` And for all it's descendants to be able to use the route syntax without the request parameter: ```cs {title="Controller/UsersController.cs"} [RoutePrefix("/api/users")] public class UsersController : ControllerBase { [RoutePost] public async Task Create() { // reads the JSON data from the current request UserCreationDto? user = await Request.GetJsonContentAsync(); ... Database.Users.Add(user); return new HttpResponse(201); } } ``` More details about the current context and dependency injection can be found in the [dependency injection](https://docs.sisk-framework.org/ru/docs/features/instancing.md) 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. ```cs // will match / on any HTTP method // будет соответствовать / для любого HTTP‑метода mainRouter.MapAny("/", callbackFunction); ``` [RouteMethod.Any](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md) 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](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MapQuery.md), [Route.Query](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.Query.md), [RouteMethod.Query](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md) or the [RouteQuery](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteQueryAttribute.md) attribute to define these routes: ```cs mainRouter.MapQuery("/search", request => { string filter = request.Body; return new HttpResponse($"Searching for {filter}"); }); ``` ```cs {title="Controller/SearchController.cs"} 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](https://docs.sisk-framework.org/api/Sisk.Core.Entity.HttpHeaderCollection.AcceptQuery.md) and [HttpKnownHeaderNames.AcceptQuery](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpKnownHeaderNames.AcceptQuery.md). ## Маршруты любого пути 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](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.AnyPath.md) in its path expression, this route will listen to all requests from the HTTP server, and no other routes can be defined. ```cs // 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: ```cs 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. ```cs mainRouter.NotFoundErrorHandler = () => { return new HttpResponse(404) { // Since v0.14 // с версии 0.14 Content = new HtmlContent("

Not found

") // older versions // более старые версии Content = new StringContent("

Not found

", 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. ```cs 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](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) 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](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) 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: ```csharp 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](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) is set to false. ```cs mainRouter.CallbackErrorHandler = (ex, context) => { return new HttpResponse(500) { Content = new StringContent($"Error: {ex.Message}") }; }; ``` --- # Обработка запросов Source: https://docs.sisk-framework.org/ru/docs/fundamentals/request-handlers.html Обработчики запросов, также известные как «middleware», — это функции, которые выполняются до или после выполнения запроса роутером. Их можно определить как для отдельного маршрута, так и для всего роутера. Существует два типа обработчиков запросов: - **BeforeResponse**: определяет, что обработчик будет выполнен до вызова действия роутера. - **AfterResponse**: определяет, что обработчик будет выполнен после вызова действия роутера. Отправка HTTP‑ответа в этом контексте перезапишет ответ, сформированный действием роутера. Оба типа обработчиков могут переопределять фактический ответ функции обратного вызова роутера. Кстати, обработчики запросов могут быть полезны для проверки запроса, например аутентификации, содержимого или любой другой информации, такой как сохранение данных, журналирование или другие шаги, которые можно выполнить до или после ответа. ![](https://docs.sisk-framework.org/assets/img/requesthandlers1.png) Таким образом, обработчик запроса может прервать всё выполнение и вернуть ответ до завершения цикла, отбрасывая всё остальное в процессе. Пример: предположим, что обработчик аутентификации пользователя не аутентифицирует его. Он предотвратит продолжение жизненного цикла запроса и «зависнет». Если это происходит в обработчике на второй позиции, третий и последующие обработчики не будут оцениваться. ![](https://docs.sisk-framework.org/assets/img/requesthandlers2.png) ## Создание обработчика запросов Чтобы создать обработчик запросов, можно создать класс, наследующий интерфейс [IRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.md), в следующем виде: ```cs {title="Middleware/AuthenticateUserRequestHandler.cs"} public class AuthenticateUserRequestHandler : IRequestHandler { public RequestHandlerExecutionMode ExecutionMode { get; init; } = RequestHandlerExecutionMode.BeforeResponse; public HttpResponse? Execute(HttpRequest request, HttpContext context) { if (request.Headers.Authorization != null) { // Возврат null указывает, что цикл запроса может продолжаться return null; } else { // Возврат объекта HttpResponse указывает, что этот ответ перезапишет соседние ответы. return new HttpResponse(System.Net.HttpStatusCode.Unauthorized); } } } ``` В приведённом выше примере мы указали, что если заголовок `Authorization` присутствует в запросе, цикл должен продолжиться и будет вызван следующий обработчик запроса или обратный вызов роутера, в зависимости от того, что идёт дальше. Если обработчик выполняется после ответа (см. свойство [ExecutionMode](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.ExecutionMode.md)) и возвращает ненулевое значение, он перезапишет ответ роутера. Когда обработчик запроса возвращает `null`, это означает, что запрос должен продолжаться и должен быть вызван следующий объект, либо цикл завершится ответом роутера. > [!WARNING] > Экземпляр обработчика запроса создаётся один раз и используется всеми запросами, которые он обрабатывает, включая параллельные запросы. **Никогда не храните состояние отдельного запроса в самом классе обработчика** (поля или свойства, такие как текущий пользователь, разобранный токен или таймер), потому что это значение будет «просачиваться» в другие запросы. Храните значения, относящиеся к конкретному запросу, в [RequestBag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.RequestBag.md) объекта `HttpContext`: > > ```cs > public HttpResponse? Execute(HttpRequest request, HttpContext context) > { > // неправильно: this.currentUser = FindUser(request); > context.RequestBag.Set(FindUser(request)); > return null; > } > ``` Если вы наследуете встроенный класс [RequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RequestHandler.md), можете вернуть `Next()`, чтобы явно указать это намерение: ```cs public class AuthenticateUserRequestHandler : RequestHandler { public override HttpResponse? Execute(HttpRequest request, HttpContext context) { if (request.Headers.Authorization is not null) return Next(); return new HttpResponse(System.Net.HttpStatusCode.Unauthorized); } } ``` Для обработчиков, которым требуется ввод‑вывод, наследуйтесь от [AsyncRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.AsyncRequestHandler.md): ```cs public class LoadUserRequestHandler : AsyncRequestHandler { public override async Task ExecuteAsync(HttpRequest request, HttpContext context) { var user = await UserRepository.FindAsync(request.Headers.Authorization, request.DisconnectToken); if (user is null) return new HttpResponse(System.Net.HttpStatusCode.Unauthorized); request.Bag.Set(user); return Next(); } } ``` Небольшие встроенные обработчики также можно создать с помощью `RequestHandler.Create` или `AsyncRequestHandler.Create`: ```cs var requireJson = RequestHandler.Create((request, context) => { if (request.Headers.ContentType?.Contains("application/json") == true) return null; return new HttpResponse(System.Net.HttpStatusCode.UnsupportedMediaType); }); ``` ## Привязка обработчика запроса к отдельному маршруту Можно определить один или несколько обработчиков запросов для маршрута. ```cs {title="Router.cs"} mainRouter.Map(RouteMethod.Get, "/", IndexPage, new IRequestHandler[] { new AuthenticateUserRequestHandler(), // before request handler new ValidateJsonContentRequestHandler(), // before request handler // -- method IndexPage will be executed here new WriteToLogRequestHandler() // after request handler }); ``` Или создать объект [Route](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.md): ```cs {title="Router.cs"} Route indexRoute = Route.Get("/", IndexPage); indexRoute.RequestHandlers = new IRequestHandler[] { new AuthenticateUserRequestHandler() }; mainRouter.Map(indexRoute); ``` ## Привязка обработчика запроса к роутеру Можно определить глобальный обработчик запросов, который будет выполнен на всех маршрутах роутера. ```cs {title="Router.cs"} mainRouter.GlobalRequestHandlers = new IRequestHandler[] { new AuthenticateUserRequestHandler() }; ``` ## Привязка обработчика запроса к атрибуту Можно задать обработчик запроса в атрибуте метода вместе с атрибутом маршрута. ```cs {title="Controller/MyController.cs"} public class MyController { [RouteGet("/")] [RequestHandler] static HttpResponse Index(HttpRequest request) { return new HttpResponse() { Content = new StringContent("Hello world!") }; } } ``` Обратите внимание, что необходимо передавать тип требуемого обработчика, а не экземпляр объекта. Таким образом, обработчик будет создан парсером роутера. Аргументы можно передать в конструктор класса через свойство [ConstructorArguments](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RequestHandlerAttribute.ConstructorArguments.md). Пример: ```cs {title="Controller/MyController.cs"} [RequestHandler("arg1", 123, ...)] public HttpResponse Index(HttpRequest request) { return res = new HttpResponse() { Content = new StringContent("Hello world!") }; } ``` Можно также создать собственный атрибут, реализующий RequestHandler: ```cs {title="Middleware/Attributes/AuthenticateAttribute.cs"} public class AuthenticateAttribute : RequestHandlerAttribute { public AuthenticateAttribute() : base(typeof(AuthenticateUserRequestHandler), ConstructorArguments = new object?[] { "arg1", 123, ... }) { ; } } ``` И использовать его так: ```cs {title="Controller/MyController.cs"} [Authenticate] static HttpResponse Index(HttpRequest request) { return res = new HttpResponse() { Content = new StringContent("Hello world!") }; } ``` ## Обход глобального обработчика запросов После определения глобального обработчика запросов на маршруте, можно игнорировать этот обработчик на отдельных маршрутах. ```cs {title="Router.cs"} var myRequestHandler = new AuthenticateUserRequestHandler(); mainRouter.GlobalRequestHandlers = new IRequestHandler[] { myRequestHandler }; Route publicRoute = Route.Get("/", IndexPage); publicRoute.Name = "My route"; publicRoute.BypassGlobalRequestHandlers = new IRequestHandler[] { myRequestHandler, // ok: тот же экземпляр, что и в глобальных обработчиках new AuthenticateUserRequestHandler() // неправильно: не пропустит глобальный обработчик }; mainRouter.Map(publicRoute); ``` > [!NOTE] > Если вы обходите обработчик запросов, необходимо использовать ту же ссылку на объект, который был создан ранее, чтобы пропустить его. Создание нового экземпляра обработчика не позволит обойти глобальный обработчик, так как ссылка изменится. Помните, что следует использовать одну и ту же ссылку на обработчик как в `GlobalRequestHandlers`, так и в `BypassGlobalRequestHandlers`. --- # Запросы Source: https://docs.sisk-framework.org/ru/docs/fundamentals/requests.html Запросы — это структуры, представляющие HTTP‑сообщение запроса. Объект [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) содержит полезные функции для обработки HTTP‑сообщений во всём вашем приложении. HTTP‑запрос состоит из метода, пути, версии, заголовков и тела. В этом документе мы покажем, как получить каждый из этих элементов. ## Получение метода запроса Чтобы получить метод полученного запроса, можно использовать свойство Method: ```cs static HttpResponse Index(HttpRequest request) { HttpMethod requestMethod = request.Method; ... } ``` Это свойство возвращает метод запроса, представленный объектом [HttpMethod](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.httpmethod). > [!NOTE] > В отличие от методов маршрута, это свойство не обслуживает элемент [RouteMethod.Any](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md). Вместо этого оно возвращает реальный метод запроса. ## Получение компонентов URL запроса Вы можете получить различные компоненты URL через определённые свойства запроса. Для примера рассмотрим URL: ``` http://localhost:5000/user/login?email=foo@bar.com ``` | Имя компонента | Описание | Значение компонента | | --- | --- | --- | | [Path](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Path.md) | Получает путь запроса. | `/user/login` | | [FullPath](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.FullPath.md) | Получает путь запроса и строку запроса. | `/user/login?email=foo@bar.com` | | [FullUrl](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.FullUrl.md) | Получает полную строку URL запроса. | `http://localhost:5000/user/login?email=foo@bar.com` | | [Host](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Host.md) | Получает хост запроса. | `localhost` | | [Authority](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Authority.md) | Получает хост и порт запроса. | `localhost:5000` | | [QueryString](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.QueryString.md) | Получает строку запроса. | `?email=foo@bar.com` | | [Query](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Query.md) | Получает запрос в виде коллекции именованных значений. | `{StringValueCollection object}` | | [IsSecure](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.IsSecure.md) | Определяет, использует ли запрос SSL (true) или нет (false). | `false` | Вы также можете воспользоваться свойством [HttpRequest.Uri](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Uri.md), которое включает всё перечисленное в одном объекте. ## Метаданные запроса и отмена Sisk также добавляет к каждому запросу оперативные метаданные. Эти свойства полезны для журналов, трассировки, локализации, диагностики и длительных операций: | Свойство или метод | Назначение | | --- | --- | | [RequestId](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RequestId.md) | Уникальный идентификатор запроса. Включите [IncludeRequestIdHeader](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.IncludeRequestIdHeader.md), чтобы возвращать его как `X-Request-Id`. | | [RequestedAt](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RequestedAt.md) | Момент, когда Sisk создал объект запроса. | | [RemoteAddress](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RemoteAddress.md) | Адрес клиента, полученный из соединения, или из вашего [ForwardingResolver](https://docs.sisk-framework.org/ru/docs/advanced/forwarding-resolvers.md). | | [Culture](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Culture.md) | Наиболее подходящая культура, определённая из `Accept-Language`, с откатом к текущей культуре. | | [DisconnectToken](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.DisconnectToken.md) | Токен отмены, который сигнализируется при отключении клиента, если поддерживается настроенным HTTP‑движком. | | [Bag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Bag.md) | Типизированное хранилище ключ/значение, совместно используемое обработчиками запросов и действием маршрута. | | [GetRawHttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetRawHttpRequest.md) | Текстовое представление запроса для диагностики. | ## Получение тела запроса Некоторые запросы содержат тело, например формы, файлы или API‑транзакции. Тело запроса можно получить через свойство: ```cs // получает тело запроса как строку, используя кодировку запроса в качестве кодировщика string body = request.Body; // или получает его в виде массива байтов byte[] bodyBytes = request.RawBody; // либо можно получить поток. Stream requestStream = request.GetRequestStream(); // или прочитать тело асинхронно Memory bodyMemory = await request.GetBodyContentsAsync(); ``` Также можно определить, есть ли тело в запросе и загружено ли оно, с помощью свойств [HasContents](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.HasContents.md), которое определяет наличие содержимого, и [IsContentAvailable](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.IsContentAvailable.md), которое указывает, что HTTP‑сервер полностью получил содержимое от удалённого источника. Невозможно прочитать содержимое запроса через `GetRequestStream` более одного раза. Если вы читаете с помощью этого метода, значения в `RawBody` и `Body` также будут недоступны. Не требуется освобождать поток запроса в контексте запроса, так как он освобождается в конце HTTP‑сессии, в которой был создан. Кроме того, вы можете использовать свойство [HttpRequest.RequestEncoding](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RequestEncoding.md), чтобы получить оптимальную кодировку для ручного декодирования запроса. Сервер накладывает ограничения на чтение содержимого запроса, которые применяются как к [HttpRequest.Body](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Body.md), так и к [HttpRequest.RawBody](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Body.md). Эти свойства копируют весь входной поток в локальный буфер размером, равным [HttpRequest.ContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.ContentLength.md). Содержимое запроса ограничивается параметром [HttpServerConfiguration.MaximumContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.MaximumContentLength.md), который по умолчанию составляет 100 МБ начиная с Sisk 1.7 (в более ранних версиях было `0`, без ограничений). Ограничение применяется следующим образом: - Запрос, указывающий `Content-Length`, превышающий лимит, получает ответ `413 Content Too Large` до вызова маршрута. - Тело, передаваемое кусками или в виде потока без объявленной длины, проверяется во время чтения. Чтение за пределы лимита через [RawBody](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RawBody.md), [Body](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Body.md) или поток, возвращаемый [GetRequestStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetRequestStream.md), вызывает [InsufficientMemoryException](https://learn.microsoft.com/en-us/dotnet/api/system.insufficientmemoryexception), что приводит к ошибке `500 Internal Server Error`, если ваш код или [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) не обработают её. - То же ограничение является максимальным размером получаемого сообщения [WebSocket](https://docs.sisk-framework.org/ru/docs/features/websockets.md). Установите значение свойства в `0`, чтобы снять ограничение: ```cs using var app = HttpServer.CreateBuilder() .UseConfiguration(config => config.MaximumContentLength = 0) .Build(); ``` Даже без установленного ограничения `Body` и `RawBody` не могут буферизовать содержимое, превышающее [Array.MaxLength](https://learn.microsoft.com/en-us/dotnet/api/system.array.maxlength) (около 2 ГБ), и бросают исключение при попытке доступа к ним. По‑прежнему можно работать с большим содержимым через потоковую передачу. > [!NOTE] > Хотя Sisk позволяет это, всегда рекомендуется следовать семантике HTTP при создании приложения и не получать или обслуживать содержимое в методах, которые этого не допускают. Подробнее см. [RFC 9110 "HTTP Semantics"](https://httpwg.org/spec/rfc9110.html). ## Чтение JSON‑запросов Для JSON‑API предпочтительно использовать встроенные помощники JSON вместо чтения `Body` и ручной десериализации. Они используют [System.Text.Json](https://learn.microsoft.com/en-us/dotnet/api/system.text.json) и по умолчанию применяют [HttpRequest.DefaultJsonSerializerOptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.DefaultJsonSerializerOptions.md). ```cs public record CreateUserRequest(string Name, string Email); router.MapPost("/users", (HttpRequest request) => { CreateUserRequest? body = request.GetJsonContent(); if (body is null) return new HttpResponse(System.Net.HttpStatusCode.BadRequest); return new HttpResponse(System.Net.HttpStatusCode.Created); }); ``` Используйте асинхронную перегрузку, когда вы уже находитесь в асинхронном маршруте или хотите, чтобы отмена запроса прерывала десериализацию: ```cs router.MapPost("/users", async (HttpRequest request) => { CreateUserRequest? body = await request.GetJsonContentAsync(request.DisconnectToken); if (body is null) return new HttpResponse(System.Net.HttpStatusCode.BadRequest); return new HttpResponse(System.Net.HttpStatusCode.Created); }); ``` Для конкретной конечной точки можно передать пользовательские [JsonSerializerOptions](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializeroptions): ```cs var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { PropertyNameCaseInsensitive = true }; UserDto? user = request.GetJsonContent(options); ``` Для приложений Native AOT или чувствительных к обрезке используйте перегрузку `JsonTypeInfo`, генерируемую `JsonSerializerContext`: ```cs [JsonSerializable(typeof(CreateUserRequest))] public partial class AppJsonSerializerContext : JsonSerializerContext { } CreateUserRequest? body = await request.GetJsonContentAsync( AppJsonSerializerContext.Default.CreateUserRequest, request.DisconnectToken); ``` То же правило «прочитать один раз» применяется к JSON‑помощникам: после того как Sisk прочитает поток запроса через `GetJsonContent`, `GetJsonContentAsync`, `Body` или `RawBody`, вы не сможете позже использовать тот же тело через `GetRequestStream()`. ## Получение контекста запроса HTTP‑Context — это эксклюзивный объект Sisk, который хранит информацию о HTTP‑сервере, маршруте, роутере и обработчике запросов. Вы можете использовать его, чтобы упорядочить работу в среде, где эти объекты сложно организовать. Текущий исполняемый [HttpContext](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.md) можно получить с помощью статического метода `HttpContext.GetCurrentContext()`. Этот метод возвращает контекст запроса, который в данный момент обрабатывается в текущем потоке. ```cs HttpContext context = HttpContext.GetCurrentContext(); ``` ### Режим журналирования Свойство [HttpContext.LogMode](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.LogMode.md) позволяет управлять поведением журналирования для текущего запроса. Вы можете включать или отключать журналирование для отдельных запросов, переопределяя конфигурацию сервера по умолчанию. ```cs // Отключить журналирование для этого запроса context.LogMode = LogOutputMode.None; ``` ### Корзина запросов Объект [RequestBag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.RequestBag.md) содержит сохранённую информацию, передаваемую от одного обработчика запроса к другому, и может быть использована в конечной точке. Этот объект также может использоваться обработчиками запросов, которые выполняются после обратного вызова маршрута. > [!TIP] > Это свойство также доступно через свойство [HttpRequest.Bag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Bag.md). ```cs {title="Middleware/AuthenticateUserRequestHandler.cs"} public class AuthenticateUserRequestHandler : IRequestHandler { public string Identifier { get; init; } = Guid.NewGuid().ToString(); public RequestHandlerExecutionMode ExecutionMode { get; init; } = RequestHandlerExecutionMode.BeforeResponse; public HttpResponse? Execute(HttpRequest request, HttpContext context) { if (request.Headers.Authorization != null) { context.RequestBag.Add("AuthenticatedUser", new User("Bob")); return null; } else { return new HttpResponse(System.Net.HttpStatusCode.Unauthorized); } } } ``` Приведённый выше обработчик запроса добавит `AuthenticatedUser` в корзину запросов, и его можно будет использовать позже в финальном обратном вызове: ```cs {title="Controller/MyController.cs"} public class MyController { [RouteGet("/")] [RequestHandler] static HttpResponse Index(HttpRequest request) { User authUser = request.Context.RequestBag["AuthenticatedUser"]; return new HttpResponse() { Content = new StringContent($"Hello, {authUser.Name}!") }; } } ``` Вы также можете использовать вспомогательные методы `Bag.Set()` и `Bag.Get()` для получения или установки объектов по их типу‑синглтону. Класс `TypedValueDictionary` также предоставляет методы `GetValue` и `SetValue` для более гибкого управления. ```cs {title="Middleware/Authenticate.cs"} public class Authenticate : RequestHandler { public override HttpResponse? Execute(HttpRequest request, HttpContext context) { request.Bag.Set(authUser); } } ``` ```csharp {title="Controller/MyController.cs"} [RouteGet("/")] [RequestHandler] public static HttpResponse GetUser(HttpRequest request) { var user = request.Bag.Get(); ... } ``` ### Атомарные значения контекста Корзина запросов не синхронизирована. Когда числовые значения в корзине могут изменяться конкурентным кодом в рамках одного запроса, например параллельными задачами, запущенными маршрутом, используйте [HttpContext.Interlocked](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.Interlocked.md). Он возвращает объект [HttpContextInterlocked](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.HttpContextInterlocked.md) с атомарными операциями над именованными значениями корзины запросов: | Метод | Описание | | --- | --- | | `Add(name, value)` | Добавляет `value` к сохранённому значению, или сохраняет `value`, если `name` не установлено. | | `Exchange(name, value)` | Заменяет сохранённое значение и возвращает предыдущее, или `value`, если `name` не было установлено. | | `CompareExchange(name, value, comparand, notFound = -1)` | Заменяет сохранённое значение на `value` только если оно равно `comparand`, и возвращает оригинальное значение, или `notFound`, если `name` не установлено. | | `Inspect(name)` | Возвращает сохранённое значение, или `null`, если `name` не установлено. | Значения в корзине запросов хранятся как `double`. Перегрузки, принимающие `int` или `long`, преобразуют их в `double`. ```cs router.MapPost("/import", async (HttpRequest request) => { string[] lines = request.Body.Split('\n'); await Parallel.ForEachAsync(lines, async (line, cancellation) => { if (await TryImportAsync(line, cancellation)) request.Context.Interlocked.Add("imported", 1); }); double imported = request.Context.Interlocked.Inspect("imported") ?? 0; return new HttpResponse($"{imported} line(s) imported"); }); ``` > [!NOTE] > Операции атомарны только относительно других вызовов `Interlocked`. Чтение или запись того же ключа напрямую через `RequestBag` не синхронизированы с ними. ## Получение данных формы Вы можете получить значения данных формы в объекте [StringKeyStoreCollection](https://docs.sisk-framework.org/api/Sisk.Core.Entity.StringKeyStoreCollection.md) с помощью примера ниже: ```cs {title="Controller/Auth.cs"} [RoutePost("/auth")] public HttpResponse Index(HttpRequest request) { var form = request.GetFormContent(); string? username = form["username"]; string? password = form["password"]; if (AttempLogin(username, password)) { ... } } ``` Асинхронная версия полезна, когда тело запроса может быть большим или когда требуется поддержка отмены: ```cs var form = await request.GetFormContentAsync(request.DisconnectToken); ``` ## Получение multipart‑данных формы HTTP‑запрос Sisk позволяет получать загруженные multipart‑содержимое, такое как файлы, поля формы или любой двоичный контент. ```cs {title="Controller/Auth.cs"} [RoutePost("/upload-contents")] public HttpResponse Index(HttpRequest request) { // следующий метод читает весь ввод запроса в // массив объектов MultipartObject var multipartFormDataObjects = request.GetMultipartFormContent(); foreach (MultipartObject uploadedObject in multipartFormDataObjects) { // Имя файла, предоставленное multipart‑данными формы. // Возвращается null, если объект не является файлом. Console.WriteLine("File name : " + uploadedObject.Filename); // Имя поля объекта multipart‑данных формы. Console.WriteLine("Field name : " + uploadedObject.Name); // Длина содержимого multipart‑данных формы. Console.WriteLine("Content length : " + uploadedObject.ContentLength); // Определяет формат изображения по заголовку файла для каждого // известного типа содержимого. Если содержимое не является распознанным общим файлом // форматом, этот метод ниже вернёт MultipartObjectCommonFormat.Unknown Console.WriteLine("Common format : " + uploadedObject.GetCommonFileFormat()); } } ``` Используйте [GetMultipartFormContentAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetMultipartFormContentAsync.md), когда маршрут асинхронный: ```cs var multipartFormDataObjects = await request.GetMultipartFormContentAsync(request.DisconnectToken); ``` Подробнее о [Multipart‑объектах формы](https://docs.sisk-framework.org/api/Sisk.Core.Entity.MultipartObject.md) Sisk, их методах, свойствах и возможностях, можно узнать в документации. ## Обнаружение отключения клиента Начиная с версии v1.15 Sisk, фреймворк предоставляет токен отмены через [HttpRequest.DisconnectToken](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.DisconnectToken.md). Когда настроенный HTTP‑движок поддерживает обнаружение отключения, этот токен отменяется, если соединение клиента закрывается до завершения ответа. Это полезно для остановки длительных операций, когда клиент больше не ждёт результата. ```csharp router.MapGet("/connect", async (HttpRequest req) => { // gets the disconnection token from the request var dc = req.DisconnectToken; await LongOperationAsync(dc); return new HttpResponse(); }); ``` Этот токен не совместим со всеми HTTP‑движками, и каждый требует собственной реализации. Движок Sisk по умолчанию, основанный на `System.Net.HttpListener`, не поддерживает обнаружение отключения клиента. Когда приложение использует движок по умолчанию, `DisconnectToken` имеет значение `CancellationToken.None`; фактически это токен без возможности отмены и его следует считать недоступным. [Движок Cadente](https://docs.sisk-framework.org/ru/docs/cadente.md) поддерживает `DisconnectToken`. Если ваш маршрут зависит от отмены с учётом отключения, используйте Cadente или другой движок, который явно реализует это поведение. Даже при поддерживаемом движке отмена является кооперативной: передавайте токен в асинхронные API и проверяйте его в своей длительной работе. ## Поддержка Server‑sent events Sisk поддерживает [Server‑sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events), что позволяет отправлять фрагменты как поток и поддерживать соединение между сервером и клиентом живым. Вызов метода [HttpRequest.GetEventSource](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetEventSource.md) переводит HttpRequest в состояние слушателя. В этом случае контекст HTTP‑запроса не будет ожидать HttpResponse, так как он будет перекрывать пакеты, отправляемые серверными событиями. После отправки всех пакетов обратный вызов должен вернуть метод [Close](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequestEventSource.Close.md), который отправит окончательный ответ серверу и укажет, что поток завершён. Невозможно предсказать общую длину всех отправляемых пакетов, поэтому определить конец соединения с помощью заголовка `Content-Length` невозможно. По умолчанию большинство браузеров не поддерживают отправку HTTP‑заголовков или методов, отличных от GET, в серверных событиях. Поэтому будьте осторожны, используя обработчики запросов с event‑source, которые требуют специфических заголовков, так как, скорее всего, они не будут присутствовать. Кроме того, большинство браузеров перезапускают потоки, если метод [EventSource.close](https://developer.mozilla.org/en-US/docs/Web/API/EventSource/close) не вызывается на клиенте после получения всех пакетов, что приводит к бесконечной дополнительной обработке на стороне сервера. Чтобы избежать такой проблемы, обычно отправляют финальный пакет, указывающий, что источник событий завершил отправку всех пакетов. Пример ниже показывает, как браузер может взаимодействовать с сервером, поддерживающим серверные события. ```html {title="sse-example.html"} Fruits:
    ``` И постепенно отправлять сообщения клиенту: ```cs {title="Controller/MyController.cs"} public class MyController { [RouteGet("/event-source")] public async Task ServerEventsResponse(HttpRequest request) { var serverEvents = await request.GetEventSourceAsync (); string[] fruits = new[] { "Apple", "Banana", "Watermelon", "Tomato" }; foreach (string fruit in fruits) { await serverEvents.SendAsync(fruit); await Task.Delay(1500); } return await serverEvents.CloseAsync(); } } ``` При запуске этого кода ожидается результат, похожий на следующий: ## Разрешение проксированных IP и хостов Sisk может использоваться с прокси, поэтому IP‑адреса могут заменяться конечной точкой прокси в транзакции от клиента к прокси. Вы можете определить собственные резолверы в Sisk с помощью [forwarding resolvers](https://docs.sisk-framework.org/ru/docs/advanced/forwarding-resolvers.md). ## Кодировка заголовков Кодировка заголовков может быть проблемой для некоторых реализаций. В Windows заголовки UTF‑8 не поддерживаются, поэтому используется ASCII. Sisk имеет встроенный конвертер кодировок, который может быть полезен для декодирования неправильно закодированных заголовков. Эта операция ресурсоёмка и отключена по умолчанию, но её можно включить с помощью [HttpServerConfiguration.NormalizeHeadersEncodings](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.NormalizeHeadersEncodings.md). --- # Ответы Source: https://docs.sisk-framework.org/ru/docs/fundamentals/responses.html Ответы представляют собой объекты, являющиеся HTTP‑ответами на HTTP‑запросы. Они отправляются сервером клиенту в качестве указания на запрос ресурса, страницы, документа, файла или другого объекта. HTTP‑ответ состоит из статуса, заголовков и содержимого. В этом документе мы расскажем, как формировать HTTP‑ответы с помощью Sisk. ## Установка HTTP‑статуса Список HTTP‑статусов не изменился с версии HTTP/1.0, и Sisk поддерживает их все. ```cs HttpResponse res = new HttpResponse(); res.Status = System.Net.HttpStatusCode.Accepted; // 202 ``` Или с Fluent‑синтаксисом: ```cs new HttpResponse() .WithStatus(200) // or .WithStatus(HttpStatusCode.Ok) // or .WithStatus(HttpStatusInformation.Ok); ``` Полный список доступных HttpStatusCode вы можете увидеть [здесь](https://learn.microsoft.com/pt-br/dotnet/api/system.net.httpstatuscode). Вы также можете задать собственный код статуса, используя структуру [HttpStatusInformation](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpStatusInformation.md). ## Тело и тип содержимого Sisk поддерживает нативные .NET‑объекты содержимого для отправки тела в ответах. Например, вы можете использовать класс [StringContent](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.stringcontent) для отправки JSON‑ответа: ```cs HttpResponse res = new HttpResponse(); res.Content = new StringContent(myJson, Encoding.UTF8, "application/json"); ``` Сервер всегда будет пытаться вычислить `Content-Length` из того, что вы задали в содержимом, если вы явно не указали его в заголовке. Если сервер не может неявно получить заголовок Content-Length из содержимого ответа, ответ будет отправлен с Chunked‑Encoding. Вы также можете передавать ответ потоково, отправив [StreamContent](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.streamcontent) или используя метод [GetResponseStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetResponseStream.md). ## Заголовки ответа Вы можете добавлять, изменять или удалять заголовки, отправляемые в ответе. Пример ниже показывает, как отправить клиенту ответ с перенаправлением. ```cs HttpResponse res = new HttpResponse(); res.Status = HttpStatusCode.Moved; res.Headers.Add(HttpKnownHeaderNames.Location, "/login"); ``` Или с Fluent‑синтаксисом: ```cs new HttpResponse(301) .WithHeader("Location", "/login"); ``` Когда вы используете метод [Add](https://docs.sisk-framework.org/api/Sisk.Core.Entity.HttpHeaderCollection.Add.md) коллекции HttpHeaderCollection, вы добавляете заголовок к запросу, не изменяя уже отправленные. Метод [Set](https://docs.sisk-framework.org/api/Sisk.Core.Entity.HttpHeaderCollection.Set.md) заменяет заголовки с тем же именем указанным значением. Индексатор HttpHeaderCollection внутри вызывает метод Set для замены заголовков. Вы также можете получать значения заголовков с помощью метода [GetHeaderValue](https://docs.sisk-framework.org/api/Sisk.Core.Entity.HttpHeaderCollection.GetHeaderValue.md). Этот метод помогает получать значения как из заголовков ответа, так и из заголовков содержимого (если содержимое задано). ```cs // Возвращает значение заголовка "Content-Type", проверяя как response.Headers, так и response.Content.Headers string? contentType = response.GetHeaderValue("Content-Type"); ``` ## Отправка cookie Sisk предоставляет методы, упрощающие определение cookie в клиенте. Cookie, установленные этим методом, уже URL‑закодированы и соответствуют стандарту RFC-6265. ```cs HttpResponse res = new HttpResponse(); res.SetCookie("cookie-name", "cookie-value"); ``` Или с Fluent‑синтаксисом: ```cs new HttpResponse(301) .WithCookie("cookie-name", "cookie-value", expiresAt: DateTime.Now.Add(TimeSpan.FromDays(7))); ``` Существуют и другие [более полные версии](https://docs.sisk-framework.org/api/Sisk.Core.Helpers.CookieHelper.SetCookie.md) того же метода. ## Chunked‑ответы Вы можете установить кодировку передачи в chunked, чтобы отправлять большие ответы. ```cs HttpResponse res = new HttpResponse(); res.SendChunked = true; ``` При использовании chunked‑encoding заголовок Content-Length автоматически опускается. ## Поток ответа Потоки ответа — это управляемый способ отправки ответов сегментами. Это более низкоуровневая операция по сравнению с использованием объектов HttpResponse, так как требует вручную отправлять заголовки и содержимое, а затем закрывать соединение. Этот пример открывает поток только для чтения файла, копирует поток в выходной поток ответа и не загружает весь файл в память. Это может быть полезно при обслуживании средних или больших файлов. ```cs // получает поток вывода ответа using var fileStream = File.OpenRead("my-big-file.zip"); var responseStream = request.GetResponseStream(); // устанавливает кодировку ответа для использования chunked-encoding // также не следует отправлять заголовок content-length при использовании // chunked‑encoding responseStream.SendChunked = true; responseStream.SetStatus(200); responseStream.SetHeader(HttpKnownHeaderNames.ContentType, contentType); // копирует файловый поток в выходной поток ответа fileStream.CopyTo(responseStream.ResponseStream); // закрывает поток return responseStream.Close(); ``` ## Сжатие GZip, Deflate и Brotli Вы можете отправлять ответы со сжатым содержимым в Sisk, сжимая HTTP‑содержимое. Сначала оберните ваш объект [HttpContent](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpcontent) в один из компрессоров ниже, чтобы отправить сжатый ответ клиенту. ```cs router.MapGet("/hello.html", request => { string myHtml = "..."; return new HttpResponse () { Content = new GZipContent(new HtmlContent(myHtml)), // или Content = new BrotliContent(new HtmlContent(myHtml)), // или Content = new DeflateContent(new HtmlContent(myHtml)), }; }); ``` Вы также можете использовать эти сжатые содержимые со потоками. ```cs router.MapGet("/archive.zip", request => { // не используйте "using" здесь. HttpServer удалит ваш контент // после отправки ответа. var archive = File.OpenRead("/path/to/big-file.zip"); return new HttpResponse () { Content = new GZipContent(archive) } }); ``` Заголовки Content-Encoding устанавливаются автоматически при использовании этих содержимых. ## Автоматическое сжатие Можно автоматически сжимать HTTP‑ответы с помощью свойства [EnableAutomaticResponseCompression](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.EnableAutomaticResponseCompression.md). Это свойство автоматически оборачивает содержимое ответа из роутера в сжимаемое содержимое, которое принимается запросом, при условии, что ответ не наследуется от [CompressedContent](https://docs.sisk-framework.org/api/Sisk.Core.Http.CompressedContent.md). Для запроса выбирается только одно сжимаемое содержимое, выбранное согласно заголовку Accept-Encoding, который следует в порядке: - [BrotliContent](https://docs.sisk-framework.org/api/Sisk.Core.Http.BrotliContent.md) (br) - [GZipContent](https://docs.sisk-framework.org/api/Sisk.Core.Http.GZipContent.md) (gzip) - [DeflateContent](https://docs.sisk-framework.org/api/Sisk.Core.Http.DeflateContent.md) (deflate) Если запрос указывает, что принимает любой из этих методов сжатия, ответ будет автоматически сжат. ## Неявные типы ответов Вы можете использовать другие типы возвращаемых значений, помимо HttpResponse, но необходимо настроить роутер, как он будет обрабатывать каждый тип объекта. Концепция состоит в том, чтобы всегда возвращать ссылочный тип и преобразовывать его в действительный объект HttpResponse. Маршруты, возвращающие HttpResponse, не проходят никакого преобразования. Типы‑значения (структуры) нельзя использовать в качестве типа возврата, поскольку они несовместимы с [RouterCallback](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouterCallback.md), поэтому их необходимо обернуть в ValueResult, чтобы их можно было использовать в обработчиках. ```cs [RoutePrefix("/users")] public class UsersController : RouterModule { public List Users = new List(); [RouteGet] public IEnumerable Index(HttpRequest request) { return Users.ToArray(); } [RouteGet("")] public User View(HttpRequest request) { int id = request.RouteParameters["id"].GetInteger(); User dUser = Users.First(u => u.Id == id); return dUser; } [RoutePost] public ValueResult Create(HttpRequest request) { User fromBody = request.GetJsonContent()!; Users.Add(fromBody); return true; } } ``` С этим теперь необходимо определить в роутере, как он будет работать с каждым типом объекта. Объекты всегда являются первым аргументом обработчика, а тип вывода должен быть действительным HttpResponse. Кроме того, объекты вывода маршрута никогда не должны быть null. Для типов ValueResult не требуется указывать, что входной объект является ValueResult и только T, поскольку ValueResult — это объект, отражающий исходный компонент. Связывание типов не сравнивает то, что было зарегистрировано, с типом объекта, возвращаемого из обратного вызова роутера. Вместо этого проверяется, может ли тип результата роутера быть присвоен зарегистрированному типу. Регистрация обработчика типа Object будет использоваться как fallback для всех ранее не проверенных типов. Порядок вставки value‑обработчиков также имеет значение, поэтому регистрация обработчика Object игнорирует все остальные типо‑специфичные обработчики. Всегда регистрируйте специфичные value‑обработчики первыми, чтобы обеспечить порядок. ```cs Router r = new Router(); r.MapInstance(new UsersController()); r.RegisterValueHandler(apiResult => { return new HttpResponse() { Status = apiResult.Success ? HttpStatusCode.OK : HttpStatusCode.BadRequest, Content = apiResult.GetHttpContent(), Headers = apiResult.GetHeaders() }; }); r.RegisterValueHandler(bvalue => { return new HttpResponse() { Status = bvalue ? HttpStatusCode.OK : HttpStatusCode.BadRequest }; }); r.RegisterValueHandler>(enumerableValue => { return new HttpResponse(string.Join("\n", enumerableValue)); }); // регистрация value‑обработчика типа object должна быть последней // value‑handler, который будет использоваться как fallback r.RegisterValueHandler(fallback => { return new HttpResponse() { Status = HttpStatusCode.OK, Content = JsonContent.Create(fallback) }; }); ``` ## Отложенные действия Когда запрос попадает в роутер, он сначала проходит через [request handlers](https://docs.sisk-framework.org/ru/docs/fundamentals/request-handlers.md), обрабатывается в действии роутера, а затем через post‑execution request handlers. Результат действия роутера передаётся value‑обработчикам, а результат value‑обработчика отправляется клиенту в виде ответа. Этот жизненный цикл происходит в асинхронном контексте. Этот асинхронный контекст предоставляет переменные, которые пользователь может добавить в [HttpContext Bag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.md), чтобы делиться данными между обработчиками и действием роутера. Значение, возвращённое действием роутера, добавляется в этот асинхронный контекст и может быть доступно value‑обработчикам. Отложенные действия — это действия, которые всегда выполняются в конце цикла, после отправки ответа клиенту, но всё ещё в том же асинхронном контексте. Эти действия могут использоваться для выполнения длительных задач, не требующих завершения до отправки ответа клиенту, таких как сохранение логов, обновление базы данных, отправка писем и т.д. Исключения всё ещё перехватываются в отложенных действиях и обрабатываются так же, как исключения, выброшенные в любой части жизненного цикла запроса. Разница в том, что клиент уже получил ответ, поэтому исключение обрабатывается стандартным обработчиком ошибок. Отложить выполнение действия можно с помощью метода [HttpContext.EnqueueDeferredAction](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.EnqueueDeferredAction.md). Метод принимает асинхронную функцию, представляющую действие для выполнения, и необязательный тайм‑аут для ограничения времени выполнения действия. Если действие не завершится в пределах лимита, оно будет отменено. ```csharp [RoutePost("/send-mail")] public HttpResponse SendMail(HttpRequest request) { string to = request.Query["to"].GetString(); string subject = request.Query["subject"].GetString(); string body = request.Query["body"].GetString(); if (string.IsNullOrWhiteSpace(to) || string.IsNullOrWhiteSpace(subject) || string.IsNullOrWhiteSpace(body)) { throw new ApiException("Missing required parameters."); } // планирует длительное действие, которое будет выполнено после отправки ответа клиенту, но всё ещё в том же асинхронном контексте запроса request.Context.EnqueueDeferredAction(async (ct) => { await EmailService.SendEmailAsync(to, subject, body); }, timeout: TimeSpan.FromSeconds(30)); return new HttpResponse() { Status = 200, Content = new StringContent("Sending the email...") }; } ``` ## Примечание об перечисляемых объектах и массивах Неявные объекты ответа, реализующие [IEnumerable](https://learn.microsoft.com/pt-br/dotnet/api/system.collections.ienumerable?view=net-8.0), читаются в память через метод `ToArray()` перед преобразованием через определённый value‑handler. Для этого объект `IEnumerable` преобразуется в массив объектов, и конвертер ответа всегда получает `Object[]` вместо исходного типа. Рассмотрим следующую ситуацию: ```csharp using var host = HttpServer.CreateBuilder(12300) .UseRouter(r => { r.RegisterValueHandler>(stringEnumerable => { return new HttpResponse("String array:\n" + string.Join("\n", stringEnumerable)); }); r.RegisterValueHandler>(stringEnumerable => { return new HttpResponse("Object array:\n" + string.Join("\n", stringEnumerable)); }); r.MapGet("/", request => { return (IEnumerable)["hello", "world"]; }); }) .Build(); ``` В приведённом выше примере конвертер `IEnumerable` **никогда не будет вызван**, потому что входной объект всегда будет `Object[]` и не может быть преобразован в `IEnumerable`. Однако конвертер ниже, получающий `IEnumerable`, получит свой ввод, поскольку его значение совместимо. Если вам действительно нужно обрабатывать тип объекта, который будет перечисляться, вам придётся использовать рефлексию для получения типа элемента коллекции. Все перечисляемые объекты (списки, массивы и коллекции) конвертируются в массив объектов конвертером HTTP‑ответов. Значения, реализующие [IAsyncEnumerable](https://learn.microsoft.com/pt-br/dotnet/api/system.collections.generic.iasyncenumerable-1?view=net-8.0), обрабатываются сервером автоматически, если включено свойство [ConvertIAsyncEnumerableIntoEnumerable](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ConvertIAsyncEnumerableIntoEnumerable.md), аналогично тому, что происходит с `IEnumerable`. Эта опция включена по умолчанию в `HttpServerConfiguration`; асинхронное перечисление преобразуется в блокирующий перечислитель, а затем в синхронный массив объектов. Отключайте её только когда предоставляете собственный value‑handler или стратегию потокового ответа для асинхронных последовательностей. --- # Логирование Source: https://docs.sisk-framework.org/ru/docs/features/logging.html Вы можете настроить Sisk для автоматической записи журналов доступа и ошибок. Можно задать ротацию журналов, расширения и частоту. Класс [LogStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.md) предоставляет асинхронный способ записи журналов и хранит их в очереди записи, которую можно ожидать. Класс `LogStream` реализует `IAsyncDisposable`, гарантируя, что все ожидающие записи будут записаны до закрытия потока. В этой статье мы покажем, как настроить журналирование для вашего приложения. ## Файловые журналы доступа Запись журналов в файлы открывает файл, записывает строку текста и затем закрывает файл после каждой записи. Эта процедура была принята для поддержания отзывчивости записи в журналах. ```cs {title="Program.cs"} class Program { static async Task Main(string[] args) { using var app = HttpServer.CreateBuilder() .UseConfiguration(config => { config.AccessLogsStream = new LogStream("logs/access.log"); }) .Build(); ... await app.StartAsync(); } } ``` Приведённый код будет записывать все входящие запросы в файл `logs/access.log`. Обратите внимание, что файл создаётся автоматически, если его нет, однако папка перед ним не создаётся. Не требуется создавать каталог `logs/`, так как класс LogStream создаёт его автоматически. ## Потоковое журналирование Вы можете записывать журналы в объекты `TextWriter`, такие как `Console.Out`, передавая объект `TextWriter` в конструктор: ```cs {title="Program.cs"} using var app = HttpServer.CreateBuilder() .UseConfiguration(config => { config.AccessLogsStream = new LogStream(Console.Out); }) .Build(); ``` Для каждого сообщения, записываемого в потоковый журнал, вызывается метод `TextWriter.Flush()`. ## Форматирование журнала доступа Вы можете настроить формат журнала доступа с помощью предопределённых переменных. Рассмотрите следующую строку: ```cs config.AccessLogsFormat = "%dd/%dmm/%dy %tH:%ti:%ts %tz %ls %ri %rs://%ra%rz%rq [%sc %sd] %lin -> %lou in %lmsms [%{user-agent}]"; ``` Он запишет сообщение, похожее на: 29/mar./2023 15:21:47 -0300 Executed ::1 http://localhost:5555/ [200 OK] 689B -> 707B in 84ms [Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/111.0.0.0 Safari/537.36] Вы можете форматировать ваш журнал, используя формат, описанный в таблице: | Значение | Что представляет | Пример | |----------|------------------|--------| | %dd | День месяца (в виде двух цифр) | 05 | | %dmmm | Полное название месяца | July | | %dmm | Сокращённое название месяца (три буквы) | Jul | | %dm | Номер месяца (в виде двух цифр) | 07 | | %dy | Год (в виде четырёх цифр) | 2023 | | %th | Час в 12-часовом формате | 03 | | %tH | Час в 24-часовом формате (HH) | 15 | | %ti | Минуты (в виде двух цифр) | 30 | | %ts | Секунды (в виде двух цифр) | 45 | | %tm | Миллисекунды (в виде трёх цифр) | 123 | | %tz | Смещение часового пояса (общее количество часов в UTC) | +03:00 | | %ri | Удалённый IP-адрес клиента, определённый в начале запроса | 192.168.1.100 | | %rm | HTTP-метод (в верхнем регистре) | GET | | %rs | Схема URI (http/https) | https | | %ra | Авторитет URI (домен) | example.com | | %rh | Хост запроса | www.example.com | | %rp | Порт запроса | 443 | | %rz | Путь запроса | /path/to/resource | | %rq | Строка запроса | ?key=value&another=123 | | %sc | Код статуса HTTP-ответа | 200 | | %sd | Описание статуса HTTP-ответа | OK | | %lin | Размер запроса в человекочитаемом виде | 1.2 KB | | %linr | Необработанный размер запроса (байты) | 1234 | | %lou | Размер ответа в человекочитаемом виде | 2.5 KB | | %lour | Необработанный размер ответа (байты) | 2560 | | %lms | Прошедшее время в миллисекундах | 120 | | %ls | Статус выполнения | Executed | | %{header-name} | Представляет заголовок `header-name` запроса. | `Mozilla/5.0 (platform; rv:gecko [...]` | | %{:header-name} | Представляет заголовок `header-name` ответа. | `application/json` | | %{$key} | Представляет значение, хранящееся под `key` в [HttpContext.RequestBag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.RequestBag.md), преобразованное с помощью `ToString()`. Пусто, если ключ не установлен. | `42` | Вы также можете использовать `HttpServerConfiguration.DefaultAccessLogFormat` для применения формата журнала доступа по умолчанию. Термин `%{$key}` позволяет обработчикам запросов и маршрутам добавлять свои собственные значения в журнал доступа: ```cs config.AccessLogsFormat = "%dd/%dmm/%dy %tH:%ti:%ts %rm %rz [%sc] user=%{$UserId}"; router.MapGet("/profile", request => { request.Bag["UserId"] = "42"; return new HttpResponse("ok"); }); ``` ## Ротация журналов Вы можете настроить HTTP‑сервер так, чтобы он вращал файлы журналов в сжатый .gz файл, когда они достигают определённого размера. Размер проверяется периодически согласно заданному порогу. ```cs LogStream errorLog = new LogStream("logs/error.log") .ConfigureRotatingPolicy( maximumSize: 64 * SizeHelper.UnitMb, dueTime: TimeSpan.FromHours(6)); ``` Приведённый код будет каждые шесть часов проверять, достиг ли файл LogStream лимита в 64 МБ. Если да, файл сжимается в .gz и затем `access.log` очищается. Во время этого процесса запись в файл блокируется до завершения сжатия и очистки. Все строки, поступающие на запись в этот период, будут помещены в очередь, ожидая завершения сжатия. Эта функция работает только с файловыми LogStream. ## Журналирование ошибок Когда сервер не бросает ошибки в отладчик, он перенаправляет их в журнал, если они есть. Вы можете настроить запись ошибок с помощью: ```cs config.ThrowExceptions = false; config.ErrorsLogsStream = new LogStream("error.log"); ``` Это свойство будет записывать в журнал только в том случае, если ошибка не перехвачена обратным вызовом или свойством [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md). Ошибка, записываемая сервером, всегда включает дату и время, заголовки запроса (не тело), трассировку ошибки и трассировку внутреннего исключения, если они есть. ## Другие экземпляры журналов Ваше приложение может иметь ноль или несколько LogStream, нет ограничения на количество каналов журналов. Поэтому можно направлять журнал вашего приложения в файл, отличный от стандартного AccessLog или ErrorLog. ```cs LogStream appMessages = new LogStream("messages.log"); appMessages.WriteLine("Application started at {0}", DateTime.Now); ``` ## Безопасная запись файлов Для файлов, которые не являются потоками журналов, например отчётов или дампов состояния, [LogStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.md) предоставляет статические вспомогательные методы, которые создают недостающий родительский каталог, сериализуют конкурентные записи в один и тот же файл внутри процесса и возвращают `false` вместо выброса исключения при неудачной операции: - [LogStream.SafeWriteToFile](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeWriteToFile.md) и [LogStream.SafeWriteToFileAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeWriteToFileAsync.md) заменяют содержимое файла. - [LogStream.SafeAppendToFile](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeAppendToFile.md) и [LogStream.SafeAppendToFileAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeAppendToFileAsync.md) добавляют данные в конец файла. Каждый метод принимает строку или массив строк и имеет перегрузки, принимающие `Encoding`. ```cs bool written = await LogStream.SafeAppendToFileAsync("logs/audit.log", $"{DateTime.Now:s} user signed in\n"); if (!written) { Console.WriteLine("Could not write the audit entry."); } ``` Блокировка действует в пределах процесса: записи из других процессов в тот же файл не координируются. Если вам нужно лишь убедиться, что каталог существует, используйте [PathHelper.EnsureDirectoryExistance](https://docs.sisk-framework.org/api/Sisk.Core.Helpers.PathHelper.EnsureDirectoryExistance.md) или [PathHelper.EnsureFileDirectoryExistance](https://docs.sisk-framework.org/api/Sisk.Core.Helpers.PathHelper.EnsureFileDirectoryExistance.md), которые возвращают `true`, если каталог уже существовал, и `false`, если он был создан. ## Расширение LogStream Вы можете расширить класс `LogStream` для записи пользовательских форматов, совместимых с текущим движком журналов Sisk. Пример ниже позволяет выводить цветные сообщения в консоль с помощью библиотеки Spectre.Console: ```cs {title="CustomLogStream.cs"} public class CustomLogStream : LogStream { protected override void WriteLineInternal(string line) { base.WriteLineInternal($"[{DateTime.Now:g}] {line}"); } } ``` Другой способ автоматически записывать пользовательские журналы для каждого запроса/ответа — создать [HttpServerHandler](https://docs.sisk-framework.org/api/Sisk.Core.Http.Handlers.HttpServerHandler.md). Пример ниже более полный. Он записывает тело запроса и ответа в формате JSON в консоль. Это может быть полезно для отладки запросов в целом. В примере используется ContextBag и HttpServerHandler. ```cs {title="Program.cs"} class Program { static async Task Main(string[] args) { var app = HttpServer.CreateBuilder(host => { host.UseListeningPort(5555); host.UseHandler(); }); app.Router.MapAny("/json", request => { return new HttpResponse() .WithContent(JsonContent.Create(new { method = request.Method.Method, path = request.Path, specialMessage = "Hello, world!!" })); }); await app.StartAsync(); } } ``` ```cs {title="JsonMessageHandler.cs"} class JsonMessageHandler : HttpServerHandler { protected override void OnHttpRequestOpen(HttpRequest request) { if (request.Method != HttpMethod.Get && request.Headers["Content-Type"]?.Contains("json", StringComparison.InvariantCultureIgnoreCase) == true) { // На данном этапе соединение открыто, и клиент отправил заголовок, указывающий // что содержимое является JSON. Следующая строка читает содержимое и сохраняет его в запросе. // // Если содержимое не будет прочитано в обработчике запроса, сборщик мусора, вероятно, соберёт его // после отправки ответа клиенту, поэтому содержимое может быть недоступно после закрытия ответа. // _ = request.RawBody; // добавить подсказку в контекст, указывающую, что у этого запроса есть тело JSON request.Bag.Add("IsJsonRequest", true); } } protected override async void OnHttpRequestClose(HttpServerExecutionResult result) { string? requestJson = null, responseJson = null, responseMessage; if (result.Request.Bag.ContainsKey("IsJsonRequest")) { // переформатировать JSON с использованием библиотеки CypherPotato.LightJson var content = result.Request.Body; requestJson = JsonValue.Deserialize(content, new JsonOptions() { WriteIndented = true }).ToString(); } if (result.Response is { } response) { var content = response.Content; responseMessage = $"{(int)response.Status} {HttpStatusInformation.GetStatusCodeDescription(response.Status)}"; if (content is HttpContent httpContent && // проверить, является ли ответ JSON httpContent.Headers.ContentType?.MediaType?.Contains("json", StringComparison.InvariantCultureIgnoreCase) == true) { string json = await httpContent.ReadAsStringAsync(); responseJson = JsonValue.Deserialize(json, new JsonOptions() { WriteIndented = true }).ToString(); } } else { // получить статус внутренней обработки сервером responseMessage = result.Status.ToString(); } StringBuilder outputMessage = new StringBuilder(); if (requestJson != null) { outputMessage.AppendLine("-----"); outputMessage.AppendLine($">>> {result.Request.Method} {result.Request.Path}"); if (requestJson is not null) outputMessage.AppendLine(requestJson); } outputMessage.AppendLine($"<<< {responseMessage}"); if (responseJson != null) outputMessage.AppendLine(responseJson); outputMessage.AppendLine("-----"); await Console.Out.WriteLineAsync(outputMessage.ToString()); } } ``` --- # Server Sent Events Source: https://docs.sisk-framework.org/ru/docs/features/server-sent-events.html Sisk поддерживает отправку сообщений через Server Sent Events «из коробки». Вы можете создавать одноразовые и постоянные соединения, получать соединения во время выполнения и использовать их. Эта возможность имеет некоторые ограничения, налагаемые браузерами, такие как отправка только текстовых сообщений и невозможность полностью закрыть соединение. Соединение, закрытое на стороне сервера, будет периодически пытаться переподключиться клиенту каждые 5 секунд (3 секунды в некоторых браузерах). Эти соединения полезны для отправки событий с сервера клиенту без необходимости клиенту запрашивать информацию каждый раз. ## Creating an SSE connection SSE‑соединение работает как обычный HTTP‑запрос, но вместо того, чтобы отправить ответ и сразу закрыть соединение, соединение остаётся открытым для отправки сообщений. Вызвав метод [HttpRequest.GetEventSource()](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetEventSource.md), запрос переводится в состояние ожидания, пока создаётся экземпляр SSE. ```cs r.MapGet("/", (req) => { using var sse = req.GetEventSource(); sse.Send("Hello, world!"); return sse.Close(); }); ``` В приведённом выше коде мы создаём SSE‑соединение и отправляем сообщение «Hello, world!», затем закрываем SSE‑соединение со стороны сервера. > [!NOTE] > При закрытии соединения на стороне сервера по умолчанию клиент будет пытаться подключиться снова, и соединение будет перезапущено, вызывая метод заново, бесконечно. > > Обычно отправляют сообщение о завершении со стороны сервера, когда соединение закрывается, чтобы предотвратить повторные попытки переподключения клиента. ## Appending headers Если необходимо отправить заголовки, вы можете использовать метод [HttpRequestEventSource.AppendHeader](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.AppendHeader.md) до отправки любых сообщений. ```cs r.MapGet("/", (req) => { using var sse = req.GetEventSource(); sse.AppendHeader("Header-Key", "Header-value"); sse.Send("Hello!"); return sse.Close(); }); ``` Обратите внимание, что заголовки нужно отправлять до отправки любых сообщений. ## Wait-For-Fail connections Соединения обычно завершаются, когда сервер больше не может отправлять сообщения из‑за возможного отключения клиента. При этом соединение автоматически закрывается, а экземпляр класса удаляется. Даже при переподключении экземпляр класса не будет работать, так как он привязан к предыдущему соединению. В некоторых ситуациях вам может понадобиться это соединение позже, и вы не хотите управлять им через метод‑обратный вызов маршрута. Для этого мы можем идентифицировать SSE‑соединения с помощью идентификатора и получать их позже, даже вне обратного вызова маршрута. Кроме того, мы помечаем соединение атрибутом [WaitForFail](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.WaitForFail.md), чтобы не завершать маршрут и не закрывать соединение автоматически. SSE‑соединение в `WaitForFail` ждёт ошибку отправки, вызванную разрывом, или истечения настроенного времени простоя, прежде чем маршрут возобновится и закроет соединение. ```cs r.MapGet("/", (req) => { using var sse = req.GetEventSource("my-index-connection"); sse.WaitForFail(TimeSpan.FromSeconds(15)); // ждать 15 секунд без сообщений перед завершением соединения return sse.Close(); }); ``` Вышеприведённый метод создаст соединение, обработает его и будет ждать разъединения или ошибки. ```cs HttpRequestEventSource? evs = server.EventSources.GetByIdentifier("my-index-connection"); if (evs != null) { // соединение всё ещё активно evs.Send("Hello again!"); } ``` И приведённый выше фрагмент попытается найти только что созданное соединение и, если оно существует, отправит в него сообщение. Все активные серверные соединения, которые идентифицированы, будут доступны в коллекции [HttpServer.EventSources](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.EventSources.md). Эта коллекция хранит только активные и идентифицированные соединения. Закрытые соединения удаляются из коллекции. > [!NOTE] > Важно отметить, что keep alive имеет ограничение, установленное компонентами, которые могут быть подключены к Sisk неконтролируемым образом, например веб‑прокси, HTTP‑ядром или сетевым драйвером, и они закрывают простоящие соединения после определённого периода времени. > > Поэтому важно поддерживать соединение открытым, отправляя периодические пинги или увеличивая максимальное время до закрытия соединения. Читайте следующий раздел, чтобы лучше понять отправку периодических пингов. ## Setup connections ping policy Ping Policy — это автоматический способ отправки периодических сообщений вашему клиенту. Эта функция позволяет серверу понять, что клиент отключился от соединения, без необходимости держать соединение открытым бесконечно. ```cs [RouteGet("/sse")] public async Task Events(HttpRequest request) { using var sse = await request.GetEventSourceAsync("user-events"); sse.WithPing(ping => { ping.DataMessage = "ping-message"; ping.Interval = TimeSpan.FromSeconds(5); ping.Start(); }); await sse.WaitForFailAsync(TimeSpan.FromMinutes(10)); return await sse.CloseAsync(); } ``` В приведённом коде каждые 5 секунд клиенту будет отправляться новое ping‑сообщение. Это поддерживает TCP‑соединение живым и предотвращает его закрытие из‑за бездействия. Кроме того, когда сообщение не удаётся отправить, соединение автоматически закрывается, освобождая ресурсы, использованные соединением. Используйте [SendAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.SendAsync.md) и [CloseAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.CloseAsync.md) в асинхронных маршрутах. Если нужно отбросить накопленные события перед закрытием, вызовите [Cancel](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.Cancel.md). ## Querying connections Вы можете искать активные соединения, используя предикат по идентификатору соединения, чтобы, например, выполнить широковещательную рассылку. ```cs HttpRequestEventSource[] evs = server.EventSources.Find(es => es.StartsWith("my-connection-")); foreach (HttpRequestEventSource e in evs) { e.Send("Broadcasting to all event sources that starts with 'my-connection-'"); } ``` Также можно использовать метод [All](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpEventSourceCollection.All.md) для получения всех активных SSE‑соединений. --- # Веб-сокеты Source: https://docs.sisk-framework.org/ru/docs/features/websockets.html Sisk также поддерживает веб‑сокеты, позволяя получать и отправлять сообщения клиенту. Эта функция работает в большинстве браузеров, но в Sisk она всё ещё экспериментальная. Пожалуйста, если вы обнаружите ошибки, сообщайте о них на GitHub. ## Приём сообщений Сообщения WebSocket получаются в порядке их отправки, ставятся в очередь до обработки методом `ReceiveMessageAsync`. Этот метод не возвращает сообщение, если истек тайм‑аут, операция была отменена или клиент отключился. Максимальный размер получаемого сообщения задаётся свойством [HttpServerConfiguration.MaximumContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.MaximumContentLength.md) и по умолчанию составляет 100 МБ, начиная с Sisk 1.7. Если клиент отправит сообщение большего размера, соединение будет закрыто со статусом `MessageTooBig`, и `ReceiveMessageAsync` не вернёт сообщение. Одновременно может выполняться только одна операция чтения или записи, поэтому, пока вы ждёте сообщение с помощью `ReceiveMessageAsync`, запись клиенту невозможна. ```cs router.MapGet("/connect", async (HttpRequest req) => { using var ws = await req.GetWebSocketAsync(); while (await ws.ReceiveMessageAsync(timeout: TimeSpan.FromSeconds(30)) is { } receivedMessage) { string msgText = receivedMessage.GetString(); Console.WriteLine("Received message: " + msgText); await ws.SendAsync("Hello!"); } return await ws.CloseAsync(); }); ``` ## Постоянное соединение Ниже приведён пример, показывающий, как использовать постоянное соединение WebSocket: получать сообщения, обрабатывать их и завершать работу с сокетом. ```cs router.MapGet("/connect", async (HttpRequest req) => { using var ws = await req.GetWebSocketAsync(); WebSocketMessage? msg; askName: await ws.SendAsync("What is your name?"); msg = await ws.ReceiveMessageAsync(); if (msg is null) return await ws.CloseAsync(); string name = msg.GetString(); if (string.IsNullOrEmpty(name)) { await ws.SendAsync("Please, insert your name!"); goto askName; } askAge: await ws.SendAsync("And your age?"); msg = await ws.ReceiveMessageAsync(); if (msg is null) return await ws.CloseAsync(); if (!Int32.TryParse(msg?.GetString(), out int age)) { await ws.SendAsync("Please, insert an valid number"); goto askAge; } await ws.SendAsync($"You're {name}, and you are {age} old."); return await ws.CloseAsync(); }); ``` ## Политика Ping Подобно политике ping в Server Side Events, вы можете настроить политику ping, чтобы поддерживать TCP‑соединение открытым при отсутствии активности. ```cs ws.PingPolicy.Start( dataMessage: "ping-message", interval: TimeSpan.FromSeconds(10)); ``` ## Управляемые соединения При принятии WebSocket вы можете указать идентификатор. Идентифицированные сокеты регистрируются в [HttpServer.WebSockets](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.WebSockets.md), что позволяет серверу находить активные соединения вне маршрута, принявшего их. ```cs router.MapGet("/connect/", async (HttpRequest req) => { string userId = req.RouteParameters["userId"].GetString(); using var ws = await req.GetWebSocketAsync(identifier: $"user:{userId}"); ws.State = userId; ws.PingPolicy.Start( dataMessage: "ping", interval: TimeSpan.FromSeconds(10)); while (await ws.ReceiveMessageAsync(TimeSpan.FromMinutes(5)) is { } message) { await ws.SendAsync("Received: " + message.GetString()); } return await ws.CloseAsync(); }); ``` Из другой части приложения можно запросить коллекцию по идентификатору или предикату: ```cs HttpWebSocket? socket = server.WebSockets.GetByIdentifier("user:42"); if (socket is { IsClosed: false }) { await socket.SendAsync("Your report is ready."); } foreach (HttpWebSocket activeSocket in server.WebSockets.Find(id => id.StartsWith("user:"))) { await activeSocket.SendAsync("Broadcast message"); } ``` Каждый `HttpWebSocket` предоставляет свойства `Identifier`, `State`, `IsClosed` и `PingPolicy`. Коллекция также предоставляет методы `All()`, `Find(...)`, `GetByIdentifier(...)`, `ActiveConnections` и `DropAll()` для стратегий управляемых сервером соединений. --- # Синтаксис отбрасывания Source: https://docs.sisk-framework.org/ru/docs/features/discard-syntax.html Веб-сервер может использоваться для прослушивания запроса обратного вызова от действия, такого как аутентификация OAuth, и может быть отброшен после получения этого запроса. Это может быть полезно в случаях, когда вам нужное фоновое действие, но вы не хотите настраивать整个 веб-приложение для этого. Следующий пример показывает, как создать прослушивающий HTTP-сервер на порту 5555 с помощью [CreateListener](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.CreateListener.md) и ожидать следующий контекст: ```csharp using (var server = HttpServer.CreateListener(5555)) { // ожидать следующий HTTP-запрос var context = await server.WaitNextAsync(); Console.WriteLine($"Запрошенный путь: {context.Request.Path}"); } ``` Функция [WaitNext](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.WaitNext.md) ожидает следующий контекст завершенной обработки запроса. Как только результат этой операции получен, сервер уже полностью обработал запрос и отправил ответ клиенту. --- # Внедрение зависимостей Source: https://docs.sisk-framework.org/ru/docs/features/instancing.html Обычно посвящают члены и экземпляры, которые существуют в течение времени жизни запроса, такие как соединение с базой данных, аутентифицированный пользователь или токен сеанса. Одним из возможностей является использование [HttpContext.RequestBag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.md), который создает словарь, существующий в течение всего времени жизни запроса. Этот словарь может быть доступен [обработчиками запросов](https://docs.sisk-framework.org/ru/docs/fundamentals/request-handlers.md) и определять переменные на протяжении всего запроса. Например, обработчик запроса, который аутентифицирует пользователя, устанавливает этого пользователя в `HttpContext.RequestBag`, и в логике запроса этот пользователь может быть получен с `HttpContext.RequestBag.Get()`. Объекты, определенные в этом словаре, имеют область видимости запроса. Они удаляются в конце запроса. Не обязательно отправка ответа определяет конец времени жизни запроса. Когда [обработчики запросов](https://docs.sisk-framework.org/ru/docs/fundamentals/request-handlers.md), которые выполняются после отправки ответа, выполняются, объекты `RequestBag`仍 существуют и не были удалены. Вот пример: ```csharp {title="RequestHandlers/AuthenticateUser.cs"} public class AuthenticateUser : IRequestHandler { public RequestHandlerExecutionMode ExecutionMode { get; init; } = RequestHandlerExecutionMode.BeforeResponse; public HttpResponse? Execute(HttpRequest request, HttpContext context) { User authenticatedUser = AuthenticateUser(request); context.RequestBag.Set(authenticatedUser); return null; // advance to the next request handler or request logic } } `` ```csharp {title="Controllers/HelloController.cs"} [RouteGet("/hello")] [RequestHandler] public HttpResponse SayHello(HttpRequest request) { var authenticatedUser = request.Bag.Get(); return new HttpResponse() { Content = new StringContent($"Hello {authenticatedUser.Name}!") }; } `` Это предварительный пример этой операции. Экземпляр `User` был создан в обработчике запроса, посвященном аутентификации, и все маршруты, которые используют этот обработчик запроса, будут иметь гарантию, что в их экземпляре `HttpContext.RequestBag` будет `User`. Возможно определить логику для получения экземпляров, когда они не были предварительно определены в `RequestBag`, через методы, такие как [GetOrAdd](https://docs.sisk-framework.org/api/Sisk.Core.Entity.TypedValueDictionary.GetOrAdd.md) или [GetOrAddAsync](https://docs.sisk-framework.org/api/Sisk.Core.Entity.TypedValueDictionary.GetOrAddAsync.md). С версии 1.3 был введен статический свойство [HttpContext.Current](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.Current.md), которое позволяет получить доступ к текущему контексту запроса. Это позволяет экспонировать члены `HttpContext` вне текущего запроса и определять экземпляры в объектах маршрутов. В примере ниже определяется контроллер, который имеет члены, часто доступные контекстом запроса. ```csharp {title="Controllers/Controller.cs"} public abstract class Controller : RouterModule { // Получить существующий или создать новый экземпляр базы данных для этого запроса protected DbContext Database => HttpContext.Current.RequestBag.GetOrAdd(() => new DbContext()); // Ленивая загрузка репозиториев также распространена protected IUserRepository Users => HttpContext.Current.RequestBag.GetOrAdd(() => new UserRepository(Database)); protected IBlogRepository Blogs => HttpContext.Current.RequestBag.GetOrAdd(() => new BlogRepository(Database)); protected IBlogPostRepository BlogPosts => HttpContext.Current.RequestBag.GetOrAdd(() => new BlogPostRepository(Database)); // следующая строка выдаст исключение, если свойство доступно, когда пользователь не // определен в пакете запроса protected User AuthenticatedUser => => HttpContext.Current.RequestBag.Get(); // Экспонирование экземпляра HttpRequest также поддерживается protected HttpRequest Request => HttpContext.Current.Request } `` И определяют типы, которые наследуются от контроллера: ```csharp {title="Controllers/PostsController.cs"} [RoutePrefix("/api/posts/{author}")] sealed class PostsController : Controller { protected Guid AuthorId => Request.RouteParameters["author"].GetInteger(); [RouteGet] public IAsyncEnumerable ListPosts() { return BlogPosts.GetPostsAsync(authorId: AuthorId); } [RouteGet("")] public async Task GetPost() { int postId = Request.RouteParameters["id"].GetInteger(); Post? post = await BlogPosts .FindPostAsync(post => post.Id == postId && post.AuthorId == AuthorId); return post; } } `` Для примера выше вам необходимо настроить [обработчик значения](https://docs.sisk-framework.org/ru/docs/fundamentals/responses.md#implicit-response-types) в вашем маршрутизаторе, чтобы объекты, возвращаемые маршрутизатором, были преобразованы в допустимый [HttpResponse](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpResponse.md). Обратите внимание, что методы не имеют аргумента `HttpRequest request`, как это присутствует в других методах. Это потому, что, начиная с версии 1.3, маршрутизатор поддерживает два типа делегатов для маршрутизации ответов: [RouteAction](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteAction.md), который является делегатом по умолчанию, получающим аргумент `HttpRequest`, и [ParameterlessRouteAction](https://docs.sisk-framework.org/api/Sisk.Core.Routing.ParameterlessRouteAction.md). Объект `HttpRequest` все равно может быть доступен обоими делегатами через свойство [Request](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.Request.md) статического `HttpContext` на потоке. В примере выше мы определили удаляемый объект, `DbContext`, и нам необходимо обеспечить, чтобы все экземпляры, созданные в `DbContext`, были удалены, когда HTTP-сессия заканчивается. Для этого мы можем использовать два способа достижения этого. Один из них - создать [обработчик запроса](https://docs.sisk-framework.org/ru/docs/fundamentals/request-handlers.md), который выполняется после действия маршрутизатора, а другой способ - через пользовательский [обработчик сервера](https://docs.sisk-framework.org/ru/docs/advanced/http-server-handlers.md). Для первого метода мы можем создать обработчик запроса trực в методе [OnSetup](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouterModule.OnSetup.md), унаследованном от `RouterModule`: ```csharp {title="Controllers/PostsController.cs"} public abstract class Controller : RouterModule { ... protected override void OnSetup(Router parentRouter) { base.OnSetup(parentRouter); HasRequestHandler(RequestHandler.Create( execute: (req, ctx) => { // получить один экземпляр DbContext, определенный в контексте обработчика запроса, и // удалить его ctx.RequestBag.GetOrDefault()?.Dispose(); return null; }, executionMode: RequestHandlerExecutionMode.AfterResponse)); } } `` > [!TIP] > > Начиная с версии Sisk 1.4, свойство [HttpServerConfiguration.DisposeDisposableContextValues](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.DisposeDisposableContextValues.md) было введено и включено по умолчанию, которое определяет, должен ли HTTP-сервер удалять все `IDisposable`-значения в контекстном пакете, когда HTTP-сессия закрывается. Метод выше обеспечит удаление `DbContext`, когда HTTP-сессия будет завершена. Вы можете сделать это для других членов, которые необходимо удалить в конце ответа. Для второго метода вы можете создать пользовательский [обработчик сервера](https://docs.sisk-framework.org/ru/docs/advanced/http-server-handlers.md), который будет удалять `DbContext`, когда HTTP-сессия будет завершена. ```csharp {title="Server/Handlers/ObjectDisposerHandler.cs"} public class ObjectDisposerHandler : HttpServerHandler { protected override void OnHttpRequestClose(HttpServerExecutionResult result) { result.Context.RequestBag.GetOrDefault()?.Dispose(); } } `` И использовать его в вашем построителе приложения: ```csharp {title="Program.cs"} using var host = HttpServer.CreateBuilder() .UseHandler() .Build(); `` Это один из способов обработки очистки кода и сохранения зависимостей запроса, разделенных по типу модуля, который будет использоваться, уменьшая количество дублирующего кода внутри каждого действия маршрутизатора. Это похоже на то, для чего используется внедрение зависимостей в фреймворках, таких как ASP.NET. --- # Потоковая передача контента Source: https://docs.sisk-framework.org/ru/docs/features/content-streaming.html Sisk поддерживает чтение и отправку потоков контента клиенту и от клиента. Эта функция полезна для удаления нагрузки на память при сериализации и десериализации контента во время жизни запроса. ## Поток контента запроса Маленькие содержимые автоматически загружаются в буфер памяти HTTP-соединения, быстро загружая это содержимое в [HttpRequest.Body](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Body.md) и [HttpRequest.RawBody](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RawBody.md). Для более крупных содержимых можно использовать метод [HttpRequest.GetRequestStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetRequestStream.md), чтобы получить поток чтения контента запроса. Стоит отметить, что метод [HttpRequest.GetMultipartFormContent](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetMultipartFormContent.md) читает весь контент запроса в память, поэтому он может не быть полезен для чтения крупных содержимых. Рассмотрим следующий пример: ```csharp {title="Controller/UploadDocument.cs"} [RoutePost ( "/api/upload-document/" )] public async Task UploadDocument ( HttpRequest request ) { var fileName = request.RouteParameters [ "filename" ].GetString (); if (!request.HasContents) { // запрос не содержит контента return new HttpResponse ( HttpStatusInformation.BadRequest ); } var contentStream = request.GetRequestStream (); var outputFileName = Path.Combine ( AppDomain.CurrentDomain.BaseDirectory, "uploads", fileName ); using (var fs = File.Create ( outputFileName )) { await contentStream.CopyToAsync ( fs ); } return new HttpResponse () { Content = JsonContent.Create ( new { message = "Файл отправлен успешно." } ) }; } ``` В примере выше метод `UploadDocument` читает контент запроса и сохраняет контент в файл. Не производится дополнительная аллокация памяти, кроме буфера чтения, используемого `Stream.CopyToAsync`. Пример выше удаляет нагрузку на аллокацию памяти для очень крупного файла, что может оптимизировать производительность приложения. Хорошей практикой является всегда использовать [CancellationToken](https://learn.microsoft.com/pt-br/dotnet/api/system.threading.cancellationtoken) в операции, которая может занять много времени, такой как отправка файлов, поскольку она зависит от скорости сети между клиентом и сервером. Настройка с помощью `CancellationToken` может быть выполнена следующим образом: ```csharp {title="Controller/UploadDocument.cs"} // токен отмены ниже бросит исключение, если будет достигнута 30-секундная задержка. CancellationTokenSource copyCancellation = new CancellationTokenSource ( delay: TimeSpan.FromSeconds ( 30 ) ); try { using (var fs = File.Create ( outputFileName )) { await contentStream.CopyToAsync ( fs, copyCancellation.Token ); } } catch (OperationCanceledException) { return new HttpResponse ( HttpStatusInformation.BadRequest ) { Content = JsonContent.Create ( new { Error = "Загрузка превысила максимальное время загрузки (30 секунд)." } ) }; } ``` ## Поток контента ответа Отправка контента ответа также возможна. В настоящее время существует два способа сделать это: через метод [HttpRequest.GetResponseStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetResponseStream.md) и используя контент типа [StreamContent](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.streamcontent?view=net-9.0). Рассмотрим сценарий, в котором нам нужно предоставить файл изображения. Для этого можно использовать следующий код: ```csharp {title="Controller/ImageController.cs"} [RouteGet ( "/api/profile-picture" )] public async Task UploadDocument ( HttpRequest request ) { // пример метода для получения изображения профиля var profilePictureFilename = "profile-picture.jpg"; byte[] profilePicture = await File.ReadAllBytesAsync ( profilePictureFilename ); return new HttpResponse () { Content = new ByteArrayContent ( profilePicture ), Headers = new () { ContentType = "image/jpeg", ContentDisposition = $"inline; filename={profilePictureFilename}" } }; } ``` Метод выше производит аллокацию памяти каждый раз, когда он читает контент изображения. Если изображение большое, это может вызвать проблему производительности, и в пиковых ситуациях даже привести к переполнению памяти и краху сервера. В таких ситуациях кэширование может быть полезным, но оно не устранит проблему, поскольку память все равно будет зарезервирована для этого файла. Кэширование облегчит нагрузку на аллокацию памяти для каждого запроса, но для крупных файлов оно не будет достаточно. Отправка изображения через поток может быть решением проблемы. Вместо чтения всего контента изображения создается поток чтения файла и копируется клиенту с помощью небольшого буфера. #### Отправка через метод GetResponseStream Метод [HttpRequest.GetResponseStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetResponseStream.md) создает объект, который позволяет отправлять фрагменты HTTP-ответа по мере подготовки потока контента. Этот метод более ручной, требующий определения статуса, заголовков и размера контента перед отправкой контента. ```csharp {title="Controller/ImageController.cs"} [RouteGet ( "/api/profile-picture" )] public async Task UploadDocument ( HttpRequest request ) { var profilePictureFilename = "profile-picture.jpg"; // в этой форме отправки необходимо определить статус и заголовки // перед отправкой контента var requestStreamManager = request.GetResponseStream (); requestStreamManager.SetStatus ( System.Net.HttpStatusCode.OK ); requestStreamManager.SetHeader ( HttpKnownHeaderNames.ContentType, "image/jpeg" ); requestStreamManager.SetHeader ( HttpKnownHeaderNames.ContentDisposition, $"inline; filename={profilePictureFilename}" ); using (var fs = File.OpenRead ( profilePictureFilename )) { // в этой форме отправки также необходимо определить размер контента // перед отправкой его. requestStreamManager.SetContentLength ( fs.Length ); // если вы не знаете размер контента, можно использовать chunked-encoding // для отправки контента requestStreamManager.SendChunked = true; // и затем записать в поток вывода await fs.CopyToAsync ( requestStreamManager.ResponseStream ); } } ``` #### Отправка контента через StreamContent Класс [StreamContent](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.streamcontent?view=net-9.0) позволяет отправлять контент из источника данных в виде потока байтов. Эта форма отправки проще, удаляя предыдущие требования, и даже позволяет использовать [кодирование сжатия](https://docs.sisk-framework.org/ru/docs/fundamentals/responses.md#gzip-deflate-and-brotli-compression), чтобы уменьшить размер контента. ```csharp {title="Controller/ImageController.cs"} [RouteGet ( "/api/profile-picture" )] public HttpResponse UploadDocument ( HttpRequest request ) { var profilePictureFilename = "profile-picture.jpg"; return new HttpResponse () { Content = new StreamContent ( File.OpenRead ( profilePictureFilename ) ), Headers = new () { ContentType = "image/jpeg", ContentDisposition = $"inline; filename=\"{profilePictureFilename}\"" } }; } ``` > [!IMPORTANT] > > В этом типе контента не заключайте поток в блок `using`. Контент будет автоматически удален HTTP-сервером, когда поток контента будет завершен, с ошибками или без них. --- # Включение CORS (Cross-Origin Resource Sharing) в Sisk Source: https://docs.sisk-framework.org/ru/docs/features/cors.html Sisk предоставляет инструмент, который может быть полезен для обработки [Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) при публичном предоставлении вашего сервиса. Эта возможность не является частью протокола HTTP, а представляет собой специфическую функцию веб‑браузеров, определённую W3C. Этот механизм безопасности препятствует веб‑странице выполнять запросы к другому домену, отличному от того, с которого была получена сама страница. Поставщик сервиса может разрешить доступ к своим ресурсам определённым доменам или только одному. ## Один и тот же источник Чтобы ресурс был идентифицирован как «один и тот же источник», запрос должен содержать заголовок [Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Origin): ```http GET /api/users HTTP/1.1 Host: example.com Origin: http://example.com ... ``` А удалённый сервер должен ответить заголовком [Access-Control-Allow-Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) со значением, совпадающим с запрошенным origin: ```http HTTP/1.1 200 OK Access-Control-Allow-Origin: http://example.com ... ``` Эта проверка **явная**: хост, порт и протокол должны совпадать с запрошенными. Рассмотрим пример: - Сервер отвечает, что его `Access-Control-Allow-Origin` равен `https://example.com`: - `https://example.net` – домен отличается. - `http://example.com` – схема отличается. - `http://example.com:5555` – порт отличается. - `https://www.example.com` – хост отличается. В спецификации допускается только синтаксис для обоих заголовков, как для запросов, так и для ответов. Путь URL игнорируется. Порт также опускается, если он является портом по умолчанию (80 для HTTP и 443 для HTTPS). ```http Origin: null Origin: :// Origin: ://: ``` ## Включение CORS Нативно у вас есть объект [CrossOriginResourceSharingHeaders](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.md) внутри вашего [ListeningHost](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.md). Вы можете настроить CORS при инициализации сервера: ```csharp static async Task Main(string[] args) { using var app = HttpServer.CreateBuilder() .UseCors(new CrossOriginResourceSharingHeaders( allowOrigin: "http://example.com", allowHeaders: ["Authorization"], exposeHeaders: ["Content-Type"])) .Build(); await app.StartAsync(); } ``` Код выше отправит следующие заголовки для **всех ответов**: ```http HTTP/1.1 200 OK Access-Control-Allow-Origin: http://example.com Access-Control-Allow-Headers: Authorization Access-Control-Expose-Headers: Content-Type ``` Эти заголовки должны отправляться во всех ответах клиенту, включая ошибки и перенаправления. Вы можете заметить, что класс [CrossOriginResourceSharingHeaders](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.md) имеет два похожих свойства: [AllowOrigin](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigin.md) и [AllowOrigins](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigins.md). Обратите внимание, что одно — во множественном числе, другое — в единственном. - Свойство **AllowOrigin** статично: только указанный вами origin будет отправлен во всех ответах. - Свойство **AllowOrigins** динамично: сервер проверяет, содержится ли origin запроса в этом списке. Если найдено, он будет отправлен в ответе для данного origin. Когда разрешённый origin зависит от запроса, то есть когда вы используете **AllowOrigins** или значение [AutoAllowOrigin](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AutoAllowOrigin.md), описанное ниже, Sisk также добавляет заголовок `Vary: Origin`, чтобы кэши не переиспользовали ответ для другого origin. ### Подстановочные знаки и автоматические заголовки В качестве альтернативы можно использовать подстановочный символ (`*`) в origin ответа, указывая, что любой origin разрешён для доступа к ресурсу. Однако такое значение недопустимо для запросов, содержащих учётные данные (заголовки авторизации), и такая операция [приведёт к ошибке](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS/Errors/CORSNotSupportingCredentials). Обойти эту проблему можно, явно перечислив разрешённые origin через свойство [AllowOrigins](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigins.md) или используя константу [AutoAllowOrigin](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AutoAllowOrigin.md) в значении [AllowOrigin](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigin.md). Это «магическое» свойство задаст заголовок `Access-Control-Allow-Origin` со значением, совпадающим с заголовком `Origin` запроса. Также можно использовать [AutoFromRequestMethod](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AutoFromRequestMethod.md) и [AutoFromRequestHeaders](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AutoFromRequestHeaders.md) для поведения, аналогичного `AllowOrigin`, которое автоматически отвечает на основе полученных заголовков. ```csharp using var host = HttpServer.CreateBuilder() .UseCors(new CrossOriginResourceSharingHeaders( // Отвечает в зависимости от заголовка Origin запроса allowOrigin: CrossOriginResourceSharingHeaders.AutoAllowOrigin, // Отвечает в зависимости от заголовка Access-Control-Request-Method или метода запроса allowMethods: [CrossOriginResourceSharingHeaders.AutoFromRequestMethod], // Отвечает в зависимости от заголовка Access-Control-Request-Headers или отправленных заголовков allowHeaders: [CrossOriginResourceSharingHeaders.AutoFromRequestHeaders], exposeHeaders: [HttpKnownHeaderNames.ContentType, "X-Authenticated-Account-Id"], allowCredentials: true)) .Build(); ``` ## Другие способы применения CORS Если вы работаете с [service providers](https://docs.sisk-framework.org/ru/docs/extensions/service-providers.md), вы можете переопределить значения, определённые в файле конфигурации: ```csharp static async Task Main(string[] args) { using var app = HttpServer.CreateBuilder() .UsePortableConfiguration(...) .UseCors(cors => { // Перезапишет origin, определённый в конфигурации // файл. cors.AllowOrigin = "http://example.com"; }) .Build(); await app.StartAsync(); } ``` ## Отключение CORS на конкретных маршрутах Свойство `UseCors` доступно как для маршрутов, так и для всех атрибутов маршрутов и может быть отключено следующим примером: ```csharp [RoutePrefix("api/widgets")] public class WidgetController : Controller { // GET /api/widgets/colors [RouteGet("/colors", UseCors = false)] public IEnumerable GetWidgets() { return new[] { "Green widget", "Red widget" }; } } ``` ## Замена значений в ответе Вы можете явно заменить или удалить значения в действии роутера: ```csharp [RoutePrefix("api/widgets")] public class WidgetController : Controller { public IEnumerable GetWidgets(HttpRequest request) { // Удаляет заголовок Access-Control-Allow-Credentials request.Context.OverrideHeaders.AccessControlAllowCredentials = string.Empty; // Заменяет заголовок Access-Control-Allow-Origin request.Context.OverrideHeaders.AccessControlAllowOrigin = "https://contorso.com"; return new[] { "Green widget", "Red widget" }; } } ``` ## Preflight‑запросы Preflight‑запрос — это запрос метода [OPTIONS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/OPTIONS), который клиент отправляет перед фактическим запросом. Когда запрос с методом `OPTIONS` совпадает с путём маршрута, но для `OPTIONS` явно не настроен маршрут, сервер Sisk отвечает `200 OK` и соответствующими CORS‑заголовками, после чего клиент может выполнить фактический запрос. - Если маршрут явно сопоставлен с [RouteMethod.Options](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md), например с помощью `MapOptions`, этот маршрут обрабатывает запрос вместо автоматического ответа. - Если у маршрута, совпадающего с путём, установлено `UseCors = false`, автоматический ответ всё равно отправляется, но без CORS‑заголовков. > [!NOTE] > До версии Sisk 1.7 автоматический ответ на preflight‑запрос всегда включал CORS‑заголовки, даже для маршрутов с `UseCors = false`. ## Отключение CORS глобально Это невозможно. Чтобы не использовать CORS, просто не настраивайте его. --- # Файловый сервер Source: https://docs.sisk-framework.org/ru/docs/features/file-server.html Sisk предоставляет пространство имён `Sisk.Http.FileSystem`, которое содержит инструменты для обслуживания статических файлов, отображения содержимого каталогов и конвертации файлов. Эта возможность позволяет обслуживать файлы из локального каталога, поддерживая запросы диапазонов (стриминг аудио/видео) и пользовательскую обработку файлов. ## Serving static files Самый простой способ обслуживать статические файлы — использовать [Router.MapFileSystem](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MapFileSystem.md). Этот метод сопоставляет префикс URL с каталогом на диске. ```cs using Sisk.Core.Http; using Sisk.Core.Http.FileSystem; // сопоставляет корень сервера с текущей директорией mainRouter.MapFileSystem("/", Directory.GetCurrentDirectory()); // сопоставляет /assets с папкой "public/assets" mainRouter.MapFileSystem( "/assets", Path.Combine(Directory.GetCurrentDirectory(), "public", "assets")); ``` Когда запрос совпадает с префиксом маршрута, `HttpFileServerHandler` будет искать файл в указанном каталоге. Если файл найден, он будет отдан клиенту; иначе будет возвращён ответ 404 (или 403, если доступ запрещён). `HttpFileServer.CreateServingRoute` всё ещё доступен, когда необходимо явно создать объект `Route`, но `MapFileSystem` — самый прямой вариант для кода приложения. Оба метода создают [PrefixRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.PrefixRoute.md) (регулярный маршрут до версии Sisk 1.7), который является [dynamic route](https://docs.sisk-framework.org/ru/docs/fundamentals/routing.md#matching-precedence). Статические маршруты всегда проверяются раньше, поэтому файловый сервер, смонтированный на `/`, работает как резервный вариант для путей, которые не обрабатывают другие маршруты. Маршруты файлового сервера не зависят от [HttpServerConfiguration.ForceTrailingSlash](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ForceTrailingSlash.md). ## HttpFileServerHandler Для более тонкого контроля над тем, как обслуживаются файлы, можно создать и настроить `HttpFileServerHandler` вручную. ```cs var fileHandler = new HttpFileServerHandler("/var/www/html"); // включить отображение каталога (по умолчанию отключено) fileHandler.AllowDirectoryListing = true; // задать пользовательский префикс маршрута (он будет удалён из пути запроса) fileHandler.RoutePrefix = "/public"; // зарегистрировать обработчик по пути /public mainRouter.MapFileSystem("/public", fileHandler); ``` ### Configuration | Property | Description | |---|---| | `RootDirectoryPath` | Абсолютный или относительный путь к корневому каталогу, из которого обслуживаются файлы. | | `RoutePrefix` | Префикс маршрута, который будет удалён из пути запроса при разрешении файлов. По умолчанию `/`. | | `AllowDirectoryListing` | Если `true`, включает отображение содержимого каталога при запросе каталога без файла‑индекса. По умолчанию `false`. | | `FileConverters` | Список `HttpFileServerFileConverter`, используемых для преобразования файлов перед их отдачей. | ## Directory Listing Когда включено `AllowDirectoryListing` и пользователь запрашивает путь к каталогу, Sisk генерирует HTML‑страницу со списком содержимого этого каталога. В отображении каталога присутствует: - Навигация к родительскому каталогу (`..`). - Список подкаталогов. - Список файлов с указанием размера и даты последнего изменения. ## File Converters Конвертеры файлов позволяют перехватывать определённые типы файлов и обрабатывать их иначе. Например, можно транскодировать изображение, сжимать файл «на лету» или обслуживать файл частично (Range‑запросы). Sisk включает два встроенных конвертера для потоковой передачи медиа: - `HttpFileAudioConverter`: Обрабатывает `.mp3`, `.ogg`, `.wav`, `.flac`, `.ogv`. - `HttpFileVideoConverter`: Обрабатывает `.webm`, `.avi`, `.mkv`, `.mpg`, `.mpeg`, `.wmv`, `.mov`, `.mp4`. Эти конвертеры обеспечивают поддержку **HTTP Range Requests**, позволяя клиентам перемещаться по аудио‑ и видеофайлам. ### Creating a custom converter Чтобы создать собственный конвертер файлов, наследуйте `HttpFileServerFileConverter` и реализуйте методы `CanConvert` и `Convert`. ```cs using Sisk.Core.Http; using Sisk.Core.Http.FileSystem; public class MyTextConverter : HttpFileServerFileConverter { public override bool CanConvert(FileInfo file) { // применять только к файлам .txt return file.Extension.Equals(".txt", StringComparison.OrdinalIgnoreCase); } public override HttpResponse Convert(FileInfo file, HttpRequest request) { string content = File.ReadAllText(file.FullName); // преобразовать весь текст в верхний регистр return new HttpResponse(200) { Content = new StringContent(content.ToUpper()) }; } } ``` Затем добавьте его в ваш обработчик: ```cs var handler = new HttpFileServerHandler("./files"); handler.FileConverters.Add(new MyTextConverter()); ``` --- # Протокол контекста модели Source: https://docs.sisk-framework.org/ru/docs/extensions/mcp.html Можно создавать приложения, которые предоставляют контекст моделям‑агентам, использующим большие языковые модели (LLM), с помощью пакета [Sisk.ModelContextProtocol](https://www.nuget.org/packages/Sisk.ModelContextProtocol/) : dotnet add package Sisk.ModelContextProtocol --prerelease Этот пакет предоставляет полезные классы и методы для построения MCP‑серверов, работающих по протоколу [Streamable HTTP](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http), поддерживая одиночные запрос‑ответ сообщения. Начиная с версии 1.7.0‑rc1 он поддерживает безсостояние запросов версии протокола `2026-07-28` и сохраняет устаревший процесс инициализации для клиентов, использующих версии `2025-11-25`, `2025-06-18` и `2025-03-26`. Поддерживаются инструменты и навыки. > [!NOTE] > > Прежде чем начать, обратите внимание, что этот пакет находится в разработке и может вести себя не в соответствии со спецификацией. Прочитайте [подробности пакета](https://github.com/sisk-http/core/tree/main/extensions/Sisk.ModelContextProtocol), чтобы узнать, что уже реализовано, а что ещё не работает. ## Начало работы с MCP Класс [McpProvider](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpProvider.md) является точкой входа для определения MCP‑сервера. Это запечатлённый объект‑провайдер, который можно настроить при запуске. Ваше приложение Sisk может иметь один или несколько MCP‑провайдеров. ```csharp McpProvider mcp = new McpProvider( serverName: "math-server", serverTitle: "Mathematics server", serverVersion: new Version(1, 0)); mcp.Tools.Add(new McpTool( name: "math_sum", description: "Sums one or more numbers.", schema: JsonSchema.CreateObjectSchema( properties: new Dictionary() { { "numbers", JsonSchema.CreateArraySchema( itemsSchema: JsonSchema.CreateNumberSchema(), minItems: 1, description: "The numbers to sum.") } }, requiredProperties: ["numbers"]), executionHandler: async (McpToolContext context) => { var numbers = context.Arguments["numbers"].GetJsonArray().ToArray(); var sum = numbers.Sum(); return await Task.FromResult(McpToolResult.CreateText($"Sum result: {sum:N4}")); })); ``` Если ваше приложение будет предоставлять только один MCP‑провайдер, можно воспользоваться синглтоном билдера: ```csharp static void Main(string[] args) { using var host = HttpServer.CreateBuilder() .UseMcp(mcp => { mcp.ServerName = "math-server"; mcp.ServerTitle = "Mathematics server"; mcp.Tools.Add(new McpTool( name: "math_sum", description: "Sums one or more numbers.", schema: JsonSchema.CreateObjectSchema( properties: new Dictionary() { { "numbers", JsonSchema.CreateArraySchema( itemsSchema: JsonSchema.CreateNumberSchema(), minItems: 1, description: "The numbers to sum.") } }, requiredProperties: ["numbers"]), executionHandler: async (McpToolContext context) => { var numbers = context.Arguments["numbers"].GetJsonArray().ToArray(); var sum = numbers.Sum(); return await Task.FromResult(McpToolResult.CreateText($"Sum result: {sum:N4}")); })); }) .UseRouter(router => { router.MapAny("/mcp", async (HttpRequest req) => { return await req.HandleMcpRequestAsync(); }); }) .Build(); host.Start(); } ``` Точка доступа должна принимать как `GET`, так и `POST` запросы, поэтому `MapAny` — самое простое сопоставление маршрута. `HandleMcpRequestAsync` возвращает [HttpResponse](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpResponse.md), и ваш маршрут должен вернуть его. Если нужны несколько провайдеров в одном приложении, пропустите синглтон и вызывайте [McpProvider.HandleRequestAsync](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpProvider.HandleRequestAsync.md) напрямую из каждого маршрута: ```csharp var mathProvider = new McpProvider("math-server", "Mathematics server", new Version(1, 0)); router.MapAny("/mcp/math", async request => { return await mathProvider.HandleRequestAsync(request); }); ``` ## Создание JSON‑схем для функций Библиотека [Sisk.ModelContextProtocol] использует форк [LightJson](https://github.com/CypherPotato/LightJson) для работы с JSON и JSON‑схемами. Эта реализация предоставляет удобный построитель JSON‑Schema для различных объектов: - JsonSchema.CreateObjectSchema - JsonSchema.CreateArraySchema - JsonSchema.CreateBooleanSchema - JsonSchema.CreateNumberSchema - JsonSchema.CreateStringSchema - JsonSchema.Empty Пример: ```csharp JsonSchema.CreateObjectSchema( properties: new Dictionary() { { "numbers", JsonSchema.CreateArraySchema( itemsSchema: JsonSchema.CreateNumberSchema(), minItems: 1, description: "The numbers to sum.") } }, requiredProperties: ["numbers"]); ``` Создаёт следующую схему: ```json { "type": "object", "properties": { "numbers": { "type": "array", "items": { "type": "number" }, "minItems": 1, "description": "The numbers to sum." } }, "required": ["numbers"] } ``` ## Обработка вызовов функций Функция, определённая в параметре `executionHandler` класса [McpTool](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpTool.md), предоставляет JsonObject, содержащий аргументы вызова, которые можно удобно читать: ```csharp mcp.Tools.Add(new McpTool( name: "browser_do_action", description: "Run an browser action, such as scrolling, refreshing or navigating.", schema: JsonSchema.CreateObjectSchema( properties: new Dictionary() { { "action_name", JsonSchema.CreateStringSchema( enums: ["go_back", "refresh", "scroll_bottom", "scroll_top"], description: "The action name.") }, { "action_data", JsonSchema.CreateStringSchema( description: "Action parameter." ) } }, requiredProperties: ["action_name"]), executionHandler: async (McpToolContext context) => { // читаем имя действия. будет выброшено исключение, если значение null или не является строкой string actionName = context.Arguments["action_name"].GetString(); // action_data объявлен как необязательный, поэтому здесь может быть null string? actionData = context.Arguments["action_data"].MaybeNull()?.GetString(); // Обрабатываем действие браузера на основе actionName return await Task.FromResult( McpToolResult.CreateText($"Performed browser action: {actionName}")); })); ``` Аргументы инструмента проверяются по схеме до выполнения вашего обработчика. Если проверка не проходит, провайдер возвращает клиенту MCP ошибку и не вызывает обработчик инструмента. ## Результаты функций Объект [McpToolResult](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpToolResult.md) предоставляет три метода для создания содержимого ответа инструмента: - [CreateAudio(ReadOnlySpan, string)](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpToolResult.CreateAudio.md): создаёт аудио‑ответ для клиента MCP. - [CreateImage(ReadOnlySpan, string)](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpToolResult.CreateImage.md): создаёт изображение‑ответ для клиента MCP. - [CreateText(string)](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpToolResult.CreateText.md): создаёт текстовый ответ (по умолчанию) для клиента MCP. Кроме того, можно объединить несколько разных типов содержимого в один JSON‑ответ инструмента: ```csharp mcp.Tools.Add(new McpTool( ... executionHandler: async (McpToolContext context) => { // simulate real work byte[] browserScreenshot = await browser.ScreenshotAsync(); return McpToolResult.Combine( McpToolResult.CreateText("Heres the screenshot of the browser:"), McpToolResult.CreateImage(browserScreenshot, "image/png") ) })); ``` В данный момент провайдер обрабатывает методы `server/discover`, устаревшие `initialize` и `ping`, `tools/list`, `tools/call`, `skills/list`, `skills/get`, `resources/list`, `resources/read` и уведомления. Неподдерживаемые методы JSON‑RPC возвращают ошибку JSON‑RPC. ## Публикация навыков MCP‑провайдер также может публиковать [Agent Skills](https://github.com/modelcontextprotocol/ext-skills) через расширение `io.modelcontextprotocol/skills`. Добавьте объекты [McpSkill](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpSkill.md) в [McpProvider.Skills](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpProvider.Skills.md) до того, как провайдер начнёт обрабатывать запросы: ```csharp mcp.Skills.Add(new McpSkill( path: "acme/refunds", frontmatter: new JsonObject { ["name"] = "refunds", ["description"] = "Follow the refund workflow." }, body: "# Refunds\nRead references/policy.md before using the refund tools.", files: new Dictionary { ["references/policy.md"] = File.ReadAllBytes("skills/refunds/references/policy.md") })); ``` `McpSkill` генерирует файл `SKILL.md` из frontmatter и тела Markdown. Последний сегмент `path` должен совпадать с полем `name` в frontmatter. Файлы навыка копируются при его создании и больше не читаются с диска провайдером. Клиенты получают список и читают навыки через `skills/list`, `skills/get` и `resources/read`, используя версию протокола `2026-07-28`. См. [подробности пакета](https://github.com/sisk-http/core/tree/main/extensions/Sisk.ModelContextProtocol) для ограничений и правил валидации. ## Дальнейшая работа Протокол контекста модели — это коммуникационный протокол для моделей‑агентов и приложений, предоставляющих им контент. Это новый протокол, поэтому его спецификация часто обновляется: появляются устаревания, новые возможности и несовместимые изменения. Важно понять, какие задачи решает [Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro), прежде чем начинать создавать агентные приложения. Также ознакомьтесь со спецификацией пакета [Sisk.ModelContextProtocol](https://github.com/sisk-http/core/tree/main/extensions/Sisk.ModelContextProtocol), чтобы понять его текущее состояние, прогресс и возможности использования. --- # Расширение JSON-RPC Source: https://docs.sisk-framework.org/ru/docs/extensions/json-rpc.html Sisk имеет экспериментальный модуль для API [JSON-RPC 2.0](https://www.jsonrpc.org/specification), который позволяет создавать ещё более простые приложения. Это расширение строго реализует транспортный интерфейс JSON-RPC 2.0 и предлагает транспорт через HTTP GET, POST запросы, а также веб‑сокеты с Sisk. Вы можете установить расширение через NuGet с помощью команды ниже. Обратите внимание, что в экспериментальных/бета‑версиях следует включить опцию поиска предрелизных пакетов в Visual Studio. ```bash dotnet add package Sisk.JsonRpc ``` ## Транспортный интерфейс JSON-RPC — это безсостояний, асинхронный протокол удалённого вызова процедур (RPC), использующий JSON для передачи данных. Запрос JSON‑RPC обычно идентифицируется по ID, а ответ возвращается с тем же ID, который был отправлен в запросе. Не все запросы требуют ответа; такие запросы называются «уведомлениями». Спецификация [JSON-RPC 2.0](https://www.jsonrpc.org/specification) подробно объясняет, как работает транспорт. Этот транспорт не зависит от места применения. Sisk реализует протокол через HTTP, следуя требованиям [JSON-RPC over HTTP](https://www.jsonrpc.org/historical/json-rpc-over-http.html), который частично поддерживает GET‑запросы, но полностью поддерживает POST‑запросы. Веб‑сокеты также поддерживаются, обеспечивая асинхронную передачу сообщений. Запрос JSON‑RPC выглядит примерно так: ```json { "jsonrpc": "2.0", "method": "Sum", "params": [1, 2, 4], "id": 1 } ``` А успешный ответ выглядит примерно так: ```json { "jsonrpc": "2.0", "result": 7, "id": 1 } ``` ## Методы JSON-RPC В следующем примере показано, как создать API JSON‑RPC с помощью Sisk. Класс математических операций выполняет удалённые операции и возвращает сериализованный ответ клиенту. ```csharp {title="Program.cs"} using var app = HttpServer.CreateBuilder(port: 5555) .UseJsonRPC((sender, args) => { // добавить все методы, помеченные атрибутом WebMethod, в обработчик JSON‑RPC args.Handler.Methods.AddMethodsFromType(new MathOperations()); // сопоставляет маршрут /service для обработки JSON‑RPC POST и GET запросов args.Router.MapPost("/service", args.Handler.Transport.HttpPost); args.Router.MapGet("/service", args.Handler.Transport.HttpGet); // сопоставляет транспорт JSON‑RPC WebSocket на GET /ws args.Router.MapGet("/ws", args.Handler.Transport.WebSocket); }) .Build(); await app.StartAsync(); ``` ```csharp {title="MathOperations.cs"} public class MathOperations { [WebMethod] public float Sum(float a, float b) { return a + b; } [WebMethod] public double Sqrt(float a) { return Math.Sqrt(a); } } ``` Приведённый пример сопоставит методы `Sum` и `Sqrt` с обработчиком JSON‑RPC, и эти методы будут доступны по `GET /service`, `POST /service` и `GET /ws`. Имена методов нечувствительны к регистру. Параметры методов автоматически десериализуются в их конкретные типы. Также поддерживается запрос с именованными параметрами. Сериализация JSON выполняется библиотекой [LightJson](https://github.com/CypherPotato/LightJson). Если тип десериализуется некорректно, вы можете создать специальный [JSON‑конвертер](https://github.com/CypherPotato/LightJson?tab=readme-ov-file#json-converters) для этого типа и связать его с [JsonRpcHandler.JsonSerializerOptions](https://docs.sisk-framework.org/api/Sisk.JsonRPC.JsonRpcHandler.JsonSerializerOptions.md). Вы также можете получить необработанный объект `$.params` из запроса JSON‑RPC непосредственно в вашем методе. ```csharp {title="MathOperations.cs"} [WebMethod] public float Sum(JsonArray|JsonObject @params) { ... } ``` Для этого `@params` должен быть **единственным** параметром вашего метода и иметь точно имя `params` (в C# символ `@` необходим для экранирования этого имени параметра). Десериализация параметров происходит как для именованных объектов, так и для позиционных массивов. Например, следующий метод можно вызвать удалённо обоими типами запросов: ```csharp [WebMethod] public float AddUserToStore(string apiKey, User user, UserStore store) { ... } ``` Для массива порядок параметров должен соблюдаться. ```json { "jsonrpc": "2.0", "method": "AddUserToStore", "params": [ "1234567890", { "name": "John Doe", "email": "john@example.com" }, { "name": "My Store" } ], "id": 1 } ``` ## Настройка сериализатора Вы можете настроить JSON‑сериализатор в свойстве [JsonRpcHandler.JsonSerializerOptions](https://docs.sisk-framework.org/api/Sisk.JsonRPC.JsonRpcHandler.JsonSerializerOptions.md). В этом свойстве можно включить использование [JSON5](https://json5.org/) для десериализации сообщений. Хотя это не соответствует спецификации JSON‑RPC 2.0, JSON5 является расширением JSON, позволяющим писать более человекочитаемый и удобный код. ```csharp {title="Program.cs"} using var host = HttpServer.CreateBuilder ( 5556 ) .UseJsonRPC ( ( o, e ) => { // использует сравниватель имён с очисткой. этот сравниватель сравнивает только буквы // и цифры в имени, игнорируя другие символы. пример: // foo_bar10 == FooBar10 e.Handler.JsonSerializerOptions.PropertyNameComparer = new JsonSanitizedComparer (); // включает поддержку JSON5 для JSON‑интерпретатора. даже при включении этого, обычный JSON по‑прежнему разрешён e.Handler.JsonSerializerOptions.SerializationFlags = LightJson.Serialization.JsonSerializationFlags.Json5; // сопоставляет маршрут POST /service с обработчиком JSON RPC e.Router.MapPost ( "/service", e.Handler.Transport.HttpPost ); } ) .Build (); host.Start (); ``` --- # Поставщики сервисов Source: https://docs.sisk-framework.org/ru/docs/extensions/service-providers.html Поставщики сервисов — это способ перенести ваше приложение Sisk в разные среды с помощью переносимого файла конфигурации. Эта возможность позволяет изменять порт сервера, параметры и другие опции, не меняя код приложения для каждой среды. Этот модуль зависит от синтаксиса построения Sisk и может быть настроен через метод UsePortableConfiguration. Поставщик конфигурации реализуется с помощью IConfigurationProvider, который предоставляет считыватель конфигурации и может принимать любую реализацию. По умолчанию Sisk предоставляет JSON‑считыватель конфигурации, но также существует пакет для INI‑файлов. Вы также можете создать собственный поставщик конфигурации и зарегистрировать его с помощью: ```csharp using var app = HttpServer.CreateBuilder() .UsePortableConfiguration(config => { config.WithConfigReader(); }) .Build(); ``` Как упоминалось ранее, провайдер по умолчанию — это JSON‑файл. По умолчанию ищется файл с именем service-config.json, и поиск производится в текущем каталоге работающего процесса, а не в каталоге исполняемого файла. Вы можете изменить имя файла, а также указать, где Sisk должен искать файл конфигурации, с помощью: ```csharp using Sisk.Core.Http; using Sisk.Core.Http.Hosting; using var app = HttpServer.CreateBuilder() .UsePortableConfiguration(config => { config.WithConfigFile("config.toml", createIfDontExists: true, lookupDirectories: ConfigurationFileLookupDirectory.CurrentDirectory | ConfigurationFileLookupDirectory.AppDirectory); }) .Build(); ``` Приведённый выше код будет искать файл config.toml в текущем каталоге работающего процесса. Если файл не найден, будет выполнен поиск в каталоге, где находится исполняемый файл. Если файл не существует, параметр createIfDontExists будет учтён: файл будет создан без содержимого в последнем проверенном пути (в зависимости от lookupDirectories), и в консоли будет выведена ошибка, препятствующая инициализации приложения. > [!TIP] > > Вы можете посмотреть исходный код INI‑считывателя конфигурации и JSON‑считывателя конфигурации, чтобы понять, как реализован IConfigurationProvider. ## Чтение конфигураций из JSON‑файла По умолчанию Sisk предоставляет поставщик конфигурации, который читает настройки из JSON‑файла. Этот файл имеет фиксированную структуру и состоит из следующих параметров: ```json { "Server": { "DefaultEncoding": "UTF-8", "ThrowExceptions": true, "IncludeRequestIdHeader": true }, "ListeningHost": { "Label": "My sisk application", "Ports": [ "http://localhost:80/", "https://localhost:443/", // Файлы конфигурации также поддерживают комментарии ], "CrossOriginResourceSharingPolicy": { "AllowOrigin": "*", "AllowOrigins": [ "*" ], // новое в версии 0.14 "AllowMethods": [ "*" ], "AllowHeaders": [ "*" ], "MaxAge": 3600 }, "Parameters": { "MySqlConnection": "server=localhost;user=root;" } } } ``` Параметры, созданные из файла конфигурации, можно получить в конструкторе сервера: ```csharp using var app = HttpServer.CreateBuilder() .UsePortableConfiguration(config => { config.WithParameters(paramCollection => { string databaseConnection = paramCollection.GetValueOrThrow("MySqlConnection"); }); }) .Build(); ``` Каждый считыватель конфигурации предоставляет способ чтения параметров инициализации сервера. Некоторые свойства указываются в окружении процесса, а не в файле конфигурации, например чувствительные данные API, ключи API и т.д. ## Структура файла конфигурации
    Свойство Обязательно Описание
    Server Required Представляет сам сервер с его настройками.
    Server.AccessLogsStream Optional По умолчанию console. Указывает поток вывода журнала доступа. Может быть именем файла, null или console.
    Server.ErrorsLogsStream Optional По умолчанию null. Указывает поток вывода журнала ошибок. Может быть именем файла, null или console.
    Server.MaximumContentLength Optional
    Server.MaximumContentLength Optional Указывает максимальную длину содержимого в байтах. Ноль означает бесконечность. Когда объект Server присутствует и это свойство опущено, значение равно 0 (бесконечно), что переопределяет значение по умолчанию 100 МБ свойства HttpServerConfiguration.MaximumContentLength.
    Server.IncludeRequestIdHeader Optional По умолчанию false. Указывает, должен ли HTTP‑сервер отправлять заголовок X-Request-Id.
    Server.ThrowExceptions Optional По умолчанию true. Указывает, должны ли выбрасываться необработанные исключения. Установите false в продакшн‑среде и true при отладке.
    ListeningHost Required Представляет хост, прослушиваемый сервером.
    ListeningHost.Label Optional Представляет метку приложения.
    ListeningHost.Ports Required Представляет массив строк, соответствующий синтаксису ListeningPort.
    ListeningHost.CrossOriginResourceSharingPolicy Optional Настраивает заголовки CORS для приложения.
    ListeningHost.CrossOriginResourceSharingPolicy.AllowCredentials Optional По умолчанию false. Указывает заголовок Allow-Credentials.
    ListeningHost.CrossOriginResourceSharingPolicy.ExposeHeaders Optional По умолчанию null. Ожидает массив строк. Указывает заголовок Expose-Headers.
    ListeningHost.CrossOriginResourceSharingPolicy.AllowOrigin Optional По умолчанию null. Ожидает строку. Указывает заголовок Allow-Origin.
    ListeningHost.CrossOriginResourceSharingPolicy.AllowOrigins Optional По умолчанию null. Ожидает массив строк. Указывает несколько заголовков Allow-Origin. См. AllowOrigins для получения дополнительной информации.
    ListeningHost.CrossOriginResourceSharingPolicy.AllowMethods Optional По умолчанию null. Ожидает массив строк. Указывает заголовок Allow-Methods.
    ListeningHost.CrossOriginResourceSharingPolicy.AllowHeaders Optional По умолчанию null. Ожидает массив строк. Указывает заголовок Allow-Headers.
    ListeningHost.CrossOriginResourceSharingPolicy.MaxAge Optional По умолчанию null. Ожидает целое число. Указывает заголовок Max-Age в секундах.
    ListeningHost.Parameters Optional Указывает свойства, передаваемые методу настройки приложения.
    --- # Поставщик конфигурации INI Source: https://docs.sisk-framework.org/ru/docs/extensions/ini-configuration.html Sisk имеет способ получения стартовых конфигураций, отличных от JSON. На самом деле любой конвейер, реализующий [IConfigurationReader](https://docs.sisk-framework.org/api/Sisk.Core.Http.Hosting.IConfigurationReader.md), можно использовать с [PortableConfigurationBuilder.WithConfigurationPipeline](https://docs.sisk-framework.org/api/Sisk.Core.Http.Hosting.PortableConfigurationBuilder.md), читая конфигурацию сервера из любого типа файлов. Пакет [Sisk.IniConfiguration](https://www.nuget.org/packages/Sisk.IniConfiguration/) предоставляет потоковый читатель INI‑файлов, который не бросает исключения при типичных синтаксических ошибках и имеет простой синтаксис конфигурации. Этот пакет можно использовать вне фреймворка Sisk, что даёт гибкость проектам, которым нужен эффективный читатель INI‑документов. ## Установка Чтобы установить пакет, вы можете начать с: ```bash $ dotnet add package Sisk.IniConfiguration ``` Вы также можете установить основной пакет, который не включает INI‑[IConfigurationReader](https://docs.sisk-framework.org/api/Sisk.Core.Http.Hosting.IConfigurationReader), не зависит от Sisk, а содержит только сериализаторы INI: ```bash $ dotnet add package Sisk.IniConfiguration.Core ``` С основным пакетом вы можете использовать его в коде, как показано в примере ниже: ```cs class Program { static HttpServerHostContext Host = null!; static void Main(string[] args) { Host = HttpServer.CreateBuilder() .UsePortableConfiguration(config => { config.WithConfigFile("app.ini", createIfDontExists: true); // использует конфигурационный читатель IniConfigurationReader config.WithConfigurationPipeline(); }) .UseRouter(r => { r.MapGet("/", SayHello); }) .Build(); Host.Start(); } static HttpResponse SayHello(HttpRequest request) { string? name = Host.Parameters["name"] ?? "world"; return new HttpResponse($"Hello, {name}!"); } } ``` Код выше будет искать файл `app.ini` в текущем каталоге процесса (`CurrentDirectory`). Файл INI выглядит так: ```ini [Server] # Multiple listen addresses are supported Listen = http://localhost:5552/ Listen = http://localhost:5553/ ThrowExceptions = false AccessLogsStream = console [Cors] AllowMethods = GET, POST AllowHeaders = Content-Type, Authorization AllowOrigin = * [Parameters] Name = "Kanye West" ``` ## Синтаксис и особенности INI Текущий вариант реализации: - Имена свойств и секций **не чувствительны к регистру**. - Имена свойств и их значения **обрезаются**, если только значения не заключены в кавычки. - Значения могут быть заключены в одинарные или двойные кавычки. Внутри кавычек могут быть переносы строк. - Комментарии поддерживаются с помощью `#` и `;`. Кроме того, **допускаются конечные (trailing) комментарии**. - Свойства могут иметь несколько значений. Подробную документацию о «вкусе» INI‑парсера, используемого в Sisk, можно найти в [этом документе](https://github.com/sisk-http/archive/blob/master/ext/ini-reader-syntax.md). Используя следующий код ini в качестве примера: ```ini One = 1 Value = this is an value Another value = "this value has an line break on it" ; the code below has some colors [some section] Color = Red Color = Blue Color = Yellow ; do not use yellow ``` Разберите его с помощью: ```csharp // разбирает ini‑текст из строки IniDocument doc = IniDocument.FromString(iniText); // получить одно значение string? one = doc.Global.GetOne("one"); string? anotherValue = doc.Global.GetOne("another value"); // получить несколько значений string[]? colors = doc.GetSection("some section")?.GetMany("color"); ``` ## Параметры конфигурации | Раздел и имя | Разрешено несколько значений | Описание | | ------------ | --------------------------- | -------- | | `Server.Listen` | Yes | Адреса/порты, на которых сервер слушает. | | `Server.Encoding` | No | Кодировка по умолчанию сервера. | | `Server.MaximumContentLength` | No | Максимальный размер `content-length` сервера в байтах. Если не указано, используется значение по умолчанию сервера (100 МБ начиная с Sisk 1.7). Используйте `0` для отсутствия ограничения. | | `Server.IncludeRequestIdHeader` | No | Указывает, должен ли HTTP‑сервер отправлять заголовок `X-Request-Id`. | | `Server.ThrowExceptions` | No | Указывает, должны ли необработанные исключения выбрасываться. | | `Server.AccessLogsStream` | No | Указывает поток вывода журнала доступа. | | `Server.ErrorsLogsStream` | No | Указывает поток вывода журнала ошибок. | | `Cors.AllowMethods` | No | Указывает значение заголовка CORS `Allow-Methods`. | | `Cors.AllowHeaders` | No | Указывает значение заголовка CORS `Allow-Headers`. | | `Cors.AllowOrigins` | No | Указывает несколько заголовков `Allow-Origin`, разделённых запятыми. Подробнее см. [AllowOrigins](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigins.md). | | `Cors.AllowOrigin` | No | Указывает один заголовок `Allow-Origin`. | | `Cors.ExposeHeaders` | No | Указывает значение заголовка CORS `Expose-Headers`. | | `Cors.AllowCredentials` | No | Указывает значение заголовка CORS `Allow-Credentials`. | | `Cors.MaxAge` | No | Указывает значение заголовка CORS `Max-Age`. | --- # Документация API Source: https://docs.sisk-framework.org/ru/docs/extensions/api-documentation.html Расширение `Sisk.Documenting` позволяет автоматически генерировать документацию API для вашего приложения Sisk. Оно использует структуру вашего кода и атрибуты для создания полноценного сайта документации, поддерживая экспорт в формат Open API (Swagger). > [!WARNING] > Этот пакет опубликован как предварительная версия (1.7.0-rc3). Его поведение и API могут измениться в будущих обновлениях. Установите пакет [Sisk.Documenting](https://www.nuget.org/packages/Sisk.Documenting/) из NuGet. Чтобы обслуживать документацию в виде HTML‑страницы, также установите [Sisk.Documenting.Html](https://www.nuget.org/packages/Sisk.Documenting.Html/): ```sh dotnet add package Sisk.Documenting --prerelease dotnet add package Sisk.Documenting.Html --prerelease ``` Исходный код доступен [здесь](https://github.com/sisk-http/core/tree/main/extensions/Sisk.Documenting). Чтобы использовать `Sisk.Documenting`, необходимо зарегистрировать его в построителе приложения и пометить обработчики маршрутов атрибутами документации. ### Регистрация генерации документации Вызовите метод расширения `UseApiDocumentation` у вашего `HttpServerHostContextBuilder`, чтобы открыть сгенерированную документацию API из того же роутера, который обслуживает приложение. ```csharp using Sisk.Documenting; using Sisk.Documenting.Exporters; // ... host.UseApiDocumentation( context: new ApiGenerationContext() { ApplicationName = "My Application", ApplicationDescription = "Description of my application.", ApplicationVersion = "1.0.0" }, routerPath: "/api/docs", exporter: new OpenApiExporter() { ServerUrls = ["http://localhost:5555/"] }); ``` - **context**: определяет метаданные о вашем приложении, такие как имя, описание и версия. - **routerPath**: URL‑путь, по которому будет доступен пользовательский интерфейс документации (или JSON). - **exporter**: настраивает способ экспорта документации. `OpenApiExporter` включает поддержку Open API (Swagger). ### Документирование конечных точек Вы можете описывать свои конечные точки, используя атрибуты `[ApiEndpoint]` и `[ApiQueryParameter]` на методах‑обработчиках маршрутов. ### `ApiEndpoint` Атрибут `[ApiEndpoint]` позволяет задать описание для конечной точки. ```csharp [ApiEndpoint(Description = "Returns a greeting message.")] public HttpResponse Index(HttpRequest request) { ... } ``` ### `ApiQueryParameter` Атрибут `[ApiQueryParameter]` документирует параметры строки запроса, которые принимает конечная точка. ```csharp [ApiQueryParameter(name: "name", IsRequired = false, Description = "The name of the person to greet.", Type = "string")] public HttpResponse Index(HttpRequest request) { ... } ``` - **name**: имя параметра запроса. - **IsRequired**: указывает, обязателен ли параметр. - **Description**: человекочитаемое описание параметра. - **Type**: ожидаемый тип данных (например, `"string"`, `"int"`). ### `ApiEndpoint` Аннотирует конечную точку общей информацией. * **Name** (string, обязателен в конструкторе): имя конечной точки API. * **Description** (string): краткое описание того, что делает конечная точка. * **Group** (string): позволяет группировать конечные точки (например, по контроллеру или модулю). * **InheritDescriptionFromXmlDocumentation** (bool, по умолчанию: `true`): если `true`, пытается использовать сводку XML‑документации метода, если `Description` не задано. ### `ApiHeader` Документирует конкретный HTTP‑заголовок, который ожидает или использует конечная точка. * **HeaderName** (string, обязателен в конструкторе): ключ заголовка (например, `"Authorization"`). * **Description** (string): описание назначения заголовка. * **IsRequired** (bool): указывает, обязателен ли заголовок для запроса. ### `ApiParameter` Определяет общий параметр для конечной точки, часто используемый для полей формы или параметров тела, не покрытых другими атрибутами. * **Name** (string, обязателен в конструкторе): имя параметра. * **TypeName** (string, обязателен в конструкторе): тип данных параметра (например, `"string"`, `"int"`). * **Description** (string): описание параметра. * **IsRequired** (bool): указывает, обязателен ли параметр. ### `ApiParameterExample` Добавляет пример значения для документированного параметра в конкретной ситуации. Атрибут можно применять несколько раз к одному методу. * **ParameterName** (string, обязателен в конструкторе): имя параметра, к которому относится пример. * **Title** (string, обязателен в конструкторе): заголовок, идентифицирующий иллюстрируемую ситуацию. * **Description** (string): когда или почему применяется пример. * **Example** (string): значение примера. * **ExampleLanguage** (string): язык, используемый для форматирования значения примера, если применимо. ```csharp [ApiQueryParameter("status", Type = "string", Description = "Filters orders by status.")] [ApiParameterExample("status", "Pending orders", Example = "pending")] [ApiParameterExample("status", "Shipped orders", Example = "shipped", Description = "Orders that left the warehouse.")] ``` ### `ApiParametersFrom` Автоматически генерирует документацию параметров из свойств указанного класса или типа. * **Type** (Type, обязателен в конструкторе): тип класса, свойства которого следует отразить. ### `ApiPathParameter` Документирует переменную пути (например, в `/users/{id}`). * **Name** (string, обязателен в конструкторе): имя параметра пути. * **Description** (string): описание того, что представляет параметр. * **Type** (string): ожидаемый тип данных. ### `ApiQueryParameter` Документирует параметр строки запроса (например, `?page=1`). * **Name** (string, обязателен в конструкторе): ключ параметра запроса. * **Description** (string): описание параметра. * **Type** (string): ожидаемый тип данных. * **IsRequired** (bool): указывает, обязателен ли параметр в запросе. ### `ApiRequest` Описывает ожидаемое тело запроса. * **Description** (string, обязателен в конструкторе): описание тела запроса. * **Example** (string): необработанная строка с примером тела запроса. * **ExampleLanguage** (string): язык примера (например, `"json"`, `"xml"`). * **PayloadType** (Type): если задан, пример и схема будут сгенерированы автоматически из этого типа, когда соответствующие обработчики контекста поддерживают это. ### `ApiResponse` Описывает возможный ответ конечной точки. * **StatusCode** (HttpStatusCode, обязателен в конструкторе): возвращаемый HTTP‑статус (например, `HttpStatusCode.OK`). * **Description** (string): описание условия, при котором возвращается этот ответ. * **Example** (string): необработанная строка с примером тела ответа. * **ExampleLanguage** (string): язык примера. * **PayloadType** (Type): если задан, пример и схема будут сгенерированы автоматически из этого типа, когда соответствующие обработчики контекста поддерживают это. ## Обработчики типов Обработчики типов отвечают за преобразование ваших .NET‑типов (классов, перечислений и т.д.) в примеры для документации. Это особенно полезно для автоматической генерации примеров запросов и ответов на основе ваших моделей данных. Эти обработчики настраиваются в `ApiGenerationContext`. ```csharp using Sisk.Documenting.Content; var context = new ApiGenerationContext() { // ... BodyExampleTypeHandler = new JsonContentTypeHandler(), ParameterExampleTypeHandler = new JsonContentTypeHandler(), ContentSchemaTypeHandler = new JsonContentTypeHandler() }; ``` ### JsonContentTypeHandler `JsonContentTypeHandler` — встроенный обработчик, генерирующий JSON‑примеры, примеры параметров и JSON‑схемы. Он реализует `IExampleBodyTypeHandler`, `IExampleParameterTypeHandler` и `IContentSchemaTypeHandler`. Его можно настроить с помощью конкретных `JsonSerializerOptions` или `IJsonTypeInfoResolver`, чтобы соответствовать логике сериализации вашего приложения. ```csharp var jsonHandler = new JsonContentTypeHandler(new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true }); context.BodyExampleTypeHandler = jsonHandler; context.ParameterExampleTypeHandler = jsonHandler; context.ContentSchemaTypeHandler = jsonHandler; ``` ### Пользовательские обработчики типов Вы можете реализовать собственные обработчики для поддержки других форматов (например, XML) или для кастомизации генерации примеров. #### IExampleBodyTypeHandler Реализуйте этот интерфейс, чтобы генерировать примеры тела для типов запросов и ответов. ```csharp public class XmlExampleTypeHandler : IExampleBodyTypeHandler { public BodyExampleResult? GetBodyExampleForType(Type type) { // Генерировать XML‑строку для типа string xmlContent = MyXmlGenerator.Generate(type); return new BodyExampleResult(xmlContent, "xml"); } } ``` #### IExampleParameterTypeHandler Реализуйте этот интерфейс, чтобы генерировать подробные описания параметров из типа (используется `[ApiParametersFrom]`). ```csharp public class CustomParameterHandler : IExampleParameterTypeHandler { public ParameterExampleResult[] GetParameterExamplesForType(Type type) { var properties = type.GetProperties(); var examples = new List(); foreach (var prop in properties) { examples.Add(new ParameterExampleResult( name: prop.Name, typeName: prop.PropertyType.Name, isRequired: true, description: "Generated description" )); } return examples.ToArray(); } } ``` ## Экспортеры Экспортеры отвечают за преобразование собранных метаданных документации API в конкретный формат, который может быть использован другими инструментами или отображён пользователю. ### OpenApiExporter Стандартный экспортер — `OpenApiExporter`, который генерирует JSON‑файл в соответствии со [спецификацией OpenAPI 3.0.0](https://spec.openapis.org/oas/v3.0.0). ```csharp new OpenApiExporter() { OpenApiVersion = "3.0.0", ServerUrls = new[] { "http://localhost:5555" }, Contact = new OpenApiContact() { Name = "Support", Email = "support@example.com", Url = "https://example.com/support" }, License = new OpenApiLicense() { Name = "MIT", Url = "https://opensource.org/licenses/MIT" }, TermsOfService = "https://example.com/terms" } ``` ### LlmsApiExporter `LlmsApiExporter` генерирует простой текстовый документ в стиле [llms.txt](https://llmstxt.org/), предназначенный для чтения языковыми моделями. Вы можете задать `Hostname`, используемый в примерах, а также `HeaderText` и `FooterText`, которые будут записаны в начале и в конце документа: ```csharp new LlmsApiExporter( hostname: "https://api.example.com", headerText: "Public API of the Example service.", footerText: "Contact support@example.com for an API key.") ``` ### Создание собственного экспортера Вы можете создать собственный экспортер, реализовав интерфейс `IApiDocumentationExporter`. Это позволит выводить документацию в форматах, таких как Markdown, HTML, коллекция Postman или любой другой пользовательский формат. Интерфейс требует реализации единственного метода: `ExportDocumentationContent`. ```csharp using Sisk.Core.Http; using Sisk.Documenting; public class MyCustomExporter : IApiDocumentationExporter { public HttpContent ExportDocumentationContent(ApiDocumentation documentation) { // 1. Обработать объект документации var sb = new StringBuilder(); sb.AppendLine($"# {documentation.ApplicationName}"); foreach(var endpoint in documentation.Endpoints) { sb.AppendLine($"## {endpoint.Method} {endpoint.Path}"); sb.AppendLine(endpoint.Description); } // 2. Вернуть содержимое как HttpContent return new StringContent(sb.ToString(), Encoding.UTF8, "text/markdown"); } } ``` Затем просто используйте его в конфигурации: ```csharp host.UseApiDocumentation( // ... exporter: new MyCustomExporter() ); ``` ### Полный пример Ниже приведён полный пример, демонстрирующий настройку `Sisk.Documenting` и документирование простого контроллера. ```csharp using Sisk.Core.Entity; using Sisk.Core.Http; using Sisk.Core.Routing; using Sisk.Documenting; using Sisk.Documenting.Annotations; using Sisk.Documenting.Exporters; using var host = HttpServer.CreateBuilder(5555) .UseCors(CrossOriginResourceSharingHeaders.CreatePublicContext()) .UseApiDocumentation( context: new ApiGenerationContext() { ApplicationName = "My application", ApplicationDescription = "It greets someone." }, routerPath: "/api/docs", exporter: new OpenApiExporter() { ServerUrls = ["http://localhost:5555/"] }) .UseRouter(router => { router.MapInstance(new MyController()); }) .Build(); await host.StartAsync(); class MyController { [RouteGet] [ApiEndpoint(Description = "Returns a greeting message.")] [ApiQueryParameter(name: "name", IsRequired = false, Description = "The name of the person to greet.", Type = "string")] public HttpResponse Index(HttpRequest request) { string? name = request.Query["name"].MaybeNullOrEmpty() ?? "world"; return new HttpResponse($"Hello, {name}!"); } } ``` В этом примере обращение к `/api/docs` будет обслуживать сгенерированную документацию для API «My application», описывая конечную точку `GET /` и её параметр `name`. --- # Базовая аутентификация (устарела) Source: https://docs.sisk-framework.org/ru/docs/extensions/basic-auth.html > [!CAUTION] > **Discontinued, do not use.** The Sisk.BasicAuth package is no longer maintained and is deprecated on NuGet, with no replacement package. Its `BasicAuthenticateRequestHandler` does not decode the Base64 credentials sent by standard HTTP clients, so authenticated requests are rejected. Implement Basic authentication with your own [request handler](https://docs.sisk-framework.org/ru/docs/fundamentals/request-handlers.md) that reads [HttpHeaderCollection.Authorization](https://docs.sisk-framework.org/api/Sisk.Core.Entity.HttpHeaderCollection.Authorization.md) instead. This page is kept for historical reference only. Пакет Basic Auth добавляет обработчик запросов, способный обрабатывать схему базовой аутентификации в вашем приложении Sisk с минимальной конфигурацией и усилиями. Базовая HTTP‑аутентификация — это простейшая форма аутентификации запросов по идентификатору пользователя и паролю, при которой сессия полностью контролируется клиентом и не используются токены аутентификации или доступа. ![Базовая аутентификация](https://docs.sisk-framework.org/assets/img/basic-auth.svg) Подробнее о схеме базовой аутентификации читайте в [спецификации MDN](https://developer.mozilla.org/pt-BR/docs/Web/HTTP/Authentication). ## Installing Чтобы начать, установите пакет Sisk.BasicAuth в ваш проект: > dotnet add package Sisk.BasicAuth Больше способов установки вы можете посмотреть в [репозитории NuGet](https://www.nuget.org/packages/Sisk.BasicAuth/0.15.0). ## Creating your auth handler Вы можете управлять схемой аутентификации для всего модуля или для отдельных маршрутов. Для начала напишем наш первый обработчик базовой аутентификации. В примере ниже устанавливается соединение с базой данных, проверяется наличие пользователя и корректность пароля, после чего пользователь сохраняется в контекст‑мешок. ```cs public class UserAuthHandler : BasicAuthenticateRequestHandler { public UserAuthHandler() : base() { Realm = "To enter this page, please, inform your credentials."; } public override HttpResponse? OnValidating(BasicAuthenticationCredentials credentials, HttpContext context) { DbContext db = new DbContext(); // в данном случае мы используем email в качестве поля идентификатора пользователя, поэтому // будем искать пользователя по его email. User? user = db.Users.FirstOrDefault(u => u.Email == credentials.UserId); if (user == null) { return base.CreateUnauthorizedResponse("Sorry! No user was found by this email."); } // проверяет, что пароль из учетных данных действителен для этого пользователя. if (!user.ValidatePassword(credentials.Password)) { return base.CreateUnauthorizedResponse("Invalid credentials."); } // добавляет вошедшего пользователя в http‑контекст // и продолжает выполнение context.Bag.Add("loggedUser", user); return null; } } ``` Итак, просто привяжите этот обработчик запросов к нашему маршруту или классу. ```cs public class UsersController { [RouteGet("/")] [RequestHandler(typeof(UserAuthHandler))] public string Index(HttpRequest request) { User loggedUser = request.Bag.Get(); return $"Hello, {loggedUser.Name}!"; } } ``` Или используя класс [RouterModule](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouterModule.md): ```cs public class UsersController : RouterModule { public ClientModule() { // все маршруты внутри этого класса будут обрабатываться // UserAuthHandler. base.HasRequestHandler(new UserAuthHandler()); } [RouteGet("/")] public string Index(HttpRequest request) { User loggedUser = request.Bag.Get(); return $"Hello, {loggedUser.Name}!"; } } ``` ## Remarks Основная ответственность за базовую аутентификацию возлагается на клиентскую сторону. Хранение, управление кэшем и шифрование полностью осуществляются локально на клиенте. Сервер лишь получает учетные данные и проверяет, разрешён ли доступ. Обратите внимание, что этот метод не считается особо безопасным, поскольку возлагает значительную ответственность на клиент, что может затруднить отслеживание и поддержание безопасности его учетных данных. Кроме того, крайне важно передавать пароли по защищённому соединению (SSL), так как они не имеют собственного шифрования. Даже кратковременный перехват заголовков запроса может раскрыть учетные данные вашего пользователя. Для приложений в продакшене выбирайте более надёжные решения аутентификации и избегайте избыточного использования готовых компонентов, которые могут не соответствовать требованиям вашего проекта и привести к угрозам безопасности. --- # SSL Proxy (прекращённый) Source: https://docs.sisk-framework.org/ru/docs/extensions/ssl-proxy.html > [!CAUTION] > **Прекращён, не использовать.** Пакет Sisk.SslProxy больше не поддерживается и помечен как устаревший в NuGet, без заменяющего пакета. Он больше не компилируется с текущим API Cadente. Для обслуживания HTTPS используйте встроенную поддержку SSL в [Cadente engine](https://docs.sisk-framework.org/ru/docs/cadente.md) или один из вариантов в [Working with SSL](https://docs.sisk-framework.org/ru/docs/ssl.md). Эта страница сохраняется только в исторических целях. Sisk SSL Proxy — это модуль, который обеспечивает HTTPS‑соединение для [ListeningHost](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.md) в Sisk и перенаправляет HTTPS‑сообщения в небезопасный HTTP‑контекст. Модуль был создан для обеспечения SSL‑соединения для сервиса, использующего [HttpListener](https://learn.microsoft.com/pt-br/dotnet/api/system.net.httplistener?view=net-8.0) для работы, который не поддерживает SSL. Прокси работает внутри того же приложения и прослушивает сообщения HTTP/1.1, пересылая их тем же протоколом в Sisk. В настоящее время эта функция находится в экспериментальной стадии и может быть настолько нестабильной, что её не следует использовать в продакшене. На данный момент SslProxy поддерживает почти все возможности HTTP/1.1, такие как keep-alive, chunked‑encoding, websockets и т.д. Для открытого соединения с SSL‑прокси создаётся TCP‑соединение с целевым сервером, и прокси перенаправляется на установленное соединение. SslProxy можно использовать с HttpServer.CreateBuilder следующим образом: ```csharp using var app = HttpServer.CreateBuilder(port: 5555) .UseRouter(r => { r.MapGet("/", request => { return new HttpResponse("Hello, world!"); }); }) // добавить SSL в проект .UseSsl( sslListeningPort: 5567, new X509Certificate2(@".\ssl.pfx", password: "12345") ) .Build(); app.Start(); ``` Вы должны предоставить действительный SSL‑сертификат для прокси. Чтобы гарантировать, что сертификат принимается браузерами, не забудьте импортировать его в операционную систему, чтобы он функционировал корректно. --- # Ручная (расширенная) настройка Source: https://docs.sisk-framework.org/ru/docs/advanced/manual-setup.html Используйте ручную настройку, когда необходимо собрать части сервера самостоятельно, например, когда один процесс должен предоставлять несколько хостов, портов, маршрутизаторов или пользовательскую конфигурацию сервера. Для большинства приложений API построителя короче и предпочтительнее. Ручная настройка полезна, когда нужен прямой контроль над четырьмя основными компонентами: `Router`, один или несколько объектов `ListeningHost`, `HttpServerConfiguration` и конечный `HttpServer`. Сначала нам нужно понять концепцию запрос/ответ. Она довольно проста: для каждого запроса должен быть ответ. Sisk следует этому принципу. Давайте создадим метод, который отвечает сообщением «Hello, World!» в HTML, указывая код статуса и заголовки. ```csharp // Program.cs using Sisk.Core.Http; using Sisk.Core.Routing; static HttpResponse IndexPage(HttpRequest request) { HttpResponse indexResponse = new HttpResponse { Status = System.Net.HttpStatusCode.OK, Content = new HtmlContent(@"

    Hello, world!

    ") }; return indexResponse; } ``` Следующий шаг — связать этот метод с HTTP‑маршрутом. ## Маршрутизаторы Маршрутизаторы — это абстракции маршрутов запросов и служат мостом между запросами и ответами сервиса. Маршрутизаторы управляют маршрутами сервиса, функциями и ошибками. Маршрутизатор может иметь несколько маршрутов, и каждый маршрут может выполнять разные операции по этому пути, такие как вызов функции, отдача страницы или предоставление ресурса с сервера. Создадим наш первый маршрутизатор и привяжем метод `IndexPage` к пути индекса. ```csharp Router mainRouter = new Router(); mainRouter.MapGet("/", IndexPage); ``` Теперь наш маршрутизатор может принимать запросы и отправлять ответы. Однако `mainRouter` не привязан к хосту или серверу, поэтому сам по себе он не будет работать. Следующий шаг — создать наш `ListeningHost`. ## Хосты прослушивания и порты [ListeningHost](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.md) может размещать маршрутизатор и несколько прослушиваемых портов для одного и того же маршрутизатора. [ListeningPort](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningPort.md) — это префикс, на котором HTTP‑сервер будет слушать. Здесь мы можем создать `ListeningHost`, который указывает на два конечных пункта для нашего маршрутизатора: ```csharp ListeningHost myHost = new ListeningHost { Router = mainRouter, Ports = new ListeningPort[] { new ListeningPort("http://localhost:5000/") } }; ``` Теперь наш HTTP‑сервер будет слушать указанные конечные пункты и перенаправлять запросы к нашему маршрутизатору. ## Конфигурация сервера Конфигурация сервера отвечает за большую часть поведения самого HTTP‑сервера. В этой конфигурации мы можем связать `ListeningHosts` с нашим сервером. ```csharp HttpServerConfiguration config = new HttpServerConfiguration(); config.ListeningHosts.Add(myHost); // Добавляем наш ListeningHost в эту конфигурацию сервера ``` Общие параметры конфигурации сервера: | Свойство | По умолчанию | Когда использовать | Примечания | | --- | --- | --- | --- | | [RemoteRequestsAction](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.RemoteRequestsAction.md) | `RequestListenAction.Accept` | Сервис должен отклонять нелокальные запросы, если они не проходят через доверенный обратный прокси. | Устанавливайте `Drop` только при ясной топологии развертывания. | | [IncludeRequestIdHeader](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.IncludeRequestIdHeader.md) | `false` | Клиентам или прокси нужен идентификатор запроса Sisk в заголовке ответа `X-Request-Id`. | Сочетайте с журналами, содержащими `HttpRequest.RequestId`. | | [IdleConnectionTimeout](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.IdleConnectionTimeout.md) | `120` seconds | Неактивные keep-alive соединения следует закрывать рано или поздно. | Применяется движком HTTP. | | [NormalizeHeadersEncodings](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.NormalizeHeadersEncodings.md) | `false` | Вы получаете заголовки с несоответствием кодировок. | Это требует дополнительных ресурсов; оставляйте отключённым, если не требуется. | | [SendSiskHeader](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.SendSiskHeader.md) | `true` | Нужно скрыть или показать заголовок `X-Powered-By` от Sisk. | Отключайте для более строгих политик заголовков в продакшене. | | [OptionsLogMode](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.OptionsLogMode.md) | `LogOutput.Both` | Нужно уменьшить или перенаправить логи, генерируемые автоматической обработкой `OPTIONS`. | Использует те же значения режима логов, что и маршруты. | | [AsyncRequestProcessing](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.AsyncRequestProcessing.md) | `true` | Требуется детерминированная обработка одиночных запросов для диагностики. | Отключение снижает пропускную способность. | | [DisposeDisposableContextValues](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.DisposeDisposableContextValues.md) | `true` | Значения в контейнере запроса, реализующие `IDisposable`, должны автоматически освобождаться. | Оставляйте включённым, если управление владением не реализовано иначе. | | [ConvertIAsyncEnumerableIntoEnumerable](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ConvertIAsyncEnumerableIntoEnumerable.md) | `true` | Обработчики значений должны получать асинхронные перечисления как блокирующие перечисления. | Отключайте, если реализуете собственную обработку async‑stream. | | [KeepAlive](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.KeepAlive.md) | `true` | Соединения должны оставаться переиспользуемыми после ответов. | Отключайте для клиентов или промежуточных узлов, плохо работающих с постоянными соединениями. | | [ForceTrailingSlash](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ForceTrailingSlash.md) | `false` | GET‑маршруты должны перенаправлять на URL с завершающим слешем. | Применяется только к маршрутам без регулярных выражений. | | [MaximumContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.MaximumContentLength.md) | `100 MB` | Тела запросов или сообщения WebSocket требуют другого ограничения размера. | По умолчанию было `0` (неограниченно) до Sisk 1.7. Установите `0`, чтобы снять ограничение. | | [EnableAutomaticResponseCompression](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.EnableAutomaticResponseCompression.md) | `false` | Ответы должны автоматически сжиматься, если клиент это поддерживает. | Уже сжатые ответы `CompressedContent` не сжимаются повторно. | Далее создаём наш HTTP‑сервер: ```csharp HttpServer server = new HttpServer(config); server.Start(); // Запускает сервер Console.ReadKey(); // Предотвращает завершение приложения ``` Теперь мы можем собрать исполняемый файл и запустить наш HTTP‑сервер командой: ```bash dotnet watch ``` Во время выполнения откройте браузер и перейдите по пути сервера — вы должны увидеть: --- # Жизненный цикл запроса Source: https://docs.sisk-framework.org/ru/docs/advanced/request-lifecycle.html Ниже объясняется весь жизненный цикл запроса на примере HTTP‑запроса. - **Получение запроса:** каждый запрос создает HTTP‑контекст между самим запросом и ответом, который будет доставлен клиенту. Этот контекст берётся из встроенного слушателя в Sisk, которым может быть [HttpListener](https://learn.microsoft.com/en-us/dotnet/api/system.net.httplistener?view=net-9.0), [Kestrel](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel?view=aspnetcore-9.0) или [Cadente](https://blog.sisk-framework.org/posts/2025-01-29-cadente-experiment/). - Внешняя проверка запроса: проверка [HttpServerConfiguration.RemoteRequestsAction](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.RemoteRequestsAction.md) выполняется для запроса. - Если запрос внешний и свойство имеет значение `Drop`, соединение закрывается без ответа клиенту с `HttpServerExecutionStatus = RemoteRequestDropped`. - Конфигурация Forwarding Resolver: если настроен [ForwardingResolver](https://docs.sisk-framework.org/ru/docs/advanced/forwarding-resolvers.md), он вызовет метод [OnResolveRequestHost](https://docs.sisk-framework.org/api/Sisk.Core.Http.ForwardingResolver.OnResolveRequestHost.md) на оригинальном хосте запроса. - Сопоставление DNS: после разрешения хоста и при наличии более одного настроенного [ListeningHost](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.md) сервер будет искать соответствующий хост для запроса. - Если ни один ListeningHost не подходит, клиенту возвращается ответ 400 Bad Request, а в HTTP‑контекст записывается статус `HttpServerExecutionStatus = DnsUnknownHost`. - Если найден подходящий ListeningHost, но его [Router](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.Router.md) ещё не инициализирован, клиенту возвращается ответ 503 Service Unavailable, а в HTTP‑контекст записывается статус `HttpServerExecutionStatus = ListeningHostNotReady`. - Привязка роутера: роутер соответствующего ListeningHost связывается с полученным HTTP‑сервером. - Если роутер уже связан с другим HTTP‑сервером, что недопустимо, поскольку роутер активно использует ресурсы конфигурации сервера, выбрасывается `InvalidOperationException`. Это происходит только при инициализации HTTP‑сервера, а не при создании HTTP‑контекста. - Предопределение заголовков: - Предопределяет заголовок `X-Request-Id` в ответе, если он настроен. - Предопределяет заголовок `X-Powered-By` в ответе, если он настроен. - Проверка размера содержимого: проверяется, что размер содержимого запроса меньше [HttpServerConfiguration.MaximumContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.MaximumContentLength.md) (по умолчанию 100 МБ), если он больше нуля. - Если запрос отправляет `Content-Length`, превышающий настроенный, клиенту возвращается ответ 413 Payload Too Large, а в HTTP‑контекст записывается статус `HttpServerExecutionStatus = ContentTooLarge`. - Событие `OnHttpRequestOpen` вызывается для всех настроенных обработчиков HTTP‑сервера. - **Маршрутизация действия:** сервер вызывает роутер для полученного запроса. - Если роутер не находит маршрут, соответствующий запросу: - Если свойство [Router.NotFoundErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.NotFoundErrorHandler.md) настроено, вызывается действие, и его ответ пересылается HTTP‑клиенту. - Если предыдущее свойство равно null, клиенту возвращается стандартный ответ 404 Not Found. - Если роутер находит подходящий маршрут, но метод маршрута не совпадает с методом запроса: - Если свойство [Router.MethodNotAllowedErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MethodNotAllowedErrorHandler.md) настроено, вызывается действие, и его ответ пересылается HTTP‑клиенту. - Если предыдущее свойство равно null, клиенту возвращается стандартный ответ 405 Method Not Allowed. - Если запрос имеет метод `OPTIONS`: - Роутер возвращает клиенту ответ 200 Ok только если ни один маршрут не соответствует методу запроса (метод маршрута явно не указан как [RouteMethod.Options](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md)). - Если свойство [HttpServerConfiguration.ForceTrailingSlash](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ForceTrailingSlash.md) включено, найденный маршрут не является регулярным выражением, путь запроса не заканчивается символом `/`, а метод запроса — `GET`: - Клиенту возвращается HTTP‑ответ 307 Temporary Redirect с заголовком `Location`, указывающим путь и запрос к тому же месту, но с завершающим `/`. - Событие `OnContextBagCreated` вызывается для всех настроенных обработчиков HTTP‑сервера. - Все глобальные экземпляры [IRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.md) с флагом `BeforeResponse` выполняются. - Если любой обработчик возвращает ненулевой ответ, он пересылается HTTP‑клиенту, и контекст закрывается. - Если на этом этапе возникает ошибка и [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) отключён: - Если свойство [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) включено, оно вызывается, и полученный ответ возвращается клиенту. - Если предыдущее свойство не определено, сервер получает пустой ответ и формирует ответ в зависимости от типа выброшенного исключения, обычно это 500 Internal Server Error. - Все экземпляры [IRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.md), определённые в маршруте и с флагом `BeforeResponse`, выполняются. - Если любой обработчик возвращает ненулевой ответ, он пересылается HTTP‑клиенту, и контекст закрывается. - Если на этом этапе возникает ошибка и [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) отключён: - Если свойство [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) включено, оно вызывается, и полученный ответ возвращается клиенту. - Если предыдущее свойство не определено, сервер получает пустой ответ и формирует ответ в зависимости от типа выброшенного исключения, обычно это 500 Internal Server Error. - Действие роутера вызывается и преобразуется в HTTP‑ответ. - Если на этом этапе возникает ошибка и [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) отключён: - Если свойство [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) включено, оно вызывается, и полученный ответ возвращается клиенту. - Если предыдущее свойство не определено, сервер получает пустой ответ и формирует ответ в зависимости от типа выброшенного исключения, обычно это 500 Internal Server Error. - Все глобальные экземпляры [IRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.md) с флагом `AfterResponse` выполняются. - Если любой обработчик возвращает ненулевой ответ, ответ обработчика заменяет предыдущий и немедленно пересылается HTTP‑клиенту. - Если на этом этапе возникает ошибка и [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) отключён: - Если свойство [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) включено, оно вызывается, и полученный ответ возвращается клиенту. - Если предыдущее свойство не определено, сервер получает пустой ответ и формирует ответ в зависимости от типа выброшенного исключения, обычно это 500 Internal Server Error. - Все экземпляры [IRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.md), определённые в маршруте и с флагом `AfterResponse`, выполняются. - Если любой обработчик возвращает ненулевой ответ, ответ обработчика заменяет предыдущий и немедленно пересылается HTTP‑клиенту. - Если на этом этапе возникает ошибка и [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) отключён: - Если свойство [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) включено, оно вызывается, и полученный ответ возвращается клиенту. - Если предыдущее свойство не определено, сервер получает пустой ответ и формирует ответ в зависимости от типа выброшенного исключения, обычно это 500 Internal Server Error. - **Обработка ответа:** когда ответ готов, сервер подготавливает его к отправке клиенту. - Заголовки политики Cross-Origin Resource Sharing (CORS) добавляются в ответ в соответствии с настройкой текущего [ListeningHost.CrossOriginResourceSharingPolicy](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.CrossOriginResourceSharingPolicy.md). - Код состояния и заголовки ответа отправляются клиенту. - Содержимое ответа отправляется клиенту: - Если содержимое ответа является наследником [ByteArrayContent](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.bytearraycontent), байты ответа копируются напрямую в поток вывода ответа. - Если предыдущее условие не выполнено, ответ сериализуется в поток и копируется в поток вывода ответа. - Потоки закрываются, а содержимое ответа отбрасывается. - Если включено [HttpServerConfiguration.DisposeDisposableContextValues](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.DisposeDisposableContextValues.md), все объекты, определённые в контексте запроса и наследующие [IDisposable](https://learn.microsoft.com/en-us/dotnet/api/system.idisposable), отбрасываются. - Событие `OnHttpRequestClose` вызывается для всех настроенных обработчиков HTTP‑сервера. - Если на сервере было выброшено исключение, событие `OnException` вызывается для всех настроенных обработчиков HTTP‑сервера. - Если маршрут разрешает ведение журнала доступа и [HttpServerConfiguration.AccessLogsStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.AccessLogsStream.md) не равен null, в вывод журнала записывается строка. - Если маршрут разрешает ведение журнала ошибок, возникло исключение и [HttpServerConfiguration.ErrorsLogsStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ErrorsLogsStream.md) не равен null, в вывод журнала ошибок записывается строка. - Если сервер ожидает запрос через [HttpServer.WaitNext](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.WaitNext.md), мьютекс освобождается, и контекст становится доступным пользователю. --- # Forwarding Resolvers Source: https://docs.sisk-framework.org/ru/docs/advanced/forwarding-resolvers.html Forwarding Resolver — это вспомогательный компонент, который помогает декодировать информацию, идентифицирующую клиента через запрос, прокси, CDN или балансировщик нагрузки. Когда ваш сервис Sisk работает через обратный или прямой прокси, IP‑адрес клиента, хост и протокол могут отличаться от оригинального запроса, поскольку происходит переадресация от одного сервиса к другому. Эта возможность Sisk позволяет контролировать и определять эту информацию до работы с запросом. Такие прокси обычно предоставляют полезные заголовки для идентификации своего клиента. В настоящее время с классом [ForwardingResolver](https://docs.sisk-framework.org/api/Sisk.Core.Http.ForwardingResolver.md) можно определить IP‑адрес клиента, хост и используемый HTTP‑протокол. Начиная с версии 1.0 Sisk, сервер больше не имеет стандартной реализации декодирования этих заголовков по соображениям безопасности, которые различаются от сервиса к сервису. Например, заголовок `X-Forwarded-For` содержит информацию об IP‑адресах, которые переадресовали запрос. Этот заголовок используется прокси для передачи цепочки информации конечному сервису и включает IP всех задействованных прокси, включая реальный адрес клиента. Проблема в том, что иногда сложно определить удалённый IP клиента, и нет единого правила для распознавания этого заголовка. Настоятельно рекомендуется ознакомиться с документацией по заголовкам, которые вы собираетесь использовать, ниже: - Подробнее о заголовке `X-Forwarded-For` — [здесь](https://developer.mozilla.org/en-US/docs/ru/Web/HTTP/Headers/X-Forwarded-For#security_and_privacy_concerns). - Подробнее о заголовке `X-Forwarded-Host` — [здесь](https://developer.mozilla.org/en-US/docs/ru/Web/HTTP/Headers/X-Forwarded-Host). - Подробнее о заголовке `X-Forwarded-Proto` — [здесь](https://developer.mozilla.org/en-US/docs/ru/Web/HTTP/Headers/X-Forwarded-Proto). ## The ForwardingResolver class Этот класс содержит три виртуальных метода, позволяющих реализовать наиболее подходящее решение для каждого сервиса. Каждый метод отвечает за определение информации из запроса через прокси: IP‑адрес клиента, хост запроса и используемый протокол безопасности. По умолчанию Sisk всегда использует данные оригинального запроса, не обрабатывая заголовки. Ниже приведён пример того, как можно использовать эту реализацию. Пример определяет IP клиента через заголовок `X-Forwarded-For` и генерирует ошибку, если в запросе передано более одного IP‑адреса. > [!IMPORTANT] > Не используйте этот пример в продакшн‑коде. Всегда проверяйте, подходит ли реализация для вашего случая. Ознакомьтесь с документацией заголовков перед их внедрением. ```cs class Program { static void Main(string[] args) { using var host = HttpServer.CreateBuilder() .UseForwardingResolver() .UseListeningPort(5555) .Build(); host.Router.MapAny(Route.AnyPath, request => new HttpResponse("Hello, world!!!")); host.Start(); } class Resolver : ForwardingResolver { public override IPAddress OnResolveClientAddress(HttpRequest request, IPEndPoint connectingEndpoint) { string? forwardedFor = request.Headers.XForwardedFor; if (forwardedFor is null) { throw new Exception("The X-Forwarded-For header is missing."); } string[] ipAddresses = forwardedFor.Split(','); if (ipAddresses.Length != 1) { throw new Exception("Too many addresses in the X-Forwarded-For header."); } return IPAddress.Parse(ipAddresses[0]); } } } ``` --- # Обработчики HTTP‑сервера Source: https://docs.sisk-framework.org/ru/docs/advanced/http-server-handlers.html В версии Sisk 0.16 мы представили класс `HttpServerHandler`, который предназначен для расширения общего поведения Sisk и предоставления дополнительных обработчиков событий, таких как обработка HTTP‑запросов, маршрутизаторов, контекстных пакетов и прочего. Класс концентрирует события, происходящие в течение жизни всего HTTP‑сервера, а также отдельного запроса. Протокол HTTP не поддерживает сессии, поэтому невозможно сохранять информацию от одного запроса к другому. На данный момент Sisk предоставляет возможность реализовать сессии, контексты, соединения с базой данных и другие полезные провайдеры, помогающие в работе. Смотрите [this page](https://docs.sisk-framework.org/api/Sisk.Core.Http.Handlers.HttpServerHandler.md), чтобы узнать, где каждый событие вызывается и какова его цель. Вы также можете посмотреть [lifecycle of an HTTP request](https://docs.sisk-framework.org/ru/docs/advanced/request-lifecycle.md), чтобы понять, что происходит с запросом и где генерируются события. HTTP‑сервер позволяет использовать несколько обработчиков одновременно. Каждый вызов события синхронный, то есть текущий поток блокируется для каждого запроса или контекста, пока не будут выполнены и завершены все обработчики, связанные с этой функцией. В отличие от RequestHandlers, они не могут быть применены к отдельным группам маршрутов или конкретным маршрутам. Вместо этого они применяются ко всему HTTP‑серверу. Вы можете задавать условия внутри вашего Http Server Handler. Кроме того, для каждого `HttpServerHandler` в каждом приложении Sisk создаётся единственный экземпляр (singleton), то есть существует только один объект `HttpServerHandler`. Практический пример использования HttpServerHandler — автоматическое освобождение соединения с базой данных в конце обработки запроса. ```cs // DatabaseConnectionHandler.cs public class DatabaseConnectionHandler : HttpServerHandler { protected override void OnHttpRequestClose(HttpServerExecutionResult result) { var requestBag = result.Request.Context.RequestBag; // проверяет, определён ли у запроса DbContext // в его контекстном пакете if (requestBag.IsSet()) { var db = requestBag.Get(); db.Dispose(); } } } public static class DatabaseConnectionHandlerExtensions { public static DbContext GetDbContext(this HttpRequest request) { return request.Bag.GetOrAdd(() => new DbContext()); } } ``` С помощью приведённого кода расширение `GetDbContext` позволяет создать контекст соединения непосредственно из объекта `HttpRequest`. Неосвобождённое соединение может вызвать проблемы при работе с базой данных, поэтому оно закрывается в `OnHttpRequestClose`. Вы можете зарегистрировать обработчик на HTTP‑сервере в вашем билдере или напрямую через [HttpServer.RegisterHandler](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.RegisterHandler.md). ```cs // Program.cs class Program { static void Main(string[] args) { using var app = HttpServer.CreateBuilder() .UseHandler() .Build(); app.Router.MapInstance(new UserController()); app.Start(); } } ``` Таким образом, класс `UsersController` может использовать контекст базы данных следующим образом: ```cs // UserController.cs [RoutePrefix("/users")] public class UserController : ApiController { [RouteGet()] public async Task List(HttpRequest request) { var db = request.GetDbContext(); var users = db.Users.ToArray(); return JsonOk(users); } [RouteGet("")] public async Task View(HttpRequest request) { var db = request.GetDbContext(); int userId = request.RouteParameters["id"].GetInteger(); var user = db.Users.FirstOrDefault(u => u.Id == userId); return JsonOk(user); } [RoutePost] public async Task Create(HttpRequest request) { var db = request.GetDbContext(); var user = await request.GetJsonContentAsync(); ArgumentNullException.ThrowIfNull(user); db.Users.Add(user); await db.SaveChangesAsync(); return JsonMessage("User added."); } } ``` В приведённом коде используются методы `JsonOk` и `JsonMessage`, встроенные в `ApiController`, который наследуется от `RouterController`: ```cs // ApiController.cs public class ApiController : RouterModule { public HttpResponse JsonOk(object value) { return new HttpResponse(200) .WithContent(JsonContent.Create(value, null, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true })); } public HttpResponse JsonMessage(string message, int statusCode = 200) { return new HttpResponse(statusCode) .WithContent(JsonContent.Create(new { Message = message })); } } ``` ## Исключения, выбрасываемые обработчиками Начиная с Sisk 1.7, исключения, выбрасываемые обработчиками сервера, обрабатываются следующим образом: - Исключения, возникшие в `OnServerStarting`, `OnServerStarted` и `OnSetupRouter`, останавливают запуск сервера. Они оборачиваются в `InvalidOperationException` с оригинальным исключением в `InnerException`. - Исключения, возникшие в других событиях, записываются в [ErrorsLogsStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ErrorsLogsStream.md), а остальные обработчики продолжают вызываться, если только не включён [ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md). При включённом `ThrowExceptions` они также оборачиваются в `InvalidOperationException`. - Исключения, возникшие в `OnHttpRequestClose` и `OnException`, а также при форматировании журналов доступа и ошибок, никогда не попадают в HTTP‑движок, даже при включённом `ThrowExceptions`. Они записываются в `ErrorsLogsStream`. В более ранних версиях такие исключения могли завершать процесс или оставлять соединение открытым. Разработчики могут реализовывать сессии, контексты и соединения с базой данных, используя этот класс. Приведённый код демонстрирует практический пример с `DatabaseConnectionHandler`, автоматизирующий освобождение соединения с базой данных в конце каждого запроса. Интеграция проста: обработчики регистрируются во время настройки сервера. Класс `HttpServerHandler` предоставляет мощный набор инструментов для управления ресурсами и расширения поведения Sisk в HTTP‑приложениях. --- # Несколько прослушивающих хостов на сервере Source: https://docs.sisk-framework.org/ru/docs/advanced/multi-host-setup.html Фреймворк Sisk всегда поддерживал использование более одного хоста на сервере, то есть один HTTP‑сервер может прослушивать несколько портов, и каждый порт имеет свой собственный роутер и собственный сервис, работающий на нём. Таким образом, легко разделять обязанности и управлять сервисами на одном HTTP‑сервере с помощью Sisk. Пример ниже показывает создание двух ListeningHost, каждый из которых прослушивает свой порт, имеет отдельный роутер и действия. Читайте [manually creating your app](https://docs.sisk-framework.org/ru/docs/advanced/manual-setup.md), чтобы понять детали этой абстракции. ```cs static void Main(string[] args) { // создаём два прослушивающих хоста, каждый из которых имеет свой роутер // и прослушивает свой порт // ListeningHost hostA = new ListeningHost(); hostA.Ports = [new ListeningPort(12000)]; hostA.Router = new Router(); hostA.Router.MapGet("/", request => new HttpResponse().WithContent("Hello from the host A!")); ListeningHost hostB = new ListeningHost(); hostB.Ports = [new ListeningPort(12001)]; hostB.Router = new Router(); hostB.Router.MapGet("/", request => new HttpResponse().WithContent("Hello from the host B!")); // создаём конфигурацию сервера и добавляем в неё оба // прослушивающих хоста // HttpServerConfiguration configuration = new HttpServerConfiguration(); configuration.ListeningHosts.Add(hostA); configuration.ListeningHosts.Add(hostB); // создаём HTTP‑сервер, использующий указанную // конфигурацию // HttpServer server = new HttpServer(configuration); // запускаем сервер server.Start(); Console.WriteLine("Попробуйте обратиться к хосту A по адресу {0}", server.ListeningPrefixes[0]); Console.WriteLine("Попробуйте обратиться к хосту B по адресу {0}", server.ListeningPrefixes[1]); Thread.Sleep(-1); } ``` --- # HTTP Server Engines Source: https://docs.sisk-framework.org/ru/docs/advanced/server-engines.html Фреймворк Sisk разделен на несколько пакетов, где основной пакет (Sisk.HttpServer) не включает базовый HTTP-сервер - по умолчанию используется [HttpListener](https://learn.microsoft.com/en-us/dotnet/api/system.net.httplistener?view=net-9.0) в качестве основного движка Sisk для выполнения низкоуровневой роли сервера. Движок HTTP выполняет роль слоя ниже слоя приложения, предлагаемого Sisk. Этот слой отвечает за управление соединениями, сериализацию и десериализацию сообщений, контроль очереди сообщений и общение с сокетом машины. Класс [HttpServerEngine](https://docs.sisk-framework.org/api/Sisk.Core.Http.Engine.HttpServerEngine.md) предоставляет API для реализации всех необходимых функций HTTP-движка для использования в верхних слоях с Sisk, таких как маршрутизация, SSE, middleware и т. д. Эти функции не являются ответственностью HTTP-движка, а rather подмножества библиотек, которые будут использовать HTTP-движок в качестве основы для выполнения. С этой абстракцией возможно перенести Sisk для использования с любым другим HTTP-движком, написанным на .NET или не на .NET, например Kestrel. В настоящее время Sisk остается использовать абстракцию родного .NET [HttpListener](https://learn.microsoft.com/en-us/dotnet/api/system.net.httplistener?view=net-9.0) в качестве значения по умолчанию для новых проектов. Это значение по умолчанию приносит некоторые специфические проблемы, такие как неопределенное поведение на разных платформах (HttpListener имеет одну реализацию для Windows и другую для других платформ), отсутствие поддержки SSL и не очень приятную производительность вне Windows. Также доступна экспериментальная реализация высокопроизводительного сервера, написанного чисто на C#, в качестве HTTP-движка для Sisk, называемого проектом [Cadente](https://github.com/sisk-http/core/tree/main/cadente), который является экспериментом управляемого сервера, который можно использовать с Sisk или без него. ## Реализация HTTP-движка для Sisk Вы можете создать мост соединения между существующим HTTP-сервером и Sisk, расширяя класс [HttpServerEngine](https://docs.sisk-framework.org/api/Sisk.Core.Http.Engine.HttpServerEngine.md).除了 этого класса, вам также придется реализовать абстракции для контекстов, запросов и ответов. Полный пример абстракции доступен на [GitHub](https://github.com/sisk-http/core/blob/main/src/Http/Engine/HttpListenerAbstractEngine.cs) для просмотра. Он выглядит следующим образом: ```csharp /// /// Provides an implementation of using . /// public sealed class HttpListenerAbstractEngine : HttpServerEngine { private HttpListener _listener; private static Lazy shared = new Lazy ( () => new HttpListenerAbstractEngine () ); /// /// Gets the shared instance of the class. /// public static HttpListenerAbstractEngine Shared => shared.Value; /// /// Initializes a new instance of the class. /// public HttpListenerAbstractEngine () { _listener = new HttpListener { IgnoreWriteExceptions = true }; } /// public override TimeSpan IdleConnectionTimeout { get => _listener.TimeoutManager.IdleConnection; set => _listener.TimeoutManager.IdleConnection = value; } // ... } ``` ## Выбор цикла событий Во время создания HTTP-движка сервер будет слушать запросы в цикле и создавать контексты для обработки каждого из них в отдельных потоках. Для этого вам придется выбрать [HttpServerEngineContextEventLoopMechanism](https://docs.sisk-framework.org/api/Sisk.Core.Http.Engine.HttpServerEngineContextEventLoopMechanism.md): - `InlineAsynchronousGetContext` цикл событий линеен - обработка контекста HTTP происходит в асинхронном цикле. - `UnboundAsynchronousGetContext` цикл событий передается через методы `BeginGetContext` и `EndGetContext`. ```csharp public override HttpServerEngineContextEventLoopMechanism EventLoopMechanism => HttpServerEngineContextEventLoopMechanism.UnboundAsynchronousGetContext; ``` Вам не нужно реализовывать оба цикла событий. Выберите тот, который имеет больше смысла для вашего HTTP-движка. ## Тестирование После связывания вашего HTTP-движка крайне важно провести тесты, чтобы убедиться, что все функции Sisk имеют идентичное поведение при использовании других движков. **Это крайне важно** иметь одинаковое поведение Sisk для разных HTTP-движков. Вы можете посетить репозиторий тестов на [GitHub](https://github.com/sisk-http/core/tree/main/tests).