# 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. --- # 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 is running using Sisk.ServiceProvider package, 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 the Sisk.ServiceProvider package, 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 the Sisk.ServiceProvider package, 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. ## Through Sisk.SslProxy package - Available on: Linux, macOS, Windows - Effort: easy > [!IMPORTANT] > > The Sisk.SslProxy package is deprecated in favor of the `Sisk.Cadente.CoreEngine` package and will no longer be maintained. The Sisk.SslProxy package is a simple way to enable SSL on your Sisk application. However, it is an **extremely experimental** package. It may be unstable to work with this package, but you can be part of the small percentage of people who will contribute to making this package viable and stable. To get started, you can install the Sisk.SslProxy package with: ```sh dotnet add package Sisk.SslProxy ``` > [!NOTE] > > You must enable "Include prerelease" in the Visual Studio Package Manager to install Sisk.SslProxy. Again, it is an experimental project, so don't even think about putting it into production. At the moment, Sisk.SslProxy can handle most HTTP/1.1 features, including HTTP Continue, Chunked-Encoding, WebSockets, and SSE. Read more about SslProxy [here](https://docs.sisk-framework.org/docs/extensions/ssl-proxy.md). --- # 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 or Sisk.SslProxy) 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 --prerelease ``` ## 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(); ``` ### 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 writer = new StreamWriter(context.Response.GetResponseStream()); await writer.WriteLineAsync("Hello, world!"); } } ``` --- # 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). --- # 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. This test does not have priority and is exclusive to a single route. 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. Sisk checks for the possibility of route collisions to avoid these problems. When defining routes, Sisk will look for possible routes that might collide with the route being defined. This test includes checking the path and the method that the route is set to accept. ### 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 mark a route to be interpreted with Regex. ```cs Route indexRoute = new RegexRoute(RouteMethod.Get, @"\/[a-z]+\/", IndexPage); mainRouter.Map(indexRoute); ``` Or with [RegexRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RegexRoute.md) class: ```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}"); } } ``` ## 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); ``` ## 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 routes where it's regex-matching. ## 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. 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). A response with status 413 Content Too Large is returned to the client if the content sent is larger than [HttpServerConfiguration.MaximumContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.MaximumContentLength.md) defined in the user configuration. Additionally, if there is no configured limit or if it is too large, the server will throw an [OutOfMemoryException](https://learn.microsoft.com/en-us/dotnet/api/system.outofmemoryexception?view=net-8.0) when the content sent by the client exceeds [Int32.MaxValue](https://learn.microsoft.com/en-us/dotnet/api/system.int32.maxvalue) (2 GB) and if the content is attempted to be accessed through one of the properties mentioned above. You can still deal with the 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(); ... } ``` ## 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 | 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` | You can also use `HttpServerConfiguration.DefaultAccessLogFormat` to use the default access log format. ## 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); ``` ## 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. 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. ### 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. The Sisk server will always respond to the request with a `200 OK` and the applicable CORS headers, and then the client can proceed with the actual request. This condition is only not applied when a route exists for the request with the [RouteMethod](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md) explicitly configured for `Options`. ## 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. ## 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 This package exposes useful classes and methods for building MCP servers that work over [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http). The current implementation supports tools over protocol version `2025-06-18`. > [!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 initialization, `tools/list`, `tools/call`, `ping`, and `notifications/*`. Unsupported JSON-RPC methods return a JSON-RPC error response. ## 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 (); ``` --- # SSL Proxy Source: https://docs.sisk-framework.org/docs/extensions/ssl-proxy.html > [!WARNING] > This feature is experimental and should not be used in production. Please refer to [this document](https://docs.sisk-framework.org/docs/deploying.md#proxying-your-application) if you want to make Sisk work with SSL. 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. --- # Basic Auth Source: https://docs.sisk-framework.org/docs/extensions/basic-auth.html 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. --- # 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 Default to 0. Specifies the maximum content length in bytes. Zero means infinite.
    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. | | `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 currently under development and is not yet published. Its behavior and API may be subject to change in future updates. Since this package is not yet available on NuGet, you must incorporate the source code directly into your project or reference it as a project dependency. You can access the source code [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. ### `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" } ``` ### 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. --- # 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) | `0` | Request bodies need a size limit. | `0` means unlimited until framework or memory limits are reached. | | [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) 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 })); } } ``` 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).