# Sisk Framework documentation > Sisk is a lightweight, agnostic and robust .NET web development framework. This file contains the complete Sisk documentation (English). 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/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/docs/fundamentals/routing.md) | A path router that supports prefixes, custom methods, path variables, value converters and more. | | [Request Handlers](https://docs.sisk-framework.org/docs/fundamentals/request-handlers.md) | Also known as *middlewares*, provides an interface to build your own request-handlers that work with the request before or after an action. | | [Compression](https://docs.sisk-framework.org/docs/fundamentals/responses.md#gzip-deflate-and-brotli-compression) | Compress your response contents easily with Sisk. | | [Web sockets](https://docs.sisk-framework.org/docs/features/websockets.md) | Provides routes that accept complete web-sockets, for reading and writing to the client. | | [Server-sent events](https://docs.sisk-framework.org/docs/features/server-sent-events.md) | Provides the sending of server events to clients that support the SSE protocol. | | [Logging](https://docs.sisk-framework.org/docs/features/logging.md) | Simplified logging. Log errors, access, define rotating logs by size, multiple output streams for the same log, and more. | | [Multi-host](https://docs.sisk-framework.org/docs/advanced/multi-host-setup.md) | Have an HTTP server for multiple ports, and each port with its own router, and each router with its own application. | | [Server handlers](https://docs.sisk-framework.org/docs/advanced/http-server-handlers.md) | Extend your own implementation of the HTTP server. Customize with extensions, improvements, and new features. | ## 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] > You may be interested in a quickstart project. Check [this repository](https://github.com/sisk-http/quickstart) for more information. ## 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/docs/advanced/manual-setup.md) of the documentation, which explains the behavior and relationships between the HttpServer, Router, ListeningPort, and other components. --- # Installing Source: https://docs.sisk-framework.org/docs/installing.html You can install Sisk through Nuget, dotnet cli or [another options](https://www.nuget.org/packages/Sisk.HttpServer/). You can easily setup your Sisk environment by running this command in your developer console: ```sh dotnet add package Sisk.HttpServer ``` This command will install the latest version of Sisk in your project. Sisk 1.7 targets .NET 8, .NET 9 and .NET 10. The [Cadente engine](https://docs.sisk-framework.org/docs/cadente.md) packages target .NET 9 and .NET 10 only. --- # Native AOT Support Source: https://docs.sisk-framework.org/docs/native-aot.html [.NET Native AOT](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/) allows the publication of native .NET applications that are self-sufficient and do not require the .NET runtime installed on the target host. Additionally, Native AOT provides benefits such as: - Much smaller applications - Significantly faster initialization - Lower memory consumption Sisk Framework, by its explicit nature, allows the use of Native AOT for almost all it's features without requiring rework on the source code to adapt it to Native AOT. ## Not supported features However, Sisk does use reflection, albeit minimal, for some features. The features mentioned below may be partially available or entirely unavailable during native code execution: - [Auto-scanning of modules](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.AutoScanModules.md) of the router: this resource scans the types embedded in the executing Assembly and registers the types that are [router modules](https://docs.sisk-framework.org/docs/fundamentals/routing.md). This resource requires types that can be excluded during assembly trimming. All other features are compatible with AOT in Sisk. It is common to find one or another method that gives an AOT warning, but the same, if not mentioned here, has an overload that indicates the passing of a type, parameter, or type information that assists the AOT compiler in compiling the object. --- # Deploying your Sisk Application Source: https://docs.sisk-framework.org/docs/deploying.html The process of deploying a Sisk application consists of publishing your project into production. Although the process is relatively simple, it is worth noting details that can be lethal to the security and stability of the deployment's infrastructure. Ideally, you should be ready to deploy your application to the cloud, after carrying out all possible tests to have your application ready. ## Publishing your app Publishing your Sisk application or service is generating binaries ready and optimized for production. In this example, we will compile the binaries for production to run on a machine that has the .NET Runtime installed on the machine. You will need .NET SDK installed in your machine in order to build your app, and .NET Runtime installed on the target server to run your app. You can learn how to install .NET Runtime in your Linux server [here](https://learn.microsoft.com/en-us/dotnet/core/install/linux), [Windows](https://learn.microsoft.com/en-us/dotnet/core/install/windows?tabs=net70) and [Mac OS](https://learn.microsoft.com/en-us/dotnet/core/install/macos). In the folder where your project is located, open a terminal and use the .NET publish command: ```shell $ dotnet publish -r linux-x64 -c Release ``` This will generate your binaries inside `bin/Release/publish/linux-x64`. > [!NOTE] > If your app uses a [portable configuration file](https://docs.sisk-framework.org/docs/extensions/service-providers.md), you should copy your `service-config.json` into your host server along all binaries generated by `dotnet publish`. > You can leave the file preconfigured, with environment variables, listening ports and hosts, and additional server configurations. The next step is to take these files to the server where your application will be hosted. After that, give execution permissions to your binary file. In this case, let's consider that our project name is "my-app": ```shell $ cd /home/htdocs $ chmod +x my-app $ ./my-app ``` After running your application, check to see if it produces any error messages. If it didn't produce, it's because your application is running. At this point, it will probably not be possible to access your application by external net ouside your server, as access rules such as Firewall have not been configured. We will consider this in the next steps. You should have the address of the virtual host where your application is listening to. This is set manually in the application, and depends on how you are instantiating your Sisk service. If you're **not** using a portable configuration file, you should find it where you defined your HttpServer instance: ```cs HttpServer server = HttpServer.Emit(5000, out HttpServerConfiguration config, out var host, out var router); // sisk should listen on http://localhost:5000/ ``` Associating an ListeningHost manually: ```cs config.ListeningHosts.Add(new ListeningHost("https://localhost:5000/", router)); ``` Or if you're using a portable configuration file, in your service-config.json: ```json { "Server": { }, "ListeningHost": { "Ports": [ "http://localhost:5000/" ] } } ``` From this, we can create a reverse proxy to listen to your service and make the traffic available over the open network. ## Proxying your application Proxying your service means not directly exposing your Sisk service to an external network. This practice is very common for server deployments because: - Allows you to associate an SSL certificate in your application; - Create access rules before accessing the service and avoid overloads; - Control bandwidth and request limits; - Separate load-balancers for your application; - Prevent security damage to failing infrastructure. You can serve your application through a reverse proxy like [Nginx](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/linux-nginx?view=aspnetcore-7.0&tabs=linux-ubuntu#install-nginx) or [Apache](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/linux-apache?view=aspnetcore-7.0), or you can use an http-over-dns tunnel like [Cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/install-and-setup/tunnel-guide/). Also, remember to correctly resolve your proxy's forwarding headers to obtain your client's information, such as IP address and host, through [forwarding resolvers](https://docs.sisk-framework.org/docs/advanced/forwarding-resolvers.md). The next step after creating your tunnel, firewall configuration and having your application running, is to create a service for your application. > [!NOTE] > Using SSL certificates directly in the Sisk service on non-Windows systems is not possible. This is a point of the implementation of HttpListener, which is the central module for how HTTP queue management is done in Sisk, and this implementation varies from operating system to operating system. You can use SSL in your Sisk service if you [associate a certificate with the virtual host with IIS](https://learn.microsoft.com/en-us/iis/manage/configuring-security/how-to-set-up-ssl-on-iis). For other systems, using a reverse proxy is highly recommended. ## Creating an service Creating a service will make your application always available, even after restarting your server instance or a non-recoverable crash. In this simple tutorial, we will use the content from the previous tutorial as a showcase to keep your service always active. 1. Access the folder where the service configuration files are located: ```sh cd /etc/systemd/system ``` 2. Create your `my-app.service` file and include the contents: ```ini {title="my-app.service"} [Unit] Description= [Service] # set the user which will launch the service on User= # the ExecStart path is not relative to WorkingDirectory. # set it as the full path to the executeable file WorkingDirectory=/home/htdocs ExecStart=/home/htdocs/my-app # set the service to always restart on crash Restart=always RestartSec=3 [Install] WantedBy=multi-user.target ``` 3. Restart your service manager module: ```sh $ sudo systemctl daemon-reload ``` 4. Start your new created service from the name of the file you set and check if they are running: ```sh $ sudo systemctl start my-app $ sudo systemctl status my-app ``` 5. Now if your app is running ("Active: active"), enable your service to keep run after an system reboot: ```sh $ sudo systemctl enable my-app ``` Now you're ready to go and present your Sisk application to everyone. --- # Working with SSL Source: https://docs.sisk-framework.org/docs/ssl.html Working with SSL for development may be necessary when working in contexts that require security, such as most web development scenarios. Sisk operates on top of HttpListener, which does not support native HTTPS, only HTTP. However, there are workarounds that allow you to work with SSL in Sisk. See them below: ## Through the Sisk.Cadente.CoreEngine - Available on: Linux, macOS, Windows - Effort: easy It is possible to use the experimental [**Cadente**](https://docs.sisk-framework.org/docs/cadente.md) engine in Sisk projects, without requiring additional configuration on the computer or in the project. You will need to install the `Sisk.Cadente.CoreEngine` package in your project to be able to use the Cadente server in the Sisk server. To configure SSL, you can use the `UseSsl` and `UseEngine` methods of the builder: ```csharp using var http = HttpServer.CreateBuilder() .UseEngine() .UseSsl(CertificateHelper.CreateTrustedDevelopmentCertificate("localhost")) ``` > Note: this package is still in the experimental phase. ## Through IIS on Windows - Available on: Windows - Effort: medium If you are on Windows, you can use IIS to enable SSL on your HTTP server. For this to work, it is advisable that you follow [this tutorial](https://docs.sisk-framework.org/docs/registering-namespace.md) beforehand if you want your application to be listening on a host other than "localhost." For this to work, you must install IIS through Windows features. IIS is available for free to Windows and Windows Server users. To configure SSL in your application, have the SSL certificate ready, even if it is self-signed. Next, you can see [how to set up SSL on IIS 7 or higher](https://learn.microsoft.com/en-us/iis/manage/configuring-security/how-to-set-up-ssl-on-iis). ## Through mitmproxy - Available on: Linux, macOS, Windows - Effort: easy **mitmproxy** is an interception proxy tool that allows developers and security testers to inspect, modify, and record HTTP and HTTPS traffic between a client (such as a web browser) and a server. You can use the **mitmdump** utility to start a reverse SSL proxy between your client and your Sisk application. 1. Firstly, install [mitmproxy](https://mitmproxy.org/) on your machine. 2. Start your Sisk application. For this example, we'll use port 8000 as the insecure HTTP port. 3. Start the mitmproxy server to listen on the secure port at 8001: ```sh mitmdump --mode reverse:http://localhost:8000/ -p 8001 ``` And you're ready to go! You can already access your application through `https://localhost:8001/`. Your application does not need to be running for you to start `mitmdump`. Alternatively, you can add a reference to the [mitmproxy helper](https://github.com/sisk-http/core/tree/main/extensions/Sisk.Helpers.mitmproxy) in your project. This still requires that mitmproxy is installed on your computer. ## Sisk.SslProxy package (discontinued) > [!CAUTION] > > The Sisk.SslProxy package is discontinued and deprecated on NuGet. It no longer compiles against the current Cadente API. Do not use it: use the native SSL support of the [Cadente engine](#through-the-siskcadentecoreengine) instead. --- # Cadente Source: https://docs.sisk-framework.org/docs/cadente.html Cadente is an experimental managed HTTP/1.1 listener implementation for Sisk. It serves as a replacement for the default `System.Net.HttpListener`, offering greater control and flexibility, especially on non-Windows platforms. ## Overview By default, Sisk uses `HttpListener` (from `System.Net`) as its underlying HTTP server engine. While `HttpListener` is stable and performant on Windows (where it uses the kernel-mode HTTP.sys driver), its implementation on Linux and macOS is managed and historically has had limitations, such as lack of native SSL support (requiring a reverse proxy like Nginx) and varying performance characteristics. Cadente aims to solve these issues by providing a fully managed HTTP/1.1 server written in C#. Its key goals are: - **Native SSL Support:** Works on all platforms without needing external proxies or complex configuration. - **Cross-Platform Consistency:** Identical behavior on Windows, Linux, and macOS. - **Performance:** Designed to be a high-performance alternative to the managed `HttpListener`. - **Independence:** Decoupled from `System.Net.HttpListener`, insulating Sisk from potential future deprecations or lack of maintenance of that component in .NET. > [!WARNING] > **Experimental Status** > > Cadente is currently in an experimental stage (Beta). It is not yet recommended for critical production environments. The API and behavior may change. ## Installation Cadente is available as a separate package. To use it with Sisk, you need the `Sisk.Cadente.CoreEngine` package. ```bash dotnet add package Sisk.Cadente.CoreEngine ``` `Sisk.Cadente` and `Sisk.Cadente.CoreEngine` target .NET 9 and .NET 10. They cannot be used in projects that target .NET 8, even though `Sisk.HttpServer` supports it. ## Using with Sisk To use Cadente as the HTTP engine for your Sisk application, you need to configure the `HttpServer` to use `CadenteHttpServerEngine` instead of the default engine. The `CadenteHttpServerEngine` adapts the Cadente `HttpHost` to the `HttpServerEngine` abstraction required by 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(); ``` When a listening host is configured with SSL, [HttpServer.ListeningPrefixes](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.ListeningPrefixes.md) reports its addresses with the `https` scheme. If a client sends plain HTTP to a port configured with SSL, Cadente answers with a `301 Moved Permanently` redirect to the `https` address of the same host and path. ### Advanced Configuration You can customize the underlying `HttpHost` instance by passing a setup action to the `CadenteHttpServerEngine` constructor. This is useful for configuring timeouts or other low-level settings. ```csharp using var engine = new CadenteHttpServerEngine(host => { // Configure client read/write timeouts host.TimeoutManager.ClientReadTimeout = TimeSpan.FromSeconds(30); host.TimeoutManager.ClientWriteTimeout = TimeSpan.FromSeconds(30); }); ``` ## Standalone Usage Although primarily designed for Sisk, Cadente can be used as a standalone HTTP server (similar to `HttpListener`). ```csharp using Sisk.Cadente; var host = new HttpHost(15000) { Handler = new MyHostHandler() }; host.Start(); Thread.Sleep(-1); class MyHostHandler : HttpHostHandler { public override async Task OnContextCreatedAsync(HttpHost host, HttpHostContext context) { context.Response.StatusCode = 200; using var stream = await context.Response.GetResponseStreamAsync(chunked: true); using var writer = new StreamWriter(stream); await writer.WriteLineAsync("Hello, world!"); } } ``` When the response body is small and already in memory, `WriteInlineContent` and `WriteInlineContentAsync` write the headers and the body in a single operation. The response must have a `Content-Length` header: ```csharp byte[] body = "Hello, world!"u8.ToArray(); context.Response.StatusCode = 200; context.Response.Headers.Set(new HttpHeader("Content-Length", body.Length.ToString())); await context.Response.WriteInlineContentAsync(body); ``` ## Request validation Cadente validates the request framing before it reaches the handler. Since version 1.7, it: - supports pipelined requests on keep-alive connections; - rejects request methods that are not valid HTTP tokens and request targets that contain bytes outside the visible ASCII range (`%x21-7E`); - rejects requests whose `Transfer-Encoding` is anything other than a single `chunked` value, and requests with conflicting `Content-Length` headers; - answers `431 Request Header Fields Too Large` and closes the connection when the request line and headers exceed 8 KB. Invalid requests are answered with an error and the connection is closed, without calling the handler. --- # Configuring namespace reservations on Windows Source: https://docs.sisk-framework.org/docs/registering-namespace.html > [!NOTE] > This configuration is optional and only required when you want Sisk to listen on hosts other than "localhost" on Windows using the HttpListener engine. Sisk works with the HttpListener network interface, which binds a virtual host to the system to listen for requests. On Windows, this binding is a bit restrictive, only allowing localhost to be bound as a valid host. When attempting to listen to another host, an access denied error is thrown on the server. This tutorial explains how to grant authorization to listen on any host you want on the system. ```bat {title="Namespace Setup.bat"} @echo off :: insert prefix here, without spaces or quotes SET PREFIX= SET DOMAIN=%ComputerName%\%USERNAME% netsh http add urlacl url=%PREFIX% user=%DOMAIN% pause ``` Where in `PREFIX`, is the prefix ("Listening Host->Port") that your server will listen to. It must be formatted with the URL scheme, host, port and a slash at the end, example: ```bat {title="Namespace Setup.bat"} SET PREFIX=http://my-application.example.test/ ``` So that you can be listened in your application through: ```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(); } } ``` --- # Changelogs Source: https://docs.sisk-framework.org/docs/changelogs.html Every change made to Sisk is recorded through the changelog. You can view the changelogs for all Sisk versions [here](https://github.com/sisk-http/archive/tree/master/changelogs). ## Migrating from 1.6 to 1.7 Sisk 1.7.0 ships `Sisk.HttpServer`, `Sisk.Cadente`, `Sisk.Cadente.CoreEngine` and `Sisk.IniConfiguration` 1.7.0. `Sisk.Documenting`, `Sisk.Documenting.Html` and `Sisk.Monitoring` ship as 1.7.0-rc3, and `Sisk.ModelContextProtocol` as 1.7.0-rc1. Read the [full 1.7.0 changelog](https://github.com/sisk-http/archive/blob/master/changelogs/core/v1.7/v1.7.0.md) for every change. ### Breaking changes - **`Route.UseRegex` can no longer be set.** The property is obsolete and only reports whether the route is a `RegexRoute`. Create a [RegexRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RegexRoute.md) instead. Route attributes with `UseRegex = true` and the `[RegexRoute]` attribute keep working. See [regex routes](https://docs.sisk-framework.org/docs/fundamentals/routing.md#regex-routes). ```cs // before router.Map(new Route(RouteMethod.Get, @"^/items/(?\d+)$", action) { UseRegex = true }); // after router.Map(new RegexRoute(RouteMethod.Get, @"^/items/(?\d+)$", action)); ``` - **Request bodies are limited to 100 MB by default.** The default value of [HttpServerConfiguration.MaximumContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.MaximumContentLength.md) changed from `0` (unlimited) to 100 MB. The limit also applies to request streams and to WebSocket messages. Set it to `0` to restore the previous behavior. See [getting the request body](https://docs.sisk-framework.org/docs/fundamentals/requests.md#getting-the-request-body). ```cs using var app = HttpServer.CreateBuilder() .UseConfiguration(config => config.MaximumContentLength = 0) .Build(); ``` - **Static routes are tested before dynamic routes.** Regex routes, prefix routes (including file servers) and custom matching routes are tested after every static route, regardless of their registration order. See [matching precedence](https://docs.sisk-framework.org/docs/fundamentals/routing.md#matching-precedence). - **Duplicate route names throw at startup.** Two or more routes with the same `Name` cause an `ArgumentException` when the server starts, while [Router.CheckForRouteCollisions](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CheckForRouteCollisions.md) is enabled (the default). - **File server routes are prefix routes.** `Router.MapFileSystem` and `HttpFileServer.CreateServingRoute` create a [PrefixRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.PrefixRoute.md) instead of a regex route, and are no longer affected by `ForceTrailingSlash`. - **`RouteMethod.Any` includes `QUERY`.** Routes mapped with `MapAny` also answer `QUERY` requests. The numeric value of `RouteMethod.Any` changed, so recompile code that stores or compares it. - **CORS preflight respects `UseCors`.** The automatic `OPTIONS` response no longer sends CORS headers for routes with `UseCors = false`, and routes explicitly mapped for `OPTIONS` handle the request instead. See [preflight requests](https://docs.sisk-framework.org/docs/features/cors.md#preflight-requests). - **Cadente rejects malformed requests.** Invalid request methods and targets, unsupported `Transfer-Encoding` values and conflicting `Content-Length` headers are rejected, and header blocks larger than 8 KB receive `431 Request Header Fields Too Large`. See [Cadente request validation](https://docs.sisk-framework.org/docs/cadente.md#request-validation). ### Discontinued packages `Sisk.BasicAuth`, `Sisk.SslProxy` and `Sisk.ServiceProvider` are no longer maintained and are deprecated on NuGet, with no replacement package. Remove them from your projects: - Replace `Sisk.BasicAuth` with your own [request handler](https://docs.sisk-framework.org/docs/fundamentals/request-handlers.md) that validates the `Authorization` header. - Replace `Sisk.SslProxy` with the native SSL support of the [Cadente engine](https://docs.sisk-framework.org/docs/cadente.md). - Replace `Sisk.ServiceProvider` with the [portable configuration](https://docs.sisk-framework.org/docs/extensions/service-providers.md) built into `Sisk.HttpServer` through `UsePortableConfiguration`. ### New features - [Custom route matching](https://docs.sisk-framework.org/docs/fundamentals/routing.md#custom-route-matching) through `Route.Match`, `RouteMatch`, `Route.IsStatic` and `Route.AllowRewrites`, and the new [prefix routes](https://docs.sisk-framework.org/docs/fundamentals/routing.md#prefix-routes). - Support for the HTTP [QUERY method](https://docs.sisk-framework.org/docs/fundamentals/routing.md#query-method-routes) with `MapQuery`, `Route.Query`, `[RouteQuery]` and the `Accept-Query` header. - [Atomic context values](https://docs.sisk-framework.org/docs/fundamentals/requests.md#atomic-context-values) through `HttpContext.Interlocked`. - The `%{$key}` [access log term](https://docs.sisk-framework.org/docs/features/logging.md#access-log-formatting), which prints values of the request bag, and the [safe file writing helpers](https://docs.sisk-framework.org/docs/features/logging.md#writing-files-safely) `LogStream.SafeWriteToFile` and `LogStream.SafeAppendToFile`. - In Cadente: pipelined keep-alive requests, `WriteInlineContent` and `WriteInlineContentAsync`, and a redirect to HTTPS for plain HTTP requests received on an SSL port. - In the extensions: parameter examples and `LlmsApiExporter.FooterText` in [Sisk.Documenting](https://docs.sisk-framework.org/docs/extensions/api-documentation.md), skills and the `2026-07-28` protocol revision in [Sisk.ModelContextProtocol](https://docs.sisk-framework.org/docs/extensions/mcp.md), and HTTP traffic, latency, route and exception telemetry in the Sisk.Monitoring dashboard. ### Other behavior changes - The `HttpListener` engine dispatches each request to the thread pool, so a slow request no longer delays the next accepted connection. Its `ContextAbortedToken` returns `CancellationToken.None` instead of throwing. - Exceptions thrown by server handlers at the end of a request, and while writing logs, are written to the error log instead of escaping to the HTTP engine. See [exceptions thrown by handlers](https://docs.sisk-framework.org/docs/advanced/http-server-handlers.md#exceptions-thrown-by-handlers). - CORS responses send `Vary: Origin` when the allowed origin depends on the request. - The `%ri` access log term uses the client address resolved at the start of the request. - `Sisk.HttpServer` targets .NET 8, .NET 9 and .NET 10. `Sisk.Cadente` and `Sisk.Cadente.CoreEngine` target .NET 9 and .NET 10 and publish their debug symbols as `.snupkg` packages. --- # Frequently Asked Questions Source: https://docs.sisk-framework.org/docs/faq.html Frequently asked questions about Sisk. ## Is Sisk open-source? Totally. All source code used by Sisk is published and frequently updated on [GitHub](https://github.com/sisk-http). ## Are contributions accepted? As long as they are compatible with the [Sisk philosophy](/), all contributions are very welcome! Contributions don't have to be just code! You can contribute with documentation, tests, translations, donations, and posts, for example. ## Is Sisk funded? No. No organization or project currently sponsors Sisk. ## Can I use Sisk in production? Absolutely. The project has been in development for more than three years and has had intense testing in commercial applications that have been in production since then. Sisk is used in important commercial projects as main infrastructure. A guide on how to [deploy](https://docs.sisk-framework.org/docs/deploying.md) in different systems and environments has been written and is available. ## Does Sisk have authentication, monitoring, and database services? No. Sisk does not have any of these. It's a framework for developing HTTP web applications, but it's still a minimal framework that delivers what's needed for your application to work. You can implement all the services you want using any third-party library you prefer. Sisk was made to be agnostic, flexible, and work with anything. ## Why should I use Sisk instead of ? I don't know. You tell me. Sisk was created to fill a generic scenario for HTTP web applications in .NET. Established projects, such as ASP.NET, solve various problems, but with different biases. Unlike larger frameworks, Sisk requires the user to know what they're doing and building. Basic notions of web development and the HTTP protocol are essential for working with Sisk. Sisk is closer to the Express of Node.js than ASP.NET Core. It's a high-level abstraction that allows you to create applications with HTTP logic that you want. ## What do I need to learn Sisk? You need the basics of: - Web development (HTTP, Restful, etc.) - .NET That's it. Having a notion of what these two topics are, you can dedicate a few hours to developing an advanced application with Sisk. ## Can I develop commercial applications with Sisk? Absolutely. Sisk was created under the MIT license, which means you can use Sisk in any commercial project, commercially or non-commercially, without the need for a proprietary license. What we ask is that somewhere in your application, you have a notice of the open-source projects used in your project, and that Sisk is there. --- # Routing Source: https://docs.sisk-framework.org/docs/fundamentals/routing.html The [Router](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.md) is the first step in building the server. It is responsible for housing [Route](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.md) objects, which are endpoints that map URLs and their methods to actions executed by the server. Each action is responsible for receiving a request and delivering a response to the client. The routes are pairs of path expressions ("path pattern") and the HTTP method that they can listen to. When a request is made to the server, it will attempt to find a route that matches the received request, then it will call the action of that route and deliver the resulting response to the client. There are multiple ways to define routes in Sisk: they can be static, dynamic or auto-scanned, defined by attributes, or directly in the Router object. ```cs Router mainRouter = new Router(); // maps the GET / route into the following action mainRouter.MapGet("/", request => { return new HttpResponse("Hello, world!"); }); ``` To understand what a route is capable of doing, we need to understand what a request is capable of doing. An [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) will contain everything you need. Sisk also includes some extra features that speed up the overral development. For every action received by the server, a delegate of type [RouteAction](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteAction.md) will be called. This delegate contains an parameter holding an [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) with all the necessary information about the request received by the server. The resulting object from this delegate must be an [HttpResponse](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpResponse.md) or an object that maps to it through [implicit response types](https://docs.sisk-framework.org/docs/fundamentals/responses.md#implicit-response-types). ## Matching routes When a request is received by the HTTP server, Sisk searches for a route that satisfies the expression of the path received by the request. The expression is always tested between the route and the request path, without considering the query string. The first route whose path and method match the request handles it. When no route is matched with that request, the [Router.NotFoundErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.NotFoundErrorHandler.md) response is returned to the client. When the path pattern is matched, but the HTTP method is mismatched, the [Router.MethodNotAllowedErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MethodNotAllowedErrorHandler.md) response is sent back to the client. The order in which routes are tested is described in [matching precedence](#matching-precedence). Sisk checks for the possibility of route collisions between static routes. When the server starts, Sisk will look for static routes whose path patterns and methods overlap, and throws an exception if any is found. In the same check, Sisk throws an `ArgumentException` if two or more routes share the same [Route.Name](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.Name.md). Both checks are controlled by [Router.CheckForRouteCollisions](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CheckForRouteCollisions.md), which is enabled by default. ### Matching precedence Every route is either **static** or **dynamic**, as indicated by [Route.IsStatic](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.IsStatic.md): - **Static routes** strictly follow their path pattern, such as `/users/`. Routes created with `Map*` methods, `Route` instances and route attributes are static. - **Dynamic routes** interpret their path in their own way and may match paths that cannot be described by a path pattern. [RegexRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RegexRoute.md), [PrefixRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.PrefixRoute.md) (used by [file servers](https://docs.sisk-framework.org/docs/features/file-server.md)) and [custom matching routes](#custom-route-matching) are dynamic. When the server starts, the router reorders its routes once so that every static route is tested before every dynamic route. Within each group, routes keep their registration order. This means that a static route always takes precedence over a dynamic route, regardless of the order in which they were defined: ```cs // dynamic: serves any GET path from the "wwwroot" directory mainRouter.MapFileSystem("/", "wwwroot"); // static: handles GET /api/status, even though it was defined after the file server mainRouter.MapGet("/api/status", request => new HttpResponse("ok")); ``` In the example above, the file server acts as a fallback for any GET request that is not handled by a static route. The two groups also differ in how they are validated and transformed: | | Static routes | Dynamic routes | |---|---|---| | Matching order | First, in registration order | After all static routes, in registration order | | Route collision check | Yes | No | | [Router.Prefix](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.Prefix.md) is applied | Yes | No | > [!WARNING] > Dynamic routes are not checked for collisions, since Sisk cannot know which paths they match. When two dynamic routes can match the same request, the one defined first wins. For example, a file server mapped to `/` defined before a file server mapped to `/static` makes the second one unreachable. > [!IMPORTANT] > In versions before 1.7, routes were tested only in their registration order, and regex routes could take precedence over static routes defined after them. If your application relies on a regex route handling a path that is also matched by a static route, move that logic into the static route or narrow the static path pattern. ### Creating routes using path patterns For new applications, prefer the `Map*` methods. They keep the HTTP method visible at the call site and match the current `Router` API. The older `SetRoute` methods still exist as compatibility wrappers, but new examples should use `Map`, `MapGet`, `MapPost`, `MapPut`, `MapDelete`, `MapPatch`, `MapAny`, `MapOptions`, or `MapHead`. ```cs // Map* methods are the usual way to define method-specific routes. mainRouter.MapGet("/hey/", (request) => { string name = request.RouteParameters["name"].GetString(); return new HttpResponse($"Hello, {name}"); }); mainRouter.MapPost("/form", (request) => { var formData = request.GetFormContent(); return new HttpResponse(); // empty 200 ok }); // Map can also receive a Route instance when you need route options. mainRouter.Map(Route.Get("/image.png", (request) => { var imageStream = File.OpenRead("image.png"); return new HttpResponse() { // the StreamContent inner // stream is disposed after sending // the response. Content = new StreamContent(imageStream) }; })); // multiple parameters mainRouter.MapGet("/hey//surname/", (request) => { string name = request.RouteParameters["name"].GetString(); string surname = request.RouteParameters["surname"].GetString(); return new HttpResponse($"Hello, {name} {surname}!"); }); ``` The [RouteParameters](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RouteParameters.md) property of HttpRequest contains all the information about the path variables of the received request. Every path received by the server is normalized before the path pattern test is executed, following these rules: - All empty segments are removed from the path, eg: `////foo//bar` becomes `/foo/bar`. - Path matching is **case-sensitive**, unless [Router.MatchRoutesIgnoreCase](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MatchRoutesIgnoreCase.md) is set to `true`. The [Query](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Query.md) and [RouteParameters](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RouteParameters.md) properties of [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) return a [StringValueCollection](https://docs.sisk-framework.org/api/Sisk.Core.Entity.StringValueCollection.md) object, where each indexed property returns a non-null [StringValue](https://docs.sisk-framework.org/api/Sisk.Core.Entity.StringValue.md), which can be used as an option/monad to convert its raw value into a managed object. The example below reads the route parameter "id" and obtains a `Guid` from it. If the parameter is not a valid Guid, an exception is thrown, and a 500 error is returned to the client if the server is not handling [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md). ```cs mainRouter.MapGet("/user/", (request) => { Guid id = request.RouteParameters["id"].GetGuid(); return new HttpResponse($"User id: {id}"); }); ``` > [!NOTE] > Paths have their trailing `/` ignored in both request and route path, that is, if you try to access a route defined as `/index/page` you'll be able to access using `/index/page/` too. > > You can also force URLs to terminate with `/` by enabling [HttpServerConfiguration.ForceTrailingSlash](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ForceTrailingSlash.md). ### Creating routes using class instances You can also define routes dynamically using reflection with the attribute [RouteAttribute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteAttribute.md). This way, the instance of a class in which its methods implement this attribute will have their routes defined in the target router. For a method to be defined as a route, it must be marked with a [RouteAttribute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteAttribute.md), such as the attribute itself or a [RouteGetAttribute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteGetAttribute.md). The method can be static, instance, public, or private. Use `MapInstance` when you want to map instance and static route methods from an object. Use `MapType` when you want to map only static route methods from a type. ```cs {title="Controller/MyController.cs"} public class MyController { // will match GET / [RouteGet] HttpResponse Index(HttpRequest request) { HttpResponse res = new HttpResponse(); res.Content = new StringContent("Index!"); return res; } // static methods works too [RouteGet("/hello")] static HttpResponse Hello(HttpRequest request) { HttpResponse res = new HttpResponse(); res.Content = new StringContent("Hello world!"); return res; } } ``` The line below will define both the `Index` and `Hello` methods of `MyController` as routes, as both are marked as routes, and an instance of the class has been provided, not its type. If its type had been provided instead of an instance, only the static methods would be defined. ```cs var myController = new MyController(); mainRouter.MapInstance(myController); ``` To map only static route methods from a type, use: ```cs mainRouter.MapType(); ``` Since Sisk version 0.16, it is possible to enable AutoScan, which will search for user-defined classes that implement `RouterModule` and will automatically associate it with the router. This is not supported with AOT compilation. ```cs mainRouter.AutoScanModules(); ``` The above instruction will search for all types which implements `ApiController` but **not the type itself**. The two optional parameters indicate how the method will search for these types. The first argument implies the Assembly where the types will be searched and the second indicates the way in which the types will be defined. ## Regex routes Instead of using the default HTTP path matching methods, you can define a route which path is interpreted as a regular expression with the [RegexRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RegexRoute.md) class. Regex routes are [dynamic routes](#matching-precedence). ```cs mainRouter.Map(new RegexRoute(RouteMethod.Get, @"\/[a-z]+\/", request => { return new HttpResponse("hello, world"); })); ``` You can also capture groups from the regex pattern into the [HttpRequest.RouteParameters](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RouteParameters.md) contents: ```cs {title="Controller/MyController.cs"} public class MyController { [RegexRoute(RouteMethod.Get, @"/uploads/(?.*\.(jpeg|jpg|png))")] static HttpResponse RegexRoute(HttpRequest request) { string filename = request.RouteParameters["filename"].GetString(); return new HttpResponse().WithContent($"Acessing file {filename}"); } } ``` > [!NOTE] > The regular expression is not anchored automatically: a pattern such as `/uploads` also matches `/files/uploads/a`. Use `^` and `$` to match the whole path. > [!IMPORTANT] > Since Sisk 1.7, the `Route.UseRegex` property can no longer be set. It is obsolete and only reports whether the route is a [RegexRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RegexRoute.md). The `UseRegex` property of route attributes, such as `[Route(RouteMethod.Get, "...", UseRegex = true)]`, and the `[RegexRoute]` attribute keep working. To migrate code that created regex routes with `Route.UseRegex`, replace the route with a `RegexRoute`: ```cs // before 1.7 mainRouter.Map(new Route(RouteMethod.Get, @"^/items/(?\d+)$", action) { UseRegex = true }); // 1.7 and later mainRouter.Map(new RegexRoute(RouteMethod.Get, @"^/items/(?\d+)$", action)); ``` ## Prefix routes A [PrefixRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.PrefixRoute.md) matches its path and every path nested under it, comparing whole path segments. It is the route used by [Router.MapFileSystem](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MapFileSystem.md). Prefix routes are [dynamic routes](#matching-precedence). ```cs // matches /docs, /docs/ and /docs/any/nested/path, but not /docsfoo mainRouter.Map(new PrefixRoute(RouteMethod.Get, "/docs", null, request => { return new HttpResponse($"Requested {request.Path}"); }, null)); ``` ## Custom route matching You can create your own matching rules by inheriting [Route](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.md) and overriding [Route.Match](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.Match.md). The method receives the request path and the router that is matching the route, and returns a [RouteMatch](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMatch.md) with the result and, optionally, the captured route parameters. The router validates the HTTP method only after `Match` succeeds, so `Match` should only test the path. This keeps the [405 Method Not Allowed](#method-not-allowed-405-callback-handler) and `OPTIONS` responses working for custom routes. ```cs {title="Routing/ExtensionRoute.cs"} // matches any path ending with the given file extension, such as /reports/2024.csv public sealed class ExtensionRoute : Route { public ExtensionRoute(string extension, RouteAction action) : base(RouteMethod.Get, extension, action) { } // this route does not follow a path pattern public override bool IsStatic => false; // the path is not a path pattern, so it cannot be redirected to its trailing-slash form public override bool AllowRewrites => false; public override RouteMatch Match(string requestPath, Router router) { StringComparison comparison = router.MatchRoutesIgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; if (!requestPath.EndsWith(Path, comparison)) return RouteMatch.NotMatched; var parameters = new NameValueCollection { ["file"] = requestPath[..^Path.Length] }; return new RouteMatch(true, parameters); } } ``` ```cs mainRouter.Map(new ExtensionRoute(".csv", request => { string file = request.RouteParameters["file"].GetString(); return new HttpResponse($"Exporting {file} as CSV"); })); ``` When overriding `Match`, keep in mind: - Override [Route.IsStatic](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.IsStatic.md) to return `false` whenever the route does not strictly follow a path pattern. Otherwise, the router treats its `Path` as a path pattern when checking collisions and applying [Router.Prefix](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.Prefix.md), which can produce false collisions or invalid paths. - Override [Route.AllowRewrites](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.AllowRewrites.md) to return `false` when the route path cannot be redirected to its trailing-slash form by [HttpServerConfiguration.ForceTrailingSlash](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ForceTrailingSlash.md). - `Match` is called for every request that reaches the route, possibly from multiple threads at once. Avoid allocations when the route does not match and keep any cached state thread-safe. Return [RouteMatch.NotMatched](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMatch.NotMatched.md) for unsuccessful matches. - Respect [Router.MatchRoutesIgnoreCase](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MatchRoutesIgnoreCase.md) through the `router` parameter. ## Prefixing routes You can prefix all routes in a class or module with the [RoutePrefix](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RoutePrefixAttribute.md) attribute and set the prefix as a string. See the example below using the BREAD architecture (Browse, Read, Edit, Add and Delete): ```cs {title="Controller/Api/UsersController.cs"} [RoutePrefix("/api/users")] public class UsersController { // GET /api/users [RouteGet] public async Task Browse() { ... } // GET /api/users/ [RouteGet("/")] public async Task Read() { ... } // PATCH /api/users/ [RoutePatch("/")] public async Task Edit() { ... } // POST /api/users [RoutePost] public async Task Add() { ... } // DELETE /api/users/ [RouteDelete("/")] public async Task Delete() { ... } } ``` In the above example, the HttpResponse parameter is omitted in favor of being used through the global context [HttpContext.Current](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.Current.md). Read more in the section that follows. ## Routes without request parameter Routes can be defined without the [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) parameter and still be possible to obtain the request and its components in the request context. Let's consider an abstraction `ControllerBase` that serves as a foundation for all controllers of an API, and that abstraction provides the `Request` property to obtain the [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) currently. ```cs {title="Controller/ControllerBase.cs"} public abstract class ControllerBase { // gets the request from the current thread public HttpRequest Request { get => HttpContext.Current.Request; } // the line below, when called, gets the database from the current HTTP session, // or creates a new one if it doesn't exist public DbContext Database { get => HttpContext.Current.RequestBag.GetOrAdd(); } } ``` And for all it's descendants to be able to use the route syntax without the request parameter: ```cs {title="Controller/UsersController.cs"} [RoutePrefix("/api/users")] public class UsersController : ControllerBase { [RoutePost] public async Task Create() { // reads the JSON data from the current request UserCreationDto? user = await Request.GetJsonContentAsync(); ... Database.Users.Add(user); return new HttpResponse(201); } } ``` More details about the current context and dependency injection can be found in the [dependency injection](https://docs.sisk-framework.org/docs/features/instancing.md) tutorial. ## Any method routes You can define a route to be matched only by its path and skip the HTTP method. This can be useful for you to do method validation inside the route callback. ```cs // will match / on any HTTP method mainRouter.MapAny("/", callbackFunction); ``` [RouteMethod.Any](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md) includes `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `QUERY`, `HEAD` and `OPTIONS`. ## QUERY method routes Sisk supports the HTTP `QUERY` method, a safe and idempotent method that carries the query in the request body. Use [Router.MapQuery](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MapQuery.md), [Route.Query](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.Query.md), [RouteMethod.Query](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md) or the [RouteQuery](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteQueryAttribute.md) attribute to define these routes: ```cs mainRouter.MapQuery("/search", request => { string filter = request.Body; return new HttpResponse($"Searching for {filter}"); }); ``` ```cs {title="Controller/SearchController.cs"} public class SearchController { [RouteQuery("/products")] static HttpResponse SearchProducts(HttpRequest request) { return new HttpResponse($"Searching for {request.Body}"); } } ``` The `Accept-Query` header, which advertises the media types accepted in `QUERY` requests, is available through [HttpHeaderCollection.AcceptQuery](https://docs.sisk-framework.org/api/Sisk.Core.Entity.HttpHeaderCollection.AcceptQuery.md) and [HttpKnownHeaderNames.AcceptQuery](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpKnownHeaderNames.AcceptQuery.md). ## Any path routes Any path routes test for any path received by the HTTP server, subject to the route method being tested. If the route method is RouteMethod.Any and the route uses [Route.AnyPath](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.AnyPath.md) in its path expression, this route will listen to all requests from the HTTP server, and no other routes can be defined. ```cs // the following route will match all POST requests mainRouter.Map(RouteMethod.Post, Route.AnyPath, callbackFunction); ``` ## Ignore case route matching By default, the interpretation of routes with requests are case-sensitive. To make it ignore case, enable this option: ```cs mainRouter.MatchRoutesIgnoreCase = true; ``` This will also enable the option `RegexOptions.IgnoreCase` for regex routes and case-insensitive comparison for prefix routes. ## Not Found (404) callback handler You can create a custom callback for when a request doesn't match any known routes. ```cs mainRouter.NotFoundErrorHandler = () => { return new HttpResponse(404) { // Since v0.14 Content = new HtmlContent("

Not found

") // older versions Content = new StringContent("

Not found

", Encoding.UTF8, "text/html") }; }; ``` ## Method not allowed (405) callback handler You can also create a custom callback for when a request matches it's path, but doens't match the method. ```cs mainRouter.MethodNotAllowedErrorHandler = (context) => { return new HttpResponse(405) { Content = new StringContent($"Method not allowed for this route.") }; }; ``` ## Error Handling Exceptions can be thrown within a request lifecycle, which spans from the pre-execution request handler, through the router action, to the post-execution request handlers and value handlers. These exceptions are managed by the mechanism: - If [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) is `true`, exceptions will be thrown normally and will not be caught by Sisk, and the HTTP server may be interrupted if the exception is not caught. - If [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) is `false`, exceptions will be caught and handled by Sisk. After that, if `Router.CallbackErrorHandler` is defined, it will be called with the caught exception and the request context, and it **will not** be forwarded to the standard error output. If `Router.CallbackErrorHandler` is not defined, the exception will be forwarded to the standard error output, and the client will receive an HTTP 500 error response. If the standard error output is not defined, the error will be silently ignored. Note: within `Router.CallbackErrorHandler`, you can set the log mode for errors, access log, both, or none, and alter the default log writing behavior: ```csharp router.CallbackErrorHandler = (ex, ctx) => { ctx.LogMode = LogOutput.Both; // override log mode to log the error in both access and error logs } ``` ## Internal error handler Route callbacks can throw errors during server execution. If not handled correctly, the overall functioning of the HTTP server can be terminated. The router has a callback for when a route callback fails and prevents service interruption. This method is only reacheable when [ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) is set to false. ```cs mainRouter.CallbackErrorHandler = (ex, context) => { return new HttpResponse(500) { Content = new StringContent($"Error: {ex.Message}") }; }; ``` --- # Request handling Source: https://docs.sisk-framework.org/docs/fundamentals/request-handlers.html Request handlers, also known as "middlewares", are functions that run before or after a request is executed on the router. They can be defined per route or per router. There are two types of request handlers: - **BeforeResponse**: defines that the request handler will be executed before calling the router action. - **AfterResponse**: defines that the request handler will be executed after calling the router action. Sending an HTTP response in this context will overwrite the router's action response. Both requests handlers can override the actual router callback function response. By the way, request handlers can be useful for validating a request, such as authentication, content, or any other information, such as storing information, logs, or other steps that can be performed before or after a response. ![](https://docs.sisk-framework.org/assets/img/requesthandlers1.png) This way, a request handler can interrupt all this execution and return a response before finishing the cycle, discarding everything else in the process. Example: let's assume that a user authentication request handler does not authenticate him. It will prevent the request lifecycle from being continued and will hang. If this happens in the request handler at position two, the third and onwards will not be evaluated. ![](https://docs.sisk-framework.org/assets/img/requesthandlers2.png) ## Creating an request handler To create a request handler, we can create a class that inherits the [IRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.md) interface, in this format: ```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) { // Returning null indicates that the request cycle can be continued return null; } else { // Returning an HttpResponse object indicates that this response will overwrite adjacent responses. return new HttpResponse(System.Net.HttpStatusCode.Unauthorized); } } } ``` In the above example, we indicated that if the `Authorization` header is present in the request, it should continue and the next request handler or the router callback should be called, whichever comes next. If it's a request handler is executed after the response by their property [ExecutionMode](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.ExecutionMode.md) and return an non-null value, it will overwrite the router's response. Whenever a request handler returns `null`, it indicates that the request must continue and the next object must be called or the cycle must end with the router's response. > [!WARNING] > A request handler instance is created once and shared by every request it handles, including concurrent requests. **Never store individual request state in the request handler class itself** (fields or properties such as the current user, a parsed token, or a timer), because that value leaks into other requests. Store per-request values in the [RequestBag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.RequestBag.md) of the `HttpContext` instead: > > ```cs > public HttpResponse? Execute(HttpRequest request, HttpContext context) > { > // wrong: this.currentUser = FindUser(request); > context.RequestBag.Set(FindUser(request)); > return null; > } > ``` If you inherit from the built-in [RequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RequestHandler.md) class, you can return `Next()` to make that intent explicit: ```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); } } ``` For handlers that need I/O, inherit from [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(); } } ``` Small inline handlers can also be created with `RequestHandler.Create` or `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); }); ``` ## Associating a request handler with a single route You can define one or more request handlers for a route. ```cs {title="Router.cs"} mainRouter.Map(RouteMethod.Get, "/", IndexPage, new IRequestHandler[] { new AuthenticateUserRequestHandler(), // before request handler new ValidateJsonContentRequestHandler(), // before request handler // -- method IndexPage will be executed here new WriteToLogRequestHandler() // after request handler }); ``` Or creating an [Route](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.md) object: ```cs {title="Router.cs"} Route indexRoute = Route.Get("/", IndexPage); indexRoute.RequestHandlers = new IRequestHandler[] { new AuthenticateUserRequestHandler() }; mainRouter.Map(indexRoute); ``` ## Associating a request handler with a router You can define a global request handler that will runned on all routes on a router. ```cs {title="Router.cs"} mainRouter.GlobalRequestHandlers = new IRequestHandler[] { new AuthenticateUserRequestHandler() }; ``` ## Associating a request handler with an attribute You can define a request handler on a method attribute along with a route attribute. ```cs {title="Controller/MyController.cs"} public class MyController { [RouteGet("/")] [RequestHandler] static HttpResponse Index(HttpRequest request) { return new HttpResponse() { Content = new StringContent("Hello world!") }; } } ``` Note that it is necessary to pass the desired request handler type and not an object instance. That way, the request handler will be instantiated by the router parser. You can pass arguments in the class constructor with the [ConstructorArguments](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RequestHandlerAttribute.ConstructorArguments.md) property. Example: ```cs {title="Controller/MyController.cs"} [RequestHandler("arg1", 123, ...)] public HttpResponse Index(HttpRequest request) { return res = new HttpResponse() { Content = new StringContent("Hello world!") }; } ``` You can also create your own attribute that implements RequestHandler: ```cs {title="Middleware/Attributes/AuthenticateAttribute.cs"} public class AuthenticateAttribute : RequestHandlerAttribute { public AuthenticateAttribute() : base(typeof(AuthenticateUserRequestHandler), ConstructorArguments = new object?[] { "arg1", 123, ... }) { ; } } ``` And use it as: ```cs {title="Controller/MyController.cs"} [Authenticate] static HttpResponse Index(HttpRequest request) { return res = new HttpResponse() { Content = new StringContent("Hello world!") }; } ``` ## Bypassing an global request handler After defining a global request handler on a route, you can ignore this request handler on specific routes. ```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: the same instance of what is in the global request handlers new AuthenticateUserRequestHandler() // wrong: will not skip the global request handler }; mainRouter.Map(publicRoute); ``` > [!NOTE] > If you're bypassing a request handler you must use the same reference of what you instanced before to skip. Creating another request handler instance will not skip the global request handler since it's reference will change. Remember to use the same request handler reference used in both GlobalRequestHandlers and BypassGlobalRequestHandlers. --- # Requests Source: https://docs.sisk-framework.org/docs/fundamentals/requests.html Requests are structures that represent an HTTP request message. The [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) object contains useful functions for handling HTTP messages throughout your application. An HTTP request is formed by the method, path, version, headers and body. In this document, we will teach you how to obtain each of these elements. ## Getting the request method To obtain the method of the received request, you can use the Method property: ```cs static HttpResponse Index(HttpRequest request) { HttpMethod requestMethod = request.Method; ... } ``` This property returns the request's method represented by an [HttpMethod](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.httpmethod) object. > [!NOTE] > Unlike route methods, this property does not serves the [RouteMethod.Any](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md) item. Instead, it returns the real request method. ## Getting request url components You can get various component from a URL through certain properties of a request. For this example, let's consider the URL: ``` http://localhost:5000/user/login?email=foo@bar.com ``` | Component name | Description | Component value | | --- | --- | --- | | [Path](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Path.md) | Gets the request path. | `/user/login` | | [FullPath](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.FullPath.md) | Gets the request path and the query string. | `/user/login?email=foo@bar.com` | | [FullUrl](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.FullUrl.md) | Gets the entire URL request string. | `http://localhost:5000/user/login?email=foo@bar.com` | | [Host](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Host.md) | Gets the request host. | `localhost` | | [Authority](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Authority.md) | Gets the request host and port. | `localhost:5000` | | [QueryString](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.QueryString.md) | Gets the request query. | `?email=foo@bar.com` | | [Query](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Query.md) | Gets the request query in a named value collection. | `{StringValueCollection object}` | | [IsSecure](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.IsSecure.md) | Determines if the request is using SSL (true) or not (false). | `false` | You can also opt by using the [HttpRequest.Uri](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Uri.md) property, which includes everything above in one object. ## Request metadata and cancellation Sisk also attaches operational metadata to each request. These properties are useful for logs, tracing, localization, diagnostics, and long-running operations: | Property or method | Use | | --- | --- | | [RequestId](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RequestId.md) | A unique identifier for the request. Enable [IncludeRequestIdHeader](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.IncludeRequestIdHeader.md) to return it as `X-Request-Id`. | | [RequestedAt](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RequestedAt.md) | The moment when Sisk created the request object. | | [RemoteAddress](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RemoteAddress.md) | The client address resolved from the connection, or from your [ForwardingResolver](https://docs.sisk-framework.org/docs/advanced/forwarding-resolvers.md). | | [Culture](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Culture.md) | The best culture resolved from `Accept-Language`, falling back to the current culture. | | [DisconnectToken](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.DisconnectToken.md) | A cancellation token signaled when the client disconnects, when supported by the configured HTTP engine. | | [Bag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Bag.md) | A typed key/value store shared across request handlers and the route action. | | [GetRawHttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetRawHttpRequest.md) | A text representation of the request for diagnostics. | ## Getting the request body Some requests include body such as forms, files, or API transactions. You can get the body of a request from the property: ```cs // gets the request body as an string, using the request encoding as the encoder string body = request.Body; // or gets it in an byte array byte[] bodyBytes = request.RawBody; // or else, you can stream it. Stream requestStream = request.GetRequestStream(); // or read the body asynchronously Memory bodyMemory = await request.GetBodyContentsAsync(); ``` It is also possible to determine if there is a body in the request and if it is loaded with the properties [HasContents](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.HasContents.md), which determines if the request has contents and [IsContentAvailable](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.IsContentAvailable.md) which indicates that the HTTP server fully received the content from the remote point. It is not possible to read the request content through `GetRequestStream` more than once. If you read with this method, the values in `RawBody` and `Body` will also not be available. It's not necessary to dispose the request stream in the context of the request, as it is disposed at the end of the HTTP session in which it is created. Also, you can use [HttpRequest.RequestEncoding](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RequestEncoding.md) property to get the best encoding to decode the request manually. The server has limits for reading the request content, which applies to both [HttpRequest.Body](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Body.md) and [HttpRequest.RawBody](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Body.md). These properties copies the entire input stream to an local buffer of the same size of [HttpRequest.ContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.ContentLength.md). The request content is limited by [HttpServerConfiguration.MaximumContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.MaximumContentLength.md), which defaults to 100 MB since Sisk 1.7 (it was `0`, unlimited, in earlier versions). The limit applies as follows: - A request that declares a `Content-Length` larger than the limit receives a `413 Content Too Large` response before the route is invoked. - A chunked or streamed body without a declared length is checked while it is read. Reading past the limit through [RawBody](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RawBody.md), [Body](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Body.md) or the stream returned by [GetRequestStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetRequestStream.md) throws an [InsufficientMemoryException](https://learn.microsoft.com/en-us/dotnet/api/system.insufficientmemoryexception), which results in a `500 Internal Server Error` unless your code or [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) handles it. - The same limit is the maximum size of a received [WebSocket](https://docs.sisk-framework.org/docs/features/websockets.md) message. Set the property to `0` to remove the limit: ```cs using var app = HttpServer.CreateBuilder() .UseConfiguration(config => config.MaximumContentLength = 0) .Build(); ``` Even without a configured limit, `Body` and `RawBody` cannot buffer content larger than [Array.MaxLength](https://learn.microsoft.com/en-us/dotnet/api/system.array.maxlength) (about 2 GB) and throw an exception when you try to access them. You can still deal with larger content through streaming. > [!NOTE] > Although Sisk allows it, it is always a good idea to follow HTTP Semantics to create your application and not obtain or serve content in methods that do not allow it. Read about [RFC 9110 "HTTP Semantics"](https://httpwg.org/spec/rfc9110.html). ## Reading JSON requests For JSON APIs, prefer the built-in JSON helpers instead of reading `Body` and deserializing manually. They use [System.Text.Json](https://learn.microsoft.com/en-us/dotnet/api/system.text.json) and default to [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); }); ``` Use the async overload when you are already in an async route or want request cancellation to stop deserialization: ```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); }); ``` You can pass custom [JsonSerializerOptions](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializeroptions) for a specific endpoint: ```cs var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { PropertyNameCaseInsensitive = true }; UserDto? user = request.GetJsonContent(options); ``` For Native AOT or trimming-sensitive applications, use the `JsonTypeInfo` overload generated by a `JsonSerializerContext`: ```cs [JsonSerializable(typeof(CreateUserRequest))] public partial class AppJsonSerializerContext : JsonSerializerContext { } CreateUserRequest? body = await request.GetJsonContentAsync( AppJsonSerializerContext.Default.CreateUserRequest, request.DisconnectToken); ``` The same read-once rule applies to JSON helpers: after Sisk reads the request stream through `GetJsonContent`, `GetJsonContentAsync`, `Body`, or `RawBody`, you cannot later consume the same body through `GetRequestStream()`. ## Getting the request context The HTTP Context is an exclusive Sisk object that stores HTTP server, route, router and request handler information. You can use it to be able to organize yourself in an environment where these objects are difficult to organize. You can get the currently executing [HttpContext](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.md) using the static method `HttpContext.GetCurrentContext()`. This method returns the context of the request currently being processed in the current thread. ```cs HttpContext context = HttpContext.GetCurrentContext(); ``` ### Log Mode The [HttpContext.LogMode](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.LogMode.md) property allows you to control the logging behavior for the current request. You can enable or disable logging for specific requests, overriding the default server configuration. ```cs // Disable logging for this request context.LogMode = LogOutputMode.None; ``` ### Request Bag The [RequestBag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.RequestBag.md) object contains stored information that is passed from an request handler to another point, and can be consumed at the final destination. This object can also be used by request handlers that run after the route callback. > [!TIP] > This property is also acessible by [HttpRequest.Bag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Bag.md) property. ```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); } } } ``` The above request handler will define `AuthenticatedUser` in the request bag, and can be consumed later in the final callback: ```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}!") }; } } ``` You can also use the `Bag.Set()` and `Bag.Get()` helper methods to get or set objects by their type singletons. The `TypedValueDictionary` class also provides `GetValue` and `SetValue` methods for more control. ```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(); ... } ``` ### Atomic context values The request bag is not synchronized. When numeric values in the bag can be changed by concurrent code during the same request, such as parallel tasks started by the route, use [HttpContext.Interlocked](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.Interlocked.md). It returns an [HttpContextInterlocked](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.HttpContextInterlocked.md) object with atomic operations over named values of the request bag: | Method | Description | | --- | --- | | `Add(name, value)` | Adds `value` to the stored value, or stores `value` when `name` is not set. | | `Exchange(name, value)` | Replaces the stored value and returns the previous one, or `value` when `name` was not set. | | `CompareExchange(name, value, comparand, notFound = -1)` | Replaces the stored value with `value` only when it equals `comparand`, and returns the original value, or `notFound` when `name` is not set. | | `Inspect(name)` | Returns the stored value, or `null` when `name` is not set. | Values are stored as `double` in the request bag. The overloads that receive `int` or `long` values convert them to `double`. ```cs router.MapPost("/import", async (HttpRequest request) => { string[] lines = request.Body.Split('\n'); await Parallel.ForEachAsync(lines, async (line, cancellation) => { if (await TryImportAsync(line, cancellation)) request.Context.Interlocked.Add("imported", 1); }); double imported = request.Context.Interlocked.Inspect("imported") ?? 0; return new HttpResponse($"{imported} line(s) imported"); }); ``` > [!NOTE] > The operations are atomic only with respect to other `Interlocked` calls. Reading or writing the same key directly through `RequestBag` is not synchronized with them. ## Getting form data You can get form data values in a [StringKeyStoreCollection](https://docs.sisk-framework.org/api/Sisk.Core.Entity.StringKeyStoreCollection.md) with the example below: ```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)) { ... } } ``` The async version is useful when the request body may be large or when you want cancellation support: ```cs var form = await request.GetFormContentAsync(request.DisconnectToken); ``` ## Getting multipart form data Sisk's HTTP request lets you get uploaded multipart contents, such as a files, form fields, or any binary content. ```cs {title="Controller/Auth.cs"} [RoutePost("/upload-contents")] public HttpResponse Index(HttpRequest request) { // the following method reads the entire request input into an // array of MultipartObjects var multipartFormDataObjects = request.GetMultipartFormContent(); foreach (MultipartObject uploadedObject in multipartFormDataObjects) { // The name of the file provided by Multipart form data. // Null is returned if the object is not a file. Console.WriteLine("File name : " + uploadedObject.Filename); // The multipart form data object field name. Console.WriteLine("Field name : " + uploadedObject.Name); // The multipart form data content length. Console.WriteLine("Content length : " + uploadedObject.ContentLength); // Determine the image format based in the file header for each // known content type. If the content ins't an recognized common file // format, this method below will return MultipartObjectCommonFormat.Unknown Console.WriteLine("Common format : " + uploadedObject.GetCommonFileFormat()); } } ``` Use [GetMultipartFormContentAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetMultipartFormContentAsync.md) when the route is asynchronous: ```cs var multipartFormDataObjects = await request.GetMultipartFormContentAsync(request.DisconnectToken); ``` You can read more about Sisk [Multipart form objects](https://docs.sisk-framework.org/api/Sisk.Core.Entity.MultipartObject.md) and it's methods, properties and functionalities. ## Detecting client disconnection Since version v1.15 of Sisk, the framework provides a cancellation token through [HttpRequest.DisconnectToken](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.DisconnectToken.md). When the configured HTTP engine supports disconnect detection, this token is canceled when the client connection is closed before the response is completed. This is useful for stopping long-running operations when the client is no longer waiting for the result. ```csharp router.MapGet("/connect", async (HttpRequest req) => { // gets the disconnection token from the request var dc = req.DisconnectToken; await LongOperationAsync(dc); return new HttpResponse(); }); ``` This token is not compatible with all HTTP engines, and each requires an implementation. The default Sisk engine, based on `System.Net.HttpListener`, does not support client-disconnection detection. When your application uses the default engine, `DisconnectToken` is `CancellationToken.None`; in practice, it is a non-canceling token and should be treated as unavailable. The [Cadente engine](https://docs.sisk-framework.org/docs/cadente.md) supports `DisconnectToken`. If your route depends on disconnection-aware cancellation, use Cadente or another engine that explicitly implements this behavior. Even with a supported engine, cancellation is cooperative: pass the token to async APIs and check it in your own long-running work. ## Server-sent events support Sisk supports [Server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events), which allows sending chunks as an stream and keeping the connection between the server and the client alive. Calling the [HttpRequest.GetEventSource](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetEventSource.md) method will put the HttpRequest in it's listener state. From this, the context of this HTTP request will not expect an HttpResponse as it will overlap the packets sent by server side events. After sending all packets, the callback must return the [Close](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequestEventSource.Close.md) method, which will send the final response to the server and indicate that the streaming has ended. It's not possible to predict what the total length of all packets that will be sent, so it is not possible to determine the end of the connection with `Content-Length` header. By most browsers defaults, server-side events does not support sending HTTP headers or methods other than the GET method. Therefore, be careful when using request handlers with event-source requests that require specific headers in the request, as it probably they ins't going to have them. Also, most browsers restart streams if the [EventSource.close](https://developer.mozilla.org/en-US/docs/Web/API/EventSource/close) method ins't called on the client side after receiving all the packets, causing infinite additional processing on the server side. To avoid this kind of problem, it's common to send an final packet indicating that the event source has finished sending all packets. The example below shows how the browser can communicate with the server that supports Server-side events. ```html {title="sse-example.html"} Fruits:
    ``` And progressively send the messages to the client: ```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(); } } ``` When running this code, we expect a result similar to this: ## Resolving proxied IPs and hosts Sisk can be used with proxies, and therefore IP addresses can be replaced by the proxy endpoint in the transaction from a client to the proxy. You can define your own resolvers in Sisk with [forwarding resolvers](https://docs.sisk-framework.org/docs/advanced/forwarding-resolvers.md). ## Headers encoding Header encoding can be a problem for some implementations. On Windows, UTF-8 headers are not supported, so ASCII is used. Sisk has a built-in encoding converter, which can be useful for decoding incorrectly encoded headers. This operation is costly and disabled by default, but can be enabled with [HttpServerConfiguration.NormalizeHeadersEncodings](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.NormalizeHeadersEncodings.md). --- # Responses Source: https://docs.sisk-framework.org/docs/fundamentals/responses.html Responses represent objects that are HTTP responses to HTTP requests. They are sent by the server to the client as an indication of the request for a resource, page, document, file or other object. An HTTP response is formed up of status, headers and content. In this document, we will teach you how to architect HTTP responses with Sisk. ## Setting an HTTP status The HTTP status list is the same since HTTP/1.0, and Sisk supports all of them. ```cs HttpResponse res = new HttpResponse(); res.Status = System.Net.HttpStatusCode.Accepted; // 202 ``` Or with Fluent Syntax: ```cs new HttpResponse() .WithStatus(200) // or .WithStatus(HttpStatusCode.Ok) // or .WithStatus(HttpStatusInformation.Ok); ``` You can see the full list of available HttpStatusCode [here](https://learn.microsoft.com/pt-br/dotnet/api/system.net.httpstatuscode). You can also provide your own status code by using the [HttpStatusInformation](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpStatusInformation.md) structure. ## Body and content-type Sisk supports native .NET content objects to send body in responses. You can use the [StringContent](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.stringcontent) class to send a JSON response for example: ```cs HttpResponse res = new HttpResponse(); res.Content = new StringContent(myJson, Encoding.UTF8, "application/json"); ``` The server will always attempt to calculate the `Content-Length` from what you have defined in the content if you haven't explicitly defined it in a header. If the server cannot implicitly obtain the Content-Length header from the response content, the response will be sent with Chunked-Encoding. You can also stream the response by sending a [StreamContent](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.streamcontent) or using the method [GetResponseStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetResponseStream.md). ## Response headers You can add, edit or remove headers you're sending in the response. The example below shows how to send an redirect response to the client. ```cs HttpResponse res = new HttpResponse(); res.Status = HttpStatusCode.Moved; res.Headers.Add(HttpKnownHeaderNames.Location, "/login"); ``` Or with Fluent Syntax: ```cs new HttpResponse(301) .WithHeader("Location", "/login"); ``` When you use the [Add](https://docs.sisk-framework.org/api/Sisk.Core.Entity.HttpHeaderCollection.Add.md) method of HttpHeaderCollection, you are adding a header to the request without altering the ones already sent. The [Set](https://docs.sisk-framework.org/api/Sisk.Core.Entity.HttpHeaderCollection.Set.md) method replaces the headers with the same name with the instructed value. The indexer of HttpHeaderCollection internally calls the Set method to replace the headers. You can also retrieve header values using the [GetHeaderValue](https://docs.sisk-framework.org/api/Sisk.Core.Entity.HttpHeaderCollection.GetHeaderValue.md) method. This method helps in obtaining values from both the response headers and the content headers (if any content is set). ```cs // Returns the value of the "Content-Type" header, checking both response.Headers and response.Content.Headers string? contentType = response.GetHeaderValue("Content-Type"); ``` ## Sending cookies Sisk has methods that facilitate the definition of cookies in the client. Cookies set by this method are already URL encoded and fit the RFC-6265 standard. ```cs HttpResponse res = new HttpResponse(); res.SetCookie("cookie-name", "cookie-value"); ``` Or with Fluent Syntax: ```cs new HttpResponse(301) .WithCookie("cookie-name", "cookie-value", expiresAt: DateTime.Now.Add(TimeSpan.FromDays(7))); ``` There are other [more complete versions](https://docs.sisk-framework.org/api/Sisk.Core.Helpers.CookieHelper.SetCookie.md) of the same method. ## Chunked responses You can set the transfer encoding to chunked to send large responses. ```cs HttpResponse res = new HttpResponse(); res.SendChunked = true; ``` When using chunked-encoding, the Content-Length header is automatically omitted. ## Response stream Response streams are an managed way that allow you to send responses in a segmented way. It's a lower level operation than using HttpResponse objects, as they require you to send the headers and content manually, and then close the connection. This example opens an read-only stream for the file, copies the stream to the response output stream and doens't loads the entire file in the memory. This can be useful to serving medium or big files. ```cs // gets the response output stream using var fileStream = File.OpenRead("my-big-file.zip"); var responseStream = request.GetResponseStream(); // sets the response encoding to use chunked-encoding // also you shouldn't send content-length header when using // chunked encoding responseStream.SendChunked = true; responseStream.SetStatus(200); responseStream.SetHeader(HttpKnownHeaderNames.ContentType, contentType); // copies the file stream to the response output stream fileStream.CopyTo(responseStream.ResponseStream); // closes the stream return responseStream.Close(); ``` ## GZip, Deflate and Brotli compression You can send responses with compressed content in Sisk with compressing HTTP contents. Firstly, encapsulate your [HttpContent](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpcontent) object within one of the compressors below to send the compressed response to the client. ```cs router.MapGet("/hello.html", request => { string myHtml = "..."; return new HttpResponse () { Content = new GZipContent(new HtmlContent(myHtml)), // or Content = new BrotliContent(new HtmlContent(myHtml)), // or Content = new DeflateContent(new HtmlContent(myHtml)), }; }); ``` You can also use these compressed contents with streams. ```cs router.MapGet("/archive.zip", request => { // do not apply "using" here. the HttpServer will discard your content // after sending the response. var archive = File.OpenRead("/path/to/big-file.zip"); return new HttpResponse () { Content = new GZipContent(archive) } }); ``` The Content-Encoding headers are automatically set when using these contents. ## Automatic compression It is possible to automatically compress HTTP responses with the [EnableAutomaticResponseCompression](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.EnableAutomaticResponseCompression.md) property. This property automatically encapsulates the response content from the router in a compressible content that is accepted by the request, provided the response is not inherited from a [CompressedContent](https://docs.sisk-framework.org/api/Sisk.Core.Http.CompressedContent.md). Only one compressible content is chosen for a request, chosen according to the Accept-Encoding header, which follows the order: - [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) If the request specifies that it accepts any of these compression methods, the response will be automatically compressed. ## Implicit response types You can use other return types besides HttpResponse, but it is necessary to configure the router how it will handle each type of object. The concept is to always return a reference type and turn it into a valid HttpResponse object. Routes that return HttpResponse do not undergo any conversion. Value types (structures) cannot be used as a return type because they are not compatible with the [RouterCallback](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouterCallback.md), so they must be wrapped in a ValueResult to be able to be used in handlers. Consider the following example from a router module not using HttpResponse in the return type: ```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; } } ``` With that, now it is necessary to define in the router how it will deal with each type of object. Objects are always the first argument of the handler and the output type must be a valid HttpResponse. Also, the output objects of a route should never be null. For ValueResult types it is not necessary to indicate that the input object is a ValueResult and only T, since ValueResult is an object reflected from its original component. The association of types does not compare what was registered with the type of the object returned from the router callback. Instead, it checks whether the type of the router result is assignable to the registered type. Registering a handler of type Object will fallback to all previously unvalidated types. The inserting order of the value handlers also matters, so registering an Object handler will ignore all other type-specific handlers. Always register specific value handlers first to ensure order. ```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)); }); // registering an value handler of object must be the last // value handler which will be used as an fallback r.RegisterValueHandler(fallback => { return new HttpResponse() { Status = HttpStatusCode.OK, Content = JsonContent.Create(fallback) }; }); ``` ## Deferred Actions When a request reaches the router, it first passes through the [request handlers](https://docs.sisk-framework.org/docs/fundamentals/request-handlers.md), is processed in the router action, and then by the post-execution request handlers. The result of the router action is what is passed to the value handlers, and the result of the value handler is what is sent to the client as a response. This lifecycle occurs within an asynchronous context. This asynchronous context exposes variables that the user can add to the [HttpContext Bag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.md) to share data between handlers and the router action. The value returned by the router action is added to this asynchronous context and can be accessed by the value handlers. Deferred actions are actions that will always execute at the end of the cycle, after delivering the response to the client, but still within the same asynchronous context. These actions can be used to execute long-running tasks that do not need to be completed to send a response to the client, such as saving logs, updating the database, sending emails, etc. Exceptions are still caught in deferred actions and will be handled in the same way as an exception thrown anywhere in the request lifecycle. The difference is that the client will already have a response, so the exception is handled by the default error handling. Defer the execution of an action using the [HttpContext.EnqueueDeferredAction](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.EnqueueDeferredAction.md) method. The method receives an asynchronous function that represents the action to be executed and an optional timeout to limit the execution time of the action. If the action is not completed within the time limit, it will be canceled. ```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."); } // schedules a long-running action that will be executed after sending the response to the client, but still within the same asynchronous context of the request 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...") }; } ``` ## Note on enumerable objects and arrays Implicit response objects that implement [IEnumerable](https://learn.microsoft.com/pt-br/dotnet/api/system.collections.ienumerable?view=net-8.0) are read into memory through the `ToArray()` method before being converted through a defined value handler. For this to occur, the `IEnumerable` object is converted to an array of objects, and the response converter will always receive an `Object[]` instead of the original type. Consider the following scenario: ```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(); ``` In the above example, the `IEnumerable` converter **will never be called**, because the input object will always be an `Object[]` and it is not convertible to an `IEnumerable`. However, the converter below that receives an `IEnumerable` will receive its input, since its value is compatible. If you need to actually handle the type of the object that will be enumerated, you will need to use reflection to get the type of the collection element. All enumerable objects (lists, arrays, and collections) are converted to an array of objects by the HTTP response converter. Values that implements [IAsyncEnumerable](https://learn.microsoft.com/pt-br/dotnet/api/system.collections.generic.iasyncenumerable-1?view=net-8.0) are handled automatically by the server if the [ConvertIAsyncEnumerableIntoEnumerable](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ConvertIAsyncEnumerableIntoEnumerable.md) property is enabled, similar to what happens with `IEnumerable`. This option is enabled by default in `HttpServerConfiguration`; an asynchronous enumeration is converted to a blocking enumerator, and then converted to a synchronous array of objects. Disable it only when you provide your own value handler or streaming response strategy for asynchronous sequences. --- # Logging Source: https://docs.sisk-framework.org/docs/features/logging.html You can configure Sisk to write access and error logs automatically. It is possible to define log rotation, extensions and frequency. The [LogStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.md) class provides an asynchronous way of writing logs and keeping them in an awaitable write queue. The `LogStream` class implements `IAsyncDisposable`, ensuring that all pending logs are written before the stream is closed. In this article we will show you how to configure logging for your application. ## File based access logs Logs to files open the file, write the line text, and then close the file for every line written. This procedure was adopted to maintain write responsiveness in the 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(); } } ``` The above code will write all incoming requests to the `logs/access.log` file. Note that, the file is created automatically if it does not exist, however the folder before it does not. It's not necessary to create the `logs/` directory as the LogStream class automatically creates it. ## Stream based logging You can write log files to TextWriter objects instances, such as `Console.Out`, by passing an TextWriter object in the constructor: ```cs {title="Program.cs"} using var app = HttpServer.CreateBuilder() .UseConfiguration(config => { config.AccessLogsStream = new LogStream(Console.Out); }) .Build(); ``` For every message written in the stream-based log, the `TextWriter.Flush()` method is called. ## Access log formatting You can customize the access log format by predefined variables. Consider the following line: ```cs config.AccessLogsFormat = "%dd/%dmm/%dy %tH:%ti:%ts %tz %ls %ri %rs://%ra%rz%rq [%sc %sd] %lin -> %lou in %lmsms [%{user-agent}]"; ``` It will write an message like: 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] You can format your log file by the format described by the table: | Value | What it represents | Example | |--------|-----------------------------------------------------------------------------------|---------------------------------------| | %dd | Day of the month (formatted as two digits) | 05 | | %dmmm | Full name of the month | July | | %dmm | Abbreviated name of the month (three letters) | Jul | | %dm | Month number (formatted as two digits) | 07 | | %dy | Year (formatted as four digits) | 2023 | | %th | Hour in 12-hour format | 03 | | %tH | Hour in 24-hour format (HH) | 15 | | %ti | Minutes (formatted as two digits) | 30 | | %ts | Seconds (formatted as two digits) | 45 | | %tm | Milliseconds (formatted as three digits) | 123 | | %tz | Time zone offset (total hours in UTC) | +03:00 | | %ri | Client's remote IP address, as resolved at the start of the request | 192.168.1.100 | | %rm | HTTP method (uppercase) | GET | | %rs | URI scheme (http/https) | https | | %ra | URI authority (domain) | example.com | | %rh | Host of the request | www.example.com | | %rp | Port of the request | 443 | | %rz | Path of the request | /path/to/resource | | %rq | Query string | ?key=value&another=123 | | %sc | HTTP response status code | 200 | | %sd | HTTP response status description | OK | | %lin | Human-readable size of the request | 1.2 KB | | %linr | Raw size of the request (bytes) | 1234 | | %lou | Human-readable size of the response | 2.5 KB | | %lour | Raw size of the response (bytes) | 2560 | | %lms | Elapsed time in milliseconds | 120 | | %ls | Execution status | Executed | | %{header-name} | Represents the `header-name` header of the request. | `Mozilla/5.0 (platform; rv:gecko [...]` | | %{:header-name} | Represents the `header-name` header of the response. | `application/json` | | %{$key} | Represents the value stored under `key` in [HttpContext.RequestBag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.RequestBag.md), converted with `ToString()`. Empty when the key is not set. | `42` | You can also use `HttpServerConfiguration.DefaultAccessLogFormat` to use the default access log format. The `%{$key}` term lets request handlers and routes add their own values to the access log: ```cs config.AccessLogsFormat = "%dd/%dmm/%dy %tH:%ti:%ts %rm %rz [%sc] user=%{$UserId}"; router.MapGet("/profile", request => { request.Bag["UserId"] = "42"; return new HttpResponse("ok"); }); ``` ## Rotating logs You can configure the HTTP server to rotate the log files to a compressed .gz file when they reach a certain size. The size is checked periodically by the limiar you define. ```cs LogStream errorLog = new LogStream("logs/error.log") .ConfigureRotatingPolicy( maximumSize: 64 * SizeHelper.UnitMb, dueTime: TimeSpan.FromHours(6)); ``` The above code will check every six hours if the LogStream's file has reached it's 64MB limit. If so, the file is compressed to an .gz file and it then `access.log` is cleaned. During this process, writing to the file is locked until the file is compressed and cleaned. All lines that enter to be written in this period will be in a queue waiting for the end of compression. This function only works with file-based LogStreams. ## Error logging When a server is not throwing errors to the debugger, it forwards the errors to log writing when there are any. You can configure error writing with: ```cs config.ThrowExceptions = false; config.ErrorsLogsStream = new LogStream("error.log"); ``` This property will only write something to the log if the error is not captured by the callback or the [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) property. The error written by the server always writes the date and time, the request headers (not the body), the error trace, and the inner exception trace, if theres any. ## Other logging instances Your application can have zero or multiple LogStreams, there is no limit on how many log channels it can have. Therefore, it is possible to direct your application's log to a file other than the default AccessLog or ErrorLog. ```cs LogStream appMessages = new LogStream("messages.log"); appMessages.WriteLine("Application started at {0}", DateTime.Now); ``` ## Writing files safely For files that are not log streams, such as reports or state dumps, [LogStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.md) provides static helpers that create the missing parent directory, serialize concurrent writes to the same file within the process and return `false` instead of throwing when the operation fails: - [LogStream.SafeWriteToFile](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeWriteToFile.md) and [LogStream.SafeWriteToFileAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeWriteToFileAsync.md) replace the file contents. - [LogStream.SafeAppendToFile](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeAppendToFile.md) and [LogStream.SafeAppendToFileAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeAppendToFileAsync.md) append to the file. Each method accepts a string or an array of lines and has overloads that receive an `Encoding`. ```cs bool written = await LogStream.SafeAppendToFileAsync("logs/audit.log", $"{DateTime.Now:s} user signed in\n"); if (!written) { Console.WriteLine("Could not write the audit entry."); } ``` The lock is per process: writes from other processes to the same file are not coordinated. If you only need to make sure a directory exists, use [PathHelper.EnsureDirectoryExistance](https://docs.sisk-framework.org/api/Sisk.Core.Helpers.PathHelper.EnsureDirectoryExistance.md) or [PathHelper.EnsureFileDirectoryExistance](https://docs.sisk-framework.org/api/Sisk.Core.Helpers.PathHelper.EnsureFileDirectoryExistance.md), which return `true` when the directory already existed and `false` when it was created. ## Extending LogStream You can extend the `LogStream` class to write custom formats, compatible with the current Sisk log engine. The example below allows to write colorful messages into the Console through Spectre.Console library: ```cs {title="CustomLogStream.cs"} public class CustomLogStream : LogStream { protected override void WriteLineInternal(string line) { base.WriteLineInternal($"[{DateTime.Now:g}] {line}"); } } ``` Another way to automatically write custom logs for each request/response is to create an [HttpServerHandler](https://docs.sisk-framework.org/api/Sisk.Core.Http.Handlers.HttpServerHandler.md). The example below is a little more complete. It writes the body of the request and response in JSON to the Console. It can be useful for debugging requests in general. This example makes use of ContextBag and 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) { // At this point, the connection is open and the client has sent the header specifying // that the content is JSON.The line below reads the content and leaves it stored in the request. // // If the content is not read in the request action, the GC is likely to collect the content // after sending the response to the client, so the content may not be available after the response is closed. // _ = request.RawBody; // add hint in the context to tell that this request has an json body on it request.Bag.Add("IsJsonRequest", true); } } protected override async void OnHttpRequestClose(HttpServerExecutionResult result) { string? requestJson = null, responseJson = null, responseMessage; if (result.Request.Bag.ContainsKey("IsJsonRequest")) { // reformats the JSON using the CypherPotato.LightJson library 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 && // check if the response is 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 { // gets the internal server handling status 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/docs/features/server-sent-events.html Sisk supports sending messages through Server Sent Events out of the box. You can create disposable and persistent connections, get the connections during runtime and use them. This feature has some limitations imposed by browsers, such as sending only texts messages and not being able to permanently close a connection. A server-side closed connection will have a client periodically trying to reconnect every 5 seconds (3 for some browsers). These connections are useful for sending events from the server to the client without having the client request the information every time. ## Creating an SSE connection A SSE connection works like a regular HTTP request, but instead of sending a response and immediately closing the connection, the connection is kept open to send messages. Calling the [HttpRequest.GetEventSource()](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetEventSource.md) method, the request is put in a waiting state while the SSE instance is created. ```cs r.MapGet("/", (req) => { using var sse = req.GetEventSource(); sse.Send("Hello, world!"); return sse.Close(); }); ``` In the above code, we create an SSE connection and send a "Hello, world" message, then we close the SSE connection from the server side. > [!NOTE] > When closing a server-side connection, by default the client will try to connect again at that end and the connection will be restarted, executing the method again, forever. > > It's common to forward a termination message from the server whenever the connection is closed from the server to prevent the client from trying to reconnect again. ## Appending headers If you need to send headers, you can use the [HttpRequestEventSource.AppendHeader](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.AppendHeader.md) method before sending any messages. ```cs r.MapGet("/", (req) => { using var sse = req.GetEventSource(); sse.AppendHeader("Header-Key", "Header-value"); sse.Send("Hello!"); return sse.Close(); }); ``` Note that it is necessary to send the headers before sending any messages. ## Wait-For-Fail connections Connections are normally terminated when the server is no longer able to send messages due to an possible client-side disconnection. With that, the connection is automatically terminated and the instance of the class is discarded. Even with a reconnection, the instance of the class will not work, as it is linked to the previous connection. In some situations, you may need this connection later and you don't want to manage it via the callback method of the route. For this, we can identify the SSE connections with an identifier and get them using it later, even outside the callback of the route. In addition, we mark the connection with [WaitForFail](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.WaitForFail.md) so as not to terminate the route and terminate the connection automatically. An SSE connection in `WaitForFail` waits for a send error caused by disconnection, or for the configured idle tolerance to elapse, before the route resumes and closes the connection. ```cs r.MapGet("/", (req) => { using var sse = req.GetEventSource("my-index-connection"); sse.WaitForFail(TimeSpan.FromSeconds(15)); // wait for 15 seconds without any message before terminating the connection return sse.Close(); }); ``` The above method will create the connection, handle it and wait for a disconnection or error. ```cs HttpRequestEventSource? evs = server.EventSources.GetByIdentifier("my-index-connection"); if (evs != null) { // the connection is still alive evs.Send("Hello again!"); } ``` And the snippet above will try to look for the newly created connection, and if it exists, it will send a message to it. All active server connections that are identified will be available in the collection [HttpServer.EventSources](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.EventSources.md). This collection only stores active and identified connections. Closed connections are removed from the collection. > [!NOTE] > It is important to note that keep alive has a limit established by components that may be connected to Sisk in an uncontrollable way, such as an web proxy, an HTTP kernel or a network driver, and they close idle connections after a certain period of time. > > Therefore, it is important to keep the connection open by sending periodic pings or extending the maximum time before the connection is closed. Read the next section to better understand sending periodic pings. ## Setup connections ping policy Ping Policy is an automated way of sending periodic messages to your client. This function allows the server to understand when the client has disconnected from that connection without having to keep the connection open indefinitely. ```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(); } ``` In the code above, every 5 seconds, a new ping message will be sent to the client. This will keep the TCP connection alive and prevent it from being closed due to inactivity. Also, when a message fails to be sent, the connection is automatically closed, freeing up the resources used by the connection. Use [SendAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.SendAsync.md) and [CloseAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.CloseAsync.md) in asynchronous routes. If you need to discard queued events before closing, call [Cancel](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.Cancel.md). ## Querying connections You can search for active connections using a predicate on the connection identifier, to be able to broadcast, for example. ```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-'"); } ``` You can also use the [All](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpEventSourceCollection.All.md) method to get all active SSE connections. --- # Web Sockets Source: https://docs.sisk-framework.org/docs/features/websockets.html Sisk supports web sockets as well, such as receiving and sending messages to their client. This feature works fine in most browsers, but in Sisk it is still experimental. Please, if you find any bugs, report it on github. ## Accepting messages WebSocket messages are received in order, queued until processed by `ReceiveMessageAsync`. This method returns no message when the timeout is reached, when the operation is canceled, or when the client is disconnected. The maximum size of a received message is [HttpServerConfiguration.MaximumContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.MaximumContentLength.md), which defaults to 100 MB since Sisk 1.7. When a client sends a larger message, the connection is closed with the `MessageTooBig` status and `ReceiveMessageAsync` returns no message. Only one read and write operation can occur simultaneously, therefore, while you are waiting for a message with `ReceiveMessageAsync`, it is not possible to write to the connected client. ```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(); }); ``` ## Persistent connection The example below contains a way for you to use a persistent websocket connection, where you receive the messages, deal with them, and finish using the socket. ```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 Policy Similar to how ping policy in Server Side Events works, you can also configure a ping policy to keep the TCP connection open if there is inactivity in it. ```cs ws.PingPolicy.Start( dataMessage: "ping-message", interval: TimeSpan.FromSeconds(10)); ``` ## Managed connections When accepting a WebSocket, you can provide an identifier. Identified sockets are registered in [HttpServer.WebSockets](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.WebSockets.md), which lets the server find active connections outside the route that accepted them. ```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(); }); ``` From another part of the application, query the collection by identifier or predicate: ```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"); } ``` Each `HttpWebSocket` exposes `Identifier`, `State`, `IsClosed`, and `PingPolicy`. The collection also exposes `All()`, `Find(...)`, `GetByIdentifier(...)`, `ActiveConnections`, and `DropAll()` for server-managed connection strategies. --- # Discard syntax Source: https://docs.sisk-framework.org/docs/features/discard-syntax.html The HTTP server can be used to listen for a callback request from an action, such as OAuth authentication, and can be discarded after receiving that request. This can be useful in cases where you need a background action but do not want to set up an entire HTTP application for it. The following example show us how to create an listening HTTP server at port 5555 with [CreateListener](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.CreateListener.md) and wait the next context: ```csharp using (var server = HttpServer.CreateListener(5555)) { // wait for the next http request var context = await server.WaitNextAsync(); Console.WriteLine($"Requested path: {context.Request.Path}"); } ``` The [WaitNext](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.WaitNext.md) function waits for the next context of a completed request processing. Once the result of this operation is obtained, the server has already fully handled the request and sent the response to the client. --- # Dependency injection Source: https://docs.sisk-framework.org/docs/features/instancing.html It is common to dedicate members and instances that last for the lifetime of a request, such as a database connection, an authenticated user, or a session token. One of the possibilities is through the [HttpContext.RequestBag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.md), which creates a dictionary that lasts for the entire lifetime of a request. This dictionary can be accessed by [request handlers](https://docs.sisk-framework.org/docs/fundamentals/request-handlers.md) and define variables throughout that request. For example, a request handler that authenticates a user sets this user within the `HttpContext.RequestBag`, and within the request logic, this user can be retrieved with `HttpContext.RequestBag.Get()`. The objects defined in this dictionary are scoped to the request lifecycle. They are disposed of at the end of the request. Not necessarily does sending a response define the end of the request lifecycle. When [request handlers](https://docs.sisk-framework.org/docs/fundamentals/request-handlers.md) that run after sending a response are executed, the `RequestBag` objects still exist and have not been disposed of. Here’s an example: ```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}!") }; } ``` This is a preliminary example of this operation. The instance of `User` was created within the request handler dedicated to authentication, and all routes that use this request handler will have the guarantee that there will be a `User` in their instance of `HttpContext.RequestBag`. It is possible to define logic to obtain instances when not previously defined in the `RequestBag` through methods like [GetOrAdd](https://docs.sisk-framework.org/api/Sisk.Core.Entity.TypedValueDictionary.GetOrAdd.md) or [GetOrAddAsync](https://docs.sisk-framework.org/api/Sisk.Core.Entity.TypedValueDictionary.GetOrAddAsync.md). Since version 1.3, the static property [HttpContext.Current](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.Current.md) was introduced, allowing access to the currently executing `HttpContext` of the request context. This enables exposing members of the `HttpContext` outside the current request and defining instances in route objects. The example below defines a controller that has members commonly accessed by the context of a request. ```csharp {title="Controllers/Controller.cs"} public abstract class Controller : RouterModule { // Get the existing or create a new database instance for this request protected DbContext Database => HttpContext.Current.RequestBag.GetOrAdd(() => new DbContext()); // Lazy loading repositories is common too 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)); // the following line will throw if the property is accessed when the User is not // defined in the request bag protected User AuthenticatedUser => => HttpContext.Current.RequestBag.Get(); // Exposing the HttpRequest instance is supported too protected HttpRequest Request => HttpContext.Current.Request } ``` And define types that inherit from the controller: ```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; } } ``` For the example above, you will need to configure a [value handler](https://docs.sisk-framework.org/docs/fundamentals/responses.md#implicit-response-types) in your router so that the objects returned by the router are transformed into a valid [HttpResponse](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpResponse.md). Note that the methods do not have an `HttpRequest request` argument as present in other methods. This is because, since version 1.3, the router supports two types of delegates for routing responses: [RouteAction](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteAction.md), which is the default delegate that receives an `HttpRequest` argument, and [ParameterlessRouteAction](https://docs.sisk-framework.org/api/Sisk.Core.Routing.ParameterlessRouteAction.md). The `HttpRequest` object can still be accessed by both delegates through the [Request](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.Request.md) property of the static `HttpContext` on the thread. In the example above, we defined a disposable object, the `DbContext`, and we need to ensure that all instances created in a `DbContext` are disposed of when the HTTP session ends. For this, we can use two ways to achieve this. One is to create a [request handler](https://docs.sisk-framework.org/docs/fundamentals/request-handlers.md) that is executed after the router's action, and the other way is through a custom [server handler](https://docs.sisk-framework.org/docs/advanced/http-server-handlers.md). For the first method, we can create the request handler inline directly in the [OnSetup](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouterModule.OnSetup.md) method inherited from `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) => { // get one DbContext defined in the request handler context and // dispose it ctx.RequestBag.GetOrDefault()?.Dispose(); return null; }, executionMode: RequestHandlerExecutionMode.AfterResponse)); } } ``` > [!TIP] > > Since Sisk version 1.4, the property [HttpServerConfiguration.DisposeDisposableContextValues](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.DisposeDisposableContextValues.md) is introduced and enabled by default, which defines whether the HTTP server should dispose all `IDisposable` values in the context bag when an HTTP session is closed. The method above will ensure that the `DbContext` is disposed of when the HTTP session is finalized. You can do this for more members that need to be disposed of at the end of a response. For the second method, you can create a custom [server handler](https://docs.sisk-framework.org/docs/advanced/http-server-handlers.md) that will dispose of the `DbContext` when the HTTP session is finalized. ```csharp {title="Server/Handlers/ObjectDisposerHandler.cs"} public class ObjectDisposerHandler : HttpServerHandler { protected override void OnHttpRequestClose(HttpServerExecutionResult result) { result.Context.RequestBag.GetOrDefault()?.Dispose(); } } ``` And use it in your app builder: ```csharp {title="Program.cs"} using var host = HttpServer.CreateBuilder() .UseHandler() .Build(); ``` This is a way to handle code cleanup and keep the dependencies of a request separated by the type of module that will be used, reducing the amount of duplicated code within each action of a router. It is a practice similar to what dependency injection is used for in frameworks like ASP.NET. --- # Streaming Content Source: https://docs.sisk-framework.org/docs/features/content-streaming.html The Sisk supports reading and sending streams of content to and from the client. This feature is useful for removing memory overhead for serializing and deserializing content during the lifetime of a request. ## Request content stream Small contents are automatically loaded into the HTTP connection buffer memory, quickly loading this content to [HttpRequest.Body](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Body.md) and [HttpRequest.RawBody](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RawBody.md). For larger contents, the [HttpRequest.GetRequestStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetRequestStream.md) method can be used to obtain the request content read stream. It is worth noting that the [HttpRequest.GetMultipartFormContent](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetMultipartFormContent.md) method reads the entire request content into memory, so it may not be useful for reading large contents. Consider the following example: ```csharp {title="Controller/UploadDocument.cs"} [RoutePost ( "/api/upload-document/" )] public async Task UploadDocument ( HttpRequest request ) { var fileName = request.RouteParameters [ "filename" ].GetString (); if (!request.HasContents) { // request does not have content 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 = "File sent successfully." } ) }; } ``` In the example above, the `UploadDocument` method reads the request content and saves the content to a file. No additional memory allocation is made except for the read buffer used by `Stream.CopyToAsync`. The example above removes the pressure of memory allocation for a very large file, which can optimize application performance. A good practice is to always use a [CancellationToken](https://learn.microsoft.com/pt-br/dotnet/api/system.threading.cancellationtoken) in an operation that can be time-consuming, such as sending files, as it depends on the network speed between the client and the server. The adjustment with a CancellationToken can be made in the following way: ```csharp {title="Controller/UploadDocument.cs"} // the cancellation token below will throw an exception if the 30-second timeout is reached. 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 = "The upload exceeded the maximum upload time (30 seconds)." } ) }; } ``` ## Response content stream Sending response content is also possible. Currently, there are two ways to do this: through the [HttpRequest.GetResponseStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetResponseStream.md) method and using a content of type [StreamContent](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.streamcontent?view=net-9.0). Consider a scenario where we need to serve an image file. To do this, we can use the following code: ```csharp {title="Controller/ImageController.cs"} [RouteGet ( "/api/profile-picture" )] public async Task UploadDocument ( HttpRequest request ) { // example method to obtain a profile picture 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}" } }; } ``` The method above makes a memory allocation every time it reads the image content. If the image is large, this can cause a performance problem, and in peak situations, even a memory overload and crash the server. In these situations, caching can be useful, but it will not eliminate the problem, since memory will still be reserved for that file. Caching will alleviate the pressure of having to allocate memory for every request, but for large files, it will not be enough. Sending the image through a stream can be a solution to the problem. Instead of reading the entire image content, a read stream is created on the file and copied to the client using a tiny buffer. #### Sending through the GetResponseStream method The [HttpRequest.GetResponseStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetResponseStream.md) method creates an object that allows sending chunks of the HTTP response as the content flow is prepared. This method is more manual, requiring you to define the status, headers, and content size before sending the content. ```csharp {title="Controller/ImageController.cs"} [RouteGet ( "/api/profile-picture" )] public async Task UploadDocument ( HttpRequest request ) { var profilePictureFilename = "profile-picture.jpg"; // in this form of sending, the status and header must be defined // before the content is sent 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 )) { // in this form of sending, it is also necessary to define the content size // before sending it. requestStreamManager.SetContentLength ( fs.Length ); // if you don't know the content size, you can use chunked-encoding // to send the content requestStreamManager.SendChunked = true; // and then, write to the output stream await fs.CopyToAsync ( requestStreamManager.ResponseStream ); } } ``` #### Sending content through a StreamContent The [StreamContent](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.streamcontent?view=net-9.0) class allows sending content from a data source as a byte stream. This form of sending is easier, removing the previous requirements, and even allowing the use of [compression encoding](https://docs.sisk-framework.org/docs/fundamentals/responses.md#gzip-deflate-and-brotli-compression) to reduce the content size. ```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] > > In this type of content, do not encapsulate the stream in a `using` block. The content will be automatically discarded by the HTTP server when the content flow is finalized, with or without errors. --- # Enabling CORS (Cross-Origin Resource Sharing) in Sisk Source: https://docs.sisk-framework.org/docs/features/cors.html Sisk has a tool that can be useful for handling [Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) when exposing your service publicly. This feature is not part of the HTTP protocol but a specific feature of web browsers defined by the W3C. This security mechanism prevents a web page from making requests to a different domain than the one that provided the web page. A service provider can allow certain domains to access its resources, or just one. ## Same Origin For a resource to be identified as "same origin", a request must identify the [Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Origin) header in its request: ```http GET /api/users HTTP/1.1 Host: example.com Origin: http://example.com ... ``` And the remote server must respond with an [Access-Control-Allow-Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) header with the same value as the requested origin: ```http HTTP/1.1 200 OK Access-Control-Allow-Origin: http://example.com ... ``` This verification is **explicit**: the host, port, and protocol must be the same as requested. Check the example: - A server responds that its `Access-Control-Allow-Origin` is `https://example.com`: - `https://example.net` - the domain is different. - `http://example.com` - the scheme is different. - `http://example.com:5555` - the port is different. - `https://www.example.com` - the host is different. In the specification, only the syntax is allowed for both headers, whether for requests and responses. The URL path is ignored. The port is also omitted if it is a default port (80 for HTTP and 443 for HTTPS). ```http Origin: null Origin: :// Origin: ://: ``` ## Enabling CORS Natively, you have the [CrossOriginResourceSharingHeaders](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.md) object within your [ListeningHost](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.md). You can configure CORS when initializing the server: ```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(); } ``` The code above will send the following headers for **all responses**: ```http HTTP/1.1 200 OK Access-Control-Allow-Origin: http://example.com Access-Control-Allow-Headers: Authorization Access-Control-Expose-Headers: Content-Type ``` These headers need to be sent for all responses to a web client, including errors and redirects. You may notice that the [CrossOriginResourceSharingHeaders](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.md) class has two similar properties: [AllowOrigin](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigin.md) and [AllowOrigins](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigins.md). Note that one is plural, while the other is singular. - The **AllowOrigin** property is static: only the origin you specify will be sent for all responses. - The **AllowOrigins** property is dynamic: the server checks if the request's origin is contained in this list. If it is found, it is sent for the response of that origin. When the allowed origin depends on the request, that is, when you use **AllowOrigins** or the [AutoAllowOrigin](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AutoAllowOrigin.md) value described below, Sisk also sends the `Vary: Origin` header so that caches do not reuse a response for a different origin. ### Wildcards and automatic headers Alternatively, you can use a wildcard (`*`) in the response's origin to specify that any origin is allowed to access the resource. However, this value is not allowed for requests that have credentials (authorization headers) and this operation [will result in an error](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS/Errors/CORSNotSupportingCredentials). You can work around this problem by explicitly listing which origins will be allowed through the [AllowOrigins](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigins.md) property or also use the [AutoAllowOrigin](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AutoAllowOrigin.md) constant in the value of [AllowOrigin](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigin.md). This magic property will define the `Access-Control-Allow-Origin` header for the same value as the `Origin` header of the request. You can also use [AutoFromRequestMethod](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AutoFromRequestMethod.md) and [AutoFromRequestHeaders](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AutoFromRequestHeaders.md) for behavior similar to `AllowOrigin`, which automatically responds based on the headers sent. ```csharp using var host = HttpServer.CreateBuilder() .UseCors(new CrossOriginResourceSharingHeaders( // Responds based on the request's Origin header allowOrigin: CrossOriginResourceSharingHeaders.AutoAllowOrigin, // Responds based on the Access-Control-Request-Method header or the request method allowMethods: [CrossOriginResourceSharingHeaders.AutoFromRequestMethod], // Responds based on the Access-Control-Request-Headers header or the sent headers allowHeaders: [CrossOriginResourceSharingHeaders.AutoFromRequestHeaders], exposeHeaders: [HttpKnownHeaderNames.ContentType, "X-Authenticated-Account-Id"], allowCredentials: true)) .Build(); ``` ## Other Ways to Apply CORS If you are dealing with [service providers](https://docs.sisk-framework.org/docs/extensions/service-providers.md), you can override values defined in the configuration file: ```csharp static async Task Main(string[] args) { using var app = HttpServer.CreateBuilder() .UsePortableConfiguration(...) .UseCors(cors => { // Will override the origin defined in the configuration // file. cors.AllowOrigin = "http://example.com"; }) .Build(); await app.StartAsync(); } ``` ## Disabling CORS on Specific Routes The `UseCors` property is available for both routes and all route attributes and can be disabled with the following example: ```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" }; } } ``` ## Replacing Values in the Response You can replace or remove values explicitly in a router action: ```csharp [RoutePrefix("api/widgets")] public class WidgetController : Controller { public IEnumerable GetWidgets(HttpRequest request) { // Removes the Access-Control-Allow-Credentials header request.Context.OverrideHeaders.AccessControlAllowCredentials = string.Empty; // Replaces the Access-Control-Allow-Origin request.Context.OverrideHeaders.AccessControlAllowOrigin = "https://contorso.com"; return new[] { "Green widget", "Red widget" }; } } ``` ## Preflight Requests A preflight request is an [OPTIONS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/OPTIONS) method request that the client sends before the actual request. When a request with the `OPTIONS` method matches the path of a route, but no route is explicitly configured for `OPTIONS`, the Sisk server responds with a `200 OK` and the applicable CORS headers, and then the client can proceed with the actual request. - If a route is explicitly mapped with [RouteMethod.Options](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md), for example with `MapOptions`, that route handles the request instead of the automatic response. - If the route that matches the path has `UseCors = false`, the automatic response is still sent, but without CORS headers. > [!NOTE] > Before Sisk 1.7, the automatic preflight response always carried the CORS headers, even for routes with `UseCors = false`. ## Disabling CORS Globally It is not possible to do this. To not use CORS, do not configure it. --- # File Server Source: https://docs.sisk-framework.org/docs/features/file-server.html Sisk provides the `Sisk.Http.FileSystem` namespace, which contains tools for serving static files, directory listing and file conversion. This feature allows you to serve files from a local directory, with support for range requests (audio/video streaming) and custom file processing. ## Serving static files The easiest way to serve static files is [Router.MapFileSystem](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MapFileSystem.md). This method maps a URL prefix to a directory on disk. ```cs using Sisk.Core.Http; using Sisk.Core.Http.FileSystem; // maps the root of the server to the current directory mainRouter.MapFileSystem("/", Directory.GetCurrentDirectory()); // maps /assets to the "public/assets" folder mainRouter.MapFileSystem( "/assets", Path.Combine(Directory.GetCurrentDirectory(), "public", "assets")); ``` When a request matches the route prefix, the `HttpFileServerHandler` will look for a file in the specified directory. If found, it will serve the file; otherwise, it will return a 404 response (or 403 if access is denied). `HttpFileServer.CreateServingRoute` is still available when you need to create a `Route` object explicitly, but `MapFileSystem` is the most direct option for application code. Both methods create a [PrefixRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.PrefixRoute.md) (a regex route before Sisk 1.7), which is a [dynamic route](https://docs.sisk-framework.org/docs/fundamentals/routing.md#matching-precedence). Static routes are always tested before it, so a file server mapped to `/` works as a fallback for the paths that your other routes do not handle. File server routes are not affected by [HttpServerConfiguration.ForceTrailingSlash](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ForceTrailingSlash.md). ## HttpFileServerHandler For more control over how files are served, you can instantiate and configure `HttpFileServerHandler` manually. ```cs var fileHandler = new HttpFileServerHandler("/var/www/html"); // enable directory listing (disabled by default) fileHandler.AllowDirectoryListing = true; // set a custom route prefix (this will be trimmed from the request path) fileHandler.RoutePrefix = "/public"; // register the handler under /public mainRouter.MapFileSystem("/public", fileHandler); ``` ### Configuration | Property | Description | |---|---| | `RootDirectoryPath` | The absolute or relative path to the root directory from which files are served. | | `RoutePrefix` | The route prefix that will be trimmed from the request path when resolving files. Default is `/`. | | `AllowDirectoryListing` | If set to `true`, enables directory listing when a directory is requested and no index file is found. Default is `false`. | | `FileConverters` | A list of `HttpFileServerFileConverter` used to transform files before serving them. | ## Directory Listing When `AllowDirectoryListing` is enabled, and the user requests a directory path, Sisk will generate an HTML page listing the contents of that directory. The directory listing includes: - Navigation to the parent directory (`..`). - List of subdirectories. - List of files with size and last modification date. ## File Converters File converters allow you to intercept specific file types and handle them differently. For example, you might want to transcode an image, compress a file on the fly, or serve a file using partial content (Range requests). Sisk includes two built-in converters for media streaming: - `HttpFileAudioConverter`: Handles `.mp3`, `.ogg`, `.wav`, `.flac`, `.ogv`. - `HttpFileVideoConverter`: Handles `.webm`, `.avi`, `.mkv`, `.mpg`, `.mpeg`, `.wmv`, `.mov`, `.mp4`. These converters enable support for **HTTP Range Requests**, allowing clients to seek through audio and video files. ### Creating a custom converter To create a custom file converter, inherit from `HttpFileServerFileConverter` and implement `CanConvert` and `Convert`. ```cs using Sisk.Core.Http; using Sisk.Core.Http.FileSystem; public class MyTextConverter : HttpFileServerFileConverter { public override bool CanConvert(FileInfo file) { // apply only to .txt files return file.Extension.Equals(".txt", StringComparison.OrdinalIgnoreCase); } public override HttpResponse Convert(FileInfo file, HttpRequest request) { string content = File.ReadAllText(file.FullName); // uppercase all text content return new HttpResponse(200) { Content = new StringContent(content.ToUpper()) }; } } ``` Then, add it to your handler: ```cs var handler = new HttpFileServerHandler("./files"); handler.FileConverters.Add(new MyTextConverter()); ``` --- # Model Context Protocol Source: https://docs.sisk-framework.org/docs/extensions/mcp.html It is possible to build applications that provide context to agent models using large language models (LLMs) using the [Sisk.ModelContextProtocol](https://www.nuget.org/packages/Sisk.ModelContextProtocol/) package: dotnet add package Sisk.ModelContextProtocol --prerelease This package exposes useful classes and methods for building MCP servers that work over [Streamable HTTP](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http), supporting single request/response messages. Since version 1.7.0-rc1, it supports stateless requests of protocol version `2026-07-28` and keeps the legacy initialization flow for clients that use the `2025-11-25`, `2025-06-18` and `2025-03-26` versions. It supports tools and skills. > [!NOTE] > > Before you start, note that this package is under development and may exhibit behaviors that do not conform to the specification. Read the [package details](https://github.com/sisk-http/core/tree/main/extensions/Sisk.ModelContextProtocol) to learn what is under development and what does not work yet. ## Getting Started with MCP The [McpProvider](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpProvider.md) class is the entry point for defining an MCP server. It is a sealed provider object that can be configured at startup. Your Sisk application can have one or more MCP providers. ```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}")); })); ``` If your application will provide only one MCP provider, you can use the builder's singleton: ```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(); } ``` The endpoint must accept both `GET` and `POST` requests, so `MapAny` is the simplest route mapping. `HandleMcpRequestAsync` returns an [HttpResponse](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpResponse.md), and your route must return it. If you need multiple providers in one app, skip the singleton and call [McpProvider.HandleRequestAsync](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpProvider.HandleRequestAsync.md) directly from each route: ```csharp var mathProvider = new McpProvider("math-server", "Mathematics server", new Version(1, 0)); router.MapAny("/mcp/math", async request => { return await mathProvider.HandleRequestAsync(request); }); ``` ## Creating JSON Schemas for Functions The [Sisk.ModelContextProtocol] library uses a fork of [LightJson](https://github.com/CypherPotato/LightJson) for JSON and JSON schema manipulation. This implementation provides a fluent JSON Schema builder for various objects: - JsonSchema.CreateObjectSchema - JsonSchema.CreateArraySchema - JsonSchema.CreateBooleanSchema - JsonSchema.CreateNumberSchema - JsonSchema.CreateStringSchema - JsonSchema.Empty Example: ```csharp JsonSchema.CreateObjectSchema( properties: new Dictionary() { { "numbers", JsonSchema.CreateArraySchema( itemsSchema: JsonSchema.CreateNumberSchema(), minItems: 1, description: "The numbers to sum.") } }, requiredProperties: ["numbers"]); ``` Produces the following schema: ```json { "type": "object", "properties": { "numbers": { "type": "array", "items": { "type": "number" }, "minItems": 1, "description": "The numbers to sum." } }, "required": ["numbers"] } ``` ## Handling Function Calls The function defined in the `executionHandler` parameter of [McpTool](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpTool.md) provides a JsonObject containing the call arguments that can be read fluently: ```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}")); })); ``` Tool arguments are validated against the schema before your handler runs. If validation fails, the provider returns an error result to the MCP client and does not invoke the tool handler. ## Function Results The [McpToolResult](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpToolResult.md) object provides three methods for creating content for a tool response: - [CreateAudio(ReadOnlySpan, string)](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpToolResult.CreateAudio.md): creates an audio-based response for the MCP client. - [CreateImage(ReadOnlySpan, string)](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpToolResult.CreateImage.md): creates an image-based response for the MCP client. - [CreateText(string)](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpToolResult.CreateText.md): creates a text-based response (the default) for the MCP client. Additionally, it is possible to combine multiple different contents into a single JSON tool response: ```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") ) })); ``` The provider currently handles `server/discover`, the legacy `initialize` and `ping` methods, `tools/list`, `tools/call`, `skills/list`, `skills/get`, `resources/list`, `resources/read`, and notifications. Unsupported JSON-RPC methods return a JSON-RPC error response. ## Publishing skills An MCP provider can also publish [Agent Skills](https://github.com/modelcontextprotocol/ext-skills) through the `io.modelcontextprotocol/skills` extension. Add [McpSkill](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpSkill.md) objects to [McpProvider.Skills](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpProvider.Skills.md) before the provider starts handling requests: ```csharp mcp.Skills.Add(new McpSkill( path: "acme/refunds", frontmatter: new JsonObject { ["name"] = "refunds", ["description"] = "Follow the refund workflow." }, body: "# Refunds\nRead references/policy.md before using the refund tools.", files: new Dictionary { ["references/policy.md"] = File.ReadAllBytes("skills/refunds/references/policy.md") })); ``` `McpSkill` generates the `SKILL.md` file from the frontmatter and the Markdown body. The last segment of `path` must be equal to the `name` field of the frontmatter. The skill files are copied when the skill is created and are never read from disk by the provider. Clients list and read skills with `skills/list`, `skills/get` and `resources/read`, using the `2026-07-28` protocol version. See the [package details](https://github.com/sisk-http/core/tree/main/extensions/Sisk.ModelContextProtocol) for the limits and validation rules. ## Continuing Work The Model Context Protocol is a communication protocol for agent models and applications that provide content to them. It is a new protocol, so it is common for its specification to be constantly updated with deprecations, new features, and breaking changes. It is crucial to understand the problems that the [Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro) solves before starting to build agent applications. Also read the specification of the [Sisk.ModelContextProtocol](https://github.com/sisk-http/core/tree/main/extensions/Sisk.ModelContextProtocol) package to understand its progress, status, and what can be done with it. --- # JSON-RPC Extension Source: https://docs.sisk-framework.org/docs/extensions/json-rpc.html Sisk has an experimental module for a [JSON-RPC 2.0](https://www.jsonrpc.org/specification) API, which allows you to create even simpler applications. This extension strictly implements the JSON-RPC 2.0 transport interface and offers transport via HTTP GET, POST requests, and also web-sockets with Sisk. You can install the extension via Nuget with the command below. Note that, in experimental/beta versions, you should enable the option to search for pre-release packages in Visual Studio. ```bash dotnet add package Sisk.JsonRpc ``` ## Transport Interface JSON-RPC is a stateless, asynchronous remote procedure call (RPC) protocol that uses JSON for data communication. A JSON-RPC request is typically identified by an ID, and a response is delivered by the same ID that was sent in the request. Not all requests require a response, which are called "notifications". The [JSON-RPC 2.0 specification](https://www.jsonrpc.org/specification) explains in detail how the transport works. This transport is agnostic of where it will be used. Sisk implements this protocol through HTTP, following the conformities of [JSON-RPC over HTTP](https://www.jsonrpc.org/historical/json-rpc-over-http.html), which partially supports GET requests, but completely supports POST requests. Web-sockets are also supported, providing asynchronous message communication. A JSON-RPC request looks similar to: ```json { "jsonrpc": "2.0", "method": "Sum", "params": [1, 2, 4], "id": 1 } ``` And a successful response looks similar to: ```json { "jsonrpc": "2.0", "result": 7, "id": 1 } ``` ## JSON-RPC Methods The following example shows how to create a JSON-RPC API using Sisk. A mathematical operations class performs the remote operations and delivers the serialized response to the client. ```csharp {title="Program.cs"} using var app = HttpServer.CreateBuilder(port: 5555) .UseJsonRPC((sender, args) => { // add all methods tagged with WebMethod to the JSON-RPC handler args.Handler.Methods.AddMethodsFromType(new MathOperations()); // maps the /service route to handle JSON-RPC POST and GET requests args.Router.MapPost("/service", args.Handler.Transport.HttpPost); args.Router.MapGet("/service", args.Handler.Transport.HttpGet); // maps the JSON-RPC WebSocket transport on 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); } } ``` The above example will map the `Sum` and `Sqrt` methods to the JSON-RPC handler, and these methods will be available at `GET /service`, `POST /service` and `GET /ws`. Method names are case-insensitive. Method parameters are automatically deserialized to their specific types. Using a request with named parameters is also supported. JSON serialization is done by the [LightJson](https://github.com/CypherPotato/LightJson) library. When a type is not correctly deserialized, you can create a specific [JSON converter](https://github.com/CypherPotato/LightJson?tab=readme-ov-file#json-converters) for that type and associate it with [JsonRpcHandler.JsonSerializerOptions](https://docs.sisk-framework.org/api/Sisk.JsonRPC.JsonRpcHandler.JsonSerializerOptions.md). You can also get the `$.params` raw object from the JSON-RPC request directly in your method. ```csharp {title="MathOperations.cs"} [WebMethod] public float Sum(JsonArray|JsonObject @params) { ... } ``` For this to occur, `@params` must be the **only** parameter in your method, with exactly the name `params` (in C#, the `@` is necessary to escape this parameter name). Parameter deserialization occurs for both named objects or positional arrays. For example, the following method can be called remotely by both requests: ```csharp [WebMethod] public float AddUserToStore(string apiKey, User user, UserStore store) { ... } ``` For an array, the order of the parameters must be followed. ```json { "jsonrpc": "2.0", "method": "AddUserToStore", "params": [ "1234567890", { "name": "John Doe", "email": "john@example.com" }, { "name": "My Store" } ], "id": 1 } ``` ## Customizing the serializer You can customize the JSON serializer in the [JsonRpcHandler.JsonSerializerOptions](https://docs.sisk-framework.org/api/Sisk.JsonRPC.JsonRpcHandler.JsonSerializerOptions.md) property. In this property, you can enable the use of [JSON5](https://json5.org/) for deserializing messages. Although not a conformity with JSON-RPC 2.0, JSON5 is an extension of JSON that allows for more human-readable and legible writing. ```csharp {title="Program.cs"} using var host = HttpServer.CreateBuilder ( 5556 ) .UseJsonRPC ( ( o, e ) => { // uses a sanitized name comparer. this comparer compares only letters // and digits in a name, and discards other symbols. ex: // foo_bar10 == FooBar10 e.Handler.JsonSerializerOptions.PropertyNameComparer = new JsonSanitizedComparer (); // enables JSON5 for the JSON interpreter. even activating this, plain JSON is still allowed e.Handler.JsonSerializerOptions.SerializationFlags = LightJson.Serialization.JsonSerializationFlags.Json5; // maps the POST /service route to the JSON RPC handler e.Router.MapPost ( "/service", e.Handler.Transport.HttpPost ); } ) .Build (); host.Start (); ``` --- # Service Providers Source: https://docs.sisk-framework.org/docs/extensions/service-providers.html Service Providers is a way to port your Sisk application to different environments with a portable configuration file. This feature allows you to change the server's port, parameters, and other options without having to modify the application code for each environment. This module depends on the Sisk construction syntax and can be configured through the UsePortableConfiguration method. A configuration provider is implemented with IConfigurationProvider, which provides a configuration reader and can receive any implementation. By default, Sisk provides a JSON configuration reader, but there is also a package for INI files. You can also create your own configuration provider and register it with: ```csharp using var app = HttpServer.CreateBuilder() .UsePortableConfiguration(config => { config.WithConfigReader(); }) .Build(); ``` As mentioned earlier, the default provider is a JSON file. By default, the file name searched for is service-config.json, and it is searched in the current directory of the running process, not the executable directory. You can choose to change the file name, as well as where Sisk should look for the configuration file, with: ```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(); ``` The code above will look for the config.toml file in the current directory of the running process. If not found, it will then search in the directory where the executable is located. If the file does not exist, the createIfDontExists parameter is honored, creating the file, without any content, in the last tested path (based on lookupDirectories), and an error is thrown in the console, preventing the application from initializing. > [!TIP] > > You can look at the source code of the INI configuration reader and the JSON configuration reader to understand how an IConfigurationProvider is implemented. ## Reading configurations from a JSON file By default, Sisk provides a configuration provider that reads configurations from a JSON file. This file follows a fixed structure and is composed of the following parameters: ```json { "Server": { "DefaultEncoding": "UTF-8", "ThrowExceptions": true, "IncludeRequestIdHeader": true }, "ListeningHost": { "Label": "My sisk application", "Ports": [ "http://localhost:80/", "https://localhost:443/", // Configuration files also support comments ], "CrossOriginResourceSharingPolicy": { "AllowOrigin": "*", "AllowOrigins": [ "*" ], // new on 0.14 "AllowMethods": [ "*" ], "AllowHeaders": [ "*" ], "MaxAge": 3600 }, "Parameters": { "MySqlConnection": "server=localhost;user=root;" } } } ``` The parameters created from a configuration file can be accessed in the server constructor: ```csharp using var app = HttpServer.CreateBuilder() .UsePortableConfiguration(config => { config.WithParameters(paramCollection => { string databaseConnection = paramCollection.GetValueOrThrow("MySqlConnection"); }); }) .Build(); ``` Each configuration reader provides a way to read the server initialization parameters. Some properties are indicated to be in the process environment instead of being defined in the configuration file, such as sensitive API data, API keys, etc. ## Configuration file structure The JSON configuration file is composed of the following properties:
    Property Mandatory Description
    Server Required Represents the server itself with its settings.
    Server.AccessLogsStream Optional Default to console. Specifies the access log output stream. Can be a filename, null or console.
    Server.ErrorsLogsStream Optional Default to null. Specifies the error log output stream. Can be a filename, null or console.
    Server.MaximumContentLength Optional
    Server.MaximumContentLength Optional Specifies the maximum content length in bytes. Zero means infinite. When the Server object is present and this property is omitted, the value is 0 (infinite), which overrides the 100 MB default of HttpServerConfiguration.MaximumContentLength.
    Server.IncludeRequestIdHeader Optional Default to false. Specifies if the HTTP server should send the X-Request-Id header.
    Server.ThrowExceptions Optional Default to true. Specifies if unhandled exceptions should be thrown. Set to false when production and true when debugging.
    ListeningHost Required Represents the server listening host.
    ListeningHost.Label Optional Represents the application label.
    ListeningHost.Ports Required Represents an array of strings, matching the ListeningPort syntax.
    ListeningHost.CrossOriginResourceSharingPolicy Optional Setup the CORS headers for the application.
    ListeningHost.CrossOriginResourceSharingPolicy.AllowCredentials Optional Defaults to false. Specifies the Allow-Credentials header.
    ListeningHost.CrossOriginResourceSharingPolicy.ExposeHeaders Optional Defaults to null. This property expects an array of strings. Specifies the Expose-Headers header.
    ListeningHost.CrossOriginResourceSharingPolicy.AllowOrigin Optional Defaults to null. This property expects an string. Specifies the Allow-Origin header.
    ListeningHost.CrossOriginResourceSharingPolicy.AllowOrigins Optional Defaults to null. This property expects an array of strings. Specifies multiples Allow-Origin headers. See AllowOrigins for more information.
    ListeningHost.CrossOriginResourceSharingPolicy.AllowMethods Optional Defaults to null. This property expects an array of strings. Specifies the Allow-Methods header.
    ListeningHost.CrossOriginResourceSharingPolicy.AllowHeaders Optional Defaults to null. This property expects an array of strings. Specifies the Allow-Headers header.
    ListeningHost.CrossOriginResourceSharingPolicy.MaxAge Optional Defaults to null. This property expects an integer. Specifies the Max-Age header in seconds.
    ListeningHost.Parameters Optional Specifies the properties provided to the application setup method.
    --- # INI configuration provider Source: https://docs.sisk-framework.org/docs/extensions/ini-configuration.html Sisk has a method for obtaining startup configurations other than JSON. In fact, any pipeline that implements [IConfigurationReader](https://docs.sisk-framework.org/api/Sisk.Core.Http.Hosting.IConfigurationReader.md) can be used with [PortableConfigurationBuilder.WithConfigurationPipeline](https://docs.sisk-framework.org/api/Sisk.Core.Http.Hosting.PortableConfigurationBuilder.md), reading the server configuration from any file type. The [Sisk.IniConfiguration](https://www.nuget.org/packages/Sisk.IniConfiguration/) package provides a stream-based INI file reader that does not throw exceptions for common syntax errors and has a simple configuration syntax. This package can be used outside the Sisk framework, offering flexibility for projects that require an efficient INI document reader. ## Installing To install the package, you can start with: ```bash $ dotnet add package Sisk.IniConfiguration ``` You can also install the core package, which doens't includes the INI [IConfigurationReader](https://docs.sisk-framework.org/api/Sisk.Core.Http.Hosting.IConfigurationReader), neither the Sisk dependency, just the INI serializers: ```bash $ dotnet add package Sisk.IniConfiguration.Core ``` With the main package, you can use it in your code as shown in the example below: ```cs class Program { static HttpServerHostContext Host = null!; static void Main(string[] args) { Host = HttpServer.CreateBuilder() .UsePortableConfiguration(config => { config.WithConfigFile("app.ini", createIfDontExists: true); // uses the IniConfigurationReader configuration reader 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}!"); } } ``` The code above will look for an app.ini file in the process's current directory (CurrentDirectory). The INI file looks like this: ```ini [Server] # Multiple listen addresses are supported Listen = http://localhost:5552/ Listen = http://localhost:5553/ ThrowExceptions = false AccessLogsStream = console [Cors] AllowMethods = GET, POST AllowHeaders = Content-Type, Authorization AllowOrigin = * [Parameters] Name = "Kanye West" ``` ## INI flavor and syntax Current implementation flavor: - Properties and section names are **case-insensitive**. - Properties names and values are **trimmed**, unless values are quoted. - Values can be quoted with single or double quotes. Quotes can have line-breaks inside them. - Comments are supported with `#` and `;`. Also, **trailing comments are allowed**. - Properties can have multiple values. In detail, the documentation for the "flavor" of the INI parser used in Sisk is [available in this document](https://github.com/sisk-http/archive/blob/master/ext/ini-reader-syntax.md). Using the following ini code as example: ```ini One = 1 Value = this is an value Another value = "this value has an line break on it" ; the code below has some colors [some section] Color = Red Color = Blue Color = Yellow ; do not use yellow ``` Parse it with: ```csharp // parse the ini text from the string IniDocument doc = IniDocument.FromString(iniText); // get one value string? one = doc.Global.GetOne("one"); string? anotherValue = doc.Global.GetOne("another value"); // get multiple values string[]? colors = doc.GetSection("some section")?.GetMany("color"); ``` ## Configuration parameters | Section and name | Allow multiple values | Description | | ---------------- | --------------------- | ----------- | | `Server.Listen` | Yes | The server listening addresses/ports. | | `Server.Encoding` | No | The server default encoding. | | `Server.MaximumContentLength` | No | The server max content-length size in bytes. When omitted, the server default is kept (100 MB since Sisk 1.7). Use `0` for no limit. | | `Server.IncludeRequestIdHeader` | No | Specifies if the HTTP server should send the X-Request-Id header. | | `Server.ThrowExceptions` | No | Specifies if unhandled exceptions should be thrown. | | `Server.AccessLogsStream` | No | Specifies the access log output stream. | | `Server.ErrorsLogsStream` | No | Specifies the error log output stream. | | `Cors.AllowMethods` | No | Specifies the CORS Allow-Methods header value. | | `Cors.AllowHeaders` | No | Specifies the CORS Allow-Headers header value. | | `Cors.AllowOrigins` | No | Specifies multiples Allow-Origin headers, separated by commas. [AllowOrigins](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigins.md) for more information. | | `Cors.AllowOrigin` | No | Specifies one Allow-Origin header. | | `Cors.ExposeHeaders` | No | Specifies the CORS Expose-Headers header value. | | `Cors.AllowCredentials` | No | Specifies the CORS Allow-Credentials header value. | | `Cors.MaxAge` | No | Specifies the CORS Max-Age header value. | --- # API Documentation Source: https://docs.sisk-framework.org/docs/extensions/api-documentation.html The `Sisk.Documenting` extension allows you to generate API documentation for your Sisk application automatically. It leverages your code structure and attributes to create a comprehensive documentation site, supporting export to Open API (Swagger) format. > [!WARNING] > This package is published as a prerelease (1.7.0-rc3). Its behavior and API may be subject to change in future updates. Install the [Sisk.Documenting](https://www.nuget.org/packages/Sisk.Documenting/) package from NuGet. To serve the documentation as an HTML page, also install [Sisk.Documenting.Html](https://www.nuget.org/packages/Sisk.Documenting.Html/): ```sh dotnet add package Sisk.Documenting --prerelease dotnet add package Sisk.Documenting.Html --prerelease ``` The source code is available [here](https://github.com/sisk-http/core/tree/main/extensions/Sisk.Documenting). To use `Sisk.Documenting`, you need to register it in your application builder and decorate your route handlers with documentation attributes. ### Registering documentation generation Use the `UseApiDocumentation` extension method on your `HttpServerHostContextBuilder` to expose generated API documentation from the same router that serves your application. ```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**: Defines metadata about your application, such as name, description, and version. - **routerPath**: The URL path where the documentation user interface (or JSON) will be accessible. - **exporter**: Configures how the documentation is exported. The `OpenApiExporter` enables Open API (Swagger) support. ### Documenting Endpoints You can describe your endpoints using the `[ApiEndpoint]` and `[ApiQueryParameter]` attributes on your route handler methods. ### `ApiEndpoint` The `[ApiEndpoint]` attribute allows you to provide a description for the endpoint. ```csharp [ApiEndpoint(Description = "Returns a greeting message.")] public HttpResponse Index(HttpRequest request) { ... } ``` ### `ApiQueryParameter` The `[ApiQueryParameter]` attribute documents query string parameters that the endpoint accepts. ```csharp [ApiQueryParameter(name: "name", IsRequired = false, Description = "The name of the person to greet.", Type = "string")] public HttpResponse Index(HttpRequest request) { ... } ``` - **name**: The name of the query parameter. - **IsRequired**: Specifies if the parameter is mandatory. - **Description**: A human-readable description of the parameter. - **Type**: The expected data type (e.g., "string", "int"). ### `ApiEndpoint` Annotates an endpoint with general information. * **Name** (string, required in constructor): The name of the API endpoint. * **Description** (string): A brief description of what the endpoint does. * **Group** (string): Allows grouping endpoints (e.g., by controller or module). * **InheritDescriptionFromXmlDocumentation** (bool, default: `true`): If `true`, attempts to use the method's XML documentation summary if `Description` is not set. ### `ApiHeader` Documents a specific HTTP header that the endpoint expects or utilizes. * **HeaderName** (string, required in constructor): The key of the header (e.g., "Authorization"). * **Description** (string): Describes the header's purpose. * **IsRequired** (bool): Indicates if the header is mandatory for the request. ### `ApiParameter` Defines a generic parameter for the endpoint, often used for form fields or body parameters not covered by other attributes. * **Name** (string, required in constructor): The name of the parameter. * **TypeName** (string, required in constructor): The data type of the parameter (e.g., "string", "int"). * **Description** (string): A description of the parameter. * **IsRequired** (bool): Indicates if the parameter is mandatory. ### `ApiParameterExample` Adds an example value for a documented parameter in a particular situation. The attribute can be applied multiple times to the same method. * **ParameterName** (string, required in constructor): The name of the parameter to which the example applies. * **Title** (string, required in constructor): A title that identifies the situation illustrated by the example. * **Description** (string): When or why the example applies. * **Example** (string): The example value. * **ExampleLanguage** (string): The language used to format the example value, if applicable. ```csharp [ApiQueryParameter("status", Type = "string", Description = "Filters orders by status.")] [ApiParameterExample("status", "Pending orders", Example = "pending")] [ApiParameterExample("status", "Shipped orders", Example = "shipped", Description = "Orders that left the warehouse.")] ``` ### `ApiParametersFrom` Automatically generates parameter documentation from the properties of a specified class or type. * **Type** (Type, required in constructor): The class `Type` to reflect properties from. ### `ApiPathParameter` Documents a path variable (e.g., in `/users/{id}`). * **Name** (string, required in constructor): The name of the path parameter. * **Description** (string): Describes what the parameter represents. * **Type** (string): The expected data type. ### `ApiQueryParameter` Documents a query string parameter (e.g., `?page=1`). * **Name** (string, required in constructor): The key of the query parameter. * **Description** (string): Describes the parameter. * **Type** (string): The expected data type. * **IsRequired** (bool): Indicates if the query parameter must be present. ### `ApiRequest` Describes the expected request body. * **Description** (string, required in constructor): A description of the request body. * **Example** (string): A raw string containing an example of the request body. * **ExampleLanguage** (string): The language of the example (e.g., "json", "xml"). * **PayloadType** (Type): If set, the example and schema will be generated automatically from this type when the configured context handlers support it. ### `ApiResponse` Describes a possible response from the endpoint. * **StatusCode** (HttpStatusCode, required in constructor): The HTTP status code returned (e.g., `HttpStatusCode.OK`). * **Description** (string): Describes the condition for this response. * **Example** (string): A raw string containing an example of the response body. * **ExampleLanguage** (string): The language of the example. * **PayloadType** (Type): If set, the example and schema will be generated automatically from this type when the configured context handlers support it. ## Type Handlers Type handlers are responsible for converting your .NET types (classes, enums, etc.) into documentation examples. This is particularly useful for generating automatic request and response body examples based on your data models. These handlers are configured within the `ApiGenerationContext`. ```csharp using Sisk.Documenting.Content; var context = new ApiGenerationContext() { // ... BodyExampleTypeHandler = new JsonContentTypeHandler(), ParameterExampleTypeHandler = new JsonContentTypeHandler(), ContentSchemaTypeHandler = new JsonContentTypeHandler() }; ``` ### JsonContentTypeHandler The `JsonContentTypeHandler` is a built-in handler that generates JSON examples, parameter examples, and JSON schemas. It implements `IExampleBodyTypeHandler`, `IExampleParameterTypeHandler`, and `IContentSchemaTypeHandler`. It can be customized with specific `JsonSerializerOptions` or `IJsonTypeInfoResolver` to match your application's serialization logic. ```csharp var jsonHandler = new JsonContentTypeHandler(new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true }); context.BodyExampleTypeHandler = jsonHandler; context.ParameterExampleTypeHandler = jsonHandler; context.ContentSchemaTypeHandler = jsonHandler; ``` ### Custom Type Handlers You can implement your own handlers to support other formats (like XML) or to customize how examples are generated. #### IExampleBodyTypeHandler Implement this interface to generate body examples for request and response types. ```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 Implement this interface to generate detailed parameter descriptions from a type (used by `[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(); } } ``` ## Exporters Exporters are responsible for converting the collected API documentation metadata into a specific format that can be consumed by other tools or displayed to the user. ### OpenApiExporter The default exporter provided is the `OpenApiExporter`, which generates a JSON file following the [OpenAPI Specification 3.0.0](https://spec.openapis.org/oas/v3.0.0). ```csharp new OpenApiExporter() { OpenApiVersion = "3.0.0", ServerUrls = new[] { "http://localhost:5555" }, Contact = new OpenApiContact() { Name = "Support", Email = "support@example.com", Url = "https://example.com/support" }, License = new OpenApiLicense() { Name = "MIT", Url = "https://opensource.org/licenses/MIT" }, TermsOfService = "https://example.com/terms" } ``` ### LlmsApiExporter The `LlmsApiExporter` generates a plain text document in the [llms.txt](https://llmstxt.org/) style, intended to be read by language models. You can set the `Hostname` used in the examples, and a `HeaderText` and a `FooterText` that are written at the beginning and at the end of the document: ```csharp new LlmsApiExporter( hostname: "https://api.example.com", headerText: "Public API of the Example service.", footerText: "Contact support@example.com for an API key.") ``` ### Creating a Custom Exporter You can create your own exporter by implementing the `IApiDocumentationExporter` interface. This allows you to output documentation in formats such as Markdown, HTML, Postman Collection, or any other custom format. The interface requires you to implement a single method: `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"); } } ``` Then, simply use it in your configuration: ```csharp host.UseApiDocumentation( // ... exporter: new MyCustomExporter() ); ``` ### Full Example Below is a complete example demonstrating how to set up `Sisk.Documenting` and document a simple controller. ```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}!"); } } ``` In this example, accessing `/api/docs` will serve the generated documentation for the "My application" API, describing the `GET /` endpoint and its `name` parameter. --- # Basic Auth (discontinued) Source: https://docs.sisk-framework.org/docs/extensions/basic-auth.html > [!CAUTION] > **Discontinued, do not use.** The Sisk.BasicAuth package is no longer maintained and is deprecated on NuGet, with no replacement package. Its `BasicAuthenticateRequestHandler` does not decode the Base64 credentials sent by standard HTTP clients, so authenticated requests are rejected. Implement Basic authentication with your own [request handler](https://docs.sisk-framework.org/docs/fundamentals/request-handlers.md) that reads [HttpHeaderCollection.Authorization](https://docs.sisk-framework.org/api/Sisk.Core.Entity.HttpHeaderCollection.Authorization.md) instead. This page is kept for historical reference only. The Basic Auth package adds a request handler capable of handling basic authentication scheme in your Sisk application with very little configuration and effort. Basic HTTP authentication is a minimal input form of authenticating requests by an user id and password, where the session is controlled exclusively by the client and there are no authentication or access tokens. ![Basic Auth](https://docs.sisk-framework.org/assets/img/basic-auth.svg) Read more about the Basic authentication scheme in the [MDN specification](https://developer.mozilla.org/pt-BR/docs/Web/HTTP/Authentication). ## Installing To get started, install the Sisk.BasicAuth package in your project: > dotnet add package Sisk.BasicAuth You can view more ways to install it in your project in the [Nuget repository](https://www.nuget.org/packages/Sisk.BasicAuth/0.15.0). ## Creating your auth handler You can control the authentication scheme for an entire module or for individual routes. For that, let's first write our first basic authentication handler. In the example below, a connection is made to the database, it checks if the user exists and if the password is valid, and after that, stores the user in the context bag. ```cs public class UserAuthHandler : BasicAuthenticateRequestHandler { public UserAuthHandler() : base() { Realm = "To enter this page, please, inform your credentials."; } public override HttpResponse? OnValidating(BasicAuthenticationCredentials credentials, HttpContext context) { DbContext db = new DbContext(); // in this case, we're using the email as the user id field, so we're // going to search for an user using their email. User? user = db.Users.FirstOrDefault(u => u.Email == credentials.UserId); if (user == null) { return base.CreateUnauthorizedResponse("Sorry! No user was found by this email."); } // validates that the credentials password is valid for this user. if (!user.ValidatePassword(credentials.Password)) { return base.CreateUnauthorizedResponse("Invalid credentials."); } // adds the logged user to the http context // and continues the execution context.Bag.Add("loggedUser", user); return null; } } ``` So, just associate this request handler with our route or class. ```cs public class UsersController { [RouteGet("/")] [RequestHandler(typeof(UserAuthHandler))] public string Index(HttpRequest request) { User loggedUser = request.Bag.Get(); return $"Hello, {loggedUser.Name}!"; } } ``` Or using [RouterModule](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouterModule.md) class: ```cs public class UsersController : RouterModule { public ClientModule() { // all routes inside this class will be handled by // UserAuthHandler. base.HasRequestHandler(new UserAuthHandler()); } [RouteGet("/")] public string Index(HttpRequest request) { User loggedUser = request.Bag.Get(); return $"Hello, {loggedUser.Name}!"; } } ``` ## Remarks The primary responsibility of basic authentication is carried out on the client-side. Storage, cache control, and encryption are all handled locally on the client. The server only receives the credentials and validates whether access is allowed or not. Note that this method is not one of the most secure because it places a significant responsibility on the client, which can be difficult to trace and maintain the security of its credentials. Additionally, it is crucial for passwords to be transmitted in a secure connection context (SSL), as they do not have any inherent encryption. A brief interception in the headers of a request can expose the access credentials of your user. Opt for more robust authentication solutions for applications in production and avoid using too many off-the-shelf components, as they may not adapt to the needs of your project and end up exposing it to security risks. --- # SSL Proxy (discontinued) Source: https://docs.sisk-framework.org/docs/extensions/ssl-proxy.html > [!CAUTION] > **Discontinued, do not use.** The Sisk.SslProxy package is no longer maintained and is deprecated on NuGet, with no replacement package. It no longer compiles against the current Cadente API. To serve HTTPS, use the native SSL support of the [Cadente engine](https://docs.sisk-framework.org/docs/cadente.md) or one of the options in [Working with SSL](https://docs.sisk-framework.org/docs/ssl.md). This page is kept for historical reference only. The Sisk SSL Proxy is a module that provides an HTTPS connection for a [ListeningHost](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.md) in Sisk and routes HTTPS messages to an insecure HTTP context. The module was built to provide SSL connection for a service that uses [HttpListener](https://learn.microsoft.com/pt-br/dotnet/api/system.net.httplistener?view=net-8.0) to run, which does not support SSL. The proxy runs within the same application and listens for HTTP/1.1 messages, forwarding them in the same protocol to Sisk. Currently, this feature is highly experimental and may be unstable enough to not be used in production. At present, the SslProxy supports almost all HTTP/1.1 features, such as keep-alive, chunked encoding, websockets, etc. For an open connection to the SSL proxy, a TCP connection is created to the target server, and the proxy is forwarded to the established connection. The SslProxy can be used with HttpServer.CreateBuilder as follows: ```csharp using var app = HttpServer.CreateBuilder(port: 5555) .UseRouter(r => { r.MapGet("/", request => { return new HttpResponse("Hello, world!"); }); }) // add SSL to the project .UseSsl( sslListeningPort: 5567, new X509Certificate2(@".\ssl.pfx", password: "12345") ) .Build(); app.Start(); ``` You must provide a valid SSL certificate for the proxy. To ensure that the certificate is accepted by browsers, remember to import it into the operating system so that it functions correctly. --- # Manual (advanced) setup Source: https://docs.sisk-framework.org/docs/advanced/manual-setup.html Use manual setup when you need to assemble the server pieces yourself, such as when one process must expose multiple hosts, ports, routers, or custom server configuration. For most applications, the builder API is shorter and should be preferred. Manual setup is useful when you want direct control over the four core pieces: a `Router`, one or more `ListeningHost` objects, an `HttpServerConfiguration`, and the final `HttpServer`. First, we need to understand the request/response concept. It is quite simple: for every request, there must be a response. Sisk follows this principle as well. Let's create a method that responds with a "Hello, World!" message in HTML, specifying the status code and headers. ```csharp // Program.cs using Sisk.Core.Http; using Sisk.Core.Routing; static HttpResponse IndexPage(HttpRequest request) { HttpResponse indexResponse = new HttpResponse { Status = System.Net.HttpStatusCode.OK, Content = new HtmlContent(@"

    Hello, world!

    ") }; return indexResponse; } ``` The next step is to associate this method with an HTTP route. ## Routers Routers are abstractions of request routes and serve as the bridge between requests and responses for the service. Routers manage service routes, functions, and errors. A router can have several routes, and each route can perform different operations on that path, such as executing a function, serving a page, or providing a resource from the server. Let's create our first router and associate our `IndexPage` method with the index path. ```csharp Router mainRouter = new Router(); mainRouter.MapGet("/", IndexPage); ``` Now our router can receive requests and send responses. However, `mainRouter` is not tied to a host or a server, so it will not work on its own. The next step is to create our ListeningHost. ## Listening Hosts and Ports A [ListeningHost](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.md) can host a router and multiple listening ports for the same router. A [ListeningPort](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningPort.md) is a prefix where the HTTP server will listen. Here, we can create a `ListeningHost` that points to two endpoints for our router: ```csharp ListeningHost myHost = new ListeningHost { Router = mainRouter, Ports = new ListeningPort[] { new ListeningPort("http://localhost:5000/") } }; ``` Now our HTTP server will listen to the specified endpoints and redirect its requests to our router. ## Server Configuration Server configuration is responsible for most of the behavior of the HTTP server itself. In this configuration, we can associate `ListeningHosts` with our server. ```csharp HttpServerConfiguration config = new HttpServerConfiguration(); config.ListeningHosts.Add(myHost); // Add our ListeningHost to this server configuration ``` Common server configuration options: | Property | Default | Use when | Notes | | --- | --- | --- | --- | | [RemoteRequestsAction](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.RemoteRequestsAction.md) | `RequestListenAction.Accept` | The service should reject non-local requests unless they come through a trusted reverse proxy. | Set to `Drop` only when your deployment topology is clear. | | [IncludeRequestIdHeader](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.IncludeRequestIdHeader.md) | `false` | Clients or proxies need the Sisk request id in the `X-Request-Id` response header. | Pair with logs that include `HttpRequest.RequestId`. | | [IdleConnectionTimeout](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.IdleConnectionTimeout.md) | `120` seconds | Idle keep-alive connections should be closed sooner or later. | This is applied by the HTTP engine. | | [NormalizeHeadersEncodings](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.NormalizeHeadersEncodings.md) | `false` | You receive headers with an encoding mismatch. | This has a processing cost; leave it disabled unless needed. | | [SendSiskHeader](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.SendSiskHeader.md) | `true` | You want to hide or expose the `X-Powered-By` Sisk header. | Disable it for stricter production header policies. | | [OptionsLogMode](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.OptionsLogMode.md) | `LogOutput.Both` | You want to reduce or redirect logs generated by automatic `OPTIONS` handling. | Uses the same log mode values as routes. | | [AsyncRequestProcessing](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.AsyncRequestProcessing.md) | `true` | You need deterministic single-request processing for diagnostics. | Disabling it limits throughput. | | [DisposeDisposableContextValues](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.DisposeDisposableContextValues.md) | `true` | Request bag values that implement `IDisposable` should be disposed automatically. | Keep enabled unless ownership is managed elsewhere. | | [ConvertIAsyncEnumerableIntoEnumerable](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ConvertIAsyncEnumerableIntoEnumerable.md) | `true` | Value handlers should receive async enumerables as blocking enumerable values. | Disable when you implement your own async-stream handling. | | [KeepAlive](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.KeepAlive.md) | `true` | Connections should remain reusable after responses. | Disable for clients or intermediaries that do not handle persistent connections well. | | [ForceTrailingSlash](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ForceTrailingSlash.md) | `false` | GET routes should redirect to a trailing-slash URL. | Applies only to non-regex routes. | | [MaximumContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.MaximumContentLength.md) | `100 MB` | Request bodies or WebSocket messages need a different size limit. | The default was `0` (unlimited) before Sisk 1.7. Set it to `0` to remove the limit. | | [EnableAutomaticResponseCompression](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.EnableAutomaticResponseCompression.md) | `false` | Responses should be compressed automatically when the client supports it. | Existing `CompressedContent` responses are not compressed again. | Next, we can create our HTTP server: ```csharp HttpServer server = new HttpServer(config); server.Start(); // Starts the server Console.ReadKey(); // Prevents the application from exiting ``` Now we can compile our executable and run our HTTP server with the command: ```bash dotnet watch ``` At runtime, open your browser and navigate to the server path, and you should see: --- # Request lifecycle Source: https://docs.sisk-framework.org/docs/advanced/request-lifecycle.html Below is explained the entire life cycle of a request through an example of an HTTP request. - **Receiving the request:** each request creates an HTTP context between the request itself and the response that will be delivered to the client. This context comes from the built-in listener in Sisk, which can be the [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), or [Cadente](https://blog.sisk-framework.org/posts/2025-01-29-cadente-experiment/). - External request validation: the validation of [HttpServerConfiguration.RemoteRequestsAction](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.RemoteRequestsAction.md) is validated for the request. - If the request is external and the property is `Drop`, the connection is closed without a response to the client with an `HttpServerExecutionStatus = RemoteRequestDropped`. - Forwarding Resolver configuration: if a [ForwardingResolver](https://docs.sisk-framework.org/docs/advanced/forwarding-resolvers.md) is configured, it will call the [OnResolveRequestHost](https://docs.sisk-framework.org/api/Sisk.Core.Http.ForwardingResolver.OnResolveRequestHost.md) method on the original host of the request. - DNS matching: with the host resolved and with more than one [ListeningHost](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.md) configured, the server will look for the corresponding host for the request. - If no ListeningHost matches, a 400 Bad Request response is returned to the client and an `HttpServerExecutionStatus = DnsUnknownHost` status is returned to the HTTP context. - If a ListeningHost matches, but its [Router](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.Router.md) is not yet initialized, a 503 Service Unavailable response is returned to the client and an `HttpServerExecutionStatus = ListeningHostNotReady` status is returned to the HTTP context. - Router binding: the router of the corresponding ListeningHost is associated with the received HTTP server. - If the router is already associated with another HTTP server, which is not allowed because the router actively uses the server's configuration resources, an `InvalidOperationException` is thrown. This only occurs during the initialization of the HTTP server, not during the creation of the HTTP context. - Pre-definition of headers: - Predefines the `X-Request-Id` header in the response if it is configured to do so. - Predefines the `X-Powered-By` header in the response if it is configured to do so. - Content size validation: validates if the request content is less than [HttpServerConfiguration.MaximumContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.MaximumContentLength.md) (100 MB by default) only if it is greater than zero. - If the request sends a `Content-Length` greater than the configured one, a 413 Payload Too Large response is returned to the client and an `HttpServerExecutionStatus = ContentTooLarge` status is returned to the HTTP context. - The `OnHttpRequestOpen` event is invoked for all configured HTTP server handlers. - **Routing the action:** the server invokes the router for the received request. - If the router does not find a route that matches the request: - If the [Router.NotFoundErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.NotFoundErrorHandler.md) property is configured, the action is invoked, and the response of the action is forwarded to the HTTP client. - If the previous property is null, a default 404 Not Found response is returned to the client. - If the router finds a matching route, but the route's method does not match the request's method: - If the [Router.MethodNotAllowedErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MethodNotAllowedErrorHandler.md) property is configured, the action is invoked and the response of the action is forwarded to the HTTP client. - If the previous property is null, a default 405 Method Not Allowed response is returned to the client. - If the request is of the `OPTIONS` method: - The router returns a 200 Ok response to the client only if no route matches the request method (the route's method is not explicitly [RouteMethod.Options](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md)). - If the [HttpServerConfiguration.ForceTrailingSlash](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ForceTrailingSlash.md) property is enabled, the matched route is not a regex, the request path does not end with `/`, and the request method is `GET`: - A 307 Temporary Redirect HTTP response with the `Location` header with the path and query to the same location with a `/` at the end is returned to the client. - The `OnContextBagCreated` event is invoked for all configured HTTP server handlers. - All global [IRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.md) instances with the `BeforeResponse` flag are executed. - If any handler returns a non-null response, that response is forwarded to the HTTP client and the context is closed. - If an error is thrown in this step and [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) is disabled: - If the [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) property is enabled, it is invoked and the resulting response is returned to the client. - If the previous property is not defined, an empty response is returned to the server, which forwards a response according to the type of exception thrown, which is usually 500 Internal Server Error. - All [IRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.md) instances defined in the route and with the `BeforeResponse` flag are executed. - If any handler returns a non-null response, that response is forwarded to the HTTP client and the context is closed. - If an error is thrown in this step and [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) is disabled: - If the [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) property is enabled, it is invoked and the resulting response is returned to the client. - If the previous property is not defined, an empty response is returned to the server, which forwards a response according to the type of exception thrown, which is usually 500 Internal Server Error. - The router's action is invoked and transformed into an HTTP response. - If an error is thrown in this step and [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) is disabled: - If the [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) property is enabled, it is invoked and the resulting response is returned to the client. - If the previous property is not defined, an empty response is returned to the server, which forwards a response according to the type of exception thrown, which is usually 500 Internal Server Error. - All global [IRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.md) instances with the `AfterResponse` flag are executed. - If any handler returns a non-null response, the handler's response replaces the previous response and is immediately forwarded to the HTTP client. - If an error is thrown in this step and [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) is disabled: - If the [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) property is enabled, it is invoked and the resulting response is returned to the client. - If the previous property is not defined, an empty response is returned to the server, which forwards a response according to the type of exception thrown, which is usually 500 Internal Server Error. - All [IRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.md) instances defined in the route and with the `AfterResponse` flag are executed. - If any handler returns a non-null response, the handler's response replaces the previous response and is immediately forwarded to the HTTP client. - If an error is thrown in this step and [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) is disabled: - If the [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) property is enabled, it is invoked and the resulting response is returned to the client. - If the previous property is not defined, an empty response is returned to the server, which forwards a response according to the type of exception thrown, which is usually 500 Internal Server Error. - **Processing the response:** with the response ready, the server prepares it for sending to the client. - The Cross-Origin Resource Sharing Policy (CORS) headers are defined in the response according to what was configured in the current [ListeningHost.CrossOriginResourceSharingPolicy](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.CrossOriginResourceSharingPolicy.md). - The status code and headers of the response are sent to the client. - The response content is sent to the client: - If the response content is a descendant of [ByteArrayContent](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.bytearraycontent), the response bytes are directly copied to the response output stream. - If the previous condition is not met, the response is serialized to a stream and copied to the response output stream. - The streams are closed and the response content is discarded. - If [HttpServerConfiguration.DisposeDisposableContextValues](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.DisposeDisposableContextValues.md) is enabled, all objects defined in the request context that inherit from [IDisposable](https://learn.microsoft.com/en-us/dotnet/api/system.idisposable) are discarded. - The `OnHttpRequestClose` event is invoked for all configured HTTP server handlers. - If an exception was thrown on the server, the `OnException` event is invoked for all configured HTTP server handlers. - If the route allows access-logging and [HttpServerConfiguration.AccessLogsStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.AccessLogsStream.md) is not null, a log line is written to the log output. - If the route allows error-logging, there is an exception, and [HttpServerConfiguration.ErrorsLogsStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ErrorsLogsStream.md) is not null, a log line is written to the error log output. - If the server is waiting for a request through [HttpServer.WaitNext](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.WaitNext.md), the mutex is released and the context becomes available to the user. --- # Forwarding Resolvers Source: https://docs.sisk-framework.org/docs/advanced/forwarding-resolvers.html A Forwarding Resolver is a helper that helps decode information that identifies the client through a request, proxy, CDN or load-balancers. When your Sisk service runs through a reverse or forward proxy, the client's IP address, host and protocol may be different from the original request as it is a forwarding from one service to another. This Sisk functionality allows you to control and resolve this information before working with the request. These proxies usually provide useful headers to identify their client. Currently, with the [ForwardingResolver](https://docs.sisk-framework.org/api/Sisk.Core.Http.ForwardingResolver.md) class, it is possible to resolve the client IP address, host, and HTTP protocol used. After version 1.0 of Sisk, the server no longer has a standard implementation to decode these headers for security reasons that vary from service to service. For example, the `X-Forwarded-For` header includes information about the IP addresses that forwarded the request. This header is used by proxies to carry a chain of information to the final service and includes the IP of all proxies used, including the client's real address. The problem is: sometimes it is challenging to identify the client's remote IP and there is no specific rule to identify this header. It is highly recommended to read the documentation for the headers you are about to implement below: - Read about the `X-Forwarded-For` header [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#security_and_privacy_concerns). - Read about the `X-Forwarded-Host` header [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host). - Read about the `X-Forwarded-Proto` header [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto). ## The ForwardingResolver class This class has three virtual methods that allow the most appropriate implementation for each service. Each method is responsible for resolving information from the request through a proxy: the client's IP address, the host of the request and the security protocol used. By default, Sisk will always use the information from the original request, without resolving any headers. The example below shows how this implementation can be used. This example resolves the client's IP through the `X-Forwarded-For` header and throws an error when more than one IP was forwarded in the request. > [!IMPORTANT] > Do not use this example in production code. Always check if the implementation is appropriate for use. Read the header documentation before implementing it. ```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/docs/advanced/http-server-handlers.html In Sisk version 0.16, we've introduced the `HttpServerHandler` class, which aims to extend the overral Sisk behavior and provide additional event handlers to Sisk, such as handling Http requests, routers, context bags and more. The class concentrates events that occur during the lifetime of the entire HTTP server and also of a request. The Http protocol does not have sessions, and therefore it is not possible to preserve information from one request to another. Sisk for now provides a way for you to implement sessions, contexts, database connections and other useful providers to help your work. Please refer to [this page](https://docs.sisk-framework.org/api/Sisk.Core.Http.Handlers.HttpServerHandler.md) to read where each event is triggered and what its purpose is. You can also view the [lifecycle of an HTTP request](https://docs.sisk-framework.org/docs/advanced/request-lifecycle.md) to understand what happens with a request and where events are fired. The HTTP server allows you to use multiple handlers at the same time. Each event call is synchronous, that is, it will blocked the current thread for each request or context until all handlers associated with that function are executed and completed. Unlike RequestHandlers, they cannot be applied to some route groups or specific routes. Instead, they are applied to the entire HTTP server. You can apply conditions within your Http Server Handler. Furthermore, singletons of each HttpServerHandler are defined for every Sisk application, so only one instance per `HttpServerHandler` is defined. A practical example of using HttpServerHandler is to automatically dispose a database connection at the end of the request. ```cs // DatabaseConnectionHandler.cs public class DatabaseConnectionHandler : HttpServerHandler { protected override void OnHttpRequestClose(HttpServerExecutionResult result) { var requestBag = result.Request.Context.RequestBag; // checks if the request has defined an DbContext // in it's context bag 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()); } } ``` With the code above, the `GetDbContext` extension allows a connection context to be created directly from the HttpRequest object. An undisposed connection can cause problems when running with the database, so it is terminated in `OnHttpRequestClose`. You can register a handler on an Http server in your builder or directly with [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(); } } ``` With this, the `UsersController` class can make use of the database context as: ```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."); } } ``` The code above uses methods like `JsonOk` and `JsonMessage` that are built into `ApiController`, which is inherited from a `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 })); } } ``` ## Exceptions thrown by handlers Since Sisk 1.7, exceptions thrown by server handlers are handled as follows: - Exceptions thrown in `OnServerStarting`, `OnServerStarted` and `OnSetupRouter` stop the server startup. They are wrapped in an `InvalidOperationException`, with the original exception in `InnerException`. - Exceptions thrown in other events are written to [ErrorsLogsStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ErrorsLogsStream.md) and the remaining handlers are still called, unless [ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) is enabled. With `ThrowExceptions` enabled, they are wrapped in an `InvalidOperationException`. - Exceptions thrown in `OnHttpRequestClose` and `OnException`, or while formatting the access and error logs, never escape to the HTTP engine, even with `ThrowExceptions` enabled. They are written to `ErrorsLogsStream`. In earlier versions, they could terminate the process or leave the connection open. Developers can implement sessions, contexts, and database connections using this class. The provided code showcases a practical example with the DatabaseConnectionHandler, automating database connection disposal at the end of each request. Integration is straightforward, with handlers registered during server setup. The HttpServerHandler class offers a powerful toolset for managing resources and extending Sisk behavior in HTTP applications. --- # Multiple listening hosts per server Source: https://docs.sisk-framework.org/docs/advanced/multi-host-setup.html The Sisk Framework has always supported the use of more than one host per server, that is, a single HTTP server can listen on multiple ports and each port has its own router and its own service running on it. This way, it is easy to separate responsibilities and manage services on a single HTTP server with Sisk. The example below shows the creation of two ListeningHosts, each listening to a different port, with different routers and actions. Read [manually creating your app](https://docs.sisk-framework.org/docs/advanced/manual-setup.md) to understand the details about this abstraction. ```cs static void Main(string[] args) { // create two listening hosts, which each one has it's own router and // listens to it's own port // 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!")); // create an server configuration and adds both // listening hosts on it // HttpServerConfiguration configuration = new HttpServerConfiguration(); configuration.ListeningHosts.Add(hostA); configuration.ListeningHosts.Add(hostB); // creates an http server which uses the specified // configuration // HttpServer server = new HttpServer(configuration); // starts the server server.Start(); Console.WriteLine("Try to reach host A in {0}", server.ListeningPrefixes[0]); Console.WriteLine("Try to reach host B in {0}", server.ListeningPrefixes[1]); Thread.Sleep(-1); } ``` --- # HTTP Server Engines Source: https://docs.sisk-framework.org/docs/advanced/server-engines.html The Sisk Framework is divided into several packages, where the main one (Sisk.HttpServer) does not include a base HTTP server - by default, [HttpListener](https://learn.microsoft.com/en-us/dotnet/api/system.net.httplistener?view=net-9.0) is used as the main engine of Sisk to perform the low-level role of the server. The HTTP engine fulfills the role of the layer below the application layer offered by Sisk. This layer is responsible for connection management, serialization and deserialization of messages, message queue control, and communication with the machine's socket. The [HttpServerEngine](https://docs.sisk-framework.org/api/Sisk.Core.Http.Engine.HttpServerEngine.md) class exposes an API to implement all the necessary functionalities of an HTTP engine to be used in upper layers with Sisk, such as routing, SSE, middlewares, etc. These functions are not the responsibility of the HTTP engine, but rather of the subset of libraries that will use the HTTP engine as a base for execution. With this abstraction, it is possible to port Sisk to be used with any other HTTP engine, written in .NET or not, such as Kestrel, for example. Currently, Sisk remains using an abstraction of the native .NET [HttpListener](https://learn.microsoft.com/en-us/dotnet/api/system.net.httplistener?view=net-9.0) as the default for new projects. This default abstraction brings some specific problems, such as unspecified behavior on different platforms (HttpListener has one implementation for Windows and another for other platforms), lack of support for SSL, and not very pleasant performance outside of Windows. An experimental implementation of a high-performance server written purely in C# is also available as an HTTP engine for Sisk, called the [Cadente](https://github.com/sisk-http/core/tree/main/cadente) project, which is an experiment of a managed server that can be used with Sisk or not. ## Implementing an HTTP Engine for Sisk You can create a connection bridge between an existing HTTP server and Sisk by extending the [HttpServerEngine](https://docs.sisk-framework.org/api/Sisk.Core.Http.Engine.HttpServerEngine.md) class. In addition to this class, you will also have to implement abstractions for contexts, requests, and responses. A complete abstraction example is [available on GitHub](https://github.com/sisk-http/core/blob/main/src/Http/Engine/HttpListenerAbstractEngine.cs) for viewing. It looks like this: ```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; } // ... } ``` ## Choosing an Event Loop During the creation of an HTTP engine, the server will listen for requests in a loop and create contexts to handle each one of them in separate threads. For this, you will have to choose an [HttpServerEngineContextEventLoopMechanism](https://docs.sisk-framework.org/api/Sisk.Core.Http.Engine.HttpServerEngineContextEventLoopMechanism.md): - `InlineAsynchronousGetContext` the event loop is linear - HTTP context handling calls occur in an asynchronous loop. - `UnboundAsynchronousGetContext` the event loop is transmitted through the `BeginGetContext` and `EndGetContext` methods. ```csharp public override HttpServerEngineContextEventLoopMechanism EventLoopMechanism => HttpServerEngineContextEventLoopMechanism.UnboundAsynchronousGetContext; ``` You don't need to implement both event loops. Choose the one that makes the most sense for your HTTP engine. ## Testing After linking your HTTP engine, it is essential to perform tests to ensure that all Sisk functionalities have identical behavior when using other engines. **It is extremely important** to have the same behavior of Sisk for different HTTP engines. You can visit the test repository on [GitHub](https://github.com/sisk-http/core/tree/main/tests).