# Sisk Framework documentation > Sisk is a lightweight, agnostic and robust .NET web development framework. This file contains the complete Sisk documentation (Español). The API reference is not included; read https://docs.sisk-framework.org/api/index.md for the type index. --- # Comenzando Source: https://docs.sisk-framework.org/es/docs/getting-started.html ¡Bienvenido a la documentación de Sisk! Sisk es un framework HTTP ligero y de código abierto para .NET. Puedes usarlo para crear un servicio web independiente, incrustar un módulo HTTP dentro de una aplicación existente, o ejecutar un servicio detrás de un proxy inverso con solo la configuración que necesitas. Los valores de Sisk incluyen transparencia del código, modularidad, rendimiento y escalabilidad. Puede manejar diferentes estilos de aplicación, incluidos APIs RESTful, servicios JSON‑RPC, WebSockets, Server‑Sent Events y servir archivos estáticos. Sus principales características incluyen: | Recurso | Descripción | | ------- | ----------- | | [Routing](https://docs.sisk-framework.org/es/docs/fundamentals/routing.md) | Un enrutador de rutas que soporta prefijos, métodos personalizados, variables de ruta, convertidores de valores y más. | | [Request Handlers](https://docs.sisk-framework.org/es/docs/fundamentals/request-handlers.md) | También conocidos como *middlewares*, proporcionan una interfaz para crear tus propios manejadores de solicitud que actúan antes o después de una acción. | | [Compression](https://docs.sisk-framework.org/es/docs/fundamentals/responses.md#gzip-deflate-and-brotli-compression) | Comprime fácilmente el contenido de tus respuestas con Sisk. | | [Web sockets](https://docs.sisk-framework.org/es/docs/features/websockets.md) | Proporciona rutas que aceptan websockets completos, para leer y escribir al cliente. | | [Server-sent events](https://docs.sisk-framework.org/es/docs/features/server-sent-events.md) | Permite el envío de eventos del servidor a clientes que soportan el protocolo SSE. | | [Logging](https://docs.sisk-framework.org/es/docs/features/logging.md) | Registro simplificado. Registra errores, accesos, define rotación de logs por tamaño, múltiples flujos de salida para el mismo log, y más. | | [Multi-host](https://docs.sisk-framework.org/es/docs/advanced/multi-host-setup.md) | Tener un servidor HTTP para varios puertos, y cada puerto con su propio enrutador, y cada enrutador con su propia aplicación. | | [Server handlers](https://docs.sisk-framework.org/es/docs/advanced/http-server-handlers.md) | Extiende tu propia implementación del servidor HTTP. Personaliza con extensiones, mejoras y nuevas funcionalidades. | ## Primeros pasos Sisk puede ejecutarse en cualquier entorno .NET. En esta guía, te enseñaremos cómo crear una aplicación Sisk usando .NET. Si aún no lo has instalado, descarga el SDK desde [aquí](https://dotnet.microsoft.com/en-us/download/dotnet/7.0). En este tutorial, cubriremos cómo crear una estructura de proyecto, recibir una solicitud, obtener un parámetro de URL y enviar una respuesta. Esta guía se centrará en construir un servidor simple usando C#. También puedes usar tu lenguaje de programación favorito. > [!NOTE] > Puede que te interese un proyecto de inicio rápido. Consulta [este repositorio](https://github.com/sisk-http/quickstart) para más información. ## Creando un proyecto Llamemos a nuestro proyecto "My Sisk Application". Una vez que tengas .NET configurado, puedes crear tu proyecto con el siguiente comando: ```bash dotnet new console -n my-sisk-application ``` Luego, navega al directorio de tu proyecto e instala Sisk usando la herramienta de utilidad de .NET: ```bash cd my-sisk-application dotnet add package Sisk.HttpServer ``` Puedes encontrar formas adicionales de instalar Sisk en tu proyecto [aquí](https://www.nuget.org/packages/Sisk.HttpServer/). Ahora, creemos una instancia de nuestro servidor HTTP. Para este ejemplo, lo configuraremos para escuchar en el puerto 5000. ## Construyendo el servidor HTTP Sisk te permite construir tu aplicación paso a paso manualmente, ya que enruta al objeto HttpServer. Sin embargo, esto puede no ser muy conveniente para la mayoría de los proyectos. Por lo tanto, podemos usar el método builder, que facilita poner nuestra aplicación en marcha. ```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(); } } ``` Es importante entender cada componente vital de Sisk. Más adelante en este documento, aprenderás más sobre cómo funciona Sisk. ## Configuración manual (avanzada) Puedes aprender cómo funciona cada mecanismo de Sisk en [esta sección](https://docs.sisk-framework.org/es/docs/advanced/manual-setup.md) de la documentación, que explica el comportamiento y las relaciones entre HttpServer, Router, ListeningPort y otros componentes. --- # Instalación Source: https://docs.sisk-framework.org/es/docs/installing.html Puedes instalar Sisk a través de Nuget, dotnet cli o [otras opciones](https://www.nuget.org/packages/Sisk.HttpServer/). Puedes configurar fácilmente tu entorno de Sisk ejecutando este comando en tu consola de desarrollador: ```sh dotnet add package Sisk.HttpServer ``` Este comando instalará la última versión de Sisk en tu proyecto. --- # Soporte de AOT Nativo Source: https://docs.sisk-framework.org/es/docs/native-aot.html [.NET Native AOT](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/) permite la publicación de aplicaciones .NET nativas que son autosuficientes y no requieren que el tiempo de ejecución .NET esté instalado en el host de destino. Además, Native AOT proporciona beneficios como: - Aplicaciones mucho más pequeñas - Inicialización significativamente más rápida - Menor consumo de memoria Sisk Framework, por su naturaleza explícita, permite el uso de Native AOT para casi todas sus características sin requerir rework en el código fuente para adaptarlo a Native AOT. ## Características no compatibles Sin embargo, Sisk utiliza la reflexión, aunque mínima, para algunas características. Las características mencionadas a continuación pueden estar parcialmente disponibles o completamente no disponibles durante la ejecución de código nativo: - [Exploración automática de módulos](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.AutoScanModules.md) del enrutador: este recurso escanea los tipos incrustados en el ensamblado en ejecución y registra los tipos que son [módulos del enrutador](https://docs.sisk-framework.org/es/docs/fundamentals/routing.md). Este recurso requiere tipos que pueden ser excluidos durante el recorte del ensamblado. Todas las demás características son compatibles con AOT en Sisk. Es común encontrar uno o otro método que genera una advertencia de AOT, pero el mismo, si no se menciona aquí, tiene una sobrecarga que indica el paso de un tipo, parámetro o información de tipo que ayuda al compilador de AOT a compilar el objeto. --- # Desplegando tu aplicación Sisk Source: https://docs.sisk-framework.org/es/docs/deploying.html El proceso de desplegar una aplicación Sisk consiste en publicar tu proyecto en producción. Aunque el proceso es relativamente simple, es digno de tener en cuenta detalles que pueden ser letales para la seguridad y la estabilidad de la infraestructura de despliegue. Idealmente, deberías estar listo para desplegar tu aplicación en la nube, después de realizar todas las pruebas posibles para tener tu aplicación lista. ## Publicando tu aplicación Publicar tu aplicación o servicio Sisk es generar binarios listos y optimizados para producción. En este ejemplo, compilaremos los binarios para producción para ejecutarlos en una máquina que tenga el tiempo de ejecución de .NET instalado. Necesitarás el SDK de .NET instalado en tu máquina para compilar tu aplicación, y el tiempo de ejecución de .NET instalado en el servidor objetivo para ejecutar tu aplicación. Puedes aprender a instalar el tiempo de ejecución de .NET en tu servidor Linux [aquí](https://learn.microsoft.com/en-us/dotnet/core/install/linux), [Windows](https://learn.microsoft.com/en-us/dotnet/core/install/windows?tabs=net70) y [Mac OS](https://learn.microsoft.com/en-us/dotnet/core/install/macos). En la carpeta donde se encuentra tu proyecto, abre una terminal y utiliza el comando de publicación de .NET: ```shell $ dotnet publish -r linux-x64 -c Release ``` Esto generará tus binarios dentro de `bin/Release/publish/linux-x64`. > [!NOTE] > Si tu aplicación se ejecuta utilizando el paquete Sisk.ServiceProvider, debes copiar tu archivo `service-config.json` en tu servidor de host junto con todos los binarios generados por `dotnet publish`. > Puedes dejar el archivo preconfigurado, con variables de entorno, puertos y hosts de escucha, y configuraciones de servidor adicionales. El siguiente paso es trasladar estos archivos al servidor donde se alojará tu aplicación. Después de eso, da permisos de ejecución a tu archivo binario. En este caso, consideremos que el nombre de nuestro proyecto es "my-app": ```shell $ cd /home/htdocs $ chmod +x my-app $ ./my-app ``` Después de ejecutar tu aplicación, verifica si produce algún mensaje de error. Si no produce ninguno, es porque tu aplicación se está ejecutando. En este punto, es probable que no sea posible acceder a tu aplicación desde la red externa fuera de tu servidor, ya que no se han configurado las reglas de acceso como el Firewall. Consideraremos esto en los siguientes pasos. Debes tener la dirección del host virtual donde tu aplicación está escuchando. Esto se establece manualmente en la aplicación y depende de cómo estás instanciando tu servicio Sisk. Si **no** estás utilizando el paquete Sisk.ServiceProvider, debes encontrarla donde definiste tu instancia de HttpServer: ```cs HttpServer server = HttpServer.Emit(5000, out HttpServerConfiguration config, out var host, out var router); // sisk debe escuchar en http://localhost:5000/ ``` Asociando un ListeningHost manualmente: ```cs config.ListeningHosts.Add(new ListeningHost("https://localhost:5000/", router)); ``` O si estás utilizando el paquete Sisk.ServiceProvider, en tu archivo `service-config.json`: ```json { "Server": { }, "ListeningHost": { "Ports": [ "http://localhost:5000/" ] } } ``` A partir de esto, podemos crear un proxy inverso para escuchar a tu servicio y hacer que el tráfico esté disponible en la red abierta. ## Proxyando tu aplicación Proxyar tu servicio significa no exponer directamente tu servicio Sisk a una red externa. Esta práctica es muy común para despliegues de servidores porque: - Permite asociar un certificado SSL en tu aplicación; - Crear reglas de acceso antes de acceder al servicio y evitar sobrecargas; - Controlar el ancho de banda y los límites de solicitudes; - Separar los equilibradores de carga para tu aplicación; - Prevenir daños de seguridad a la infraestructura fallida. Puedes servir tu aplicación a través de un proxy inverso como [Nginx](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/linux-nginx?view=aspnetcore-7.0&tabs=linux-ubuntu#install-nginx) o [Apache](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/linux-apache?view=aspnetcore-7.0), o puedes utilizar un túnel http-over-dns como [Cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/install-and-setup/tunnel-guide/). También recuerda resolver correctamente los encabezados de reenvío de tu proxy para obtener la información del cliente, como la dirección IP y el host, a través de [resolutores de reenvío](https://docs.sisk-framework.org/es/docs/advanced/forwarding-resolvers.md). El siguiente paso después de crear tu túnel, configurar el firewall y tener tu aplicación en ejecución, es crear un servicio para tu aplicación. > [!NOTE] > Utilizar certificados SSL directamente en el servicio Sisk en sistemas no Windows no es posible. Esto es un punto de la implementación de HttpListener, que es el módulo central para la gestión de la cola HTTP en Sisk, y esta implementación varía de un sistema operativo a otro. Puedes utilizar SSL en tu servicio Sisk si [asocias un certificado con el host virtual con IIS](https://learn.microsoft.com/en-us/iis/manage/configuring-security/how-to-set-up-ssl-on-iis). Para otros sistemas, se recomienda altamente utilizar un proxy inverso. ## Creando un servicio Crear un servicio hará que tu aplicación esté siempre disponible, incluso después de reiniciar tu instancia de servidor o un bloqueo no recuperable. En este tutorial simple, utilizaremos el contenido del tutorial anterior como una demostración para mantener tu servicio siempre activo. 1. Accede a la carpeta donde se encuentran los archivos de configuración del servicio: ```sh cd /etc/systemd/system ``` 2. Crea tu archivo `my-app.service` e incluye el contenido: ```ini {title="my-app.service"} [Unit] Description= [Service] # establece el usuario que lanzará el servicio User= # la ruta de ExecStart no es relativa a WorkingDirectory. # establécela como la ruta completa al archivo ejecutable WorkingDirectory=/home/htdocs ExecStart=/home/htdocs/my-app # establece el servicio para que siempre se reinicie en caso de bloqueo Restart=always RestartSec=3 [Install] WantedBy=multi-user.target ``` 3. Reinicia el módulo de administración de servicios: ```sh $ sudo systemctl daemon-reload ``` 4. Inicia tu servicio recién creado desde el nombre del archivo que estableciste y verifica si se está ejecutando: ```sh $ sudo systemctl start my-app $ sudo systemctl status my-app ``` 5. Ahora, si tu aplicación se está ejecutando ("Active: active"), habilita tu servicio para que se mantenga en ejecución después de un reinicio del sistema: ```sh $ sudo systemctl enable my-app ``` Ahora estás listo para presentar tu aplicación Sisk a todos. --- # Trabajando con SSL Source: https://docs.sisk-framework.org/es/docs/ssl.html Trabajar con SSL para desarrollo puede ser necesario cuando se trabaja en contextos que requieren seguridad, como la mayoría de los escenarios de desarrollo web. Sisk funciona sobre HttpListener, que no soporta HTTPS nativo, solo HTTP. Sin embargo, existen soluciones alternativas que le permiten trabajar con SSL en Sisk. Véalas a continuación: ## A través de Sisk.Cadente.CoreEngine - Disponible en: Linux, macOS, Windows - Esfuerzo: fácil Es posible usar el motor experimental [**Cadente**](https://docs.sisk-framework.org/es/docs/cadente.md) en proyectos Sisk, sin requerir configuración adicional en el equipo o en el proyecto. Necesitará instalar el paquete `Sisk.Cadente.CoreEngine` en su proyecto para poder usar el servidor Cadente en el servidor Sisk. Para configurar SSL, puede usar los métodos `UseSsl` y `UseEngine` del constructor: ```csharp using var http = HttpServer.CreateBuilder() .UseEngine() .UseSsl(CertificateHelper.CreateTrustedDevelopmentCertificate("localhost")) ``` > Nota: este paquete aún está en fase experimental. ## A través de IIS en Windows - Disponible en: Windows - Esfuerzo: medio Si está en Windows, puede usar IIS para habilitar SSL en su servidor HTTP. Para que esto funcione, es aconsejable que siga [este tutorial](https://docs.sisk-framework.org/es/docs/registering-namespace.md) de antemano si desea que su aplicación escuche en un host distinto de "localhost". Para que esto funcione, debe instalar IIS a través de las características de Windows. IIS está disponible de forma gratuita para usuarios de Windows y Windows Server. Para configurar SSL en su aplicación, tenga listo el certificado SSL, aunque sea autofirmado. A continuación, puede ver [cómo configurar SSL en IIS 7 o superior](https://learn.microsoft.com/en-us/iis/manage/configuring-security/how-to-set-up-ssl-on-iis). ## A través de mitmproxy - Disponible en: Linux, macOS, Windows - Esfuerzo: fácil **mitmproxy** es una herramienta de proxy de interceptación que permite a desarrolladores y evaluadores de seguridad inspeccionar, modificar y registrar el tráfico HTTP y HTTPS entre un cliente (como un navegador web) y un servidor. Puede usar la utilidad **mitmdump** para iniciar un proxy SSL inverso entre su cliente y su aplicación Sisk. 1. Primero, instale [mitmproxy](https://mitmproxy.org/) en su máquina. 2. Inicie su aplicación Sisk. Para este ejemplo, usaremos el puerto 8000 como el puerto HTTP inseguro. 3. Inicie el servidor mitmproxy para escuchar en el puerto seguro 8001: ```sh mitmdump --mode reverse:http://localhost:8000/ -p 8001 ``` ¡Y ya está listo! Ya puede acceder a su aplicación a través de `https://localhost:8001/`. Su aplicación no necesita estar ejecutándose para que inicie `mitmdump`. Alternativamente, puede agregar una referencia al [mitmproxy helper](https://github.com/sisk-http/core/tree/main/extensions/Sisk.Helpers.mitmproxy) en su proyecto. Esto aún requiere que mitmproxy esté instalado en su computadora. ## A través del paquete Sisk.SslProxy - Disponible en: Linux, macOS, Windows - Esfuerzo: fácil > [!IMPORTANT] > > El paquete Sisk.SslProxy está obsoleto en favor del paquete `Sisk.Cadente.CoreEngine` y ya no se mantendrá. El paquete Sisk.SslProxy es una forma sencilla de habilitar SSL en su aplicación Sisk. Sin embargo, es un paquete **extremadamente experimental**. Puede ser inestable trabajar con este paquete, pero puede formar parte del pequeño porcentaje de personas que contribuirán a que este paquete sea viable y estable. Para comenzar, puede instalar el paquete Sisk.SslProxy con: ```sh dotnet add package Sisk.SslProxy ``` > [!NOTE] > > Debe habilitar "Incluir versiones preliminares" en el Administrador de paquetes de Visual Studio para instalar Sisk.SslProxy. Nuevamente, es un proyecto experimental, así que ni lo piense para ponerlo en producción. En este momento, Sisk.SslProxy puede manejar la mayoría de las características de HTTP/1.1, incluyendo HTTP Continue, Chunked-Encoding, WebSockets y SSE. Lea más sobre SslProxy [aquí](https://docs.sisk-framework.org/es/docs/extensions/ssl-proxy.md). --- # Cadente Source: https://docs.sisk-framework.org/es/docs/cadente.html Cadente es una implementación experimental de escucha de HTTP/1.1 administrada para Sisk. Sirve como reemplazo del `System.Net.HttpListener` predeterminado, ofreciendo un mayor control y flexibilidad, especialmente en plataformas no Windows. ## Visión general De forma predeterminada, Sisk utiliza `HttpListener` (de `System.Net`) como su motor de servidor HTTP subyacente. Si bien `HttpListener` es estable y performante en Windows (donde utiliza el controlador HTTP.sys en modo kernel), su implementación en Linux y macOS es administrada y ha tenido históricamente limitaciones, como la falta de soporte nativo SSL (que requiere un proxy inverso como Nginx o Sisk.SslProxy) y características de rendimiento variables. Cadente tiene como objetivo resolver estos problemas al proporcionar un servidor HTTP/1.1 completamente administrado escrito en C#. Sus objetivos clave son: - **Soporte nativo SSL:** Funciona en todas las plataformas sin necesidad de proxies externos o configuraciones complejas. - **Consistencia entre plataformas:** Comportamiento idéntico en Windows, Linux y macOS. - **Rendimiento:** Diseñado para ser una alternativa de alto rendimiento al `HttpListener` administrado. - **Independencia:** Desacoplado de `System.Net.HttpListener`, aislando a Sisk de posibles deprecaciones o falta de mantenimiento de ese componente en .NET. > [!WARNING] > **Estado experimental** > > Cadente se encuentra actualmente en una etapa experimental (Beta). No se recomienda su uso en entornos de producción críticos. La API y el comportamiento pueden cambiar. ## Instalación Cadente está disponible como un paquete separado. Para utilizarlo con Sisk, necesitas el paquete `Sisk.Cadente.CoreEngine`. ```bash dotnet add package Sisk.Cadente.CoreEngine --prerelease ``` ## Uso con Sisk Para utilizar Cadente como el motor HTTP para tu aplicación Sisk, debes configurar el `HttpServer` para que utilice `CadenteHttpServerEngine` en lugar del motor predeterminado. El `CadenteHttpServerEngine` adapta el `HttpHost` de Cadente a la abstracción `HttpServerEngine` requerida por 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(); ``` ### Configuración avanzada Puedes personalizar la instancia subyacente de `HttpHost` pasando una acción de configuración al constructor de `CadenteHttpServerEngine`. Esto es útil para configurar tiempos de espera o otros ajustes de bajo nivel. ```csharp using var engine = new CadenteHttpServerEngine(host => { // Configurar tiempos de espera de lectura y escritura del cliente host.TimeoutManager.ClientReadTimeout = TimeSpan.FromSeconds(30); host.TimeoutManager.ClientWriteTimeout = TimeSpan.FromSeconds(30); }); ``` ## Uso independiente Aunque está diseñado principalmente para Sisk, Cadente se puede utilizar como un servidor HTTP independiente (similar a `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("Hola, mundo!"); } } ``` --- # Configuración de reservas de espacios de nombres en Windows Source: https://docs.sisk-framework.org/es/docs/registering-namespace.html > [!NOTE] > Esta configuración es opcional y solo se requiere cuando deseas que Sisk escuche en hosts diferentes a "localhost" en Windows usando el motor HttpListener. Sisk funciona con la interfaz de red HttpListener, que enlaza un host virtual al sistema para escuchar solicitudes. En Windows, este enlace es algo restrictivo, solo permite que localhost se vincule como un host válido. Al intentar escuchar en otro host, se lanza un error de acceso denegado en el servidor. Este tutorial explica cómo otorgar autorización para escuchar en cualquier host que desees en el sistema. ```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 ``` Donde en `PREFIX` está el prefijo ("Host de escucha->Puerto") que tu servidor escuchará. Debe estar formateado con el esquema URL, host, puerto y una barra al final, por ejemplo: ```bat {title="Namespace Setup.bat"} SET PREFIX=http://my-application.example.test/ ``` De modo que puedas escuchar en tu aplicación mediante: ```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(); } } ``` --- # Registros de cambios Source: https://docs.sisk-framework.org/es/docs/changelogs.html Cada cambio realizado en Sisk se registra a través del registro de cambios. Puedes ver los registros de cambios de todas las versiones de Sisk [aquí](https://github.com/sisk-http/archive/tree/master/changelogs). --- # Preguntas Frecuentes Source: https://docs.sisk-framework.org/es/docs/faq.html Preguntas frecuentes sobre Sisk. ## ¿Es Sisk de código abierto? Totalmente. Todo el código fuente utilizado por Sisk se publica y se actualiza con frecuencia en [GitHub](https://github.com/sisk-http). ## ¿Se aceptan contribuciones? Siempre y cuando sean compatibles con la [filosofía de Sisk](/), todas las contribuciones son muy bienvenidas. Las contribuciones no tienen que ser solo código. Puedes contribuir con documentación, pruebas, traducciones, donaciones y publicaciones, por ejemplo. ## ¿Está financiado Sisk? No. Ninguna organización o proyecto patrocina actualmente a Sisk. ## ¿Puedo usar Sisk en producción? Absolutamente. El proyecto lleva en desarrollo más de tres años y ha tenido una intensa prueba en aplicaciones comerciales que han estado en producción desde entonces. Sisk se utiliza en proyectos comerciales importantes como infraestructura principal. Una guía sobre cómo [implementar](https://docs.sisk-framework.org/es/docs/deploying.md) en diferentes sistemas y entornos se ha escrito y está disponible. ## ¿Tiene Sisk autenticación, monitoreo y servicios de base de datos? No. Sisk no tiene ninguno de estos. Es un framework para desarrollar aplicaciones web HTTP, pero es un framework minimalista que entrega lo necesario para que tu aplicación funcione. Puedes implementar todos los servicios que desees utilizando cualquier biblioteca de terceros que prefieras. Sisk fue diseñado para ser agnóstico, flexible y funcionar con cualquier cosa. ## ¿Por qué debería usar Sisk en lugar de ? No lo sé. Tú dime. Sisk se creó para llenar un escenario genérico para aplicaciones web HTTP en .NET. Proyectos establecidos, como ASP.NET, resuelven varios problemas, pero con diferentes sesgos. A diferencia de los frameworks más grandes, Sisk requiere que el usuario sepa lo que está haciendo y construyendo. Los conceptos básicos de desarrollo web y el protocolo HTTP son esenciales para trabajar con Sisk. Sisk se parece más a Express de Node.js que a ASP.NET Core. Es una abstracción de alto nivel que te permite crear aplicaciones con lógica HTTP que tú desees. ## ¿Qué necesito para aprender Sisk? Necesitas los conceptos básicos de: - Desarrollo web (HTTP, Restful, etc.) - .NET Eso es todo. Teniendo una noción de estos dos temas, puedes dedicar unas horas a desarrollar una aplicación avanzada con Sisk. ## ¿Puedo desarrollar aplicaciones comerciales con Sisk? Absolutamente. Sisk se creó bajo la licencia MIT, lo que significa que puedes usar Sisk en cualquier proyecto comercial, comercial o no comercial, sin necesidad de una licencia propietaria. Lo que pedimos es que en algún lugar de tu aplicación, tengas un aviso de los proyectos de código abierto utilizados en tu proyecto, y que Sisk esté allí. --- # Enrutamiento Source: https://docs.sisk-framework.org/es/docs/fundamentals/routing.html El [Router](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.md) es el primer paso al construir el servidor. Es responsable de albergar objetos [Route](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.md), que son puntos finales que asignan URLs y sus métodos a acciones ejecutadas por el servidor. Cada acción se encarga de recibir una solicitud y entregar una respuesta al cliente. Las rutas son pares de expresiones de ruta ("patrón de ruta") y el método HTTP al que pueden escuchar. Cuando se realiza una solicitud al servidor, éste intentará encontrar una ruta que coincida con la solicitud recibida, luego llamará a la acción de esa ruta y entregará la respuesta resultante al cliente. Hay múltiples formas de definir rutas en Sisk: pueden ser estáticas, dinámicas o auto‑escaneadas, definidas por atributos, o directamente en el objeto Router. ```cs Router mainRouter = new Router(); // asigna la ruta GET / a la siguiente acción mainRouter.MapGet("/", request => { return new HttpResponse("Hello, world!"); }); ``` Para entender lo que una ruta es capaz de hacer, necesitamos entender lo que una solicitud es capaz de hacer. Un [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) contendrá todo lo que necesitas. Sisk también incluye algunas características extra que aceleran el desarrollo en general. Para cada acción recibida por el servidor, se llamará a un delegado del tipo [RouteAction](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteAction.md). Este delegado contiene un parámetro que lleva un [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) con toda la información necesaria sobre la solicitud recibida por el servidor. El objeto resultante de este delegado debe ser un [HttpResponse](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpResponse.md) o un objeto que se mapee a él mediante [implicit response types](https://docs.sisk-framework.org/es/docs/fundamentals/responses.md#implicit-response-types). ## Coincidencia de rutas Cuando una solicitud es recibida por el servidor HTTP, Sisk busca una ruta que satisfaga la expresión del camino recibido por la solicitud. La expresión siempre se prueba entre la ruta y el camino de la solicitud, sin considerar la cadena de consulta. Esta prueba no tiene prioridad y es exclusiva a una única ruta. Cuando no se encuentra ninguna ruta que coincida con esa solicitud, se devuelve la respuesta de [Router.NotFoundErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.NotFoundErrorHandler.md) al cliente. Cuando el patrón de ruta coincide, pero el método HTTP no, se envía la respuesta de [Router.MethodNotAllowedErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MethodNotAllowedErrorHandler.md) al cliente. Sisk verifica la posibilidad de colisiones de rutas para evitar estos problemas. Al definir rutas, Sisk buscará posibles rutas que puedan colisionar con la ruta que se está definiendo. Esta prueba incluye comprobar el camino y el método que la ruta está configurada para aceptar. ### Creación de rutas usando patrones de ruta Para nuevas aplicaciones, prefiere los métodos `Map*`. Mantienen el método HTTP visible en el sitio de llamada y coinciden con la API actual de `Router`. Los métodos más antiguos `SetRoute` siguen existiendo como envoltorios de compatibilidad, pero los nuevos ejemplos deberían usar `Map`, `MapGet`, `MapPost`, `MapPut`, `MapDelete`, `MapPatch`, `MapAny`, `MapOptions` o `MapHead`. ```cs // Los métodos Map* son la forma habitual de definir rutas específicas por método. mainRouter.MapGet("/hey/", (request) => { string name = request.RouteParameters["name"].GetString(); return new HttpResponse($"Hello, {name}"); }); mainRouter.MapPost("/form", (request) => { var formData = request.GetFormContent(); return new HttpResponse(); // 200 ok vacío }); // Map también puede recibir una instancia de Route cuando necesitas opciones de ruta. mainRouter.Map(Route.Get("/image.png", (request) => { var imageStream = File.OpenRead("image.png"); return new HttpResponse() { // el interior de StreamContent // el stream se libera después de enviar // la respuesta. Content = new StreamContent(imageStream) }; })); // múltiples parámetros mainRouter.MapGet("/hey//surname/", (request) => { string name = request.RouteParameters["name"].GetString(); string surname = request.RouteParameters["surname"].GetString(); return new HttpResponse($"Hello, {name} {surname}!"); }); ``` La propiedad [RouteParameters](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RouteParameters.md) de HttpRequest contiene toda la información sobre las variables de ruta de la solicitud recibida. Cada camino recibido por el servidor se normaliza antes de ejecutar la prueba del patrón de ruta, siguiendo estas reglas: - Todos los segmentos vacíos se eliminan del camino, por ejemplo: `////foo//bar` se convierte en `/foo/bar`. - La coincidencia de caminos es **sensible a mayúsculas y minúsculas**, a menos que [Router.MatchRoutesIgnoreCase](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MatchRoutesIgnoreCase.md) esté configurado en `true`. Las propiedades [Query](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Query.md) y [RouteParameters](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RouteParameters.md) de [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) devuelven un objeto [StringValueCollection](https://docs.sisk-framework.org/api/Sisk.Core.Entity.StringValueCollection.md), donde cada propiedad indexada devuelve un [StringValue](https://docs.sisk-framework.org/api/Sisk.Core.Entity.StringValue.md) no nulo, que puede usarse como una opción/monada para convertir su valor bruto en un objeto gestionado. El ejemplo a continuación lee el parámetro de ruta "id" y obtiene un `Guid` a partir de él. Si el parámetro no es un Guid válido, se lanza una excepción, y se devuelve un error 500 al cliente si el servidor no está manejando [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] > Los caminos tienen su `/` final ignorado tanto en la solicitud como en la ruta, es decir, si intentas acceder a una ruta definida como `/index/page` también podrás acceder usando `/index/page/`. > > También puedes forzar que las URLs terminen con `/` habilitando [HttpServerConfiguration.ForceTrailingSlash](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ForceTrailingSlash.md). ### Creación de rutas usando instancias de clase También puedes definir rutas dinámicamente usando reflexión con el atributo [RouteAttribute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteAttribute.md). De esta forma, la instancia de una clase cuyas métodos implementan este atributo tendrá sus rutas definidas en el router de destino. Para que un método sea definido como ruta, debe estar marcado con un [RouteAttribute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteAttribute.md), como el propio atributo o un [RouteGetAttribute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteGetAttribute.md). El método puede ser estático, de instancia, público o privado. Usa `MapInstance` cuando quieras mapear métodos de ruta de instancia y estáticos de un objeto. Usa `MapType` cuando quieras mapear solo métodos de ruta estáticos de un tipo. ```cs {title="Controller/MyController.cs"} public class MyController { // coincidirá con GET / [RouteGet] HttpResponse Index(HttpRequest request) { HttpResponse res = new HttpResponse(); res.Content = new StringContent("Index!"); return res; } // los métodos estáticos también funcionan [RouteGet("/hello")] static HttpResponse Hello(HttpRequest request) { HttpResponse res = new HttpResponse(); res.Content = new StringContent("Hello world!"); return res; } } ``` La línea siguiente definirá tanto los métodos `Index` como `Hello` de `MyController` como rutas, ya que ambos están marcados como rutas, y se ha proporcionado una instancia de la clase, no su tipo. Si se hubiera proporcionado su tipo en lugar de una instancia, solo se definirían los métodos estáticos. ```cs var myController = new MyController(); mainRouter.MapInstance(myController); ``` Para mapear solo métodos de ruta estáticos de un tipo, usa: ```cs mainRouter.MapType(); ``` Desde la versión 0.16 de Sisk, es posible habilitar AutoScan, que buscará clases definidas por el usuario que implementen `RouterModule` y las asociará automáticamente con el router. Esto no es compatible con compilación AOT. ```cs mainRouter.AutoScanModules(); ``` La instrucción anterior buscará todos los tipos que implementen `ApiController` pero **no el propio tipo**. Los dos parámetros opcionales indican cómo el método buscará esos tipos. El primer argumento implica el Assembly donde se buscarán los tipos y el segundo indica la forma en que los tipos serán definidos. ## Rutas Regex En lugar de usar los métodos predeterminados de coincidencia de caminos HTTP, puedes marcar una ruta para que sea interpretada con Regex. ```cs Route indexRoute = new RegexRoute(RouteMethod.Get, @"\/[a-z]+\/", IndexPage); mainRouter.Map(indexRoute); ``` O con la clase [RegexRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RegexRoute.md): ```cs mainRouter.Map(new RegexRoute(RouteMethod.Get, @"\/[a-z]+\/", request => { return new HttpResponse("hello, world"); })); ``` También puedes capturar grupos del patrón regex en el contenido de [HttpRequest.RouteParameters](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RouteParameters.md): ```cs {title="Controller/MyController.cs"} public class MyController { [RegexRoute(RouteMethod.Get, @"/uploads/(?.*\.(jpeg|jpg|png))")] static HttpResponse RegexRoute(HttpRequest request) { string filename = request.RouteParameters["filename"].GetString(); return new HttpResponse().WithContent($"Acessing file {filename}"); } } ``` ## Prefijado de rutas Puedes prefijar todas las rutas en una clase o módulo con el atributo [RoutePrefix](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RoutePrefixAttribute.md) y establecer el prefijo como una cadena. Mira el ejemplo siguiente usando la arquitectura BREAD (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() { ... } } ``` En el ejemplo anterior, el parámetro HttpResponse se omite a favor de ser usado a través del contexto global [HttpContext.Current](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.Current.md). Lee más en la sección que sigue. ## Rutas sin parámetro de solicitud Las rutas pueden definirse sin el parámetro [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) y aún así ser posibles de obtener la solicitud y sus componentes en el contexto de la solicitud. Consideremos una abstracción `ControllerBase` que sirve como base para todos los controladores de una API, y esa abstracción provee la propiedad `Request` para obtener el [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) actual. ```cs {title="Controller/ControllerBase.cs"} public abstract class ControllerBase { // obtiene la solicitud del hilo actual public HttpRequest Request { get => HttpContext.Current.Request; } // la línea siguiente, al llamarse, obtiene la base de datos de la sesión HTTP actual, // o crea una nueva si no existe public DbContext Database { get => HttpContext.Current.RequestBag.GetOrAdd(); } } ``` Y para que todos sus descendientes puedan usar la sintaxis de ruta sin el parámetro de solicitud: ```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); } } ``` Más detalles sobre el contexto actual y la inyección de dependencias pueden encontrarse en el tutorial de [dependency injection](https://docs.sisk-framework.org/es/docs/features/instancing.md). ## Rutas de cualquier método Puedes definir una ruta que se empareje solo por su camino y omita el método HTTP. Esto puede ser útil para que realices la validación del método dentro del callback de la ruta. ```cs // will match / on any HTTP method mainRouter.MapAny("/", callbackFunction); ``` ## Rutas de cualquier camino Las rutas de cualquier camino prueban cualquier camino recibido por el servidor HTTP, sujeto al método de la ruta que se está probando. Si el método de la ruta es RouteMethod.Any y la ruta usa [Route.AnyPath](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.AnyPath.md) en su expresión de camino, esta ruta escuchará todas las solicitudes del servidor HTTP, y no se podrán definir otras rutas. ```cs // the following route will match all POST requests mainRouter.Map(RouteMethod.Post, Route.AnyPath, callbackFunction); ``` ## Ignorar coincidencia de rutas con mayúsculas/minúsculas Por defecto, la interpretación de rutas con solicitudes es sensible a mayúsculas y minúsculas. Para hacer que ignore mayúsculas/minúsculas, habilita esta opción: ```cs mainRouter.MatchRoutesIgnoreCase = true; ``` Esto también habilitará la opción `RegexOptions.IgnoreCase` para rutas donde se use coincidencia regex. ## Manejador de callback Not Found (404) Puedes crear un callback personalizado para cuando una solicitud no coincida con ninguna ruta conocida. ```cs mainRouter.NotFoundErrorHandler = () => { return new HttpResponse(404) { // Desde v0.14 Content = new HtmlContent("

Not found

") // versiones anteriores Content = new StringContent("

Not found

", Encoding.UTF8, "text/html") }; }; ``` ## Manejador de callback Method not allowed (405) También puedes crear un callback personalizado para cuando una solicitud coincida con su camino, pero no coincida con el método. ```cs mainRouter.MethodNotAllowedErrorHandler = (context) => { return new HttpResponse(405) { Content = new StringContent($"Method not allowed for this route.") }; }; ``` ## Manejo de errores Las excepciones pueden lanzarse dentro del ciclo de vida de una solicitud, que abarca desde el manejador de solicitud previo a la ejecución, pasando por la acción del router, hasta los manejadores de solicitud posteriores a la ejecución y los manejadores de valor. Estas excepciones son gestionadas por el mecanismo: - Si [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) es `true`, las excepciones se lanzarán normalmente y no serán capturadas por Sisk, y el servidor HTTP puede interrumpirse si la excepción no es capturada. - Si [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) es `false`, las excepciones serán capturadas y manejadas por Sisk. Después de eso, si `Router.CallbackErrorHandler` está definido, se llamará con la excepción capturada y el contexto de la solicitud, y **no** será reenviada a la salida de error estándar. Si `Router.CallbackErrorHandler` no está definido, la excepción será reenviada a la salida de error estándar, y el cliente recibirá una respuesta HTTP 500. Si la salida de error estándar no está definida, el error será ignorado silenciosamente. Nota: dentro de `Router.CallbackErrorHandler`, puedes establecer el modo de registro para errores, registro de acceso, ambos o ninguno, y alterar el comportamiento predeterminado de escritura de logs: ```csharp router.CallbackErrorHandler = (ex, ctx) => { ctx.LogMode = LogOutput.Both; // sobrescribe el modo de registro para registrar el error tanto en el log de acceso como en el de errores } ``` ## Manejador interno de errores Los callbacks de ruta pueden lanzar errores durante la ejecución del servidor. Si no se manejan correctamente, el funcionamiento general del servidor HTTP puede terminar. El router tiene un callback para cuando un callback de ruta falla y evita la interrupción del servicio. Este método solo es accesible cuando [ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) está configurado en false. ```cs mainRouter.CallbackErrorHandler = (ex, context) => { return new HttpResponse(500) { Content = new StringContent($"Error: {ex.Message}") }; }; ``` --- # Manejo de solicitudes Source: https://docs.sisk-framework.org/es/docs/fundamentals/request-handlers.html Los manejadores de solicitudes, también conocidos como "middlewares", son funciones que se ejecutan antes o después de que una solicitud sea procesada por el router. Pueden definirse por ruta o por router. Existen dos tipos de manejadores de solicitudes: - **BeforeResponse**: define que el manejador de solicitudes se ejecutará antes de llamar a la acción del router. - **AfterResponse**: define que el manejador de solicitudes se ejecutará después de llamar a la acción del router. Enviar una respuesta HTTP en este contexto sobrescribirá la respuesta de la acción del router. Ambos manejadores de solicitudes pueden sobrescribir la respuesta real de la función de devolución de llamada del router. Por cierto, los manejadores de solicitudes pueden ser útiles para validar una solicitud, como autenticación, contenido, u otra información, como almacenar datos, registros, u otros pasos que pueden realizarse antes o después de una respuesta. ![](https://docs.sisk-framework.org/assets/img/requesthandlers1.png) De esta manera, un manejador de solicitudes puede interrumpir toda esta ejecución y devolver una respuesta antes de que finalice el ciclo, descartando todo lo demás en el proceso. Ejemplo: supongamos que un manejador de solicitudes de autenticación de usuario no lo autentica. Impedirá que el ciclo de vida de la solicitud continúe y se quedará colgado. Si esto ocurre en el manejador de solicitudes en la posición dos, el tercero y los siguientes no se evaluarán. ![](https://docs.sisk-framework.org/assets/img/requesthandlers2.png) ## Creando un manejador de solicitudes Para crear un manejador de solicitudes, podemos crear una clase que herede la interfaz [IRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.md), con el siguiente formato: ```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); } } } ``` En el ejemplo anterior, indicamos que si el encabezado `Authorization` está presente en la solicitud, debe continuar y se debe llamar al siguiente manejador de solicitudes o a la devolución de llamada del router, lo que sea que venga después. Si un manejador de solicitudes se ejecuta después de la respuesta mediante su propiedad [ExecutionMode](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.ExecutionMode.md) y devuelve un valor no nulo, sobrescribirá la respuesta del router. Siempre que un manejador de solicitudes devuelve `null`, indica que la solicitud debe continuar y se debe llamar al siguiente objeto o que el ciclo debe terminar con la respuesta del router. Si heredas de la clase incorporada [RequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RequestHandler.md), puedes devolver `Next()` para hacer explícita esa intención: ```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); } } ``` Para manejadores que necesiten I/O, hereda de [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(); } } ``` Pequeños manejadores en línea también pueden crearse con `RequestHandler.Create` o `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); }); ``` ## Asociando un manejador de solicitudes con una única ruta Puedes definir uno o más manejadores de solicitudes para una ruta. ```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 }); ``` O creando un objeto [Route](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.md): ```cs {title="Router.cs"} Route indexRoute = Route.Get("/", IndexPage); indexRoute.RequestHandlers = new IRequestHandler[] { new AuthenticateUserRequestHandler() }; mainRouter.Map(indexRoute); ``` ## Asociando un manejador de solicitudes con un router Puedes definir un manejador de solicitudes global que se ejecutará en todas las rutas de un router. ```cs {title="Router.cs"} mainRouter.GlobalRequestHandlers = new IRequestHandler[] { new AuthenticateUserRequestHandler() }; ``` ## Asociando un manejador de solicitudes con un atributo Puedes definir un manejador de solicitudes en un atributo de método junto con un atributo de ruta. ```cs {title="Controller/MyController.cs"} public class MyController { [RouteGet("/")] [RequestHandler] static HttpResponse Index(HttpRequest request) { return new HttpResponse() { Content = new StringContent("Hello world!") }; } } ``` Ten en cuenta que es necesario pasar el tipo de manejador de solicitudes deseado y no una instancia de objeto. De esta forma, el manejador de solicitudes será instanciado por el analizador del router. Puedes pasar argumentos en el constructor de la clase mediante la propiedad [ConstructorArguments](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RequestHandlerAttribute.ConstructorArguments.md). Ejemplo: ```cs {title="Controller/MyController.cs"} [RequestHandler("arg1", 123, ...)] public HttpResponse Index(HttpRequest request) { return res = new HttpResponse() { Content = new StringContent("Hello world!") }; } ``` También puedes crear tu propio atributo que implemente RequestHandler: ```cs {title="Middleware/Attributes/AuthenticateAttribute.cs"} public class AuthenticateAttribute : RequestHandlerAttribute { public AuthenticateAttribute() : base(typeof(AuthenticateUserRequestHandler), ConstructorArguments = new object?[] { "arg1", 123, ... }) { ; } } ``` Y usarlo así: ```cs {title="Controller/MyController.cs"} [Authenticate] static HttpResponse Index(HttpRequest request) { return res = new HttpResponse() { Content = new StringContent("Hello world!") }; } ``` ## Omitiendo un manejador de solicitudes global Después de definir un manejador de solicitudes global en una ruta, puedes ignorar este manejador de solicitudes en rutas específicas. ```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] > Si estás omitiendo un manejador de solicitudes, debes usar la misma referencia de la instancia que creaste antes para saltarlo. Crear otra instancia de manejador de solicitudes no omitirá el manejador global ya que su referencia cambiará. Recuerda usar la misma referencia del manejador de solicitudes tanto en GlobalRequestHandlers como en BypassGlobalRequestHandlers. --- # Solicitudes Source: https://docs.sisk-framework.org/es/docs/fundamentals/requests.html Las solicitudes son estructuras que representan un mensaje de solicitud HTTP. El objeto [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) contiene funciones útiles para manejar mensajes HTTP a lo largo de tu aplicación. Una solicitud HTTP se compone del método, la ruta, la versión, los encabezados y el cuerpo. En este documento, te enseñaremos cómo obtener cada uno de estos elementos. ## Obtención del método de la solicitud Para obtener el método de la solicitud recibida, puedes usar la propiedad Method: ```cs static HttpResponse Index(HttpRequest request) { HttpMethod requestMethod = request.Method; ... } ``` Esta propiedad devuelve el método de la solicitud representado por un objeto [HttpMethod](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.httpmethod). > [!NOTE] > A diferencia de los métodos de ruta, esta propiedad no sirve el elemento [RouteMethod.Any](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md). En su lugar, devuelve el método real de la solicitud. ## Obtención de componentes de la URL de la solicitud Puedes obtener varios componentes de una URL a través de ciertas propiedades de una solicitud. Para este ejemplo, consideremos la URL: ``` http://localhost:5000/user/login?email=foo@bar.com ``` | Nombre del componente | Descripción | Valor del componente | | --- | --- | --- | | [Path](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Path.md) | Obtiene la ruta de la solicitud. | `/user/login` | | [FullPath](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.FullPath.md) | Obtiene la ruta de la solicitud y la cadena de consulta. | `/user/login?email=foo@bar.com` | | [FullUrl](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.FullUrl.md) | Obtiene la cadena completa de la URL de la solicitud. | `http://localhost:5000/user/login?email=foo@bar.com` | | [Host](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Host.md) | Obtiene el host de la solicitud. | `localhost` | | [Authority](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Authority.md) | Obtiene el host y el puerto de la solicitud. | `localhost:5000` | | [QueryString](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.QueryString.md) | Obtiene la cadena de consulta de la solicitud. | `?email=foo@bar.com` | | [Query](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Query.md) | Obtiene la consulta de la solicitud en una colección de valores con nombre. | `{StringValueCollection object}` | | [IsSecure](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.IsSecure.md) | Determina si la solicitud está usando SSL (true) o no (false). | `false` | También puedes optar por usar la propiedad [HttpRequest.Uri](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Uri.md), que incluye todo lo anterior en un solo objeto. ## Metadatos de la solicitud y cancelación Sisk también adjunta metadatos operacionales a cada solicitud. Estas propiedades son útiles para registros, rastreo, localización, diagnóstico y operaciones de larga duración: | Propiedad o método | Uso | | --- | --- | | [RequestId](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RequestId.md) | Un identificador único para la solicitud. Habilita [IncludeRequestIdHeader](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.IncludeRequestIdHeader.md) para devolverlo como `X-Request-Id`. | | [RequestedAt](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RequestedAt.md) | El momento en que Sisk creó el objeto de solicitud. | | [RemoteAddress](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RemoteAddress.md) | La dirección del cliente resuelta a partir de la conexión, o de tu [ForwardingResolver](https://docs.sisk-framework.org/es/docs/advanced/forwarding-resolvers.md). | | [Culture](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Culture.md) | La mejor cultura resuelta a partir de `Accept-Language`, retrocediendo a la cultura actual. | | [DisconnectToken](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.DisconnectToken.md) | Un token de cancelación que se activa cuando el cliente se desconecta, cuando el motor HTTP configurado lo soporta. | | [Bag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Bag.md) | Un almacén tipado de clave/valor compartido entre manejadores de solicitud y la acción de ruta. | | [GetRawHttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetRawHttpRequest.md) | Una representación textual de la solicitud para diagnóstico. | ## Obtención del cuerpo de la solicitud Algunas solicitudes incluyen cuerpo, como formularios, archivos o transacciones API. Puedes obtener el cuerpo de una solicitud mediante la propiedad: ```cs // obtiene el cuerpo de la solicitud como una cadena, usando la codificación de la solicitud como codificador string body = request.Body; // o lo obtiene en un arreglo de bytes byte[] bodyBytes = request.RawBody; // o bien, puedes transmitirlo. Stream requestStream = request.GetRequestStream(); // o leer el cuerpo de forma asíncrona Memory bodyMemory = await request.GetBodyContentsAsync(); ``` También es posible determinar si hay un cuerpo en la solicitud y si está cargado con las propiedades [HasContents](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.HasContents.md), que determina si la solicitud tiene contenidos, y [IsContentAvailable](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.IsContentAvailable.md), que indica que el servidor HTTP recibió completamente el contenido desde el punto remoto. No es posible leer el contenido de la solicitud mediante `GetRequestStream` más de una vez. Si lo lees con este método, los valores en `RawBody` y `Body` tampoco estarán disponibles. No es necesario disponer del flujo de la solicitud en el contexto de la solicitud, ya que se dispone al final de la sesión HTTP en la que se crea. Además, puedes usar la propiedad [HttpRequest.RequestEncoding](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RequestEncoding.md) para obtener la mejor codificación y decodificar la solicitud manualmente. El servidor tiene límites para leer el contenido de la solicitud, lo que se aplica tanto a [HttpRequest.Body](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Body.md) como a [HttpRequest.RawBody](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Body.md). Estas propiedades copian todo el flujo de entrada a un búfer local del mismo tamaño que [HttpRequest.ContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.ContentLength.md). Se devuelve al cliente una respuesta con estado 413 Content Too Large si el contenido enviado es mayor que [HttpServerConfiguration.MaximumContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.MaximumContentLength.md) definido en la configuración del usuario. Además, si no hay un límite configurado o si es demasiado grande, el servidor lanzará una [OutOfMemoryException](https://learn.microsoft.com/en-us/dotnet/api/system.outofmemoryexception?view=net-8.0) cuando el contenido enviado por el cliente supere [Int32.MaxValue](https://learn.microsoft.com/en-us/dotnet/api/system.int32.maxvalue) (2 GB) y si se intenta acceder al contenido a través de una de las propiedades mencionadas arriba. Aún puedes manejar el contenido mediante transmisión. > [!NOTE] > Aunque Sisk lo permite, siempre es buena idea seguir la Semántica HTTP para crear tu aplicación y no obtener o servir contenido en métodos que no lo permiten. Lee sobre [RFC 9110 "HTTP Semantics"](https://httpwg.org/spec/rfc9110.html). ## Lectura de solicitudes JSON Para APIs JSON, prefiere los ayudantes JSON incorporados en lugar de leer `Body` y deserializar manualmente. Utilizan [System.Text.Json](https://learn.microsoft.com/en-us/dotnet/api/system.text.json) y por defecto usan [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); }); ``` Usa la sobrecarga asíncrona cuando ya estés en una ruta async o quieras que la cancelación de la solicitud detenga la deserialización: ```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); }); ``` Puedes pasar opciones personalizadas de [JsonSerializerOptions](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializeroptions) para un endpoint específico: ```cs var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { PropertyNameCaseInsensitive = true }; UserDto? user = request.GetJsonContent(options); ``` Para aplicaciones Native AOT o sensibles al recorte, usa la sobrecarga `JsonTypeInfo` generada por un `JsonSerializerContext`: ```cs [JsonSerializable(typeof(CreateUserRequest))] public partial class AppJsonSerializerContext : JsonSerializerContext { } CreateUserRequest? body = await request.GetJsonContentAsync( AppJsonSerializerContext.Default.CreateUserRequest, request.DisconnectToken); ``` La misma regla de lectura única se aplica a los ayudantes JSON: después de que Sisk lea el flujo de la solicitud mediante `GetJsonContent`, `GetJsonContentAsync`, `Body` o `RawBody`, no podrás consumir más tarde el mismo cuerpo mediante `GetRequestStream()`. ## Obtención del contexto de la solicitud El HTTP Context es un objeto exclusivo de Sisk que almacena información del servidor HTTP, ruta, router y manejador de solicitud. Puedes usarlo para organizarte en un entorno donde estos objetos son difíciles de gestionar. Puedes obtener el [HttpContext](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.md) que se está ejecutando actualmente usando el método estático `HttpContext.GetCurrentContext()`. Este método devuelve el contexto de la solicitud que se está procesando en el hilo actual. ```cs HttpContext context = HttpContext.GetCurrentContext(); ``` ### Modo de registro La propiedad [HttpContext.LogMode](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.LogMode.md) te permite controlar el comportamiento de registro para la solicitud actual. Puedes habilitar o deshabilitar el registro para solicitudes específicas, sobrescribiendo la configuración predeterminada del servidor. ```cs // Deshabilitar el registro para esta solicitud context.LogMode = LogOutputMode.None; ``` ### Bolsa de solicitud El objeto [RequestBag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.RequestBag.md) contiene información almacenada que se pasa de un manejador de solicitud a otro punto, y puede ser consumida en el destino final. Este objeto también puede ser usado por manejadores de solicitud que se ejecutan después del callback de ruta. > [!TIP] > Esta propiedad también es accesible mediante la propiedad [HttpRequest.Bag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Bag.md). ```cs {title="Middleware/AuthenticateUserRequestHandler.cs"} public class AuthenticateUserRequestHandler : IRequestHandler { public string Identifier { get; init; } = Guid.NewGuid().ToString(); public RequestHandlerExecutionMode ExecutionMode { get; init; } = RequestHandlerExecutionMode.BeforeResponse; public HttpResponse? Execute(HttpRequest request, HttpContext context) { if (request.Headers.Authorization != null) { context.RequestBag.Add("AuthenticatedUser", new User("Bob")); return null; } else { return new HttpResponse(System.Net.HttpStatusCode.Unauthorized); } } } ``` El manejador de solicitud anterior definirá `AuthenticatedUser` en la bolsa de solicitud, y podrá ser consumido más adelante en el callback final: ```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}!") }; } } ``` También puedes usar los métodos auxiliares `Bag.Set()` y `Bag.Get()` para obtener o establecer objetos por sus tipos singleton. La clase `TypedValueDictionary` también provee los métodos `GetValue` y `SetValue` para mayor 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(); ... } ``` ## Obtención de datos de formulario Puedes obtener los valores de datos de formulario en una [StringKeyStoreCollection](https://docs.sisk-framework.org/api/Sisk.Core.Entity.StringKeyStoreCollection.md) con el siguiente ejemplo: ```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)) { ... } } ``` La versión asíncrona es útil cuando el cuerpo de la solicitud puede ser grande o cuando deseas soporte de cancelación: ```cs var form = await request.GetFormContentAsync(request.DisconnectToken); ``` ## Obtención de datos de formulario multipart La solicitud HTTP de Sisk te permite obtener contenidos multipart cargados, como archivos, campos de formulario o cualquier contenido binario. ```cs {title="Controller/Auth.cs"} [RoutePost("/upload-contents")] public HttpResponse Index(HttpRequest request) { // el siguiente método lee toda la entrada de la solicitud en un // arreglo de MultipartObjects var multipartFormDataObjects = request.GetMultipartFormContent(); foreach (MultipartObject uploadedObject in multipartFormDataObjects) { // El nombre del archivo provisto por los datos de formulario multipart. // Se devuelve null si el objeto no es un archivo. Console.WriteLine("File name : " + uploadedObject.Filename); // El nombre del campo del objeto de datos de formulario multipart. Console.WriteLine("Field name : " + uploadedObject.Name); // La longitud del contenido del dato multipart. Console.WriteLine("Content length : " + uploadedObject.ContentLength); // Determina el formato de imagen basado en el encabezado del archivo para cada // tipo de contenido conocido. Si el contenido no es un formato de archivo común // reconocido, este método devolverá MultipartObjectCommonFormat.Unknown Console.WriteLine("Common format : " + uploadedObject.GetCommonFileFormat()); } } ``` Usa [GetMultipartFormContentAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetMultipartFormContentAsync.md) cuando la ruta es asíncrona: ```cs var multipartFormDataObjects = await request.GetMultipartFormContentAsync(request.DisconnectToken); ``` Puedes leer más sobre los [objetos de formulario multipart](https://docs.sisk-framework.org/api/Sisk.Core.Entity.MultipartObject.md) de Sisk y sus métodos, propiedades y funcionalidades. ## Detección de desconexión del cliente Desde la versión v1.15 de Sisk, el framework provee un token de cancelación a través de [HttpRequest.DisconnectToken](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.DisconnectToken.md). Cuando el motor HTTP configurado soporta la detección de desconexión, este token se cancela cuando la conexión del cliente se cierra antes de que la respuesta se complete. Esto es útil para detener operaciones de larga duración cuando el cliente ya no está esperando el resultado. ```csharp router.MapGet("/connect", async (HttpRequest req) => { // obtiene el token de desconexión de la solicitud var dc = req.DisconnectToken; await LongOperationAsync(dc); return new HttpResponse(); }); ``` Este token no es compatible con todos los motores HTTP, y cada uno requiere una implementación. El motor predeterminado de Sisk, basado en `System.Net.HttpListener`, no soporta la detección de desconexión del cliente. Cuando tu aplicación usa el motor predeterminado, `DisconnectToken` es `CancellationToken.None`; en la práctica, es un token que no se cancela y debe considerarse no disponible. El [motor Cadente](https://docs.sisk-framework.org/es/docs/cadente.md) soporta `DisconnectToken`. Si tu ruta depende de la cancelación consciente de desconexiones, usa Cadente u otro motor que implemente explícitamente este comportamiento. Incluso con un motor soportado, la cancelación es cooperativa: pasa el token a APIs async y revísalo en tu propio trabajo de larga duración. ## Soporte de eventos enviados por el servidor Sisk soporta [Server-sent events](https://developer.mozilla.org/en-US/docs/es/Web/API/Server-sent_events), que permite enviar fragmentos como un flujo y mantener viva la conexión entre el servidor y el cliente. Llamar al método [HttpRequest.GetEventSource](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetEventSource.md) pondrá el HttpRequest en su estado de escucha. A partir de esto, el contexto de esta solicitud HTTP no esperará un HttpResponse ya que se superpondrán los paquetes enviados por eventos del lado del servidor. Después de enviar todos los paquetes, el callback debe devolver el método [Close](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequestEventSource.Close.md), que enviará la respuesta final al cliente e indicará que la transmisión ha terminado. No es posible predecir la longitud total de todos los paquetes que se enviarán, por lo que no es posible determinar el fin de la conexión con el encabezado `Content-Length`. Según la mayoría de los navegadores, los eventos del lado del servidor no soportan el envío de encabezados HTTP ni métodos distintos al GET. Por lo tanto, ten cuidado al usar manejadores de solicitud con peticiones de tipo event‑source que requieran encabezados específicos, ya que probablemente no los tendrán. Además, la mayoría de los navegadores reinician los flujos si no se llama al método [EventSource.close](https://developer.mozilla.org/en-US/docs/es/Web/API/EventSource/close) del lado del cliente después de recibir todos los paquetes, lo que provoca procesamiento adicional infinito en el servidor. Para evitar este tipo de problema, es común enviar un paquete final indicando que la fuente de eventos ha terminado de enviar todos los paquetes. El ejemplo a continuación muestra cómo el navegador puede comunicarse con el servidor que soporta eventos del lado del servidor. ```html {title="sse-example.html"} Fruits:
    ``` Y enviar progresivamente los mensajes al cliente: ```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(); } } ``` Al ejecutar este código, esperamos un resultado similar a este: ## Resolución de IPs y hosts proxied Sisk puede usarse con proxies, y por ello las direcciones IP pueden ser reemplazadas por el punto final del proxy en la transacción de un cliente al proxy. Puedes definir tus propios resolutores en Sisk con los [forwarding resolvers](https://docs.sisk-framework.org/es/docs/advanced/forwarding-resolvers.md). ## Codificación de encabezados La codificación de encabezados puede ser un problema para algunas implementaciones. En Windows, los encabezados UTF‑8 no están soportados, por lo que se usa ASCII. Sisk tiene un convertidor de codificación incorporado, que puede ser útil para decodificar encabezados codificados incorrectamente. Esta operación es costosa y está deshabilitada por defecto, pero puede habilitarse con [HttpServerConfiguration.NormalizeHeadersEncodings](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.NormalizeHeadersEncodings.md). --- # Respuestas Source: https://docs.sisk-framework.org/es/docs/fundamentals/responses.html Las respuestas representan objetos que son respuestas HTTP a solicitudes HTTP. Son enviadas por el servidor al cliente como una indicación de la solicitud de un recurso, página, documento, archivo u otro objeto. Una respuesta HTTP se compone de estado, encabezados y contenido. En este documento, le enseñaremos cómo estructurar respuestas HTTP con Sisk. ## Configuración de un estado HTTP La lista de estados HTTP es la misma desde HTTP/1.0, y Sisk soporta todos ellos. ```cs HttpResponse res = new HttpResponse(); res.Status = System.Net.HttpStatusCode.Accepted; // 202 ``` O con sintaxis Fluent: ```cs new HttpResponse() .WithStatus(200) // or .WithStatus(HttpStatusCode.Ok) // or .WithStatus(HttpStatusInformation.Ok); ``` Puede ver la lista completa de HttpStatusCode disponibles [aquí](https://learn.microsoft.com/pt-br/dotnet/api/system.net.httpstatuscode). También puede proporcionar su propio código de estado usando la estructura [HttpStatusInformation](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpStatusInformation.md). ## Cuerpo y tipo de contenido Sisk soporta objetos de contenido nativos de .NET para enviar el cuerpo en respuestas. Puede usar la clase [StringContent](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.stringcontent) para enviar una respuesta JSON, por ejemplo: ```cs HttpResponse res = new HttpResponse(); res.Content = new StringContent(myJson, Encoding.UTF8, "application/json"); ``` El servidor siempre intentará calcular el `Content-Length` a partir de lo que haya definido en el contenido si no lo ha definido explícitamente en un encabezado. Si el servidor no puede obtener implícitamente el encabezado Content-Length del contenido de la respuesta, la respuesta se enviará con codificación Chunked. También puede transmitir la respuesta enviando un [StreamContent](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.streamcontent) o usando el método [GetResponseStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetResponseStream.md). ## Encabezados de respuesta Puede agregar, editar o eliminar encabezados que envía en la respuesta. El ejemplo a continuación muestra cómo enviar una respuesta de redirección al cliente. ```cs HttpResponse res = new HttpResponse(); res.Status = HttpStatusCode.Moved; res.Headers.Add(HttpKnownHeaderNames.Location, "/login"); ``` O con sintaxis Fluent: ```cs new HttpResponse(301) .WithHeader("Location", "/login"); ``` Cuando usa el método [Add](https://docs.sisk-framework.org/api/Sisk.Core.Entity.HttpHeaderCollection.Add.md) de `HttpHeaderCollection`, está añadiendo un encabezado a la solicitud sin alterar los que ya fueron enviados. El método [Set](https://docs.sisk-framework.org/api/Sisk.Core.Entity.HttpHeaderCollection.Set.md) reemplaza los encabezados con el mismo nombre por el valor indicado. El indexador de `HttpHeaderCollection` llama internamente al método `Set` para reemplazar los encabezados. También puede obtener valores de encabezados usando el método [GetHeaderValue](https://docs.sisk-framework.org/api/Sisk.Core.Entity.HttpHeaderCollection.GetHeaderValue.md). Este método ayuda a obtener valores tanto de los encabezados de respuesta como de los encabezados de contenido (si se ha establecido contenido). ```cs // Devuelve el valor del encabezado "Content-Type", verificando tanto response.Headers como response.Content.Headers string? contentType = response.GetHeaderValue("Content-Type"); ``` ## Envío de cookies Sisk tiene métodos que facilitan la definición de cookies en el cliente. Las cookies establecidas con este método ya están codificadas en URL y cumplen con el estándar RFC-6265. ```cs HttpResponse res = new HttpResponse(); res.SetCookie("cookie-name", "cookie-value"); ``` O con sintaxis Fluent: ```cs new HttpResponse(301) .WithCookie("cookie-name", "cookie-value", expiresAt: DateTime.Now.Add(TimeSpan.FromDays(7))); ``` Existen otras [versiones más completas](https://docs.sisk-framework.org/api/Sisk.Core.Helpers.CookieHelper.SetCookie.md) del mismo método. ## Respuestas fragmentadas Puede establecer la codificación de transferencia a chunked para enviar respuestas grandes. ```cs HttpResponse res = new HttpResponse(); res.SendChunked = true; ``` Al usar codificación chunked, el encabezado Content-Length se omite automáticamente. ## Flujo de respuesta Los flujos de respuesta son una forma gestionada que le permite enviar respuestas de manera segmentada. Es una operación de nivel más bajo que usar objetos `HttpResponse`, ya que requiere que envíe los encabezados y el contenido manualmente, y luego cierre la conexión. Este ejemplo abre un flujo de solo lectura para el archivo, copia el flujo al flujo de salida de la respuesta y no carga todo el archivo en memoria. Esto puede ser útil para servir archivos medianos o grandes. ```cs // obtiene el flujo de salida de la respuesta using var fileStream = File.OpenRead("my-big-file.zip"); var responseStream = request.GetResponseStream(); // establece la codificación de la respuesta para usar chunked-encoding // también no debe enviar el encabezado content-length al usar // codificación chunked responseStream.SendChunked = true; responseStream.SetStatus(200); responseStream.SetHeader(HttpKnownHeaderNames.ContentType, contentType); // copia el flujo del archivo al flujo de salida de la respuesta fileStream.CopyTo(responseStream.ResponseStream); // cierra el flujo return responseStream.Close(); ``` ## Compresión GZip, Deflate y Brotli Puede enviar respuestas con contenido comprimido en Sisk comprimiendo contenidos HTTP. Primero, encapsule su objeto [HttpContent](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpcontent) dentro de uno de los compresores a continuación para enviar la respuesta comprimida al cliente. ```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)), }; }); ``` También puede usar estos contenidos comprimidos con flujos. ```cs router.MapGet("/archive.zip", request => { // no aplique "using" aquí. el HttpServer descartará su contenido // después de enviar la respuesta. var archive = File.OpenRead("/path/to/big-file.zip"); return new HttpResponse () { Content = new GZipContent(archive) } }); ``` Los encabezados `Content-Encoding` se establecen automáticamente al usar estos contenidos. ## Compresión automática Es posible comprimir automáticamente las respuestas HTTP con la propiedad [EnableAutomaticResponseCompression](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.EnableAutomaticResponseCompression.md). Esta propiedad encapsula automáticamente el contenido de la respuesta del router en un contenido comprimible que es aceptado por la solicitud, siempre que la respuesta no herede de un [CompressedContent](https://docs.sisk-framework.org/api/Sisk.Core.Http.CompressedContent.md). Solo se elige un contenido comprimible por solicitud, seleccionado según el encabezado `Accept-Encoding`, que sigue el orden: - [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) Si la solicitud indica que acepta cualquiera de estos métodos de compresión, la respuesta se comprimirá automáticamente. ## Tipos de respuesta implícitos Puede usar otros tipos de retorno además de `HttpResponse`, pero es necesario configurar el router para que sepa cómo manejar cada tipo de objeto. El concepto es siempre devolver un tipo de referencia y convertirlo en un objeto `HttpResponse` válido. Las rutas que devuelven `HttpResponse` no sufren ninguna conversión. Los tipos de valor (estructuras) no pueden usarse como tipo de retorno porque no son compatibles con el [RouterCallback](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouterCallback.md), por lo que deben envolver‑se en un `ValueResult` para poder ser usados en los manejadores. Considere el siguiente ejemplo de un módulo de router que no usa `HttpResponse` en el tipo de retorno: ```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; } } ``` Con eso, ahora es necesario definir en el router cómo se tratará cada tipo de objeto. Los objetos son siempre el primer argumento del manejador y el tipo de salida debe ser un `HttpResponse` válido. Además, los objetos de salida de una ruta nunca deben ser nulos. Para los tipos `ValueResult` no es necesario indicar que el objeto de entrada es un `ValueResult` y solo `T`, ya que `ValueResult` es un objeto reflejado de su componente original. La asociación de tipos no compara lo que se registró con el tipo del objeto devuelto por el callback del router. En su lugar, verifica si el tipo del resultado del router es asignable al tipo registrado. Registrar un manejador del tipo `Object` actuará como fallback para todos los tipos previamente no validados. El orden de inserción de los manejadores de valor también importa, por lo que registrar un manejador `Object` ignorará todos los demás manejadores específicos de tipo. Siempre registre primero los manejadores de valor específicos para asegurar el orden. ```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)); }); // registrar un manejador de valor de tipo object debe ser el último // manejador de valor que se usará como fallback r.RegisterValueHandler(fallback => { return new HttpResponse() { Status = HttpStatusCode.OK, Content = JsonContent.Create(fallback) }; }); ``` ## Acciones diferidas Cuando una solicitud llega al router, primero pasa por los [request handlers](https://docs.sisk-framework.org/es/docs/fundamentals/request-handlers.md), se procesa en la acción del router y luego por los manejadores de solicitud post‑ejecución. El resultado de la acción del router es lo que se pasa a los manejadores de valor, y el resultado del manejador de valor es lo que se envía al cliente como respuesta. Este ciclo de vida ocurre dentro de un contexto asíncrono. Este contexto asíncrono expone variables que el usuario puede agregar al [HttpContext Bag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.md) para compartir datos entre los manejadores y la acción del router. El valor devuelto por la acción del router se agrega a este contexto asíncrono y puede ser accedido por los manejadores de valor. Las acciones diferidas son acciones que siempre se ejecutarán al final del ciclo, después de entregar la respuesta al cliente, pero aún dentro del mismo contexto asíncrono. Estas acciones pueden usarse para ejecutar tareas de larga duración que no necesitan completarse para enviar una respuesta al cliente, como guardar logs, actualizar la base de datos, enviar correos electrónicos, etc. Las excepciones siguen siendo capturadas en las acciones diferidas y se manejarán de la misma forma que una excepción lanzada en cualquier punto del ciclo de vida de la solicitud. La diferencia es que el cliente ya habrá recibido una respuesta, por lo que la excepción se maneja mediante el manejo de errores predeterminado. Aplazar la ejecución de una acción usando el método [HttpContext.EnqueueDeferredAction](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.EnqueueDeferredAction.md). El método recibe una función asíncrona que representa la acción a ejecutar y un tiempo de espera opcional para limitar el tiempo de ejecución de la acción. Si la acción no se completa dentro del límite de tiempo, será cancelada. ```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."); } // programa una acción de larga duración que se ejecutará después de enviar la respuesta al cliente, pero aún dentro del mismo contexto asíncrono de la solicitud 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...") }; } ``` ## Nota sobre objetos enumerables y matrices Los objetos de respuesta implícitos que implementan [IEnumerable](https://learn.microsoft.com/pt-br/dotnet/api/system.collections.ienumerable?view=net-8.0) se leen en memoria mediante el método `ToArray()` antes de ser convertidos a través de un manejador de valor definido. Para que esto ocurra, el objeto `IEnumerable` se convierte en una matriz de objetos, y el convertidor de respuesta siempre recibirá un `Object[]` en lugar del tipo original. Considere el siguiente escenario: ```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(); ``` En el ejemplo anterior, el convertidor `IEnumerable` **nunca será llamado**, porque el objeto de entrada siempre será un `Object[]` y no es convertible a `IEnumerable`. Sin embargo, el convertidor que recibe un `IEnumerable` sí recibirá su entrada, ya que su valor es compatible. Si necesita manejar realmente el tipo del objeto que será enumerado, deberá usar reflexión para obtener el tipo del elemento de la colección. Todos los objetos enumerables (listas, matrices y colecciones) son convertidos a una matriz de objetos por el convertidor de respuestas HTTP. Los valores que implementan [IAsyncEnumerable](https://learn.microsoft.com/pt-br/dotnet/api/system.collections.generic.iasyncenumerable-1?view=net-8.0) son manejados automáticamente por el servidor si la propiedad [ConvertIAsyncEnumerableIntoEnumerable](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ConvertIAsyncEnumerableIntoEnumerable.md) está habilitada, de forma similar a lo que ocurre con `IEnumerable`. Esta opción está habilitada por defecto en `HttpServerConfiguration`; una enumeración asíncrona se convierte en un enumerador bloqueante y luego en una matriz síncrona de objetos. Desactívela solo cuando proporcione su propio manejador de valor o una estrategia de respuesta en streaming para secuencias asíncronas. --- # Registro Source: https://docs.sisk-framework.org/es/docs/features/logging.html Puedes configurar Sisk para que escriba automáticamente registros de acceso y de errores. Es posible definir la rotación de logs, extensiones y frecuencia. La clase [LogStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.md) proporciona una forma asíncrona de escribir logs y mantenerlos en una cola de escritura esperable. La clase `LogStream` implementa `IAsyncDisposable`, asegurando que todos los logs pendientes se escriban antes de que el flujo se cierre. En este artículo te mostraremos cómo configurar el registro para tu aplicación. ## Registros de acceso basados en archivos Los logs a archivos abren el archivo, escriben la línea de texto y luego cierran el archivo por cada línea escrita. Este procedimiento se adoptó para mantener la capacidad de respuesta de escritura en los 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(); } } ``` El código anterior escribirá todas las solicitudes entrantes en el archivo `logs/access.log`. Ten en cuenta que el archivo se crea automáticamente si no existe, sin embargo la carpeta anterior no. No es necesario crear el directorio `logs/` ya que la clase LogStream lo crea automáticamente. ## Registro basado en flujos Puedes escribir archivos de registro en instancias de objetos `TextWriter`, como `Console.Out`, pasando un objeto `TextWriter` en el constructor: ```cs {title="Program.cs"} using var app = HttpServer.CreateBuilder() .UseConfiguration(config => { config.AccessLogsStream = new LogStream(Console.Out); }) .Build(); ``` Por cada mensaje escrito en el registro basado en flujo, se llama al método `TextWriter.Flush()`. ## Formato del registro de acceso Puedes personalizar el formato del registro de acceso mediante variables predefinidas. Considera la siguiente línea: ```cs config.AccessLogsFormat = "%dd/%dmm/%dy %tH:%ti:%ts %tz %ls %ri %rs://%ra%rz%rq [%sc %sd] %lin -> %lou in %lmsms [%{user-agent}]"; ``` Escribirá un mensaje como: 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] Puedes formatear tu archivo de registro con el formato descrito en la tabla: | Valor | Qué representa | Ejemplo | |-------|----------------|---------| | %dd | Día del mes (formateado con dos dígitos) | 05 | | %dmmm | Nombre completo del mes | July | | %dmm | Nombre abreviado del mes (tres letras) | Jul | | %dm | Número del mes (formateado con dos dígitos) | 07 | | %dy | Año (formateado con cuatro dígitos) | 2023 | | %th | Hora en formato de 12 horas | 03 | | %tH | Hora en formato de 24 horas (HH) | 15 | | %ti | Minutos (formateado con dos dígitos) | 30 | | %ts | Segundos (formateado con dos dígitos) | 45 | | %tm | Milisegundos (formateado con tres dígitos) | 123 | | %tz | Desplazamiento de zona horaria (horas totales en UTC) | +03:00 | | %ri | Dirección IP remota del cliente | 192.168.1.100 | | %rm | Método HTTP (mayúsculas) | GET | | %rs | Esquema URI (http/https) | https | | %ra | Autoridad URI (dominio) | example.com | | %rh | Host de la solicitud | www.example.com | | %rp | Puerto de la solicitud | 443 | | %rz | Ruta de la solicitud | /path/to/resource | | %rq | Cadena de consulta | ?key=value&another=123 | | %sc | Código de estado de la respuesta HTTP | 200 | | %sd | Descripción del estado de la respuesta HTTP | OK | | %lin | Tamaño legible por humanos de la solicitud | 1.2 KB | | %linr | Tamaño bruto de la solicitud (bytes) | 1234 | | %lou | Tamaño legible por humanos de la respuesta | 2.5 KB | | %lour | Tamaño bruto de la respuesta (bytes) | 2560 | | %lms | Tiempo transcurrido en milisegundos | 120 | | %ls | Estado de ejecución | Executed | | %{header-name} | Representa el encabezado `header-name` de la solicitud. | `Mozilla/5.0 (platform; rv:gecko [...]` | | %{:header-name} | Representa el encabezado `header-name` de la respuesta. | `application/json` | También puedes usar `HttpServerConfiguration.DefaultAccessLogFormat` para emplear el formato de registro de acceso predeterminado. ## Rotación de logs Puedes configurar el servidor HTTP para rotar los archivos de registro a un archivo comprimido .gz cuando alcancen un cierto tamaño. El tamaño se verifica periódicamente según el umbral que definas. ```cs LogStream errorLog = new LogStream("logs/error.log") .ConfigureRotatingPolicy( maximumSize: 64 * SizeHelper.UnitMb, dueTime: TimeSpan.FromHours(6)); ``` El código anterior comprobará cada seis horas si el archivo del `LogStream` ha alcanzado su límite de 64 MB. De ser así, el archivo se comprime a .gz y luego se limpia `access.log`. Durante este proceso, la escritura en el archivo está bloqueada hasta que el archivo se comprime y se limpia. Todas las líneas que intenten escribirse en este período quedarán en una cola esperando el final de la compresión. Esta función solo funciona con `LogStream` basados en archivos. ## Registro de errores Cuando un servidor no lanza errores al depurador, reenvía los errores a la escritura de logs cuando existen. Puedes configurar la escritura de errores con: ```cs config.ThrowExceptions = false; config.ErrorsLogsStream = new LogStream("error.log"); ``` Esta propiedad solo escribirá algo en el log si el error no es capturado por el callback o la propiedad [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md). El error escrito por el servidor siempre incluye la fecha y hora, los encabezados de la solicitud (no el cuerpo), la traza del error y la traza de la excepción interna, si existe alguna. ## Otras instancias de registro Tu aplicación puede tener cero o múltiples `LogStream`; no hay límite en la cantidad de canales de registro que puede tener. Por lo tanto, es posible dirigir el registro de tu aplicación a un archivo distinto del `AccessLog` o `ErrorLog` predeterminados. ```cs LogStream appMessages = new LogStream("messages.log"); appMessages.WriteLine("Application started at {0}", DateTime.Now); ``` ## Extender LogStream Puedes extender la clase `LogStream` para escribir formatos personalizados, compatibles con el motor de logs actual de Sisk. El ejemplo a continuación permite escribir mensajes coloridos en la consola mediante la biblioteca Spectre.Console: ```cs {title="CustomLogStream.cs"} public class CustomLogStream : LogStream { protected override void WriteLineInternal(string line) { base.WriteLineInternal($"[{DateTime.Now:g}] {line}"); } } ``` Otra forma de escribir automáticamente logs personalizados para cada solicitud/respuesta es crear un [HttpServerHandler](https://docs.sisk-framework.org/api/Sisk.Core.Http.Handlers.HttpServerHandler.md). El ejemplo a continuación es un poco más completo. Escribe el cuerpo de la solicitud y la respuesta en JSON a la consola. Puede ser útil para depurar solicitudes en general. Este ejemplo hace uso de `ContextBag` y `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) { // En este punto, la conexión está abierta y el cliente ha enviado el encabezado que especifica // que el contenido es JSON. La línea siguiente lee el contenido y lo deja almacenado en la solicitud. // // Si el contenido no se lee en la acción de la solicitud, el GC probablemente recoja el contenido // después de enviar la respuesta al cliente, por lo que el contenido podría no estar disponible después de que la respuesta se cierre. // _ = request.RawBody; // agrega una pista en el contexto para indicar que esta solicitud tiene un cuerpo JSON request.Bag.Add("IsJsonRequest", true); } } protected override async void OnHttpRequestClose(HttpServerExecutionResult result) { string? requestJson = null, responseJson = null, responseMessage; if (result.Request.Bag.ContainsKey("IsJsonRequest")) { // reformatea el JSON usando la biblioteca CypherPotato.LightJson var content = result.Request.Body; requestJson = JsonValue.Deserialize(content, new JsonOptions() { WriteIndented = true }).ToString(); } if (result.Response is { } response) { var content = response.Content; responseMessage = $"{(int)response.Status} {HttpStatusInformation.GetStatusCodeDescription(response.Status)}"; if (content is HttpContent httpContent && // verifica si la respuesta es 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 { // obtiene el estado interno del manejo del servidor 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()); } } ``` --- # Eventos enviados por el servidor Source: https://docs.sisk-framework.org/es/docs/features/server-sent-events.html Sisk admite el envío de mensajes a través de Server Sent Events de forma nativa. Puedes crear conexiones desechables y persistentes, obtener las conexiones durante el tiempo de ejecución y utilizarlas. Esta característica tiene algunas limitaciones impuestas por los navegadores, como el envío solo de mensajes de texto y la imposibilidad de cerrar permanentemente una conexión. Una conexión cerrada del lado del servidor hará que el cliente intente reconectarse periódicamente cada 5 segundos (3 en algunos navegadores). Estas conexiones son útiles para enviar eventos del servidor al cliente sin que el cliente tenga que solicitar la información cada vez. ## Creando una conexión SSE Una conexión SSE funciona como una solicitud HTTP normal, pero en lugar de enviar una respuesta y cerrar inmediatamente la conexión, la conexión se mantiene abierta para enviar mensajes. Al llamar al método [HttpRequest.GetEventSource()](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetEventSource.md), la solicitud queda en estado de espera mientras se crea la instancia SSE. ```cs r.MapGet("/", (req) => { using var sse = req.GetEventSource(); sse.Send("Hello, world!"); return sse.Close(); }); ``` En el código anterior, creamos una conexión SSE y enviamos un mensaje "Hello, world", luego cerramos la conexión SSE del lado del servidor. > [!NOTE] > Al cerrar una conexión del lado del servidor, por defecto el cliente intentará conectarse nuevamente en ese extremo y la conexión se reiniciará, ejecutando el método de nuevo, indefinidamente. > > Es común reenviar un mensaje de terminación desde el servidor siempre que la conexión se cierre desde el servidor para evitar que el cliente intente reconectarse nuevamente. ## Añadiendo encabezados Si necesitas enviar encabezados, puedes usar el método [HttpRequestEventSource.AppendHeader](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.AppendHeader.md) antes de enviar cualquier mensaje. ```cs r.MapGet("/", (req) => { using var sse = req.GetEventSource(); sse.AppendHeader("Header-Key", "Header-value"); sse.Send("Hello!"); return sse.Close(); }); ``` Ten en cuenta que es necesario enviar los encabezados antes de enviar cualquier mensaje. ## Conexiones Wait-For-Fail Las conexiones se terminan normalmente cuando el servidor ya no puede enviar mensajes debido a una posible desconexión del cliente. Con ello, la conexión se termina automáticamente y la instancia de la clase se descarta. Incluso con una reconexión, la instancia de la clase no funcionará, ya que está vinculada a la conexión anterior. En algunas situaciones, puedes necesitar esta conexión más adelante y no deseas gestionarla mediante el método de devolución de llamada de la ruta. Para ello, podemos identificar las conexiones SSE con un identificador y obtenerlas más tarde usando dicho identificador, incluso fuera de la devolución de llamada de la ruta. Además, marcamos la conexión con [WaitForFail](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.WaitForFail.md) para no terminar la ruta y terminar la conexión automáticamente. Una conexión SSE en `WaitForFail` espera a que ocurra un error de envío causado por una desconexión, o a que transcurra la tolerancia de inactividad configurada, antes de que la ruta se reanude y cierre la conexión. ```cs r.MapGet("/", (req) => { using var sse = req.GetEventSource("my-index-connection"); sse.WaitForFail(TimeSpan.FromSeconds(15)); // esperar 15 segundos sin ningún mensaje antes de terminar la conexión return sse.Close(); }); ``` El método anterior creará la conexión, la gestionará y esperará a una desconexión o error. ```cs HttpRequestEventSource? evs = server.EventSources.GetByIdentifier("my-index-connection"); if (evs != null) { // la conexión sigue viva evs.Send("Hello again!"); } ``` Y el fragmento anterior intentará buscar la conexión recién creada y, si existe, enviará un mensaje a ella. Todas las conexiones activas del servidor que estén identificadas estarán disponibles en la colección [HttpServer.EventSources](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.EventSources.md). Esta colección solo almacena conexiones activas e identificadas. Las conexiones cerradas se eliminan de la colección. > [!NOTE] > Es importante notar que el keep alive tiene un límite establecido por componentes que pueden estar conectados a Sisk de forma incontrolable, como un proxy web, un kernel HTTP o un controlador de red, y cierran las conexiones inactivas después de un cierto período de tiempo. > > Por lo tanto, es importante mantener la conexión abierta enviando pings periódicos o ampliando el tiempo máximo antes de que la conexión se cierre. Lee la siguiente sección para comprender mejor el envío de pings periódicos. ## Configurar la política de ping de conexiones La política de ping es una forma automatizada de enviar mensajes periódicos a tu cliente. Esta función permite al servidor saber cuándo el cliente se ha desconectado de esa conexión sin tener que mantener la conexión abierta indefinidamente. ```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(); } ``` En el código anterior, cada 5 segundos se enviará un nuevo mensaje de ping al cliente. Esto mantendrá viva la conexión TCP y evitará que se cierre por inactividad. Además, cuando un mensaje no se puede enviar, la conexión se cierra automáticamente, liberando los recursos utilizados por la conexión. Utiliza [SendAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.SendAsync.md) y [CloseAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.CloseAsync.md) en rutas asíncronas. Si necesitas descartar eventos en cola antes de cerrar, llama a [Cancel](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.Cancel.md). ## Consultar conexiones Puedes buscar conexiones activas usando un predicado sobre el identificador de la conexión, para poder difundir, por ejemplo. ```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-'"); } ``` También puedes usar el método [All](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpEventSourceCollection.All.md) para obtener todas las conexiones SSE activas. --- # Web Sockets Source: https://docs.sisk-framework.org/es/docs/features/websockets.html Sisk también soporta websockets, como recibir y enviar mensajes a su cliente. Esta característica funciona bien en la mayoría de los navegadores, pero en Sisk sigue siendo experimental. Por favor, si encuentras algún error, repórtalo en GitHub. ## Aceptar mensajes Los mensajes WebSocket se reciben en orden, encolados hasta que los procesa `ReceiveMessageAsync`. Este método no devuelve ningún mensaje cuando se alcanza el tiempo de espera, cuando la operación se cancela o cuando el cliente se desconecta. Solo puede ocurrir una operación de lectura y escritura simultáneamente, por lo tanto, mientras esperas un mensaje con `ReceiveMessageAsync`, no es posible escribir al cliente conectado. ```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(); }); ``` ## Conexión persistente El siguiente ejemplo muestra cómo usar una conexión websocket persistente, donde recibes los mensajes, los procesas y finalizas el uso del 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(); }); ``` ## Política de Ping Similar a cómo funciona la política de ping en Server Side Events, también puedes configurar una política de ping para mantener la conexión TCP abierta si hay inactividad. ```cs ws.PingPolicy.Start( dataMessage: "ping-message", interval: TimeSpan.FromSeconds(10)); ``` ## Conexiones gestionadas Al aceptar un WebSocket, puedes proporcionar un identificador. Los sockets identificados se registran en [HttpServer.WebSockets](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.WebSockets.md), lo que permite al servidor encontrar conexiones activas fuera de la ruta que los aceptó. ```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(); }); ``` Desde otra parte de la aplicación, consulta la colección por identificador o predicado: ```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"); } ``` Cada `HttpWebSocket` expone `Identifier`, `State`, `IsClosed` y `PingPolicy`. La colección también expone `All()`, `Find(...)`, `GetByIdentifier(...)`, `ActiveConnections` y `DropAll()` para estrategias de conexión gestionadas por el servidor. --- # Sintaxis de descarte Source: https://docs.sisk-framework.org/es/docs/features/discard-syntax.html El servidor HTTP se puede utilizar para escuchar una solicitud de devolución de llamada desde una acción, como la autenticación OAuth, y se puede descartar después de recibir esa solicitud. Esto puede ser útil en casos donde necesite una acción en segundo plano pero no desee configurar una aplicación HTTP completa para ello. El siguiente ejemplo muestra cómo crear un servidor HTTP de escucha en el puerto 5555 con [CreateListener](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.CreateListener.md) y esperar el siguiente contexto: ```csharp using (var server = HttpServer.CreateListener(5555)) { // esperar la siguiente solicitud http var context = await server.WaitNextAsync(); Console.WriteLine($"Ruta solicitada: {context.Request.Path}"); } ``` La función [WaitNext](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.WaitNext.md) espera el siguiente contexto de un procesamiento de solicitud completado. Una vez que se obtiene el resultado de esta operación, el servidor ya ha manejado completamente la solicitud y ha enviado la respuesta al cliente. --- # Inyección de dependencias Source: https://docs.sisk-framework.org/es/docs/features/instancing.html Es común dedicar miembros y instancias que duran toda la vida de una solicitud, como una conexión a una base de datos, un usuario autenticado o un token de sesión. Una de las posibilidades es a través de [HttpContext.RequestBag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.md), que crea un diccionario que dura toda la vida de una solicitud. Este diccionario puede ser accedido por [manejadores de solicitudes](https://docs.sisk-framework.org/es/docs/fundamentals/request-handlers.md) y definir variables a lo largo de esa solicitud. Por ejemplo, un manejador de solicitud que autentica a un usuario establece este usuario dentro del `HttpContext.RequestBag`, y dentro de la lógica de la solicitud, este usuario puede ser recuperado con `HttpContext.RequestBag.Get()`. Los objetos definidos en este diccionario están limitados al ciclo de vida de la solicitud. Se eliminan al final de la solicitud. No necesariamente, el envío de una respuesta define el final del ciclo de vida de la solicitud. Cuando se ejecutan [manejadores de solicitudes](https://docs.sisk-framework.org/es/docs/fundamentals/request-handlers.md) que se ejecutan después de enviar una respuesta, los objetos `RequestBag` todavía existen y no han sido eliminados. Aquí hay un ejemplo: ```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; // avanzar al siguiente manejador de solicitud o lógica de solicitud } } ``` ```csharp {title="Controllers/HelloController.cs"} [RouteGet("/hello")] [RequestHandler] public HttpResponse SayHello(HttpRequest request) { var authenticatedUser = request.Bag.Get(); return new HttpResponse() { Content = new StringContent($"Hola {authenticatedUser.Name}!") }; } ``` Este es un ejemplo preliminar de esta operación. La instancia de `User` se creó dentro del manejador de solicitud dedicado a la autenticación, y todas las rutas que utilizan este manejador de solicitud tendrán la garantía de que habrá un `User` en su instancia de `HttpContext.RequestBag`. Es posible definir lógica para obtener instancias cuando no se han definido previamente en el `RequestBag` a través de métodos como [GetOrAdd](https://docs.sisk-framework.org/api/Sisk.Core.Entity.TypedValueDictionary.GetOrAdd.md) o [GetOrAddAsync](https://docs.sisk-framework.org/api/Sisk.Core.Entity.TypedValueDictionary.GetOrAddAsync.md). Desde la versión 1.3, se introdujo la propiedad estática [HttpContext.Current](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.Current.md), que permite acceder al `HttpContext` actualmente en ejecución del contexto de la solicitud. Esto permite exponer miembros del `HttpContext` fuera de la solicitud actual y definir instancias en objetos de ruta. El ejemplo siguiente define un controlador que tiene miembros comúnmente accedidos por el contexto de una solicitud. ```csharp {title="Controllers/Controller.cs"} public abstract class Controller : RouterModule { // Obtener la instancia existente o crear una nueva instancia de base de datos para esta solicitud protected DbContext Database => HttpContext.Current.RequestBag.GetOrAdd(() => new DbContext()); // La carga diferida de repositorios también es común 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)); // La siguiente línea lanzará una excepción si la propiedad se accede cuando el Usuario no // está definido en la bolsa de solicitudes protected User AuthenticatedUser => => HttpContext.Current.RequestBag.Get(); // Exponer la instancia de HttpRequest también es compatible protected HttpRequest Request => HttpContext.Current.Request } ``` Y definir tipos que hereden del controlador: ```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; } } ``` Para el ejemplo anterior, necesitarás configurar un [manejador de valor](https://docs.sisk-framework.org/es/docs/fundamentals/responses.md#implicit-response-types) en tu enrutador para que los objetos devueltos por el enrutador se transformen en una respuesta [HttpResponse](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpResponse.md) válida. Tenga en cuenta que los métodos no tienen un argumento `HttpRequest request` como está presente en otros métodos. Esto se debe a que, desde la versión 1.3, el enrutador admite dos tipos de delegados para respuestas de enrutamiento: [RouteAction](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteAction.md), que es el delegado predeterminado que recibe un argumento `HttpRequest`, y [ParameterlessRouteAction](https://docs.sisk-framework.org/api/Sisk.Core.Routing.ParameterlessRouteAction.md). El objeto `HttpRequest` todavía se puede acceder a través de la propiedad [Request](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.Request.md) de la propiedad estática `HttpContext` en el subproceso. En el ejemplo anterior, definimos un objeto desechable, el `DbContext`, y necesitamos asegurarnos de que todas las instancias creadas en un `DbContext` se eliminen cuando la sesión HTTP finalice. Para esto, podemos utilizar dos formas de lograrlo. Una es crear un [manejador de solicitud](https://docs.sisk-framework.org/es/docs/fundamentals/request-handlers.md) que se ejecute después de la acción del enrutador, y la otra forma es a través de un [manejador de servidor personalizado](https://docs.sisk-framework.org/es/docs/advanced/http-server-handlers.md). Para el primer método, podemos crear el manejador de solicitud directamente en el método [OnSetup](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouterModule.OnSetup.md) heredado de `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) => { // obtener una instancia de DbContext definida en el contexto del manejador de solicitud y // eliminarla ctx.RequestBag.GetOrDefault()?.Dispose(); return null; }, executionMode: RequestHandlerExecutionMode.AfterResponse)); } } ``` > [!TIP] > > Desde la versión 1.4 de Sisk, se introduce la propiedad [HttpServerConfiguration.DisposeDisposableContextValues](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.DisposeDisposableContextValues.md) y se habilita de forma predeterminada, que define si el servidor HTTP debe eliminar todos los valores `IDisposable` en la bolsa de contexto cuando se cierra una sesión HTTP. El método anterior garantizará que el `DbContext` se elimine cuando se finalice la sesión HTTP. Puedes hacer esto para más miembros que necesitan ser eliminados al final de una respuesta. Para el segundo método, puedes crear un [manejador de servidor personalizado](https://docs.sisk-framework.org/es/docs/advanced/http-server-handlers.md) que eliminará el `DbContext` cuando se finalice la sesión HTTP. ```csharp {title="Server/Handlers/ObjectDisposerHandler.cs"} public class ObjectDisposerHandler : HttpServerHandler { protected override void OnHttpRequestClose(HttpServerExecutionResult result) { result.Context.RequestBag.GetOrDefault()?.Dispose(); } } ``` Y utilízalo en tu constructor de aplicaciones: ```csharp {title="Program.cs"} using var host = HttpServer.CreateBuilder() .UseHandler() .Build(); ``` Esta es una forma de controlar la limpieza de código y mantener las dependencias de una solicitud separadas por el tipo de módulo que se utilizará, reduciendo la cantidad de código duplicado dentro de cada acción de un enrutador. Es una práctica similar a la que se utiliza la inyección de dependencias en frameworks como ASP.NET. --- # Transmisión de contenido Source: https://docs.sisk-framework.org/es/docs/features/content-streaming.html El Sisk admite la lectura y el envío de flujos de contenido desde y hacia el cliente. Esta característica es útil para eliminar la sobrecarga de memoria para serializar y deserializar contenido durante la vida útil de una solicitud. ## Flujo de contenido de la solicitud Los contenidos pequeños se cargan automáticamente en la memoria del búfer de conexión HTTP, cargando rápidamente este contenido en [HttpRequest.Body](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Body.md) y [HttpRequest.RawBody](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RawBody.md). Para contenidos más grandes, se puede utilizar el método [HttpRequest.GetRequestStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetRequestStream.md) para obtener el flujo de lectura de contenido de la solicitud. Es importante destacar que el método [HttpRequest.GetMultipartFormContent](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetMultipartFormContent.md) lee todo el contenido de la solicitud en memoria, por lo que puede no ser útil para leer contenidos grandes. Consideremos el siguiente ejemplo: ```csharp {title="Controller/UploadDocument.cs"} [RoutePost ( "/api/upload-document/" )] public async Task UploadDocument ( HttpRequest request ) { var fileName = request.RouteParameters [ "filename" ].GetString (); if (!request.HasContents) { // la solicitud no tiene contenido 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 = "Archivo enviado con éxito." } ) }; } ``` En el ejemplo anterior, el método `UploadDocument` lee el contenido de la solicitud y lo guarda en un archivo. No se realiza ninguna asignación de memoria adicional excepto por el búfer de lectura utilizado por `Stream.CopyToAsync`. El ejemplo anterior elimina la presión de asignación de memoria para un archivo muy grande, lo que puede optimizar el rendimiento de la aplicación. Es una buena práctica utilizar siempre un [CancellationToken](https://learn.microsoft.com/pt-br/dotnet/api/system.threading.cancellationtoken) en una operación que pueda ser larga, como el envío de archivos, ya que depende de la velocidad de la red entre el cliente y el servidor. El ajuste con un CancellationToken se puede realizar de la siguiente manera: ```csharp {title="Controller/UploadDocument.cs"} // el token de cancelación a continuación lanzará una excepción si se alcanza el tiempo de espera de 30 segundos. 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 = "La carga superó el tiempo de carga máximo (30 segundos)." } ) }; } ``` ## Flujo de contenido de la respuesta Enviar contenido de respuesta también es posible. Actualmente, hay dos formas de hacerlo: a través del método [HttpRequest.GetResponseStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetResponseStream.md) y utilizando un contenido de tipo [StreamContent](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.streamcontent?view=net-9.0). Consideremos un escenario en el que necesitamos servir un archivo de imagen. Para hacer esto, podemos utilizar el siguiente código: ```csharp {title="Controller/ImageController.cs"} [RouteGet ( "/api/profile-picture" )] public async Task UploadDocument ( HttpRequest request ) { // método de ejemplo para obtener una imagen de perfil 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}" } }; } ``` El método anterior realiza una asignación de memoria cada vez que se lee el contenido de la imagen. Si la imagen es grande, esto puede causar un problema de rendimiento, y en situaciones de pico, incluso una sobrecarga de memoria y hacer que el servidor se bloquee. En estas situaciones, la caché puede ser útil, pero no eliminará el problema, ya que la memoria seguirá reservada para ese archivo. La caché aliviará la presión de tener que asignar memoria para cada solicitud, pero para archivos grandes, no será suficiente. Enviar la imagen a través de un flujo puede ser una solución al problema. En lugar de leer todo el contenido de la imagen, se crea un flujo de lectura en el archivo y se copia al cliente utilizando un búfer pequeño. #### Enviar a través del método GetResponseStream El método [HttpRequest.GetResponseStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetResponseStream.md) crea un objeto que permite enviar trozos de la respuesta HTTP a medida que se prepara el flujo de contenido. Este método es más manual, requiriendo que se defina el estado, los encabezados y el tamaño del contenido antes de enviar el contenido. ```csharp {title="Controller/ImageController.cs"} [RouteGet ( "/api/profile-picture" )] public async Task UploadDocument ( HttpRequest request ) { var profilePictureFilename = "profile-picture.jpg"; // en esta forma de envío, el estado y el encabezado deben definirse // antes de enviar el contenido 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 )) { // en esta forma de envío, también es necesario definir el tamaño del contenido // antes de enviarlo. requestStreamManager.SetContentLength ( fs.Length ); // si no se conoce el tamaño del contenido, se puede utilizar codificación por trozos // para enviar el contenido requestStreamManager.SendChunked = true; // y luego, escribir en el flujo de salida await fs.CopyToAsync ( requestStreamManager.ResponseStream ); } } ``` #### Enviar contenido a través de un StreamContent La clase [StreamContent](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.streamcontent?view=net-9.0) permite enviar contenido desde una fuente de datos como un flujo de bytes. Esta forma de envío es más fácil, eliminando los requisitos anteriores, e incluso permitiendo el uso de [codificación de compresión](https://docs.sisk-framework.org/es/docs/fundamentals/responses.md#gzip-deflate-and-brotli-compression) para reducir el tamaño del contenido. ```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] > > En este tipo de contenido, no encapsule el flujo en un bloque `using`. El contenido se descartará automáticamente por el servidor HTTP cuando se finalice el flujo de contenido, con o sin errores. --- # Habilitar CORS (Compartir recursos de origen cruzado) en Sisk Source: https://docs.sisk-framework.org/es/docs/features/cors.html Sisk tiene una herramienta que puede ser útil para manejar [Compartir recursos de origen cruzado (CORS)](https://developer.mozilla.org/en-US/docs/es/Web/HTTP/Guides/CORS) cuando se expone su servicio públicamente. Esta característica no es parte del protocolo HTTP, sino una característica específica de los navegadores web definida por la W3C. Este mecanismo de seguridad evita que una página web realice solicitudes a un dominio diferente al que proporcionó la página web. Un proveedor de servicios puede permitir que ciertos dominios accedan a sus recursos, o solo uno. ## Same Origin Para que un recurso sea identificado como "same origin", una solicitud debe identificar el encabezado [Origin](https://developer.mozilla.org/en-US/docs/es/Web/HTTP/Reference/Headers/Origin) en su solicitud: ```http GET /api/users HTTP/1.1 Host: example.com Origin: http://example.com ... ``` Y el servidor remoto debe responder con un encabezado [Access-Control-Allow-Origin](https://developer.mozilla.org/en-US/docs/es/Web/HTTP/Headers/Access-Control-Allow-Origin) con el mismo valor que el origen solicitado: ```http HTTP/1.1 200 OK Access-Control-Allow-Origin: http://example.com ... ``` Esta verificación es **explícita**: el host, el puerto y el protocolo deben ser los mismos que los solicitados. Verifique el ejemplo: - Un servidor responde que su `Access-Control-Allow-Origin` es `https://example.com`: - `https://example.net` - el dominio es diferente. - `http://example.com` - el esquema es diferente. - `http://example.com:5555` - el puerto es diferente. - `https://www.example.com` - el host es diferente. En la especificación, solo se permite la sintaxis para ambos encabezados, tanto para solicitudes como para respuestas. La ruta URL se ignora. El puerto también se omite si es un puerto predeterminado (80 para HTTP y 443 para HTTPS). ```http Origin: null Origin: :// Origin: ://: ``` ## Habilitar CORS Nativamente, tiene el objeto [CrossOriginResourceSharingHeaders](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.md) dentro de su [ListeningHost](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.md). Puede configurar CORS al inicializar el servidor: ```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(); } ``` El código anterior enviará los siguientes encabezados para **todas las respuestas**: ```http HTTP/1.1 200 OK Access-Control-Allow-Origin: http://example.com Access-Control-Allow-Headers: Authorization Access-Control-Expose-Headers: Content-Type ``` Estos encabezados deben enviarse para todas las respuestas a un cliente web, incluidos errores y redirecciones. Puede notar que la clase [CrossOriginResourceSharingHeaders](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.md) tiene dos propiedades similares: [AllowOrigin](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigin.md) y [AllowOrigins](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigins.md). Note que una es plural, mientras que la otra es singular. - La propiedad **AllowOrigin** es estática: solo el origen que especifique se enviará para todas las respuestas. - La propiedad **AllowOrigins** es dinámica: el servidor verifica si el origen de la solicitud está contenido en esta lista. Si se encuentra, se envía para la respuesta de ese origen. ### Comodines y encabezados automáticos Alternativamente, puede usar un comodín (`*`) en el origen de la respuesta para especificar que cualquier origen puede acceder al recurso. Sin embargo, este valor no está permitido para solicitudes que tienen credenciales (encabezados de autorización) y esta operación [resultará en un error](https://developer.mozilla.org/en-US/docs/es/Web/HTTP/Guides/CORS/Errors/CORSNotSupportingCredentials). Puede solucionar este problema enumerando explícitamente qué orígenes se permitirán a través de la propiedad [AllowOrigins](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigins.md) o también usar la constante [AutoAllowOrigin](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AutoAllowOrigin.md) en el valor de [AllowOrigin](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigin.md). Esta propiedad mágica definirá el encabezado `Access-Control-Allow-Origin` para el mismo valor que el encabezado `Origin` de la solicitud. También puede usar [AutoFromRequestMethod](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AutoFromRequestMethod.md) y [AutoFromRequestHeaders](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AutoFromRequestHeaders.md) para un comportamiento similar a `AllowOrigin`, que responde automáticamente en función de los encabezados enviados. ```csharp using var host = HttpServer.CreateBuilder() .UseCors(new CrossOriginResourceSharingHeaders( // Responde en función del encabezado Origin de la solicitud allowOrigin: CrossOriginResourceSharingHeaders.AutoAllowOrigin, // Responde en función del encabezado Access-Control-Request-Method o del método de la solicitud allowMethods: [CrossOriginResourceSharingHeaders.AutoFromRequestMethod], // Responde en función del encabezado Access-Control-Request-Headers o de los encabezados enviados allowHeaders: [CrossOriginResourceSharingHeaders.AutoFromRequestHeaders], exposeHeaders: [HttpKnownHeaderNames.ContentType, "X-Authenticated-Account-Id"], allowCredentials: true)) .Build(); ``` ## Otras formas de aplicar CORS Si está tratando con [proveedores de servicios](https://docs.sisk-framework.org/es/docs/extensions/service-providers.md), puede anular los valores definidos en el archivo de configuración: ```csharp static async Task Main(string[] args) { using var app = HttpServer.CreateBuilder() .UsePortableConfiguration(...) .UseCors(cors => { // Anulará el origen definido en el archivo de configuración. cors.AllowOrigin = "http://example.com"; }) .Build(); await app.StartAsync(); } ``` ## Deshabilitar CORS en rutas específicas La propiedad `UseCors` está disponible para rutas y todos los atributos de ruta y se puede deshabilitar con el siguiente ejemplo: ```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" }; } } ``` ## Reemplazar valores en la respuesta Puede reemplazar o eliminar valores explícitamente en una acción de enrutador: ```csharp [RoutePrefix("api/widgets")] public class WidgetController : Controller { public IEnumerable GetWidgets(HttpRequest request) { // Elimina el encabezado Access-Control-Allow-Credentials request.Context.OverrideHeaders.AccessControlAllowCredentials = string.Empty; // Reemplaza el Access-Control-Allow-Origin request.Context.OverrideHeaders.AccessControlAllowOrigin = "https://contorso.com"; return new[] { "Green widget", "Red widget" }; } } ``` ## Solicitudes preflight Una solicitud preflight es una solicitud del método [OPTIONS](https://developer.mozilla.org/en-US/docs/es/Web/HTTP/Reference/Methods/OPTIONS) que el cliente envía antes de la solicitud real. El servidor Sisk siempre responderá a la solicitud con un `200 OK` y los encabezados CORS aplicables, y luego el cliente puede proceder con la solicitud real. Esta condición solo no se aplica cuando existe una ruta para la solicitud con el [RouteMethod](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md) configurado explícitamente para `Options`. ## Deshabilitar CORS globalmente No es posible hacerlo. Para no usar CORS, no configurelo. --- # Servidor de Archivos Source: https://docs.sisk-framework.org/es/docs/features/file-server.html Sisk proporciona el espacio de nombres `Sisk.Http.FileSystem`, que contiene herramientas para servir archivos estáticos, listado de directorios y conversión de archivos. Esta característica le permite servir archivos desde un directorio local, con soporte para solicitudes de rango (transmisión de audio/video) y procesamiento personalizado de archivos. ## Servir archivos estáticos La forma más sencilla de servir archivos estáticos es [Router.MapFileSystem](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MapFileSystem.md). Este método asigna un prefijo de URL a un directorio en el disco. ```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")); ``` Cuando una solicitud coincide con el prefijo de ruta, el `HttpFileServerHandler` buscará un archivo en el directorio especificado. Si lo encuentra, servirá el archivo; de lo contrario, devolverá una respuesta 404 (o 403 si se niega el acceso). `HttpFileServer.CreateServingRoute` sigue estando disponible cuando necesita crear un objeto `Route` explícitamente, pero `MapFileSystem` es la opción más directa para el código de la aplicación. ## HttpFileServerHandler Para tener más control sobre cómo se sirven los archivos, puede instanciar y configurar `HttpFileServerHandler` manualmente. ```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); ``` ### Configuración | Property | Description | |---|---| | `RootDirectoryPath` | La ruta absoluta o relativa al directorio raíz desde el cual se sirven los archivos. | | `RoutePrefix` | El prefijo de ruta que se recortará del camino de la solicitud al resolver archivos. El valor predeterminado es `/`. | | `AllowDirectoryListing` | Si se establece en `true`, habilita el listado de directorios cuando se solicita un directorio y no se encuentra un archivo índice. El valor predeterminado es `false`. | | `FileConverters` | Una lista de `HttpFileServerFileConverter` utilizada para transformar archivos antes de servirlos. | ## Listado de Directorios Cuando `AllowDirectoryListing` está habilitado y el usuario solicita una ruta de directorio, Sisk generará una página HTML que enumera el contenido de ese directorio. El listado de directorios incluye: - Navegación al directorio padre (`..`). - Lista de subdirectorios. - Lista de archivos con tamaño y fecha de última modificación. ## Convertidores de Archivos Los convertidores de archivos le permiten interceptar tipos de archivo específicos y manejarlos de manera diferente. Por ejemplo, podría querer transcodificar una imagen, comprimir un archivo al vuelo, o servir un archivo usando contenido parcial (solicitudes de rango). Sisk incluye dos convertidores incorporados para transmisión de medios: - `HttpFileAudioConverter`: Maneja `.mp3`, `.ogg`, `.wav`, `.flac`, `.ogv`. - `HttpFileVideoConverter`: Maneja `.webm`, `.avi`, `.mkv`, `.mpg`, `.mpeg`, `.wmv`, `.mov`, `.mp4`. Estos convertidores habilitan el soporte para **solicitudes de rango HTTP**, permitiendo a los clientes buscar dentro de archivos de audio y video. ### Crear un convertidor personalizado Para crear un convertidor de archivo personalizado, herede de `HttpFileServerFileConverter` e implemente `CanConvert` y `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()) }; } } ``` Luego, agréguelo a su manejador: ```cs var handler = new HttpFileServerHandler("./files"); handler.FileConverters.Add(new MyTextConverter()); ``` --- # Protocolo de Contexto de Modelo Source: https://docs.sisk-framework.org/es/docs/extensions/mcp.html Es posible crear aplicaciones que proporcionen contexto a modelos de agente usando grandes modelos de lenguaje (LLMs) mediante el paquete [Sisk.ModelContextProtocol](https://www.nuget.org/packages/Sisk.ModelContextProtocol/): ```bash dotnet add package Sisk.ModelContextProtocol ``` Este paquete expone clases y métodos útiles para construir servidores MCP que funcionan sobre [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http). La implementación actual soporta herramientas sobre la versión de protocolo `2025-06-18`. > [!NOTE] > > Antes de comenzar, ten en cuenta que este paquete está en desarrollo y puede presentar comportamientos que no se ajusten a la especificación. Lee los [detalles del paquete](https://github.com/sisk-http/core/tree/main/extensions/Sisk.ModelContextProtocol) para conocer qué está en desarrollo y qué aún no funciona. ## Comenzando con MCP La clase [McpProvider](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpProvider.md) es el punto de entrada para definir un servidor MCP. Es un objeto proveedor sellado que puede configurarse al iniciar. Tu aplicación Sisk puede tener uno o más proveedores MCP. ```csharp McpProvider mcp = new McpProvider( serverName: "math-server", serverTitle: "Mathematics server", serverVersion: new Version(1, 0)); mcp.Tools.Add(new McpTool( name: "math_sum", description: "Sums one or more numbers.", schema: JsonSchema.CreateObjectSchema( properties: new Dictionary() { { "numbers", JsonSchema.CreateArraySchema( itemsSchema: JsonSchema.CreateNumberSchema(), minItems: 1, description: "The numbers to sum.") } }, requiredProperties: ["numbers"]), executionHandler: async (McpToolContext context) => { var numbers = context.Arguments["numbers"].GetJsonArray().ToArray(); var sum = numbers.Sum(); return await Task.FromResult(McpToolResult.CreateText($"Sum result: {sum:N4}")); })); ``` Si tu aplicación solo proporcionará un proveedor MCP, puedes usar el singleton del constructor: ```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(); } ``` El punto final debe aceptar tanto solicitudes `GET` como `POST`, por lo que `MapAny` es el mapeo de ruta más sencillo. `HandleMcpRequestAsync` devuelve una [HttpResponse](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpResponse.md), y tu ruta debe devolverla. Si necesitas varios proveedores en una sola aplicación, omite el singleton y llama directamente a [McpProvider.HandleRequestAsync](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpProvider.HandleRequestAsync.md) desde cada ruta: ```csharp var mathProvider = new McpProvider("math-server", "Mathematics server", new Version(1, 0)); router.MapAny("/mcp/math", async request => { return await mathProvider.HandleRequestAsync(request); }); ``` ## Creando esquemas JSON para funciones La biblioteca [Sisk.ModelContextProtocol] utiliza un fork de [LightJson](https://github.com/CypherPotato/LightJson) para la manipulación de JSON y esquemas JSON. Esta implementación proporciona un generador fluido de esquemas JSON para varios objetos: - JsonSchema.CreateObjectSchema - JsonSchema.CreateArraySchema - JsonSchema.CreateBooleanSchema - JsonSchema.CreateNumberSchema - JsonSchema.CreateStringSchema - JsonSchema.Empty Ejemplo: ```csharp JsonSchema.CreateObjectSchema( properties: new Dictionary() { { "numbers", JsonSchema.CreateArraySchema( itemsSchema: JsonSchema.CreateNumberSchema(), minItems: 1, description: "The numbers to sum.") } }, requiredProperties: ["numbers"]); ``` Produce el siguiente esquema: ```json { "type": "object", "properties": { "numbers": { "type": "array", "items": { "type": "number" }, "minItems": 1, "description": "The numbers to sum." } }, "required": ["numbers"] } ``` ## Manejando llamadas de función La función definida en el parámetro `executionHandler` de [McpTool](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpTool.md) proporciona un JsonObject que contiene los argumentos de la llamada y que puede leerse de forma fluida: ```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) => { // leer el nombre de la acción. lanzará una excepción si es nulo o no es una cadena explícita string actionName = context.Arguments["action_name"].GetString(); // action_data está definido como no requerido, por lo que puede ser nulo aquí string? actionData = context.Arguments["action_data"].MaybeNull()?.GetString(); // Manejar la acción del navegador según actionName return await Task.FromResult( McpToolResult.CreateText($"Performed browser action: {actionName}")); })); ``` Los argumentos de la herramienta se validan contra el esquema antes de que tu manejador se ejecute. Si la validación falla, el proveedor devuelve un resultado de error al cliente MCP y no invoca el manejador de la herramienta. ## Resultados de la función El objeto [McpToolResult](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpToolResult.md) ofrece tres métodos para crear contenido para una respuesta de herramienta: - [CreateAudio(ReadOnlySpan, string)](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpToolResult.CreateAudio.md): crea una respuesta basada en audio para el cliente MCP. - [CreateImage(ReadOnlySpan, string)](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpToolResult.CreateImage.md): crea una respuesta basada en imagen para el cliente MCP. - [CreateText(string)](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpToolResult.CreateText.md): crea una respuesta basada en texto (por defecto) para el cliente MCP. Además, es posible combinar varios contenidos diferentes en una única respuesta JSON de herramienta: ```csharp mcp.Tools.Add(new McpTool( ... executionHandler: async (McpToolContext context) => { // simular trabajo real byte[] browserScreenshot = await browser.ScreenshotAsync(); return McpToolResult.Combine( McpToolResult.CreateText("Heres the screenshot of the browser:"), McpToolResult.CreateImage(browserScreenshot, "image/png") ); })); ``` El proveedor actualmente maneja la inicialización, `tools/list`, `tools/call`, `ping` y `notifications/*`. Los métodos JSON-RPC no soportados devuelven una respuesta de error JSON-RPC. ## Trabajo continuo El Protocolo de Contexto de Modelo es un protocolo de comunicación para modelos de agente y aplicaciones que les proporcionan contenido. Es un protocolo nuevo, por lo que es común que su especificación se actualice constantemente con deprecaciones, nuevas funcionalidades y cambios incompatibles. Es fundamental comprender los problemas que resuelve el [Model Context Protocol](https://modelcontextprotocol.io/docs/es/getting-started/intro) antes de comenzar a crear aplicaciones de agente. También lee la especificación del paquete [Sisk.ModelContextProtocol](https://github.com/sisk-http/core/tree/main/extensions/Sisk.ModelContextProtocol) para entender su progreso, estado y lo que se puede hacer con él. --- # Extensión JSON-RPC Source: https://docs.sisk-framework.org/es/docs/extensions/json-rpc.html Sisk tiene un módulo experimental para una API [JSON-RPC 2.0](https://www.jsonrpc.org/specification), que le permite crear aplicaciones aún más simples. Esta extensión implementa estrictamente la interfaz de transporte JSON-RPC 2.0 y ofrece transporte mediante solicitudes HTTP GET, POST, y también websockets con Sisk. Puede instalar la extensión vía Nuget con el siguiente comando. Tenga en cuenta que, en versiones experimentales/beta, debe habilitar la opción de buscar paquetes prerelease en Visual Studio. ```bash dotnet add package Sisk.JsonRpc ``` ## Interfaz de Transporte JSON-RPC es un protocolo de llamada a procedimiento remoto (RPC) asíncrono y sin estado que utiliza JSON para la comunicación de datos. Una solicitud JSON-RPC se identifica típicamente por un ID, y una respuesta se entrega con el mismo ID que se envió en la solicitud. No todas las solicitudes requieren una respuesta, las cuales se denominan "notificaciones". La [especificación JSON-RPC 2.0](https://www.jsonrpc.org/specification) explica en detalle cómo funciona el transporte. Este transporte es agnóstico respecto a dónde se utilice. Sisk implementa este protocolo a través de HTTP, siguiendo las conformidades de [JSON-RPC over HTTP](https://www.jsonrpc.org/historical/json-rpc-over-http.html), que soporta parcialmente solicitudes GET, pero soporta completamente solicitudes POST. Los websockets también son compatibles, proporcionando comunicación de mensajes asíncrona. Una solicitud JSON-RPC se parece a: ```json { "jsonrpc": "2.0", "method": "Sum", "params": [1, 2, 4], "id": 1 } ``` Y una respuesta exitosa se parece a: ```json { "jsonrpc": "2.0", "result": 7, "id": 1 } ``` ## Métodos JSON-RPC El siguiente ejemplo muestra cómo crear una API JSON-RPC usando Sisk. Una clase de operaciones matemáticas realiza las operaciones remotas y entrega la respuesta serializada al cliente. ```csharp {title="Program.cs"} using var app = HttpServer.CreateBuilder(port: 5555) .UseJsonRPC((sender, args) => { // agrega todos los métodos etiquetados con WebMethod al manejador JSON-RPC args.Handler.Methods.AddMethodsFromType(new MathOperations()); // asigna la ruta /service para manejar solicitudes JSON-RPC POST y GET args.Router.MapPost("/service", args.Handler.Transport.HttpPost); args.Router.MapGet("/service", args.Handler.Transport.HttpGet); // asigna el transporte WebSocket JSON-RPC en 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); } } ``` El ejemplo anterior asignará los métodos `Sum` y `Sqrt` al manejador JSON-RPC, y estos métodos estarán disponibles en `GET /service`, `POST /service` y `GET /ws`. Los nombres de los métodos no distinguen entre mayúsculas y minúsculas. Los parámetros del método se deserializan automáticamente a sus tipos específicos. También se admite el uso de una solicitud con parámetros nombrados. La serialización JSON se realiza mediante la biblioteca [LightJson](https://github.com/CypherPotato/LightJson). Cuando un tipo no se deserializa correctamente, puede crear un [convertidor JSON](https://github.com/CypherPotato/LightJson?tab=readme-ov-file#json-converters) específico para ese tipo y asociarlo con [JsonRpcHandler.JsonSerializerOptions](https://docs.sisk-framework.org/api/Sisk.JsonRPC.JsonRpcHandler.JsonSerializerOptions.md). También puede obtener el objeto bruto `$.params` de la solicitud JSON-RPC directamente en su método. ```csharp {title="MathOperations.cs"} [WebMethod] public float Sum(JsonArray|JsonObject @params) { ... } ``` Para que esto ocurra, `@params` debe ser el **único** parámetro en su método, con exactamente el nombre `params` (en C#, el `@` es necesario para escapar este nombre de parámetro). La deserialización de parámetros ocurre tanto para objetos nombrados como para matrices posicionales. Por ejemplo, el siguiente método puede ser llamado remotamente por ambas solicitudes: ```csharp [WebMethod] public float AddUserToStore(string apiKey, User user, UserStore store) { ... } ``` Para una matriz, se debe seguir el orden de los parámetros. ```json { "jsonrpc": "2.0", "method": "AddUserToStore", "params": [ "1234567890", { "name": "John Doe", "email": "john@example.com" }, { "name": "My Store" } ], "id": 1 } ``` ## Personalizando el serializador Puede personalizar el serializador JSON en la propiedad [JsonRpcHandler.JsonSerializerOptions](https://docs.sisk-framework.org/api/Sisk.JsonRPC.JsonRpcHandler.JsonSerializerOptions.md). En esta propiedad, puede habilitar el uso de [JSON5](https://json5.org/) para deserializar mensajes. Aunque no es una conformidad con JSON-RPC 2.0, JSON5 es una extensión de JSON que permite una escritura más legible y fácil de entender. ```csharp {title="Program.cs"} using var host = HttpServer.CreateBuilder ( 5556 ) .UseJsonRPC ( ( o, e ) => { // usa un comparador de nombres sanitizado. este comparador compara solo letras // y dígitos en un nombre, y descarta otros símbolos. ej: // foo_bar10 == FooBar10 e.Handler.JsonSerializerOptions.PropertyNameComparer = new JsonSanitizedComparer ( ); // habilita JSON5 para el intérprete JSON. incluso activándolo, JSON plano sigue siendo permitido e.Handler.JsonSerializerOptions.SerializationFlags = LightJson.Serialization.JsonSerializationFlags.Json5; // asigna la ruta POST /service al manejador JSON RPC e.Router.MapPost ( "/service", e.Handler.Transport.HttpPost ); } ) .Build ( ); host.Start ( ); ``` --- # Proxy SSL Source: https://docs.sisk-framework.org/es/docs/extensions/ssl-proxy.html > [!WARNING] > Esta característica es experimental y no debe usarse en producción. Consulte [este documento](https://docs.sisk-framework.org/es/docs/deploying.md#proxying-your-application) si desea hacer que Sisk funcione con SSL. El Proxy SSL de Sisk es un módulo que proporciona una conexión HTTPS para un [ListeningHost](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.md) en Sisk y enruta mensajes HTTPS a un contexto HTTP inseguro. El módulo se creó para proporcionar una conexión SSL para un servicio que utiliza [HttpListener](https://learn.microsoft.com/pt-br/dotnet/api/system.net.httplistener?view=net-8.0) para ejecutarse, que no admite SSL. El proxy se ejecuta dentro de la misma aplicación y escucha mensajes HTTP/1.1, reenviándolos en el mismo protocolo a Sisk. Actualmente, esta característica es muy experimental y puede ser lo suficientemente inestable como para no usarse en producción. En la actualidad, el SslProxy admite casi todas las características de HTTP/1.1, como keep-alive, codificación en bloques, websockets, etc. Para una conexión abierta al proxy SSL, se crea una conexión TCP al servidor de destino y el proxy se reenvía a la conexión establecida. El SslProxy se puede utilizar con HttpServer.CreateBuilder de la siguiente manera: ```csharp using var app = HttpServer.CreateBuilder(port: 5555) .UseRouter(r => { r.MapGet("/", request => { return new HttpResponse("Hola, mundo!"); }); }) // agregar SSL al proyecto .UseSsl( sslListeningPort: 5567, new X509Certificate2(@".\ssl.pfx", password: "12345") ) .Build(); app.Start(); ``` Debe proporcionar un certificado SSL válido para el proxy. Para asegurarse de que el certificado sea aceptado por los navegadores, recuerde importarlo en el sistema operativo para que funcione correctamente. --- # Autenticación Básica Source: https://docs.sisk-framework.org/es/docs/extensions/basic-auth.html El paquete de Autenticación Básica agrega un controlador de solicitudes capaz de manejar el esquema de autenticación básica en su aplicación Sisk con muy poca configuración y esfuerzo. La autenticación HTTP básica es una forma minimalista de autenticar solicitudes mediante un identificador de usuario y una contraseña, donde la sesión es controlada exclusivamente por el cliente y no hay tokens de autenticación o acceso. ![Autenticación Básica](https://docs.sisk-framework.org/assets/img/basic-auth.svg) Lea más sobre el esquema de autenticación básica en la [especificación de MDN](https://developer.mozilla.org/pt-BR/docs/es/Web/HTTP/Authentication). ## Instalación Para empezar, instale el paquete Sisk.BasicAuth en su proyecto: > dotnet add package Sisk.BasicAuth Puede ver más formas de instalarlo en su proyecto en el [repositorio de Nuget](https://www.nuget.org/packages/Sisk.BasicAuth/0.15.0). ## Creación de su controlador de autenticación Puede controlar el esquema de autenticación para un módulo completo o para rutas individuales. Para ello, primero escribamos nuestro primer controlador de autenticación básica. En el ejemplo a continuación, se establece una conexión con la base de datos, se verifica si el usuario existe y si la contraseña es válida, y después de eso, se almacena el usuario en la bolsa de contexto. ```cs public class UserAuthHandler : BasicAuthenticateRequestHandler { public UserAuthHandler() : base() { Realm = "Para entrar en esta página, por favor, informe sus credenciales."; } public override HttpResponse? OnValidating(BasicAuthenticationCredentials credentials, HttpContext context) { DbContext db = new DbContext(); // en este caso, estamos utilizando el correo electrónico como el campo de identificador de usuario, así que // vamos a buscar un usuario utilizando su correo electrónico. User? user = db.Users.FirstOrDefault(u => u.Email == credentials.UserId); if (user == null) { return base.CreateUnauthorizedResponse("Lo sentimos, no se encontró ningún usuario con este correo electrónico."); } // valida que la contraseña de las credenciales sea válida para este usuario. if (!user.ValidatePassword(credentials.Password)) { return base.CreateUnauthorizedResponse("Credenciales inválidas."); } // agrega el usuario conectado al contexto http // y continúa la ejecución context.Bag.Add("loggedUser", user); return null; } } ``` Así que solo asocie este controlador de solicitudes con nuestra ruta o clase. ```cs public class UsersController { [RouteGet("/")] [RequestHandler(typeof(UserAuthHandler))] public string Index(HttpRequest request) { User loggedUser = request.Bag.Get(); return $"Hola, {loggedUser.Name}!"; } } ``` O utilizando la clase [RouterModule](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouterModule.md): ```cs public class UsersController : RouterModule { public ClientModule() { // todas las rutas dentro de esta clase serán manejadas por // UserAuthHandler. base.HasRequestHandler(new UserAuthHandler()); } [RouteGet("/")] public string Index(HttpRequest request) { User loggedUser = request.Bag.Get(); return $"Hola, {loggedUser.Name}!"; } } ``` ## Observaciones La responsabilidad principal de la autenticación básica se lleva a cabo en el lado del cliente. El almacenamiento, el control de caché y el cifrado se manejan localmente en el cliente. El servidor solo recibe las credenciales y valida si se permite o no el acceso. Tenga en cuenta que este método no es uno de los más seguros porque coloca una gran responsabilidad en el cliente, lo que puede ser difícil de rastrear y mantener la seguridad de sus credenciales. Además, es fundamental que las contraseñas se transmitan en un contexto de conexión segura (SSL), ya que no tienen cifrado inherente. Una breve intercepción en los encabezados de una solicitud puede exponer las credenciales de acceso de su usuario. Opte por soluciones de autenticación más robustas para aplicaciones en producción y evite utilizar demasiados componentes prefabricados, ya que pueden no adaptarse a las necesidades de su proyecto y terminar exponiéndolo a riesgos de seguridad. --- # Proveedores de Servicios Source: https://docs.sisk-framework.org/es/docs/extensions/service-providers.html Los Proveedores de Servicios son una forma de portar su aplicación Sisk a diferentes entornos con un archivo de configuración portátil. Esta característica permite cambiar el puerto del servidor, parámetros y otras opciones sin tener que modificar el código de la aplicación para cada entorno. Este módulo depende de la sintaxis de construcción de Sisk y se puede configurar a través del método UsePortableConfiguration. Un proveedor de configuración se implementa con IConfigurationProvider, que proporciona un lector de configuración y puede recibir cualquier implementación. Por defecto, Sisk proporciona un lector de configuración JSON, pero también hay un paquete para archivos INI. También puede crear su propio proveedor de configuración y registrararlo con: ```csharp using var app = HttpServer.CreateBuilder() .UsePortableConfiguration(config => { config.WithConfigReader(); }) .Build(); ``` Como se mencionó anteriormente, el proveedor predeterminado es un archivo JSON. Por defecto, el nombre del archivo que se busca es service-config.json, y se busca en el directorio actual del proceso en ejecución, no en el directorio del ejecutable. Puede elegir cambiar el nombre del archivo, así como dónde Sisk debe buscar el archivo de configuración, con: ```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(); ``` El código anterior buscará el archivo config.toml en el directorio actual del proceso en ejecución. Si no se encuentra, luego buscará en el directorio donde se encuentra el ejecutable. Si el archivo no existe, el parámetro createIfDontExists se honra, creando el archivo, sin contenido, en la última ruta probada (basada en lookupDirectories), y se lanza un error en la consola, impidiendo que la aplicación se inicialice. > [!TIP] > > Puede ver el código fuente del lector de configuración INI y el lector de configuración JSON para entender cómo se implementa un IConfigurationProvider. ## Lectura de configuraciones desde un archivo JSON Por defecto, Sisk proporciona un proveedor de configuración que lee configuraciones desde un archivo JSON. Este archivo sigue una estructura fija y está compuesto por los siguientes parámetros: ```json { "Server": { "DefaultEncoding": "UTF-8", "ThrowExceptions": true, "IncludeRequestIdHeader": true }, "ListeningHost": { "Label": "Mi aplicación Sisk", "Ports": [ "http://localhost:80/", "https://localhost:443/", // Los archivos de configuración también admiten comentarios ], "CrossOriginResourceSharingPolicy": { "AllowOrigin": "*", "AllowOrigins": [ "*" ], // Nuevo en 0.14 "AllowMethods": [ "*" ], "AllowHeaders": [ "*" ], "MaxAge": 3600 }, "Parameters": { "MySqlConnection": "server=localhost;user=root;" } } } ``` Los parámetros creados a partir de un archivo de configuración se pueden acceder en el constructor del servidor: ```csharp using var app = HttpServer.CreateBuilder() .UsePortableConfiguration(config => { config.WithParameters(paramCollection => { string databaseConnection = paramCollection.GetValueOrThrow("MySqlConnection"); }); }) .Build(); ``` Cada lector de configuración proporciona una forma de leer los parámetros de inicialización del servidor. Algunas propiedades se indican que deben estar en el entorno del proceso en lugar de estar definidas en el archivo de configuración, como datos de API sensibles, claves de API, etc. ## Estructura del archivo de configuración El archivo de configuración JSON está compuesto por las siguientes propiedades:
    Propiedad Obligatorio Descripción
    Server Requerido Representa el servidor en sí con sus configuraciones.
    Server.AccessLogsStream Opcional Predeterminado en console. Especifica la secuencia de salida de los registros de acceso. Puede ser un nombre de archivo, null o console.
    Server.ErrorsLogsStream Opcional Predeterminado en null. Especifica la secuencia de salida de los registros de errores. Puede ser un nombre de archivo, null o console.
    Server.MaximumContentLength Opcional
    Server.MaximumContentLength Opcional Predeterminado en 0. Especifica la longitud máxima de contenido en bytes. Cero significa infinito.
    Server.IncludeRequestIdHeader Opcional Predeterminado en false. Especifica si el servidor HTTP debe enviar el encabezado X-Request-Id.
    Server.ThrowExceptions Opcional Predeterminado en true. Especifica si las excepciones no controladas deben lanzarse. Establezca en false cuando esté en producción y true cuando esté depurando.
    ListeningHost Requerido Representa el host de escucha del servidor.
    ListeningHost.Label Opcional Representa la etiqueta de la aplicación.
    ListeningHost.Ports Requerido Representa una matriz de cadenas, que coincide con la sintaxis ListeningPort.
    ListeningHost.CrossOriginResourceSharingPolicy Opcional Configura los encabezados CORS para la aplicación.
    ListeningHost.CrossOriginResourceSharingPolicy.AllowCredentials Opcional Predeterminado en false. Especifica el encabezado Allow-Credentials.
    ListeningHost.CrossOriginResourceSharingPolicy.ExposeHeaders Opcional Predeterminado en null. Esta propiedad espera una matriz de cadenas. Especifica el encabezado Expose-Headers.
    ListeningHost.CrossOriginResourceSharingPolicy.AllowOrigin Opcional Predeterminado en null. Esta propiedad espera una cadena. Especifica el encabezado Allow-Origin.
    ListeningHost.CrossOriginResourceSharingPolicy.AllowOrigins Opcional Predeterminado en null. Esta propiedad espera una matriz de cadenas. Especifica múltiples encabezados Allow-Origin. Consulte AllowOrigins para obtener más información.
    ListeningHost.CrossOriginResourceSharingPolicy.AllowMethods Opcional Predeterminado en null. Esta propiedad espera una matriz de cadenas. Especifica el encabezado Allow-Methods.
    ListeningHost.CrossOriginResourceSharingPolicy.AllowHeaders Opcional Predeterminado en null. Esta propiedad espera una matriz de cadenas. Especifica el encabezado Allow-Headers.
    ListeningHost.CrossOriginResourceSharingPolicy.MaxAge Opcional Predeterminado en null. Esta propiedad espera un entero. Especifica el encabezado Max-Age en segundos.
    ListeningHost.Parameters Opcional Especifica las propiedades proporcionadas al método de configuración de la aplicación.
    --- # Proveedor de configuración INI Source: https://docs.sisk-framework.org/es/docs/extensions/ini-configuration.html Sisk tiene un método para obtener configuraciones de inicio diferentes a JSON. De hecho, cualquier canalización que implemente [IConfigurationReader](https://docs.sisk-framework.org/api/Sisk.Core.Http.Hosting.IConfigurationReader.md) se puede utilizar con [PortableConfigurationBuilder.WithConfigurationPipeline](https://docs.sisk-framework.org/api/Sisk.Core.Http.Hosting.PortableConfigurationBuilder.md), leyendo la configuración del servidor desde cualquier tipo de archivo. El paquete [Sisk.IniConfiguration](https://www.nuget.org/packages/Sisk.IniConfiguration/) proporciona un lector de archivos INI basado en flujo que no lanza excepciones por errores de sintaxis comunes y tiene una sintaxis de configuración simple. Este paquete se puede utilizar fuera del marco de Sisk, ofreciendo flexibilidad para proyectos que requieren un lector de documentos INI eficiente. ## Instalación Para instalar el paquete, puede comenzar con: ```bash $ dotnet add package Sisk.IniConfiguration ``` También puede instalar el paquete principal, que no incluye el [IConfigurationReader](https://docs.sisk-framework.org/api/Sisk.Core.Http.Hosting.IConfigurationReader) INI, ni la dependencia de Sisk, solo los serializadores INI: ```bash $ dotnet add package Sisk.IniConfiguration.Core ``` Con el paquete principal, puede utilizarlo en su código como se muestra en el ejemplo a continuación: ```cs class Program { static HttpServerHostContext Host = null!; static void Main(string[] args) { Host = HttpServer.CreateBuilder() .UsePortableConfiguration(config => { config.WithConfigFile("app.ini", createIfDontExists: true); // utiliza el lector de configuración IniConfigurationReader config.WithConfigurationPipeline(); }) .UseRouter(r => { r.MapGet("/", SayHello); }) .Build(); Host.Start(); } static HttpResponse SayHello(HttpRequest request) { string? name = Host.Parameters["name"] ?? "world"; return new HttpResponse($"Hello, {name}!"); } } ``` El código anterior buscará un archivo app.ini en el directorio actual del proceso (CurrentDirectory). El archivo INI se ve así: ```ini [Server] # Se admiten varias direcciones de escucha 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" ``` ## Sabor y sintaxis INI Implementación actual del sabor: - Los nombres de propiedades y secciones son **insensibles a mayúsculas y minúsculas**. - Los nombres de propiedades y valores son **recortados**, a menos que los valores estén entre comillas. - Los valores pueden estar entre comillas simples o dobles. Las comillas pueden tener saltos de línea dentro de ellas. - Los comentarios están soportados con `#` y `;`. También se permiten **comentarios al final**. - Las propiedades pueden tener varios valores. En detalle, la documentación para el "sabor" del analizador INI utilizado en Sisk está [disponible en este documento](https://github.com/sisk-http/archive/blob/master/ext/ini-reader-syntax.md). Utilizando el siguiente código INI como ejemplo: ```ini One = 1 Value = this is an value Another value = "this value has an line break on it" ; el código a continuación tiene algunos colores [some section] Color = Red Color = Blue Color = Yellow ; no use amarillo ``` Analícelo con: ```csharp // analice el texto INI desde la cadena IniDocument doc = IniDocument.FromString(iniText); // obtenga un valor string? one = doc.Global.GetOne("one"); string? anotherValue = doc.Global.GetOne("another value"); // obtenga varios valores string[]? colors = doc.GetSection("some section")?.GetMany("color"); ``` ## Parámetros de configuración | Sección y nombre | Permite varios valores | Descripción | | ---------------- | --------------------- | ----------- | | `Server.Listen` | Sí | Las direcciones/puertos de escucha del servidor. | | `Server.Encoding` | No | La codificación predeterminada del servidor. | | `Server.MaximumContentLength` | No | El tamaño máximo de contenido en bytes del servidor. | | `Server.IncludeRequestIdHeader` | No | Especifica si el servidor HTTP debe enviar el encabezado X-Request-Id. | | `Server.ThrowExceptions` | No | Especifica si las excepciones no controladas deben lanzarse. | | `Server.AccessLogsStream` | No | Especifica la secuencia de salida de registros de acceso. | | `Server.ErrorsLogsStream` | No | Especifica la secuencia de salida de registros de errores. | | `Cors.AllowMethods` | No | Especifica el valor del encabezado CORS Allow-Methods. | | `Cors.AllowHeaders` | No | Especifica el valor del encabezado CORS Allow-Headers. | | `Cors.AllowOrigins` | No | Especifica varios encabezados Allow-Origin, separados por comas. [AllowOrigins](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigins.md) para más información. | | `Cors.AllowOrigin` | No | Especifica un encabezado Allow-Origin. | | `Cors.ExposeHeaders` | No | Especifica el valor del encabezado CORS Expose-Headers. | | `Cors.AllowCredentials` | No | Especifica el valor del encabezado CORS Allow-Credentials. | | `Cors.MaxAge` | No | Especifica el valor del encabezado CORS Max-Age. --- # Documentación de la API Source: https://docs.sisk-framework.org/es/docs/extensions/api-documentation.html La extensión `Sisk.Documenting` le permite generar documentación de API para su aplicación Sisk automáticamente. Aprovecha la estructura de su código y los atributos para crear un sitio de documentación completo, con soporte de exportación al formato Open API (Swagger). > [!WARNING] > Este paquete está actualmente en desarrollo y aún no se ha publicado. Su comportamiento y API pueden estar sujetos a cambios en futuras actualizaciones. Dado que este paquete aún no está disponible en NuGet, debe incorporar el código fuente directamente en su proyecto o referenciarlo como una dependencia del proyecto. Puede acceder al código fuente [aquí](https://github.com/sisk-http/core/tree/main/extensions/Sisk.Documenting). Para usar `Sisk.Documenting`, necesita registrarlo en el constructor de su aplicación y decorar sus manejadores de rutas con atributos de documentación. ### Registro de generación de documentación Utilice el método de extensión `UseApiDocumentation` en su `HttpServerHostContextBuilder` para exponer la documentación de API generada desde el mismo router que sirve su aplicación. ```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**: Define los metadatos de su aplicación, como nombre, descripción y versión. - **routerPath**: La ruta URL donde la interfaz de usuario de la documentación (o JSON) será accesible. - **exporter**: Configura cómo se exporta la documentación. El `OpenApiExporter` habilita el soporte Open API (Swagger). ### Documentación de Endpoints Puede describir sus endpoints usando los atributos `[ApiEndpoint]` y `[ApiQueryParameter]` en los métodos manejadores de rutas. ### `ApiEndpoint` El atributo `[ApiEndpoint]` le permite proporcionar una descripción para el endpoint. ```csharp [ApiEndpoint(Description = "Returns a greeting message.")] public HttpResponse Index(HttpRequest request) { ... } ``` ### `ApiQueryParameter` El atributo `[ApiQueryParameter]` documenta los parámetros de cadena de consulta que el endpoint acepta. ```csharp [ApiQueryParameter(name: "name", IsRequired = false, Description = "The name of the person to greet.", Type = "string")] public HttpResponse Index(HttpRequest request) { ... } ``` - **name**: El nombre del parámetro de consulta. - **IsRequired**: Especifica si el parámetro es obligatorio. - **Description**: Una descripción legible del parámetro. - **Type**: El tipo de dato esperado (p.ej., "string", "int"). ### `ApiEndpoint` Anota un endpoint con información general. * **Name** (string, required in constructor): El nombre del endpoint de la API. * **Description** (string): Una breve descripción de lo que hace el endpoint. * **Group** (string): Permite agrupar endpoints (p.ej., por controlador o módulo). * **InheritDescriptionFromXmlDocumentation** (bool, default: `true`): Si `true`, intenta usar el resumen de la documentación XML del método si `Description` no está establecida. ### `ApiHeader` Documenta un encabezado HTTP específico que el endpoint espera o utiliza. * **HeaderName** (string, required in constructor): La clave del encabezado (p.ej., "Authorization"). * **Description** (string): Describe el propósito del encabezado. * **IsRequired** (bool): Indica si el encabezado es obligatorio para la solicitud. ### `ApiParameter` Define un parámetro genérico para el endpoint, a menudo usado para campos de formulario o parámetros de cuerpo que no están cubiertos por otros atributos. * **Name** (string, required in constructor): El nombre del parámetro. * **TypeName** (string, required in constructor): El tipo de dato del parámetro (p.ej., "string", "int"). * **Description** (string): Una descripción del parámetro. * **IsRequired** (bool): Indica si el parámetro es obligatorio. ### `ApiParametersFrom` Genera automáticamente la documentación de parámetros a partir de las propiedades de una clase o tipo especificado. * **Type** (Type, required in constructor): El `Type` de la clase del cual reflejar propiedades. ### `ApiPathParameter` Documenta una variable de ruta (p.ej., en `/users/{id}`). * **Name** (string, required in constructor): El nombre del parámetro de ruta. * **Description** (string): Describe lo que representa el parámetro. * **Type** (string): El tipo de dato esperado. ### `ApiQueryParameter` Documenta un parámetro de cadena de consulta (p.ej., `?page=1`). * **Name** (string, required in constructor): La clave del parámetro de consulta. * **Description** (string): Describe el parámetro. * **Type** (string): El tipo de dato esperado. * **IsRequired** (bool): Indica si el parámetro de consulta debe estar presente. ### `ApiRequest` Describe el cuerpo de la solicitud esperado. * **Description** (string, required in constructor): Una descripción del cuerpo de la solicitud. * **Example** (string): Una cadena cruda que contiene un ejemplo del cuerpo de la solicitud. * **ExampleLanguage** (string): El lenguaje del ejemplo (p.ej., "json", "xml"). * **PayloadType** (Type): Si se establece, el ejemplo y el esquema se generarán automáticamente a partir de este tipo cuando los manejadores de contexto configurados lo soporten. ### `ApiResponse` Describe una posible respuesta del endpoint. * **StatusCode** (HttpStatusCode, required in constructor): El código de estado HTTP devuelto (p.ej., `HttpStatusCode.OK`). * **Description** (string): Describe la condición para esta respuesta. * **Example** (string): Una cadena cruda que contiene un ejemplo del cuerpo de la respuesta. * **ExampleLanguage** (string): El lenguaje del ejemplo. * **PayloadType** (Type): Si se establece, el ejemplo y el esquema se generarán automáticamente a partir de este tipo cuando los manejadores de contexto configurados lo soporten. ## Manejadores de Tipo Los manejadores de tipo son responsables de convertir sus tipos .NET (clases, enumeraciones, etc.) en ejemplos de documentación. Esto es particularmente útil para generar ejemplos automáticos de cuerpos de solicitud y respuesta basados en sus modelos de datos. Estos manejadores se configuran dentro del `ApiGenerationContext`. ```csharp using Sisk.Documenting.Content; var context = new ApiGenerationContext() { // ... BodyExampleTypeHandler = new JsonContentTypeHandler(), ParameterExampleTypeHandler = new JsonContentTypeHandler(), ContentSchemaTypeHandler = new JsonContentTypeHandler() }; ``` ### JsonContentTypeHandler El `JsonContentTypeHandler` es un manejador incorporado que genera ejemplos JSON, ejemplos de parámetros y esquemas JSON. Implementa `IExampleBodyTypeHandler`, `IExampleParameterTypeHandler` y `IContentSchemaTypeHandler`. Puede personalizarse con opciones específicas de `JsonSerializerOptions` o `IJsonTypeInfoResolver` para que coincidan con la lógica de serialización de su aplicación. ```csharp var jsonHandler = new JsonContentTypeHandler(new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true }); context.BodyExampleTypeHandler = jsonHandler; context.ParameterExampleTypeHandler = jsonHandler; context.ContentSchemaTypeHandler = jsonHandler; ``` ### Manejadores de Tipo Personalizados Puede implementar sus propios manejadores para soportar otros formatos (como XML) o para personalizar cómo se generan los ejemplos. #### IExampleBodyTypeHandler Implemente esta interfaz para generar ejemplos de cuerpo para tipos de solicitud y respuesta. ```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 Implemente esta interfaz para generar descripciones detalladas de parámetros a partir de un tipo (usado por `[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(); } } ``` ## Exportadores Los exportadores son responsables de convertir los metadatos de documentación de API recopilados en un formato específico que pueda ser consumido por otras herramientas o mostrado al usuario. ### OpenApiExporter El exportador predeterminado proporcionado es el `OpenApiExporter`, que genera un archivo JSON siguiendo la [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" } ``` ### Creación de un Exportador Personalizado Puede crear su propio exportador implementando la interfaz `IApiDocumentationExporter`. Esto le permite generar documentación en formatos como Markdown, HTML, Postman Collection o cualquier otro formato personalizado. La interfaz requiere que implemente un único método: `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"); } } ``` Luego, simplemente úselo en su configuración: ```csharp host.UseApiDocumentation( // ... exporter: new MyCustomExporter() ); ``` ### Ejemplo Completo A continuación se muestra un ejemplo completo que demuestra cómo configurar `Sisk.Documenting` y documentar un controlador sencillo. ```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}!"); } } ``` En este ejemplo, al acceder a `/api/docs` se servirá la documentación generada para la API "My application", describiendo el endpoint `GET /` y su parámetro `name`. --- # Configuración manual (avanzada) Source: https://docs.sisk-framework.org/es/docs/advanced/manual-setup.html Utilice la configuración manual cuando necesite ensamblar los componentes del servidor usted mismo, como cuando un proceso debe exponer varios hosts, puertos, routers o una configuración de servidor personalizada. Para la mayoría de las aplicaciones, la API del constructor es más corta y debe ser preferida. La configuración manual es útil cuando desea control directo sobre los cuatro componentes principales: un `Router`, uno o más objetos `ListeningHost`, una `HttpServerConfiguration` y el `HttpServer` final. Primero, necesitamos entender el concepto de solicitud/respuesta. Es bastante simple: por cada solicitud, debe haber una respuesta. Sisk sigue también este principio. Creemos un método que responda con un mensaje "Hello, World!" en HTML, especificando el código de estado y los encabezados. ```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; } ``` El siguiente paso es asociar este método con una ruta HTTP. ## Enrutadores Los enrutadores son abstracciones de rutas de solicitud y sirven como puente entre solicitudes y respuestas para el servicio. Los enrutadores gestionan rutas del servicio, funciones y errores. Un enrutador puede tener varias rutas, y cada ruta puede realizar diferentes operaciones en esa ruta, como ejecutar una función, servir una página o proporcionar un recurso del servidor. Creemos nuestro primer enrutador y asociemos nuestro método `IndexPage` con la ruta de índice. ```csharp Router mainRouter = new Router; mainRouter.MapGet("/", IndexPage); ``` Ahora nuestro enrutador puede recibir solicitudes y enviar respuestas. Sin embargo, `mainRouter` no está vinculado a un host o a un servidor, por lo que no funcionará por sí solo. El siguiente paso es crear nuestro ListeningHost. ## Hosts de escucha y puertos Un [ListeningHost](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.md) puede alojar un enrutador y varios puertos de escucha para el mismo enrutador. Un [ListeningPort](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningPort.md) es un prefijo donde el servidor HTTP escuchará. Aquí, podemos crear un `ListeningHost` que apunte a dos puntos finales para nuestro enrutador: ```csharp ListeningHost myHost = new ListeningHost { Router = mainRouter, Ports = new ListeningPort[] { new ListeningPort("http://localhost:5000/") } }; ``` Ahora nuestro servidor HTTP escuchará los puntos finales especificados y redirigirá sus solicitudes a nuestro enrutador. ## Configuración del servidor La configuración del servidor es responsable de la mayor parte del comportamiento del propio servidor HTTP. En esta configuración, podemos asociar `ListeningHosts` con nuestro servidor. ```csharp HttpServerConfiguration config = new HttpServerConfiguration(); config.ListeningHosts.Add(myHost); // Agregar nuestro ListeningHost a esta configuración del servidor ``` Opciones comunes de configuración del servidor: | Propiedad | Predeterminado | Usar cuando | Notas | | --- | --- | --- | --- | | [RemoteRequestsAction](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.RemoteRequestsAction.md) | `RequestListenAction.Accept` | El servicio debe rechazar solicitudes no locales a menos que provengan de un proxy inverso confiable. | Establézcalo en `Drop` solo cuando la topología de despliegue sea clara. | | [IncludeRequestIdHeader](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.IncludeRequestIdHeader.md) | `false` | Los clientes o proxies necesitan el ID de solicitud de Sisk en el encabezado de respuesta `X-Request-Id`. | Combínelo con registros que incluyan `HttpRequest.RequestId`. | | [IdleConnectionTimeout](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.IdleConnectionTimeout.md) | `120` seconds | Las conexiones keep-alive inactivas deben cerrarse tarde o temprano. | Esto lo aplica el motor HTTP. | | [NormalizeHeadersEncodings](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.NormalizeHeadersEncodings.md) | `false` | Recibe encabezados con una discordancia de codificación. | Esto tiene un costo de procesamiento; déjelo deshabilitado a menos que sea necesario. | | [SendSiskHeader](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.SendSiskHeader.md) | `true` | Desea ocultar o exponer el encabezado `X-Powered-By` de Sisk. | Desactívelo para políticas de encabezados de producción más estrictas. | | [OptionsLogMode](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.OptionsLogMode.md) | `LogOutput.Both` | Desea reducir o redirigir los registros generados por el manejo automático de `OPTIONS`. | Utiliza los mismos valores de modo de registro que las rutas. | | [AsyncRequestProcessing](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.AsyncRequestProcessing.md) | `true` | Necesita procesamiento determinista de una sola solicitud para diagnóstico. | Desactivarlo limita el rendimiento. | | [DisposeDisposableContextValues](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.DisposeDisposableContextValues.md) | `true` | Los valores del contenedor de solicitud que implementan `IDisposable` deben disponerse automáticamente. | Manténgalo habilitado a menos que la propiedad se gestione en otro lugar. | | [ConvertIAsyncEnumerableIntoEnumerable](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ConvertIAsyncEnumerableIntoEnumerable.md) | `true` | Los manejadores de valores deben recibir enumerables asíncronos como valores enumerables bloqueantes. | Desactívelo cuando implemente su propio manejo de flujos asíncronos. | | [KeepAlive](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.KeepAlive.md) | `true` | Las conexiones deben permanecer reutilizables después de las respuestas. | Desactívelo para clientes o intermediarios que no manejan bien las conexiones persistentes. | | [ForceTrailingSlash](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ForceTrailingSlash.md) | `false` | Las rutas GET deben redirigir a una URL con barra diagonal final. | Se aplica solo a rutas que no son expresiones regulares. | | [MaximumContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.MaximumContentLength.md) | `0` | Los cuerpos de solicitud necesitan un límite de tamaño. | `0` significa ilimitado hasta que se alcancen los límites del framework o de la memoria. | | [EnableAutomaticResponseCompression](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.EnableAutomaticResponseCompression.md) | `false` | Las respuestas deben comprimirse automáticamente cuando el cliente lo soporta. | Las respuestas `CompressedContent` existentes no se comprimen de nuevo. | A continuación, podemos crear nuestro servidor HTTP: ```csharp HttpServer server = new HttpServer(config); server.Start(); // Inicia el servidor Console.ReadKey(); // Evita que la aplicación salga ``` Ahora podemos compilar nuestro ejecutable y ejecutar nuestro servidor HTTP con el comando: ```bash dotnet watch ``` En tiempo de ejecución, abra su navegador y navegue a la ruta del servidor, y debería ver: --- # Ciclo de vida de la solicitud Source: https://docs.sisk-framework.org/es/docs/advanced/request-lifecycle.html A continuación se explica todo el ciclo de vida de una solicitud mediante un ejemplo de una solicitud HTTP. - **Receiving the request:** cada solicitud crea un contexto HTTP entre la propia solicitud y la respuesta que se entregará al cliente. Este contexto proviene del listener incorporado en Sisk, que puede ser el [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), o [Cadente](https://blog.sisk-framework.org/posts/2025-01-29-cadente-experiment/). - Validación de solicitud externa: la validación de [HttpServerConfiguration.RemoteRequestsAction](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.RemoteRequestsAction.md) se valida para la solicitud. - Si la solicitud es externa y la propiedad es `Drop`, la conexión se cierra sin una respuesta al cliente con un `HttpServerExecutionStatus = RemoteRequestDropped`. - Configuración del Forwarding Resolver: si se configura un [ForwardingResolver](https://docs.sisk-framework.org/es/docs/advanced/forwarding-resolvers.md), se llamará al método [OnResolveRequestHost](https://docs.sisk-framework.org/api/Sisk.Core.Http.ForwardingResolver.OnResolveRequestHost.md) en el host original de la solicitud. - Coincidencia DNS: con el host resuelto y con más de un [ListeningHost](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.md) configurado, el servidor buscará el host correspondiente para la solicitud. - Si no coincide ningún ListeningHost, se devuelve una respuesta 400 Bad Request al cliente y un estado `HttpServerExecutionStatus = DnsUnknownHost` al contexto HTTP. - Si coincide un ListeningHost, pero su [Router](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.Router.md) aún no está inicializado, se devuelve una respuesta 503 Service Unavailable al cliente y un estado `HttpServerExecutionStatus = ListeningHostNotReady` al contexto HTTP. - Vinculación del router: el router del ListeningHost correspondiente se asocia con el servidor HTTP recibido. - Si el router ya está asociado a otro servidor HTTP, lo cual no está permitido porque el router usa activamente los recursos de configuración del servidor, se lanza una `InvalidOperationException`. Esto solo ocurre durante la inicialización del servidor HTTP, no durante la creación del contexto HTTP. - Predefinición de encabezados: - Predefine el encabezado `X-Request-Id` en la respuesta si está configurado para hacerlo. - Predefine el encabezado `X-Powered-By` en la respuesta si está configurado para hacerlo. - Validación del tamaño del contenido: valida si el contenido de la solicitud es menor que [HttpServerConfiguration.MaximumContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.MaximumContentLength.md) solo si es mayor que cero. - Si la solicitud envía un `Content-Length` mayor que el configurado, se devuelve una respuesta 413 Payload Too Large al cliente y un estado `HttpServerExecutionStatus = ContentTooLarge` al contexto HTTP. - Se invoca el evento `OnHttpRequestOpen` para todos los manejadores de servidor HTTP configurados. - **Routing the action:** el servidor invoca el router para la solicitud recibida. - Si el router no encuentra una ruta que coincida con la solicitud: - Si la propiedad [Router.NotFoundErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.NotFoundErrorHandler.md) está configurada, se invoca la acción y la respuesta de la acción se reenvía al cliente HTTP. - Si la propiedad anterior es nula, se devuelve una respuesta predeterminada 404 Not Found al cliente. - Si el router encuentra una ruta coincidente, pero el método de la ruta no coincide con el método de la solicitud: - Si la propiedad [Router.MethodNotAllowedErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MethodNotAllowedErrorHandler.md) está configurada, se invoca la acción y la respuesta de la acción se reenvía al cliente HTTP. - Si la propiedad anterior es nula, se devuelve una respuesta predeterminada 405 Method Not Allowed al cliente. - Si la solicitud es del método `OPTIONS`: - El router devuelve una respuesta 200 Ok al cliente solo si ninguna ruta coincide con el método de la solicitud (el método de la ruta no es explícitamente [RouteMethod.Options](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md)). - Si la propiedad [HttpServerConfiguration.ForceTrailingSlash](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ForceTrailingSlash.md) está habilitada, la ruta coincidente no es una expresión regular, la ruta de la solicitud no termina con `/`, y el método de la solicitud es `GET`: - Se devuelve al cliente una respuesta HTTP 307 Temporary Redirect con el encabezado `Location` que contiene la ruta y la consulta a la misma ubicación con un `/` al final. - Se invoca el evento `OnContextBagCreated` para todos los manejadores de servidor HTTP configurados. - Se ejecutan todas las instancias globales de [IRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.md) con la bandera `BeforeResponse`. - Si algún manejador devuelve una respuesta no nula, esa respuesta se reenvía al cliente HTTP y el contexto se cierra. - Si se lanza un error en este paso y [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) está deshabilitado: - Si la propiedad [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) está habilitada, se invoca y la respuesta resultante se devuelve al cliente. - Si la propiedad anterior no está definida, se devuelve una respuesta vacía al servidor, que reenvía una respuesta según el tipo de excepción lanzada, que normalmente es 500 Internal Server Error. - Se ejecutan todas las instancias de [IRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.md) definidas en la ruta y con la bandera `BeforeResponse`. - Si algún manejador devuelve una respuesta no nula, esa respuesta se reenvía al cliente HTTP y el contexto se cierra. - Si se lanza un error en este paso y [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) está deshabilitado: - Si la propiedad [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) está habilitada, se invoca y la respuesta resultante se devuelve al cliente. - Si la propiedad anterior no está definida, se devuelve una respuesta vacía al servidor, que reenvía una respuesta según el tipo de excepción lanzada, que normalmente es 500 Internal Server Error. - Se invoca la acción del router y se transforma en una respuesta HTTP. - Si se lanza un error en este paso y [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) está deshabilitado: - Si la propiedad [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) está habilitada, se invoca y la respuesta resultante se devuelve al cliente. - Si la propiedad anterior no está definida, se devuelve una respuesta vacía al servidor, que reenvía una respuesta según el tipo de excepción lanzada, que normalmente es 500 Internal Server Error. - Se ejecutan todas las instancias globales de [IRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.md) con la bandera `AfterResponse`. - Si algún manejador devuelve una respuesta no nula, la respuesta del manejador reemplaza la respuesta anterior y se reenvía inmediatamente al cliente HTTP. - Si se lanza un error en este paso y [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) está deshabilitado: - Si la propiedad [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) está habilitada, se invoca y la respuesta resultante se devuelve al cliente. - Si la propiedad anterior no está definida, se devuelve una respuesta vacía al servidor, que reenvía una respuesta según el tipo de excepción lanzada, que normalmente es 500 Internal Server Error. - Se ejecutan todas las instancias de [IRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.md) definidas en la ruta y con la bandera `AfterResponse`. - Si algún manejador devuelve una respuesta no nula, la respuesta del manejador reemplaza la respuesta anterior y se reenvía inmediatamente al cliente HTTP. - Si se lanza un error en este paso y [HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) está deshabilitado: - Si la propiedad [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) está habilitada, se invoca y la respuesta resultante se devuelve al cliente. - Si la propiedad anterior no está definida, se devuelve una respuesta vacía al servidor, que reenvía una respuesta según el tipo de excepción lanzada, que normalmente es 500 Internal Server Error. - **Processing the response:** con la respuesta lista, el servidor la prepara para enviarla al cliente. - Los encabezados de la política Cross-Origin Resource Sharing (CORS) se definen en la respuesta según lo configurado en el actual [ListeningHost.CrossOriginResourceSharingPolicy](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.CrossOriginResourceSharingPolicy.md). - El código de estado y los encabezados de la respuesta se envían al cliente. - El contenido de la respuesta se envía al cliente: - Si el contenido de la respuesta es descendiente de [ByteArrayContent](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.bytearraycontent), los bytes de la respuesta se copian directamente al flujo de salida de la respuesta. - Si no se cumple la condición anterior, la respuesta se serializa a un flujo y se copia al flujo de salida de la respuesta. - Los flujos se cierran y el contenido de la respuesta se descarta. - Si [HttpServerConfiguration.DisposeDisposableContextValues](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.DisposeDisposableContextValues.md) está habilitado, todos los objetos definidos en el contexto de la solicitud que heredan de [IDisposable](https://learn.microsoft.com/en-us/dotnet/api/system.idisposable) se descartan. - Se invoca el evento `OnHttpRequestClose` para todos los manejadores de servidor HTTP configurados. - Si se lanzó una excepción en el servidor, se invoca el evento `OnException` para todos los manejadores de servidor HTTP configurados. - Si la ruta permite el registro de accesos y [HttpServerConfiguration.AccessLogsStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.AccessLogsStream.md) no es nulo, se escribe una línea de registro en la salida de logs. - Si la ruta permite el registro de errores, hay una excepción, y [HttpServerConfiguration.ErrorsLogsStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ErrorsLogsStream.md) no es nulo, se escribe una línea de registro en la salida de logs de errores. - Si el servidor está esperando una solicitud a través de [HttpServer.WaitNext](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.WaitNext.md), el mutex se libera y el contexto queda disponible para el usuario. --- # Resolvedores de Reenvío Source: https://docs.sisk-framework.org/es/docs/advanced/forwarding-resolvers.html Un Resolvedor de Reenvío es un ayudante que ayuda a decodificar información que identifica al cliente a través de una solicitud, proxy, CDN o balanceadores de carga. Cuando su servicio Sisk se ejecuta a través de un proxy inverso o directo, la dirección IP del cliente, el host y el protocolo pueden ser diferentes de la solicitud original ya que es un reenvío de un servicio a otro. Esta funcionalidad de Sisk le permite controlar y resolver esta información antes de trabajar con la solicitud. Estos proxies usualmente proporcionan encabezados útiles para identificar a su cliente. Actualmente, con la clase [ForwardingResolver](https://docs.sisk-framework.org/api/Sisk.Core.Http.ForwardingResolver.md) es posible resolver la dirección IP del cliente, el host y el protocolo HTTP utilizado. Después de la versión 1.0 de Sisk, el servidor ya no tiene una implementación estándar para decodificar estos encabezados por razones de seguridad que varían de servicio a servicio. Por ejemplo, el encabezado `X-Forwarded-For` incluye información sobre las direcciones IP que reenviaron la solicitud. Este encabezado es usado por los proxies para transportar una cadena de información al servicio final e incluye la IP de todos los proxies utilizados, incluida la dirección real del cliente. El problema es: a veces es difícil identificar la IP remota del cliente y no existe una regla específica para identificar este encabezado. Se recomienda encarecidamente leer la documentación de los encabezados que está a punto de implementar a continuación: - Lea sobre el encabezado `X-Forwarded-For` [aquí](https://developer.mozilla.org/en-US/docs/es/Web/HTTP/Headers/X-Forwarded-For#security_and_privacy_concerns). - Lea sobre el encabezado `X-Forwarded-Host` [aquí](https://developer.mozilla.org/en-US/docs/es/Web/HTTP/Headers/X-Forwarded-Host). - Lea sobre el encabezado `X-Forwarded-Proto` [aquí](https://developer.mozilla.org/en-US/docs/es/Web/HTTP/Headers/X-Forwarded-Proto). ## La clase ForwardingResolver Esta clase tiene tres métodos virtuales que permiten la implementación más adecuada para cada servicio. Cada método es responsable de resolver información de la solicitud a través de un proxy: la dirección IP del cliente, el host de la solicitud y el protocolo de seguridad utilizado. Por defecto, Sisk siempre usará la información de la solicitud original, sin resolver ningún encabezado. El ejemplo a continuación muestra cómo se puede usar esta implementación. Este ejemplo resuelve la IP del cliente mediante el encabezado `X-Forwarded-For` y lanza un error cuando se han reenviado más de una IP en la solicitud. > [!IMPORTANT] > No utilice este ejemplo en código de producción. Siempre verifique si la implementación es adecuada para su uso. Lea la documentación del encabezado antes de implementarlo. ```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]); } } } ``` --- # Manejadores del servidor Http Source: https://docs.sisk-framework.org/es/docs/advanced/http-server-handlers.html En la versión 0.16 de Sisk, hemos introducido la clase `HttpServerHandler`, que tiene como objetivo ampliar el comportamiento general de Sisk y proporcionar manejadores de eventos adicionales a Sisk, como el manejo de solicitudes Http, routers, bolsas de contexto y más. Esta clase concentra los eventos que ocurren durante la vida útil de todo el servidor HTTP y también de una solicitud. El protocolo Http no tiene sesiones, por lo que no es posible conservar información de una solicitud a otra. Por ahora, Sisk ofrece una forma de que implementes sesiones, contextos, conexiones a bases de datos y otros proveedores útiles para ayudar en tu trabajo. Por favor, consulta [esta página](https://docs.sisk-framework.org/api/Sisk.Core.Http.Handlers.HttpServerHandler.md) para leer dónde se dispara cada evento y cuál es su propósito. También puedes ver el [ciclo de vida de una solicitud HTTP](https://docs.sisk-framework.org/es/docs/advanced/request-lifecycle.md) para entender qué ocurre con una solicitud y dónde se disparan los eventos. El servidor HTTP permite usar varios manejadores al mismo tiempo. Cada llamada a un evento es síncrona, es decir, bloqueará el hilo actual para cada solicitud o contexto hasta que todos los manejadores asociados a esa función se ejecuten y completen. A diferencia de los RequestHandlers, no pueden aplicarse a algunos grupos de rutas o rutas específicas. En su lugar, se aplican a todo el servidor HTTP. Puedes aplicar condiciones dentro de tu Http Server Handler. Además, los singletons de cada HttpServerHandler se definen para cada aplicación Sisk, de modo que solo existe una instancia por `HttpServerHandler`. Un ejemplo práctico de uso de HttpServerHandler es disponer automáticamente una conexión a la base de datos al final de la solicitud. ```cs // DatabaseConnectionHandler.cs public class DatabaseConnectionHandler : HttpServerHandler { protected override void OnHttpRequestClose(HttpServerExecutionResult result) { var requestBag = result.Request.Context.RequestBag; // verifica si la solicitud ha definido un DbContext // en su bolsa de contexto 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()); } } ``` Con el código anterior, la extensión `GetDbContext` permite crear un contexto de conexión directamente desde el objeto HttpRequest. Una conexión no liberada puede causar problemas al trabajar con la base de datos, por lo que se termina en `OnHttpRequestClose`. Puedes registrar un manejador en un servidor Http en tu constructor o directamente con [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(); } } ``` Con esto, la clase `UsersController` puede utilizar el contexto de base de datos de la siguiente manera: ```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."); } } ``` El código anterior utiliza métodos como `JsonOk` y `JsonMessage` que están incorporados en `ApiController`, el cual hereda de un `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 })); } } ``` Los desarrolladores pueden implementar sesiones, contextos y conexiones a bases de datos usando esta clase. El código proporcionado muestra un ejemplo práctico con el DatabaseConnectionHandler, automatizando la liberación de la conexión a la base de datos al final de cada solicitud. La integración es sencilla, con los manejadores registrados durante la configuración del servidor. La clase HttpServerHandler ofrece un conjunto de herramientas potente para gestionar recursos y ampliar el comportamiento de Sisk en aplicaciones HTTP. --- # Múltiples hosts de escucha por servidor Source: https://docs.sisk-framework.org/es/docs/advanced/multi-host-setup.html El Sisk Framework siempre ha soportado el uso de más de un host por servidor, es decir, un único servidor HTTP puede escuchar en varios puertos y cada puerto tiene su propio router y su propio servicio ejecutándose en él. De esta manera, es fácil separar responsabilidades y gestionar servicios en un único servidor HTTP con Sisk. El ejemplo a continuación muestra la creación de dos ListeningHosts, cada uno escuchando en un puerto diferente, con routers y acciones distintas. Lea [creación manual de su aplicación](https://docs.sisk-framework.org/es/docs/advanced/manual-setup.md) para entender los detalles de esta abstracción. ```cs static void Main(string[] args) { // crear dos hosts de escucha, cada uno con su propio router y // escucha en su propio puerto // 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!")); // crear una configuración de servidor y agregar ambos // hosts de escucha en ella // HttpServerConfiguration configuration = new HttpServerConfiguration(); configuration.ListeningHosts.Add(hostA); configuration.ListeningHosts.Add(hostB); // crear un servidor http que usa la // configuración especificada // HttpServer server = new HttpServer(configuration); // iniciar el servidor 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); } ``` --- # Motores de servidor HTTP Source: https://docs.sisk-framework.org/es/docs/advanced/server-engines.html El marco de trabajo Sisk se divide en varios paquetes, donde el principal (Sisk.HttpServer) no incluye un servidor HTTP base - por defecto, [HttpListener](https://learn.microsoft.com/en-us/dotnet/api/system.net.httplistener?view=net-9.0) se utiliza como el motor principal de Sisk para realizar el papel de bajo nivel del servidor. El motor HTTP cumple el papel de la capa debajo de la capa de aplicación ofrecida por Sisk. Esta capa es responsable de la gestión de conexiones, serialización y deserialización de mensajes, control de cola de mensajes y comunicación con el socket de la máquina. La clase [HttpServerEngine](https://docs.sisk-framework.org/api/Sisk.Core.Http.Engine.HttpServerEngine.md) expone una API para implementar todas las funcionalidades necesarias de un motor HTTP para ser utilizado en capas superiores con Sisk, como enrutamiento, SSE, middlewares, etc. Estas funciones no son responsabilidad del motor HTTP, sino de un subconjunto de bibliotecas que utilizarán el motor HTTP como base para la ejecución. Con esta abstracción, es posible portar Sisk para ser utilizado con cualquier otro motor HTTP, escrito en .NET o no, como Kestrel, por ejemplo. Actualmente, Sisk sigue utilizando una abstracción del [HttpListener](https://learn.microsoft.com/en-us/dotnet/api/system.net.httplistener?view=net-9.0) nativo de .NET como el valor predeterminado para nuevos proyectos. Esta abstracción predeterminada conlleva algunos problemas específicos, como un comportamiento no especificado en diferentes plataformas (HttpListener tiene una implementación para Windows y otra para otras plataformas), falta de soporte para SSL y un rendimiento no muy agradable fuera de Windows. También está disponible una implementación experimental de un servidor de alto rendimiento escrito puramente en C# como un motor HTTP para Sisk, llamado el proyecto [Cadente](https://github.com/sisk-http/core/tree/main/cadente), que es un experimento de un servidor administrado que se puede utilizar con Sisk o no. ## Implementar un motor HTTP para Sisk Puedes crear un puente de conexión entre un servidor HTTP existente y Sisk extendiendo la clase [HttpServerEngine](https://docs.sisk-framework.org/api/Sisk.Core.Http.Engine.HttpServerEngine.md). Además de esta clase, también deberás implementar abstracciones para contextos, solicitudes y respuestas. Un ejemplo de abstracción completa está [disponible en GitHub](https://github.com/sisk-http/core/blob/main/src/Http/Engine/HttpListenerAbstractEngine.cs) para su visualización. Se ve así: ```csharp /// /// Proporciona una implementación de utilizando . /// public sealed class HttpListenerAbstractEngine : HttpServerEngine { private HttpListener _listener; private static Lazy shared = new Lazy ( () => new HttpListenerAbstractEngine () ); /// /// Obtiene la instancia compartida de la clase . /// public static HttpListenerAbstractEngine Shared => shared.Value; /// /// Inicializa una nueva instancia de la clase . /// public HttpListenerAbstractEngine () { _listener = new HttpListener { IgnoreWriteExceptions = true }; } /// public override TimeSpan IdleConnectionTimeout { get => _listener.TimeoutManager.IdleConnection; set => _listener.TimeoutManager.IdleConnection = value; } // ... } ``` ## Elegir un bucle de eventos Durante la creación de un motor HTTP, el servidor escuchará las solicitudes en un bucle y creará contextos para manejar cada una de ellas en hilos separados. Para esto, deberás elegir un [HttpServerEngineContextEventLoopMechanism](https://docs.sisk-framework.org/api/Sisk.Core.Http.Engine.HttpServerEngineContextEventLoopMechanism.md): - `InlineAsynchronousGetContext` el bucle de eventos es lineal - las llamadas de manejo de contexto HTTP ocurren en un bucle asíncrono. - `UnboundAsynchronousGetContext` el bucle de eventos se transmite a través de los métodos `BeginGetContext` y `EndGetContext`. ```csharp public override HttpServerEngineContextEventLoopMechanism EventLoopMechanism => HttpServerEngineContextEventLoopMechanism.UnboundAsynchronousGetContext; ``` No necesitas implementar ambos bucles de eventos. Elige el que más sentido tenga para tu motor HTTP. ## Pruebas Después de vincular tu motor HTTP, es esencial realizar pruebas para asegurarte de que todas las funcionalidades de Sisk tengan un comportamiento idéntico al utilizar otros motores. **Es extremadamente importante** tener el mismo comportamiento de Sisk para diferentes motores HTTP. Puedes visitar el repositorio de pruebas en [GitHub](https://github.com/sisk-http/core/tree/main/tests).