# 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 или [другие варианты](https://www.nuget.org/packages/Sisk.HttpServer/). Вы можете легко настроить среду Sisk, выполнив эту команду в консоли разработчика: ```sh dotnet add package Sisk.HttpServer ``` Эта команда установит последнюю версию Sisk в вашем проекте. --- # Поддержка 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 publish: ```shell $ dotnet publish -r linux-x64 -c Release ``` Это сгенерирует ваши бинарные файлы внутри `bin/Release/publish/linux-x64`. > [!NOTE] > Если ваше приложение запускается с помощью пакета Sisk.ServiceProvider, вы должны скопировать ваш `service-config.json` на ваш сервер-хост вместе со всеми бинарными файлами, сгенерированными командой `dotnet publish`. > Вы можете оставить файл предварительно настроенным, с переменными окружения, портами прослушивания и хостами, а также дополнительными настройками сервера. Следующий шаг - перенести эти файлы на сервер, где будет размещено ваше приложение. После этого, дайте права на выполнение вашему бинарному файлу. В этом случае давайте рассмотрим, что наш проект называется "my-app": ```shell $ cd /home/htdocs $ chmod +x my-app $ ./my-app ``` После запуска вашего приложения, проверьте, не выдает ли оно какие-либо сообщения об ошибках. Если оно не выдало, это означает, что ваше приложение работает. На этом этапе, скорее всего, не будет возможно получить доступ к вашему приложению из внешней сети вне вашего сервера, поскольку правила доступа, такие как Firewall, еще не настроены. Мы рассмотрим это в следующих шагах. У вас должен быть адрес виртуального хоста, на котором прослушивает ваше приложение. Это устанавливается вручную в приложении и зависит от того, как вы создаете экземпляр вашего сервиса Sisk. Если вы **не** используете пакет Sisk.ServiceProvider, вы должны найти его там, где вы определили экземпляр вашего 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)); ``` Или если вы используете пакет Sisk.ServiceProvider, в вашем `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 - Доступно на: Linux, macOS, Windows - Сложность: легко > [!IMPORTANT] > > Пакет Sisk.SslProxy устарел в пользу пакета `Sisk.Cadente.CoreEngine` и более не будет поддерживаться. Пакет Sisk.SslProxy — простой способ включить SSL в вашем приложении Sisk. Однако это **крайне экспериментальный** пакет. Работа с ним может быть нестабильной, но вы можете стать частью небольшого процента людей, которые помогут сделать этот пакет пригодным и стабильным. Чтобы начать, вы можете установить пакет Sisk.SslProxy с помощью: ```sh dotnet add package Sisk.SslProxy ``` > [!NOTE] > > Необходимо включить опцию «Include prerelease» в менеджере пакетов Visual Studio, чтобы установить Sisk.SslProxy. Опять же, это экспериментальный проект, так что даже не думайте о его использовании в продакшене. В данный момент Sisk.SslProxy может обрабатывать большинство функций HTTP/1.1, включая HTTP Continue, Chunked-Encoding, WebSockets и SSE. Подробнее о SslProxy читайте [здесь](https://docs.sisk-framework.org/ru/docs/extensions/ssl-proxy.md). --- # 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 или Sisk.SslProxy) и различающиеся характеристики производительности. Cadente призван решить эти проблемы, предоставляя полностью управляемый HTTP/1.1-сервер, написанный на C#. Его основные цели: - **Родная поддержка SSL:** Работает на всех платформах без необходимости внешних прокси или сложной конфигурации. - **Кроссплатформенная согласованность:** Идентичное поведение на Windows, Linux и macOS. - **Производительность:** Разработан как высокопроизводительная альтернатива управляемому `HttpListener`. - **Независимость:** Отделен от `System.Net.HttpListener`, изолируя Sisk от потенциальных будущих устареваний или отсутствия поддержки этого компонента в .NET. > [!WARNING] > **Экспериментальный статус** > > Cadente в настоящее время находится на экспериментальной стадии (Бета). Он еще не рекомендуется для критических производственных сред. API и поведение могут измениться. ## Установка Cadente доступен как отдельный пакет. Чтобы использовать его с Sisk, вам нужен пакет `Sisk.Cadente.CoreEngine`. ```bash dotnet add package Sisk.Cadente.CoreEngine --prerelease ``` ## Использование с 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(); ``` ### Расширенная конфигурация Вы можете настроить базовый экземпляр `HttpHost`, передав действие настройки в конструктор `CadenteHttpServerEngine`. Это полезно для настройки таймаутов или других низкоуровневых настроек. ```csharp using var engine = new CadenteHttpServerEngine(host => { // Настройка таймаутов чтения/записи клиента host.TimeoutManager.ClientReadTimeout = TimeSpan.FromSeconds(30); host.TimeoutManager.ClientWriteTimeout = TimeSpan.FromSeconds(30); }); ``` ## Самостоятельное использование Хотя в основном предназначен для Sisk, Cadente можно использовать как самостоятельный 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 writer = new StreamWriter(context.Response.GetResponseStream()); await writer.WriteLineAsync("Привет, мир!"); } } ``` --- # Настройка резервирования пространств имён в 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). --- # Часто задаваемые вопросы 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 [Router](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.md) — первый шаг в построении сервера. Он отвечает за хранение объектов [Route](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.md), которые являются конечными точками, сопоставляющими URL‑адреса и их методы с действиями, выполняемыми сервером. Каждое действие отвечает за получение запроса и отправку ответа клиенту. Маршруты представляют собой пары выражений пути («шаблон пути») и HTTP‑метода, которые они могут обрабатывать. Когда к серверу поступает запрос, он пытается найти маршрут, соответствующий полученному запросу, затем вызывает действие этого маршрута и отправляет полученный ответ клиенту. В Sisk существует несколько способов определения маршрутов: они могут быть статическими, динамическими или автоматически сканируемыми, задаваться атрибутами или напрямую в объекте Router. ```cs Router mainRouter = new Router(); // сопоставляет GET / с следующим действием mainRouter.MapGet("/", request => { return new HttpResponse("Hello, world!"); }); ``` Чтобы понять, что может делать маршрут, нужно понять, что может делать запрос. [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) содержит всё необходимое. Sisk также включает дополнительные возможности, ускоряющие общую разработку. Для каждого действия, полученного сервером, будет вызван делегат типа [RouteAction](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteAction.md). Этот делегат принимает параметр, содержащий [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) со всей необходимой информацией о запросе, полученном сервером. Объект, возвращаемый этим делегатом, должен быть [HttpResponse](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpResponse.md) или объектом, который к нему неявно преобразуется через [implicit response types](https://docs.sisk-framework.org/ru/docs/fundamentals/responses.md#implicit-response-types). ## Сопоставление маршрутов Когда HTTP‑сервер получает запрос, Sisk ищет маршрут, удовлетворяющий выражению пути, полученного в запросе. Выражение всегда сравнивается между маршрутом и путём запроса без учёта строки запроса. Этот тест не имеет приоритета и является эксклюзивным для одного маршрута. Если ни один маршрут не совпадает с запросом, возвращается ответ [Router.NotFoundErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.NotFoundErrorHandler.md). Если шаблон пути совпадает, но HTTP‑метод не совпадает, отправляется ответ [Router.MethodNotAllowedErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MethodNotAllowedErrorHandler.md). Sisk проверяет возможность конфликтов маршрутов, чтобы избежать этих проблем. При определении маршрутов Sisk ищет потенциальные маршруты, которые могут конфликтовать с определяемым маршрутом. Этот тест включает проверку пути и метода, которые маршрут принимает. ### Создание маршрутов с помощью шаблонов пути Для новых приложений предпочтительнее использовать методы `Map*`. Они делают HTTP‑метод видимым в месте вызова и соответствуют текущему API `Router`. Более старые методы `SetRoute` всё ещё существуют как совместимые обёртки, но новые примеры следует писать с использованием `Map`, `MapGet`, `MapPost`, `MapPut`, `MapDelete`, `MapPatch`, `MapAny`, `MapOptions` или `MapHead`. ```cs // Методы 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(); // пустой 200 OK }); // Map также может принимать экземпляр Route, когда нужны параметры маршрута. mainRouter.Map(Route.Get("/image.png", (request) => { var imageStream = File.OpenRead("image.png"); return new HttpResponse() { // внутренний StreamContent // поток будет освобождён после отправки // ответа. Content = new StreamContent(imageStream) }; })); // несколько параметров mainRouter.MapGet("/hey//surname/", (request) => { string name = request.RouteParameters["name"].GetString(); string surname = request.RouteParameters["surname"].GetString(); return new HttpResponse($"Hello, {name} {surname}!"); }); ``` Свойство [RouteParameters](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RouteParameters.md) объекта HttpRequest содержит всю информацию о переменных пути полученного запроса. Каждый путь, полученный сервером, нормализуется перед выполнением теста шаблона пути согласно следующим правилам: - Все пустые сегменты удаляются из пути, например: `////foo//bar` превращается в `/foo/bar`. - Сопоставление пути **чувствительно к регистру**, если только [Router.MatchRoutesIgnoreCase](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MatchRoutesIgnoreCase.md) не установлен в `true`. Свойства [Query](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Query.md) и [RouteParameters](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RouteParameters.md) объекта [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) возвращают объект [StringValueCollection](https://docs.sisk-framework.org/api/Sisk.Core.Entity.StringValueCollection.md), где каждый индексированный элемент возвращает ненулевой [StringValue](https://docs.sisk-framework.org/api/Sisk.Core.Entity.StringValue.md), который можно использовать как опцию/монад для преобразования его сырого значения в управляемый объект. Ниже пример, читающий параметр маршрута «id» и получающий из него `Guid`. Если параметр не является корректным Guid, генерируется исключение, и клиент получает ошибку 500, если сервер не обрабатывает [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] > Конечный `/` в путях игнорируется как в запросе, так и в маршруте, то есть если вы попытаетесь обратиться к маршруту, определённому как `/index/page`, вы сможете также обратиться к нему как к `/index/page/`. > > Вы также можете принудительно требовать завершающий `/`, включив [HttpServerConfiguration.ForceTrailingSlash](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ForceTrailingSlash.md). ### Создание маршрутов с помощью экземпляров классов Вы также можете определять маршруты динамически с помощью рефлексии и атрибута [RouteAttribute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteAttribute.md). Таким образом, экземпляр класса, методы которого помечены этим атрибутом, получит свои маршруты в целевом роутере. Чтобы метод был определён как маршрут, он должен быть помечен [RouteAttribute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteAttribute.md), например самим атрибутом или [RouteGetAttribute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteGetAttribute.md). Метод может быть статическим, экземплярным, публичным или приватным. Используйте `MapInstance`, когда хотите сопоставить экземплярные и статические методы маршрутов из объекта. Используйте `MapType`, когда хотите сопоставить только статические методы маршрутов из типа. ```cs {title="Controller/MyController.cs"} public class MyController { // будет соответствовать GET / [RouteGet] HttpResponse Index(HttpRequest request) { HttpResponse res = new HttpResponse(); res.Content = new StringContent("Index!"); return res; } // статические методы тоже работают [RouteGet("/hello")] static HttpResponse Hello(HttpRequest request) { HttpResponse res = new HttpResponse(); res.Content = new StringContent("Hello world!"); return res; } } ``` Следующая строка определит оба метода `Index` и `Hello` класса `MyController` как маршруты, поскольку оба помечены как маршруты, и предоставлен экземпляр класса, а не его тип. Если бы был предоставлен тип, определялись бы только статические методы. ```cs var myController = new MyController(); mainRouter.MapInstance(myController); ``` Чтобы сопоставить только статические методы маршрутов из типа, используйте: ```cs mainRouter.MapType(); ``` Начиная с версии Sisk 0.16, можно включить AutoScan, который будет искать пользовательские классы, реализующие `RouterModule`, и автоматически связывать их с роутером. Это не поддерживается при AOT‑компиляции. ```cs mainRouter.AutoScanModules(); ``` Вышеприведённая инструкция будет искать все типы, реализующие `ApiController`, но **не сам тип**. Два необязательных параметра указывают, как метод будет искать эти типы. Первый аргумент задаёт сборку, в которой будет происходить поиск, а второй — способ определения найденных типов. ## Маршруты с регулярными выражениями Вместо использования стандартных методов сопоставления HTTP‑путей вы можете пометить маршрут как интерпретируемый с помощью Regex. ```cs Route indexRoute = new RegexRoute(RouteMethod.Get, @"\/[a-z]+\/", IndexPage); mainRouter.Map(indexRoute); ``` Или с помощью класса [RegexRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RegexRoute.md): ```cs mainRouter.Map(new RegexRoute(RouteMethod.Get, @"\/[a-z]+\/", request => { return new HttpResponse("hello, world"); })); ``` Вы также можете захватывать группы из шаблона regex в содержимое [HttpRequest.RouteParameters](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RouteParameters.md): ```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}"); } } ``` ## Префиксирование маршрутов Вы можете задать префикс для всех маршрутов в классе или модуле с помощью атрибута [RoutePrefix](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RoutePrefixAttribute.md) и указать префикс в виде строки. См. пример ниже, использующий архитектуру BREAD (Browse, Read, Edit, Add, 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() { ... } } ``` В приведённом примере параметр HttpResponse опущен в пользу использования глобального контекста [HttpContext.Current](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.Current.md). Подробнее об этом в следующем разделе. ## Маршруты без параметра запроса Маршруты могут быть определены без параметра [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) и всё равно иметь возможность получать запрос и его компоненты из контекста запроса. Рассмотрим абстракцию `ControllerBase`, служащую основой для всех контроллеров API, которая предоставляет свойство `Request` для получения текущего [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md). ```cs {title="Controller/ControllerBase.cs"} public abstract class ControllerBase { // получает запрос из текущего потока public HttpRequest Request { get => HttpContext.Current.Request; } // строка ниже, при вызове, получает базу данных из текущей HTTP‑сессии, // или создаёт новую, если её нет public DbContext Database { get => HttpContext.Current.RequestBag.GetOrAdd(); } } ``` И чтобы все его наследники могли использовать синтаксис маршрута без параметра запроса: ```cs {title="Controller/UsersController.cs"} [RoutePrefix("/api/users")] public class UsersController : ControllerBase { [RoutePost] public async Task Create() { // читает JSON‑данные из текущего запроса UserCreationDto? user = await Request.GetJsonContentAsync(); ... Database.Users.Add(user); return new HttpResponse(201); } } ``` Больше деталей о текущем контексте и внедрении зависимостей можно найти в руководстве [dependency injection](https://docs.sisk-framework.org/ru/docs/features/instancing.md). ## Маршруты любого метода Вы можете определить маршрут, который будет совпадать только по пути, игнорируя HTTP‑метод. Это может быть полезно, если вы хотите выполнять проверку метода внутри обратного вызова маршрута. ```cs // будет соответствовать / на любом HTTP‑методе mainRouter.MapAny("/", callbackFunction); ``` ## Маршруты любого пути Маршруты любого пути проверяют любой путь, полученный HTTP‑сервером, с учётом проверяемого метода маршрута. Если метод маршрута `RouteMethod.Any` и путь использует [Route.AnyPath](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.AnyPath.md), такой маршрут будет слушать все запросы сервера, и другие маршруты определять нельзя. ```cs // следующий маршрут будет соответствовать всем POST‑запросам mainRouter.Map(RouteMethod.Post, Route.AnyPath, callbackFunction); ``` ## Игнорирование регистра при сопоставлении маршрутов По умолчанию сопоставление маршрутов с запросами чувствительно к регистру. Чтобы игнорировать регистр, включите эту опцию: ```cs mainRouter.MatchRoutesIgnoreCase = true; ``` Это также включит опцию `RegexOptions.IgnoreCase` для маршрутов, использующих сопоставление через регулярные выражения. ## Обработчик обратного вызова «Не найдено» (404) Вы можете создать пользовательский обратный вызов для случая, когда запрос не совпадает ни с одним известным маршрутом. ```cs mainRouter.NotFoundErrorHandler = () => { return new HttpResponse(404) { // Начиная с v0.14 Content = new HtmlContent("

Not found

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

Not found

", Encoding.UTF8, "text/html") }; }; ``` ## Обработчик обратного вызова «Метод не разрешён» (405) Вы также можете создать пользовательский обратный вызов для случая, когда запрос совпадает по пути, но не совпадает по методу. ```cs mainRouter.MethodNotAllowedErrorHandler = (context) => { return new HttpResponse(405) { Content = new StringContent($"Method not allowed for this route.") }; }; ``` ## Обработка ошибок Исключения могут возникать в течение жизненного цикла запроса, начиная от обработчика предвыполнения, через действие роутера, до обработчиков поствыполнения и обработчиков значений. Эти исключения управляются следующим механизмом: - Если [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) установлен в `true`, исключения будут выбрасываться обычным образом и не будут перехвачены Sisk, и HTTP‑сервер может быть прерван, если исключение не будет поймано. - Если [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) установлен в `false`, исключения будут перехвачены и обработаны Sisk. После этого, если определён `Router.CallbackErrorHandler`, он будет вызван с перехваченным исключением и контекстом запроса, и **не будет** передан в стандартный вывод ошибок. Если `Router.CallbackErrorHandler` не определён, исключение будет передано в стандартный вывод ошибок, и клиент получит ответ HTTP 500. Если стандартный вывод ошибок не определён, ошибка будет тихо проигнорирована. Примечание: внутри `Router.CallbackErrorHandler` вы можете задать режим логирования для ошибок, доступа, обоих или ни одного, а также изменить поведение записи в журнал по умолчанию: ```csharp router.CallbackErrorHandler = (ex, ctx) => { ctx.LogMode = LogOutput.Both; // переопределить режим логирования, чтобы записывать ошибку и в журнал доступа, и в журнал ошибок } ``` ## Внутренний обработчик ошибок Обратные вызовы маршрутов могут бросать ошибки во время выполнения сервера. Если их не обработать корректно, общая работа HTTP‑сервера может быть прервана. У роутера есть обратный вызов для случая, когда обратный вызов маршрута завершился ошибкой и предотвращает прерывание сервиса. Этот метод доступен только когда [ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) установлен в `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`, это указывает, что запрос должен продолжаться, и следует вызвать следующий объект, либо цикл завершится ответом роутера. Если вы наследуете встроенный класс [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(), // обработчик до запроса new ValidateJsonContentRequestHandler(), // обработчик до запроса // -- метод IndexPage будет выполнен здесь new WriteToLogRequestHandler() // обработчик после запроса }); ``` Или создание объекта [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`, с fallback к текущей культуре. | | [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) (указывает, что сервер полностью получил содержимое от удалённого узла). Повторно читать содержимое запроса через `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), клиент получает ответ с кодом 413 Content Too Large. Кроме того, если ограничение не задано или слишком велико, сервер бросит [OutOfMemoryException](https://learn.microsoft.com/en-us/dotnet/api/system.outofmemoryexception?view=net-8.0), когда размер содержимого, отправленного клиентом, превысит [Int32.MaxValue](https://learn.microsoft.com/en-us/dotnet/api/system.int32.maxvalue) (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; ``` ### Request Bag Объект [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` в RequestBag, откуда его можно будет получить позже в финальном обратном вызове: ```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(); ... } ``` ## Получение данных формы Данные формы можно получить в виде [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) { // следующий метод читает весь входной поток запроса // в массив MultipartObjects 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 form objects](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) => { // получаем токен разрыва соединения из запроса 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/ru/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/ru/Web/API/EventSource/close) не был вызван на клиенте после получения всех пакетов, что приводит к бесконечной дополнительной обработке на сервере. Чтобы избежать такой проблемы, обычно отправляют финальный пакет, указывающий, что источник событий завершил передачу. Ниже пример того, как браузер может взаимодействовать с сервером, поддерживающим Server‑side events. ```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 или стратегию потокового ответа для асинхронных последовательностей. --- # Logging 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`, гарантируя, что все ожидающие записи будут записаны до закрытия потока. В этой статье мы покажем, как настроить журналирование для вашего приложения. ## File based access logs Журналы в файлы открывают файл, записывают строку текста, а затем закрывают файл для каждой записанной строки. Такая процедура была принята для поддержания отзывчивости записи в журналах. ```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 создаёт его автоматически. ## Stream based logging Вы можете записывать журналы в объекты `TextWriter`, такие как `Console.Out`, передавая объект `TextWriter` в конструктор: ```cs {title="Program.cs"} using var app = HttpServer.CreateBuilder() .UseConfiguration(config => { config.AccessLogsStream = new LogStream(Console.Out); }) .Build(); ``` Для каждого сообщения, записываемого в потоковый журнал, вызывается метод `TextWriter.Flush()`. ## Access log formatting Вы можете настроить формат журнала доступа с помощью предопределённых переменных. Рассмотрим следующую строку: ```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` | Вы также можете использовать `HttpServerConfiguration.DefaultAccessLogFormat`, чтобы применить формат журнала доступа по умолчанию. ## Rotating logs Вы можете настроить 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‑ами. ## Error logging Когда сервер не бросает ошибки в отладчик, он перенаправляет их в журнал, если они есть. Вы можете настроить запись ошибок так: ```cs config.ThrowExceptions = false; config.ErrorsLogsStream = new LogStream("error.log"); ``` Это свойство будет записывать в журнал только те ошибки, которые не перехвачены обратным вызовом или свойством [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md). Записываемая сервером ошибка всегда содержит дату и время, заголовки запроса (не тело), трассировку ошибки и трассировку внутреннего исключения, если они есть. ## Other logging instances Ваше приложение может иметь ноль или несколько LogStream‑ов, ограничений на количество каналов журнала нет. Поэтому возможно направить журнал вашего приложения в файл, отличный от журнала доступа или журнала ошибок по умолчанию. ```cs LogStream appMessages = new LogStream("messages.log"); appMessages.WriteLine("Application started at {0}", DateTime.Now); ``` ## Extending 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 is not 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`. Этот метод не возвращает сообщение, если истек тайм‑аут, операция была отменена или клиент отключился. Одновременно может выполняться только одна операция чтения или записи, поэтому, пока вы ждёте сообщение с помощью `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/ru/Web/HTTP/Guides/CORS) при публикации вашей службы. Эта функция не является частью протокола HTTP, а является специальной функцией веб-браузеров, определенной W3C. Этот механизм безопасности предотвращает отправку запросов веб-страницей на другой домен, чем тот, который предоставил веб-страницу. Поставщик службы может разрешить доступ к своим ресурсам определенным доменам или только одному. ## Same Origin Чтобы ресурс был идентифицирован как "same origin", запрос должен содержать заголовок [Origin](https://developer.mozilla.org/en-US/docs/ru/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/ru/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` - домен khác. - `http://example.com` - схема khác. - `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. ### Wildcards и автоматические заголовки Альтернативно, вы можете использовать wildcard (`*`) в ответе origin, чтобы указать, что любой origin может получить доступ к ресурсу. Однако, это значение не разрешено для запросов, которые имеют учетные данные (заголовки авторизации) и эта операция [вызовет ошибку](https://developer.mozilla.org/en-US/docs/ru/Web/HTTP/Guides/CORS/Errors/CORSNotSupportingCredentials). Вы можете обойти эту проблему, явно перечислив, какие origins будут разрешены через свойство [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 Если вы работаете с [поставщиками служб](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" }; } } ``` ## Предварительные запросы Предварительный запрос - это запрос метода [OPTIONS](https://developer.mozilla.org/en-US/docs/ru/Web/HTTP/Reference/Methods/OPTIONS), который клиент отправляет перед фактическим запросом. Сервер Sisk всегда будет отвечать на запрос с кодом `200 OK` и соответствующими заголовками CORS, и затем клиент может продолжить фактический запрос. Это условие не применяется, когда существует маршрут для запроса с явно настроенным [RouteMethod](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md) для `Options`. ## Глобальное отключение CORS Это невозможно. Чтобы не использовать CORS, не настраивайте его. --- # File Server 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` является самым прямым вариантом для кода приложения. ## 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/): ```bash dotnet add package Sisk.ModelContextProtocol ``` Этот пакет предоставляет полезные классы и методы для построения MCP‑серверов, работающих по протоколу [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http). Текущая реализация поддерживает инструменты версии протокола `2025-06-18`. > [!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) => { // read action name. will throw if null or not a explicit string string actionName = context.Arguments["action_name"].GetString(); // action_data is defined as non-required, so it may be null here string? actionData = context.Arguments["action_data"].MaybeNull()?.GetString(); // Handle the browser action based on the 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") ) })); ``` Провайдер в текущей версии обрабатывает инициализацию, `tools/list`, `tools/call`, `ping` и `notifications/*`. Неподдерживаемые методы JSON‑RPC возвращают ошибочный ответ JSON‑RPC. ## Продолжающаяся работа Протокол контекста модели — это протокол коммуникации для моделей‑агентов и приложений, предоставляющих им контент. Это новый протокол, поэтому его спецификация часто обновляется: появляются устаревания, новые возможности и несовместимые изменения. Важно понять, какие задачи решает [Model Context Protocol](https://modelcontextprotocol.io/docs/ru/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 (); ``` --- # SSL Proxy Source: https://docs.sisk-framework.org/ru/docs/extensions/ssl-proxy.html > [!WARNING] > Эта функция экспериментальная и не должна использоваться в производстве. Пожалуйста, обратитесь к [этому документу](https://docs.sisk-framework.org/ru/docs/deploying.md#proxying-your-application), если вы хотите сделать Sisk работать с SSL. 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/extensions/basic-auth.html Пакет Базовой Аутентификации добавляет обработчик запросов, способный обрабатывать базовую схему аутентификации в вашем приложении Sisk с минимальной конфигурацией и усилиями. Базовая аутентификация HTTP - это минимальная форма аутентификации запросов по идентификатору пользователя и паролю, где сессия контролируется исключительно клиентом, и нет токенов аутентификации или доступа. ![Базовая Аутентификация](https://docs.sisk-framework.org/assets/img/basic-auth.svg) Подробнее о схеме базовой аутентификации можно прочитать в [спецификации MDN](https://developer.mozilla.org/pt-BR/docs/ru/Web/HTTP/Authentication). ## Установка Чтобы начать, установите пакет Sisk.BasicAuth в вашем проекте: > dotnet add package Sisk.BasicAuth Вы можете ознакомиться с другими способами установки в вашем проекте в [репозитории Nuget](https://www.nuget.org/packages/Sisk.BasicAuth/0.15.0). ## Создание обработчика аутентификации Вы можете контролировать схему аутентификации для всего модуля или для отдельных маршрутов. Для этого давайте сначала напишем наш первый базовый обработчик аутентификации. В примере ниже устанавливается соединение с базой данных, проверяется, существует ли пользователь и действителен ли пароль, и после этого пользователь сохраняется в контексте. ```cs public class UserAuthHandler : BasicAuthenticateRequestHandler { public UserAuthHandler() : base() { Realm = "Чтобы войти на эту страницу, пожалуйста, введите ваши учетные данные."; } public override HttpResponse? OnValidating(BasicAuthenticationCredentials credentials, HttpContext context) { DbContext db = new DbContext(); // в данном случае мы используем электронную почту в качестве поля идентификатора пользователя, поэтому мы // ищем пользователя по его электронной почте. User? user = db.Users.FirstOrDefault(u => u.Email == credentials.UserId); if (user == null) { return base.CreateUnauthorizedResponse("Извините! Пользователь с таким электронным адресом не найден."); } // проверяет, что пароль учетных данных действителен для этого пользователя. if (!user.ValidatePassword(credentials.Password)) { return base.CreateUnauthorizedResponse("Недействительные учетные данные."); } // добавляет вошедшего пользователя в контекст 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 $"Привет, {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 $"Привет, {loggedUser.Name}!"; } } ``` ## Примечания Основная ответственность базовой аутентификации лежит на клиентской стороне. Хранение, контроль кэша и шифрование обрабатываются локально на клиенте. Сервер только получает учетные данные и проверяет, разрешен ли доступ или нет. Обратите внимание, что этот метод не является одним из самых безопасных, поскольку он возлагает значительную ответственность на клиента, которую может быть трудно отслеживать и поддерживать безопасность его учетных данных. Кроме того, важно передавать пароли в защищенном контексте соединения (SSL), поскольку они не имеют встроенного шифрования. Короткий перехват в заголовках запроса может раскрыть учетные данные доступа вашего пользователя. Выбирайте более надежные решения аутентификации для приложений в производстве и избегайте использования слишком многих готовых компонентов, поскольку они могут не адаптироваться к потребностям вашего проекта и в конечном итоге подвергать его риску безопасности. --- # Поставщики услуг 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": "Мое приложение Sisk", "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 и т. д. ## Структура файла конфигурации Файл конфигурации JSON состоит из следующих свойств:
    Свойство Обязательное Описание
    Server Требуется Представляет сам сервер с его настройками.
    Server.AccessLogsStream Необязательно По умолчанию console. Указывает поток вывода журнала доступа. Может быть именем файла, null или console.
    Server.ErrorsLogsStream Необязательно По умолчанию null. Указывает поток вывода журнала ошибок. Может быть именем файла, null или console.
    Server.MaximumContentLength Необязательно
    Server.MaximumContentLength Необязательно По умолчанию 0. Указывает максимальную длину содержимого в байтах. Ноль означает бесконечность.
    Server.IncludeRequestIdHeader Необязательно По умолчанию false. Указывает, должен ли HTTP-сервер отправлять заголовок X-Request-Id.
    Server.ThrowExceptions Необязательно По умолчанию true. Указывает, должны ли быть выброшены необработанные исключения. Установите в false при производстве и true при отладке.
    ListeningHost Требуется Представляет хост, на котором слушает сервер.
    ListeningHost.Label Необязательно Представляет метку приложения.
    ListeningHost.Ports Требуется Представляет массив строк, соответствующих синтаксису ListeningPort.
    ListeningHost.CrossOriginResourceSharingPolicy Необязательно Настройка заголовков CORS для приложения.
    ListeningHost.CrossOriginResourceSharingPolicy.AllowCredentials Необязательно По умолчанию false. Указывает заголовок Allow-Credentials.
    ListeningHost.CrossOriginResourceSharingPolicy.ExposeHeaders Необязательно По умолчанию null. Это свойство ожидает массив строк. Указывает заголовок Expose-Headers.
    ListeningHost.CrossOriginResourceSharingPolicy.AllowOrigin Необязательно По умолчанию null. Это свойство ожидает строку. Указывает заголовок Allow-Origin.
    ListeningHost.CrossOriginResourceSharingPolicy.AllowOrigins Необязательно По умолчанию null. Это свойство ожидает массив строк. Указывает несколько заголовков Allow-Origin. См. AllowOrigins для получения дополнительной информации.
    ListeningHost.CrossOriginResourceSharingPolicy.AllowMethods Необязательно По умолчанию null. Это свойство ожидает массив строк. Указывает заголовок Allow-Methods.
    ListeningHost.CrossOriginResourceSharingPolicy.AllowHeaders Необязательно По умолчанию null. Это свойство ожидает массив строк. Указывает заголовок Allow-Headers.
    ListeningHost.CrossOriginResourceSharingPolicy.MaxAge Необязательно По умолчанию null. Это свойство ожидает целое число. Указывает заголовок Max-Age в секундах.
    ListeningHost.Parameters Необязательно Указывает свойства, предоставляемые методу настройки приложения.
    --- # Провайдер конфигурации 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] # Множественные адреса прослушивания поддерживаются 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 Текущая реализация вкуса: - Имена свойств и секций **не чувствительны к регистру**. - Имена свойств и значения **обрезаются**, если значения не заключены в кавычки. - Значения можно заключать в одинарные или двойные кавычки. Кавычки могут содержать переносы строк внутри себя. - Комментарии поддерживаются с помощью `#` и `;`. Также **допускаются комментарии в конце строки**. - Свойства могут иметь несколько значений. В подробностях документация для "вкуса" парсера INI, используемого в Sisk, доступна [в этом документе](https://github.com/sisk-http/archive/blob/master/ext/ini-reader-syntax.md). Используя следующий код INI в качестве примера: ```ini One = 1 Value = это значение Another value = "это значение имеет перенос строки" ; код ниже имеет некоторые цвета [some section] Color = Red Color = Blue Color = 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` | Да | Адреса/порт прослушивания сервера. | | `Server.Encoding` | Нет | Кодировка сервера по умолчанию. | | `Server.MaximumContentLength` | Нет | Максимальный размер контента в байтах. | | `Server.IncludeRequestIdHeader` | Нет | Указывает, должен ли HTTP-сервер отправлять заголовок X-Request-Id. | | `Server.ThrowExceptions` | Нет | Указывает, должны ли быть выброшены необработанные исключения. | | `Server.AccessLogsStream` | Нет | Указывает поток вывода журнала доступа. | | `Server.ErrorsLogsStream` | Нет | Указывает поток вывода журнала ошибок. | | `Cors.AllowMethods` | Нет | Указывает значение заголовка CORS Allow-Methods. | | `Cors.AllowHeaders` | Нет | Указывает значение заголовка CORS Allow-Headers. | | `Cors.AllowOrigins` | Нет | Указывает несколько заголовков Allow-Origin, разделенных запятыми. [AllowOrigins](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigins.md) для более подробной информации. | | `Cors.AllowOrigin` | Нет | Указывает один заголовок Allow-Origin. | | `Cors.ExposeHeaders` | Нет | Указывает значение заголовка CORS Expose-Headers. | | `Cors.AllowCredentials` | Нет | Указывает значение заголовка CORS Allow-Credentials. | | `Cors.MaxAge` | Нет | Указывает значение заголовка CORS Max-Age. --- # Документация API Source: https://docs.sisk-framework.org/ru/docs/extensions/api-documentation.html Расширение `Sisk.Documenting` позволяет автоматически генерировать документацию API для вашего приложения Sisk. Оно использует структуру вашего кода и атрибуты для создания полноценного сайта документации, поддерживая экспорт в формат Open API (Swagger). > [!WARNING] > Этот пакет находится в разработке и ещё не опубликован. Его поведение и API могут измениться в будущих обновлениях. Поскольку пакет ещё недоступен в NuGet, вам необходимо включить исходный код напрямую в ваш проект или добавить его как зависимость проекта. Исходный код можно найти [здесь](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, required in constructor): Имя API‑конечной точки. * **Description** (string): Краткое описание того, что делает конечная точка. * **Group** (string): Позволяет группировать конечные точки (например, по контроллеру или модулю). * **InheritDescriptionFromXmlDocumentation** (bool, default: `true`): Если `true`, пытается использовать сводку XML‑документации метода, если `Description` не задано. ### `ApiHeader` Документирует конкретный HTTP‑заголовок, который ожидает или использует конечная точка. * **HeaderName** (string, required in constructor): Ключ заголовка (например, `"Authorization"`). * **Description** (string): Описывает назначение заголовка. * **IsRequired** (bool): Указывает, обязателен ли заголовок для запроса. ### `ApiParameter` Определяет общий параметр для конечной точки, часто используемый для полей формы или параметров тела, не покрытых другими атрибутами. * **Name** (string, required in constructor): Имя параметра. * **TypeName** (string, required in constructor): Тип данных параметра (например, `"string"`, `"int"`). * **Description** (string): Описание параметра. * **IsRequired** (bool): Указывает, обязателен ли параметр. ### `ApiParametersFrom` Автоматически генерирует документацию параметров из свойств указанного класса или типа. * **Type** (Type, required in constructor): Класс `Type`, свойства которого следует отразить. ### `ApiPathParameter` Документирует переменную пути (например, в `/users/{id}`). * **Name** (string, required in constructor): Имя параметра пути. * **Description** (string): Описывает, что представляет собой параметр. * **Type** (string): Ожидаемый тип данных. ### `ApiQueryParameter` Документирует параметр строки запроса (например, `?page=1`). * **Name** (string, required in constructor): Ключ параметра запроса. * **Description** (string): Описание параметра. * **Type** (string): Ожидаемый тип данных. * **IsRequired** (bool): Указывает, должен ли параметр присутствовать. ### `ApiRequest` Описывает ожидаемое тело запроса. * **Description** (string, required in constructor): Описание тела запроса. * **Example** (string): Необработанная строка с примером тела запроса. * **ExampleLanguage** (string): Язык примера (например, `"json"`, `"xml"`). * **PayloadType** (Type): Если задан, пример и схема будут сгенерированы автоматически из этого типа, когда поддерживаемые обработчики контекста позволяют это. ### `ApiResponse` Описывает возможный ответ от конечной точки. * **StatusCode** (HttpStatusCode, required in constructor): Возвращаемый 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) { // Generate XML string for the type 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" } ``` ### Создание собственного экспортёра Вы можете создать собственный экспортёр, реализовав интерфейс `IApiDocumentationExporter`. Это позволит выводить документацию в форматах, таких как Markdown, HTML, коллекция Postman или любой другой пользовательский формат. Интерфейс требует реализации единственного метода: `ExportDocumentationContent`. ```csharp using Sisk.Core.Http; using Sisk.Documenting; public class MyCustomExporter : IApiDocumentationExporter { public HttpContent ExportDocumentationContent(ApiDocumentation documentation) { // 1. Process the documentation object 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. Return the content as an 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/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(@"

    Привет, мир!

    ") }; return indexResponse; } ``` Следующий шаг — связать этот метод с HTTP‑маршрутом. ## Routers Маршрутизаторы — это абстракции маршрутов запросов и служат мостом между запросами и ответами сервиса. Маршрутизаторы управляют маршрутами сервиса, функциями и ошибками. Маршрутизатор может иметь несколько маршрутов, и каждый маршрут может выполнять разные операции по этому пути, такие как выполнение функции, отдача страницы или предоставление ресурса с сервера. Создадим наш первый маршрутизатор и свяжем метод `IndexPage` с индексным путём. ```csharp Router mainRouter = new Router(); mainRouter.MapGet("/", IndexPage); ``` Теперь наш маршрутизатор может принимать запросы и отправлять ответы. Однако `mainRouter` не привязан к хосту или серверу, поэтому он не будет работать сам по себе. Следующий шаг — создать наш ListeningHost. ## Listening Hosts and Ports Объект [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‑сервер будет слушать указанные конечные точки и перенаправлять запросы к нашему маршрутизатору. ## Server Configuration Конфигурация сервера отвечает за большую часть поведения самого 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` | Вы хотите скрыть или показать заголовок Sisk `X-Powered-By`. | Отключите его для более строгих политик заголовков в продакшене. | | [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) | `0` | Тела запросов нуждаются в ограничении размера. | `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‑запроса. - **Receiving the request:** каждый запрос создает 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) только если он больше нуля. - Если запрос отправляет `Content-Length`, превышающий настроенный, клиенту возвращается ответ 413 Payload Too Large, а в HTTP‑контекст записывается статус `HttpServerExecutionStatus = ContentTooLarge`. - Событие `OnHttpRequestOpen` вызывается для всех настроенных обработчиков HTTP‑сервера. - **Routing the action:** сервер вызывает роутер для полученного запроса. - Если роутер не находит маршрут, соответствующий запросу: - Если свойство [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. - **Processing the response:** когда ответ готов, сервер подготавливает его к отправке клиенту. - Заголовки политики 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 server handlers Source: https://docs.sisk-framework.org/ru/docs/advanced/http-server-handlers.html В версии Sisk 0.16 мы представили класс `HttpServerHandler`, который предназначен для расширения общего поведения Sisk и предоставления дополнительных обработчиков событий, таких как обработка HTTP‑запросов, маршрутизаторов, контекстных мешков и многое другое. Класс концентрирует события, происходящие в течение жизни всего HTTP‑сервера, а также отдельного запроса. Протокол HTTP не имеет сессий, поэтому невозможно сохранять информацию от одного запроса к другому. Сейчас Sisk предоставляет способ реализовать сессии, контексты, соединения с базой данных и другие полезные провайдеры, помогающие в работе. Смотрите [эту страницу](https://docs.sisk-framework.org/api/Sisk.Core.Http.Handlers.HttpServerHandler.md), чтобы узнать, где каждый событие вызывается и какова его цель. Вы также можете посмотреть [жизненный цикл HTTP‑запроса](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 })); } } ``` Разработчики могут реализовывать сессии, контексты и соединения с базой данных, используя этот класс. Приведённый пример демонстрирует практическое применение `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).