# Sisk Framework documentation > Sisk is a lightweight, agnostic and robust .NET web development framework. This file contains the complete Sisk documentation (日本語). The API reference is not included; read https://docs.sisk-framework.org/api/index.md for the type index. --- # Getting started Source: https://docs.sisk-framework.org/ja/docs/getting-started.html Welcome to the Sisk documentation! Sisk is an open-source lightweight HTTP framework for .NET. You can use it to build a standalone web service, embed an HTTP module inside an existing application, or run a service behind a reverse proxy with only the configuration you need. Sisk's values include code transparency, modularity, performance, and scalability. It can handle different application styles, including RESTful APIs, JSON-RPC services, WebSockets, Server-Sent Events, and static file serving. It's main features includes: | リソース | 説明 | | ------- | --------- | | [Routing](https://docs.sisk-framework.org/ja/docs/fundamentals/routing.md) | プレフィックス、カスタムメソッド、パス変数、値コンバータなどをサポートするパスルーターです。 | | [Request Handlers](https://docs.sisk-framework.org/ja/docs/fundamentals/request-handlers.md) | *ミドルウェア* とも呼ばれ、アクションの前後でリクエストと連携する独自のリクエストハンドラを構築するためのインターフェースを提供します。 | | [Compression](https://docs.sisk-framework.org/ja/docs/fundamentals/responses.md#gzip-deflate-and-brotli-compression) | Sisk を使ってレスポンス内容を簡単に圧縮できます。 | | [Web sockets](https://docs.sisk-framework.org/ja/docs/features/websockets.md) | クライアントとの読み書きが可能な完全な WebSocket を受け入れるルートを提供します。 | | [Server-sent events](https://docs.sisk-framework.org/ja/docs/features/server-sent-events.md) | SSE プロトコルをサポートするクライアントへサーバーイベントを送信する機能を提供します。 | | [Logging](https://docs.sisk-framework.org/ja/docs/features/logging.md) | シンプルなロギング。エラーやアクセスのログ、サイズでローテーションするログ、同一ログへの複数出力ストリームなどを定義できます。 | | [Multi-host](https://docs.sisk-framework.org/ja/docs/advanced/multi-host-setup.md) | 複数ポート用の HTTP サーバーを持ち、各ポートが独自のルーターを、各ルーターが独自のアプリケーションを持ちます。 | | [Server handlers](https://docs.sisk-framework.org/ja/docs/advanced/http-server-handlers.md) | HTTP サーバーの独自実装を拡張します。拡張機能や改善、新機能でカスタマイズできます。 | ## 最初のステップ Sisk は任意の .NET 環境で実行できます。このガイドでは、.NET を使用して Sisk アプリケーションを作成する方法を説明します。まだインストールしていない場合は、[こちら](https://dotnet.microsoft.com/en-us/download/dotnet/7.0)から SDK をダウンロードしてください。 このチュートリアルでは、プロジェクト構成の作成、リクエストの受信、URL パラメータの取得、レスポンスの送信方法を扱います。このガイドは C# を使用したシンプルなサーバー構築に焦点を当てています。好きなプログラミング言語でも使用できます。 > [!NOTE] > クイックスタートプロジェクトに興味があるかもしれません。詳細は [このリポジトリ](https://github.com/sisk-http/quickstart) をご確認ください。 ## プロジェクトの作成 プロジェクト名を「My Sisk Application」にしましょう。.NET の環境が整ったら、次のコマンドでプロジェクトを作成できます: ```bash dotnet new console -n my-sisk-application ``` 次に、プロジェクトディレクトリへ移動し、.NET ユーティリティツールで Sisk をインストールします: ```bash cd my-sisk-application dotnet add package Sisk.HttpServer ``` プロジェクトに Sisk をインストールする他の方法は、[こちら](https://www.nuget.org/packages/Sisk.HttpServer/) にあります。 それでは、HTTP サーバーのインスタンスを作成しましょう。この例ではポート 5000 でリッスンするように設定します。 ## HTTP サーバーの構築 Sisk は HttpServer オブジェクトへルーティングする形で、手動でステップバイステップにアプリケーションを構築できますが、ほとんどのプロジェクトではあまり便利ではありません。そのため、ビルダー メソッドを使用すれば、アプリを簡単に起動できます。 ```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(); } } ``` Sisk の重要なコンポーネントを理解することが重要です。このドキュメントの後半で、Sisk の仕組みについてさらに学べます。 ## 手動(高度)設定 ドキュメントの [このセクション](https://docs.sisk-framework.org/ja/docs/advanced/manual-setup.md) では、HttpServer、Router、ListeningPort など各コンポーネントの動作と関係性について学べます。 --- # インストール Source: https://docs.sisk-framework.org/ja/docs/installing.html Sisk は NuGet、dotnet CLI、または[他のオプション](https://www.nuget.org/packages/Sisk.HttpServer/)でインストールできます。開発者コンソールで次のコマンドを実行するだけで、Sisk 環境を簡単にセットアップできます。 ```sh dotnet add package Sisk.HttpServer ``` このコマンドは、プロジェクトに Sisk の最新バージョンをインストールします。 Sisk 1.7 は .NET 8、.NET 9、.NET 10 を対象としています。[Cadente エンジン](https://docs.sisk-framework.org/ja/docs/cadente.md) パッケージは .NET 9 と .NET 10 のみを対象とします。 --- # ネイティブAOTのサポート Source: https://docs.sisk-framework.org/ja/docs/native-aot.html [.NET Native AOT](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/)を使用すると、.NETランタイムがターゲットホストにインストールされていない場合でも、自己完結型のネイティブ.NETアプリケーションを公開できます。さらに、ネイティブAOTでは以下のような利点があります: - アプリケーションのサイズが大幅に小さくなる - 初期化が大幅に高速化される - メモリ消費量が低くなる Sisk Frameworkは、明示的な性質により、ほとんどの機能でネイティブAOTを使用できます。ソースコードをネイティブAOTに適応させるためのリワークは不要です。 ## サポートされていない機能 ただし、Siskは、一部の機能で最小限のリフレクションを使用しています。以下に記載されている機能は、ネイティブコードの実行中に部分的に利用可能または完全に利用不可になる可能性があります: - [モジュールの自動スキャン](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.AutoScanModules.md) of the router: このリソースは、実行中のアセンブリに埋め込まれた型をスキャンし、[ルーターモジュール](https://docs.sisk-framework.org/ja/docs/fundamentals/routing.md)である型を登録します。このリソースでは、アセンブリのトリミング中に除外される可能性のある型が必要です。 Siskの他のすべての機能はAOTと互換性があります。AOT警告を出すメソッドが見つかることがありますが、ここに記載されていない場合は、型、パラメーター、または型情報を渡すオーバーロードがあり、AOTコンパイラがオブジェクトをコンパイルするのを支援します。 --- # Sisk アプリケーションのデプロイ Source: https://docs.sisk-framework.org/ja/docs/deploying.html Sisk アプリケーションのデプロイプロセスは、プロジェクトを本番環境に公開することから成ります。プロセス自体は比較的シンプルですが、デプロイ先インフラのセキュリティや安定性に致命的な影響を与える可能性のある詳細に注意する必要があります。 理想的には、すべてのテストを実施し、アプリケーションを本番環境にデプロイできる状態にしたうえで、クラウドへデプロイする準備が整っているべきです。 ## Publishing your app Sisk アプリケーションまたはサービスの公開とは、本番環境向けに最適化されたバイナリを生成することです。この例では、.NET Runtime がインストールされたマシン上で実行できるように、本番用バイナリをコンパイルします。 アプリをビルドするにはマシンに .NET SDK がインストールされている必要があり、アプリを実行するターゲットサーバーには .NET Runtime がインストールされている必要があります。.NET Runtime のインストール方法は、Linux サーバーの場合は[こちら](https://learn.microsoft.com/en-us/dotnet/core/install/linux)、Windows の場合は[こちら](https://learn.microsoft.com/en-us/dotnet/core/install/windows?tabs=net70)、Mac OS の場合は[こちら](https://learn.microsoft.com/en-us/dotnet/core/install/macos)をご参照ください。 プロジェクトがあるフォルダーでターミナルを開き、.NET の publish コマンドを実行します: ```shell $ dotnet publish -r linux-x64 -c Release ``` これにより `bin/Release/publish/linux-x64` にバイナリが生成されます。 > [!NOTE] > アプリが [ポータブル構成ファイル](https://docs.sisk-framework.org/ja/docs/extensions/service-providers.md) を使用している場合は、`dotnet publish` によって生成されたすべてのバイナリと一緒に `service-config.json` をホストサーバーにコピーしてください。 > 環境変数やリッスンポート、ホスト、追加のサーバー構成が事前に設定されたままのファイルを残しておくことができます。 次のステップは、これらのファイルをアプリケーションをホストするサーバーへ転送することです。 その後、バイナリファイルに実行権限を付与します。この例ではプロジェクト名を「my-app」とします: ```shell $ cd /home/htdocs $ chmod +x my-app $ ./my-app ``` アプリケーションを実行したら、エラーメッセージが出力されていないか確認してください。エラーが出なければ、アプリケーションは正常に動作しています。 この時点では、ファイアウォールなどのアクセスルールが設定されていないため、外部ネットワークからアプリケーションにアクセスできない可能性が高いです。次のステップでこれを扱います。 アプリケーションがリッスンしている仮想ホストのアドレスを把握しておく必要があります。これはアプリケーション内で手動で設定され、Sisk サービスのインスタンス化方法に依存します。 **ポータブル構成ファイルを使用していない**場合は、`HttpServer` インスタンスを定義した箇所で確認できます: ```cs HttpServer server = HttpServer.Emit(5000, out HttpServerConfiguration config, out var host, out var router); // sisk は http://localhost:5000/ でリッスンすべきです ``` 手動で `ListeningHost` を関連付ける例: ```cs config.ListeningHosts.Add(new ListeningHost("https://localhost:5000/", router)); ``` ポータブル構成ファイルを使用している場合は、`service-config.json` に次のように記述します: ```json { "Server": { }, "ListeningHost": { "Ports": [ "http://localhost:5000/" ] } } ``` これに基づき、リバースプロキシを作成してサービスをリッスンさせ、オープンネットワーク上でトラフィックを利用できるようにします。 ## Proxying your application サービスのプロキシ設定とは、Sisk サービスを外部ネットワークに直接公開しないことを意味します。この手法はサーバーデプロイで非常に一般的です。その理由は次のとおりです: - アプリケーションに SSL 証明書を関連付けられるようになる - サービスへのアクセス前にアクセスルールを作成し、過負荷を防止できる - 帯域幅やリクエスト数の制限を管理できる - アプリケーション用にロードバランサーを分離できる - インフラ障害時のセキュリティリスクを低減できる [Nginx](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/linux-nginx?view=aspnetcore-7.0&tabs=linux-ubuntu#install-nginx) や [Apache](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/linux-apache?view=aspnetcore-7.0) といったリバースプロキシを介してアプリケーションを提供するか、[Cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/install-and-setup/tunnel-guide/) のような http-over-dns トンネルを利用できます。 また、プロキシの転送ヘッダーを正しく解決し、クライアントの IP アドレスやホスト情報を取得できるようにすることを忘れないでください。詳細は [転送リゾルバ](https://docs.sisk-framework.org/ja/docs/advanced/forwarding-resolvers.md) を参照してください。 トンネル作成、ファイアウォール設定、アプリケーションの実行が完了したら、次はアプリケーション用のサービスを作成します。 > [!NOTE] > 非 Windows 系統で Sisk サービスに直接 SSL 証明書を使用することはできません。これは HttpListener の実装に起因し、OS ごとに実装が異なるためです。IIS で仮想ホストに証明書を関連付ける場合は [こちら](https://learn.microsoft.com/en-us/iis/manage/configuring-security/how-to-set-up-ssl-on-iis) を参照してください。その他のシステムでは、リバースプロキシを使用することが強く推奨されます。 ## Creating an service サービスを作成すると、サーバーインスタンスの再起動や回復不能なクラッシュが発生した後でも、アプリケーションを常に利用可能な状態に保てます。 このシンプルなチュートリアルでは、前述のチュートリアルの内容を活用し、サービスを常に稼働させる方法を示します。 1. サービス設定ファイルが置かれているフォルダーにアクセスします: ```sh cd /etc/systemd/system ``` 2. `my-app.service` ファイルを作成し、以下の内容を記述します: ```ini {title="my-app.service"} [Unit] Description= [Service] # サービスを起動するユーザーを設定 User= # ExecStart のパスは WorkingDirectory に対して相対ではありません # 実行可能ファイルへのフルパスを設定 WorkingDirectory=/home/htdocs ExecStart=/home/htdocs/my-app # クラッシュ時に常にサービスを再起動するように設定 Restart=always RestartSec=3 [Install] WantedBy=multi-user.target ``` 3. サービスマネージャーモジュールを再起動します: ```sh $ sudo systemctl daemon-reload ``` 4. 作成したサービスを名前で起動し、実行状態を確認します: ```sh $ sudo systemctl start my-app $ sudo systemctl status my-app ``` 5. アプリが「Active: active」と表示されていることを確認したら、システム再起動後も自動的に実行されるようサービスを有効化します: ```sh $ sudo systemctl enable my-app ``` これで準備完了です。Sisk アプリケーションを皆に公開できます。 --- # SSL の使用 Source: https://docs.sisk-framework.org/ja/docs/ssl.html 開発時に SSL を使用する必要がある場合があります。これは、ほとんどのウェブ開発シナリオのようにセキュリティが求められる環境で作業する場合です。Sisk は HttpListener の上に構築されていますが、HttpListener はネイティブの HTTPS をサポートせず、HTTP のみです。ただし、Sisk で SSL を利用できる回避策がいくつかあります。以下をご覧ください: ## Sisk.Cadente.CoreEngine を使用する場合 - 利用可能なプラットフォーム: Linux, macOS, Windows - 作業負荷: 簡単 実験的な [**Cadente**](https://docs.sisk-framework.org/ja/docs/cadente.md) エンジンを Sisk プロジェクトで使用することが可能です。コンピュータやプロジェクトに追加の設定は不要です。Sisk サーバーで Cadente サーバーを利用するには、プロジェクトに `Sisk.Cadente.CoreEngine` パッケージをインストールする必要があります。 SSL を構成するには、ビルダーの `UseSsl` と `UseEngine` メソッドを使用します: ```csharp using var http = HttpServer.CreateBuilder() .UseEngine() .UseSsl(CertificateHelper.CreateTrustedDevelopmentCertificate("localhost")) ``` > Note: このパッケージはまだ実験段階です。 ## Windows の IIS を使用する場合 - 利用可能なプラットフォーム: Windows - 作業負荷: 中程度 Windows を使用している場合、IIS を利用して HTTP サーバーで SSL を有効にできます。この機能を利用するには、アプリケーションを「localhost」以外のホストでリッスンさせたい場合、事前に [このチュートリアル](https://docs.sisk-framework.org/ja/docs/registering-namespace.md) を確認しておくことをお勧めします。 この機能を利用するには、Windows の機能から IIS をインストールする必要があります。IIS は Windows および Windows Server のユーザーに無料で提供されています。アプリケーションで SSL を構成するには、自己署名であっても SSL 証明書を用意してください。その後、[IIS 7 以降での SSL 設定方法](https://learn.microsoft.com/en-us/iis/manage/configuring-security/how-to-set-up-ssl-on-iis) を参照できます。 ## mitmproxy を使用する場合 - 利用可能なプラットフォーム: Linux, macOS, Windows - 作業負荷: 簡単 **mitmproxy** は、開発者やセキュリティテスターがクライアント(例: Web ブラウザー)とサーバー間の HTTP および HTTPS トラフィックを検査、変更、記録できるインターセプトプロキシツールです。**mitmdump** ユーティリティを使用して、クライアントと Sisk アプリケーション間にリバース SSL プロキシを開始できます。 1. まず、マシンに [mitmproxy](https://mitmproxy.org/) をインストールします。 2. Sisk アプリケーションを起動します。この例では、非暗号化 HTTP ポートとして 8000 を使用します。 3. mitmproxy サーバーを起動し、セキュアポート 8001 でリッスンさせます: ```sh mitmdump --mode reverse:http://localhost:8000/ -p 8001 ``` これで準備完了です!`https://localhost:8001/` からアプリケーションにアクセスできます。`mitmdump` を開始するために、アプリケーションが実行中である必要はありません。 あるいは、プロジェクトに [mitmproxy helper](https://github.com/sisk-http/core/tree/main/extensions/Sisk.Helpers.mitmproxy) への参照を追加することもできます。この場合でも、コンピュータに mitmproxy がインストールされている必要があります。 ## Sisk.SslProxy パッケージ(廃止) > [!CAUTION] > > Sisk.SslProxy パッケージは廃止され、NuGet でも非推奨となっています。現在の Cadente API に対してはコンパイルできません。使用しないでください。代わりに [Cadente エンジン](#through-the-siskcadentecoreengine) のネイティブ SSL サポートを使用してください。 --- # Cadente Source: https://docs.sisk-framework.org/ja/docs/cadente.html Cadente は Sisk 用の実験的なマネージド HTTP/1.1 リスナー実装です。デフォルトの `System.Net.HttpListener` の代替として機能し、特に非 Windows プラットフォームで、より高い制御性と柔軟性を提供します。 ## 概要 デフォルトでは、Sisk は基盤となる HTTP サーバーエンジンとして `HttpListener`(`System.Net` から)を使用します。`HttpListener` は Windows 上ではカーネルモードの HTTP.sys ドライバーを利用し、安定かつ高性能ですが、Linux や macOS での実装はマネージドであり、歴史的にネイティブ SSL サポートがない(Nginx などのリバースプロキシが必要)や、パフォーマンス特性が一定でないといった制限がありました。 Cadente はこれらの問題を解決するために、C# で書かれた完全マネージドの HTTP/1.1 サーバーを提供します。その主な目標は次のとおりです: - **ネイティブ SSL サポート:** 外部プロキシや複雑な設定を必要とせず、すべてのプラットフォームで動作します。 - **クロスプラットフォームの一貫性:** Windows、Linux、macOS で同一の動作を保証します。 - **パフォーマンス:** マネージド `HttpListener` の高性能な代替として設計されています。 - **独立性:** `System.Net.HttpListener` から分離され、.NET における将来的な非推奨化やメンテナンス停止の影響から Sisk を保護します。 > [!WARNING] > **実験的ステータス** > > Cadente は現在実験段階(ベータ)にあります。重要な本番環境での使用はまだ推奨されません。API や動作は変更される可能性があります。 ## インストール Cadente は別パッケージとして提供されています。Sisk で使用するには `Sisk.Cadente.CoreEngine` パッケージが必要です。 ```bash dotnet add package Sisk.Cadente.CoreEngine ``` `Sisk.Cadente` と `Sisk.Cadente.CoreEngine` は .NET 9 および .NET 10 を対象としています。`Sisk.HttpServer` が .NET 8 をサポートしているにもかかわらず、.NET 8 を対象としたプロジェクトでは使用できません。 ## Sisk での使用 Cadente を Sisk アプリケーションの HTTP エンジンとして使用するには、デフォルトエンジンの代わりに `CadenteHttpServerEngine` を使用するように `HttpServer` を構成する必要があります。 `CadenteHttpServerEngine` は Cadente の `HttpHost` を Sisk が必要とする `HttpServerEngine` 抽象に適合させます。 ```csharp using Sisk.Core.Http; using Sisk.Cadente.CoreEngine; using var host = HttpServer.CreateBuilder() .UseEngine() .UseSsl(certificate: CertificateHelper.CreateTrustedDevelopmentCertificate("localhost")) .Build(); await host.StartAsync(); ``` リスニングホストが SSL で構成されている場合、[HttpServer.ListeningPrefixes](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.ListeningPrefixes.md) は `https` スキームのアドレスを報告します。クライアントが SSL 構成されたポートにプレーン HTTP を送信した場合、Cadente は同じホストとパスの `https` アドレスへ `301 Moved Permanently` リダイレクトで応答します。 ### 高度な構成 `CadenteHttpServerEngine` コンストラクタにセットアップアクションを渡すことで、基盤となる `HttpHost` インスタンスをカスタマイズできます。タイムアウトやその他の低レベル設定を構成する際に便利です。 ```csharp using var engine = new CadenteHttpServerEngine(host => { // クライアントの読み取り/書き込みタイムアウトを設定 host.TimeoutManager.ClientReadTimeout = TimeSpan.FromSeconds(30); host.TimeoutManager.ClientWriteTimeout = TimeSpan.FromSeconds(30); }); ``` ## スタンドアロン使用 主に Sisk 用に設計されていますが、Cadente はスタンドアロンの HTTP サーバーとしても使用できます(`HttpListener` に似ています)。 ```csharp using Sisk.Cadente; var host = new HttpHost(15000) { Handler = new MyHostHandler() }; host.Start(); Thread.Sleep(-1); class MyHostHandler : HttpHostHandler { public override async Task OnContextCreatedAsync(HttpHost host, HttpHostContext context) { context.Response.StatusCode = 200; using var stream = await context.Response.GetResponseStreamAsync(chunked: true); using var writer = new StreamWriter(stream); await writer.WriteLineAsync("Hello, world!"); } } ``` レスポンスボディが小さくメモリ上にある場合、`WriteInlineContent` と `WriteInlineContentAsync` はヘッダーとボディを一度の操作で書き込みます。レスポンスには `Content-Length` ヘッダーが必要です: ```csharp byte[] body = "Hello, world!"u8.ToArray(); context.Response.StatusCode = 200; context.Response.Headers.Set(new HttpHeader("Content-Length", body.Length.ToString())); await context.Response.WriteInlineContentAsync(body); ``` ## リクエストの検証 Cadente はハンドラに到達する前にリクエストのフレーミングを検証します。バージョン 1.7 以降、以下を行います: - キープアライブ接続上でパイプライン化されたリクエストをサポートします; - 有効な HTTP トークンでないリクエストメソッドや、表示可能な ASCII 範囲(`%x21-7E`)外のバイトを含むリクエストターゲットを拒否します; - `Transfer-Encoding` が単一の `chunked` 以外の場合や、矛盾する `Content-Length` ヘッダーがあるリクエストを拒否します; - リクエストラインとヘッダーが 8 KB を超えると `431 Request Header Fields Too Large` で応答し、接続を閉じます。 無効なリクエストにはエラーで応答し、ハンドラは呼び出されずに接続が閉じられます。 --- # Windows での名前空間予約の構成 Source: https://docs.sisk-framework.org/ja/docs/registering-namespace.html > [!NOTE] > この構成はオプションであり、Windows で HttpListener エンジンを使用して「localhost」以外のホストで Sisk をリッスンさせたい場合にのみ必要です。 Sisk は HttpListener ネットワークインターフェイスと連携し、仮想ホストをシステムにバインドしてリクエストを待ち受けます。 Windows では、このバインドはやや制限が厳しく、localhost のみが有効なホストとしてバインドできます。他のホストでリッスンしようとすると、サーバー側でアクセス拒否エラーが発生します。このチュートリアルでは、システム上で任意のホストをリッスンできるように権限を付与する方法を説明します。 ```bat {title="Namespace Setup.bat"} @echo off :: プレフィックスをここに挿入してください(スペースや引用符なし) SET PREFIX= SET DOMAIN=%ComputerName%\%USERNAME% netsh http add urlacl url=%PREFIX% user=%DOMAIN% pause ``` `PREFIX` には、サーバーがリッスンするプレフィックス(「Listening Host->Port」)を指定します。URL スキーム、ホスト、ポート、そして末尾のスラッシュを含む形式で記述する必要があります。例: ```bat {title="Namespace Setup.bat"} SET PREFIX=http://my-application.example.test/ ``` これにより、アプリケーション側で次のようにリッスンできるようになります: ```csharp {title="Program.cs"} class Program { static async Task Main(string[] args) { using var app = HttpServer.CreateBuilder() .UseListeningPort("http://my-application.example.test/") .Build(); app.Router.MapGet("/", request => { return new HttpResponse() { Status = 200, Content = new StringContent("Hello, world!") }; }); await app.StartAsync(); } } ``` --- # 変更履歴 Source: https://docs.sisk-framework.org/ja/docs/changelogs.html Sisk に加えられたすべての変更は変更履歴に記録されています。すべての Sisk バージョンの変更履歴は[こちら](https://github.com/sisk-http/archive/tree/master/changelogs)で確認できます。 ## 1.6 から 1.7 への移行 Sisk 1.7.0 では `Sisk.HttpServer`、`Sisk.Cadente`、`Sisk.Cadente.CoreEngine`、`Sisk.IniConfiguration` が 1.7.0 として提供されます。`Sisk.Documenting`、`Sisk.Documenting.Html`、`Sisk.Monitoring` は 1.7.0-rc3、`Sisk.ModelContextProtocol` は 1.7.0-rc1 として提供されます。すべての変更については [完全な 1.7.0 変更履歴](https://github.com/sisk-http/archive/blob/master/changelogs/core/v1.7/v1.7.0.md) を参照してください。 ### 破壊的変更 - **`Route.UseRegex` は設定できなくなりました。** このプロパティは廃止予定で、ルートが `RegexRoute` かどうかを示すだけです。代わりに [RegexRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RegexRoute.md) を作成してください。`UseRegex = true` のルート属性や `[RegexRoute]` 属性は引き続き機能します。[regex routes](https://docs.sisk-framework.org/ja/docs/fundamentals/routing.md#regex-routes) を参照してください。 ```cs // 前 router.Map(new Route(RouteMethod.Get, @"^/items/(?\d+)$", action) { UseRegex = true }); // 後 router.Map(new RegexRoute(RouteMethod.Get, @"^/items/(?\d+)$", action)); ``` - **リクエストボディのデフォルト上限が 100 MB に変更されました。** [HttpServerConfiguration.MaximumContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.MaximumContentLength.md) のデフォルト値は `0`(無制限)から 100 MB に変更されました。この上限はリクエストストリームや WebSocket メッセージにも適用されます。以前の動作に戻すには `0` を設定してください。[getting the request body](https://docs.sisk-framework.org/ja/docs/fundamentals/requests.md#getting-the-request-body) を参照してください。 ```cs using var app = HttpServer.CreateBuilder() .UseConfiguration(config => config.MaximumContentLength = 0) .Build(); ``` - **静的ルートが動的ルートより先に評価されます。** 正規表現ルート、プレフィックスルート(ファイルサーバーを含む)およびカスタムマッチングルートは、登録順に関係なく、すべての静的ルートの後に評価されます。[matching precedence](https://docs.sisk-framework.org/ja/docs/fundamentals/routing.md#matching-precedence) を参照してください。 - **重複したルート名は起動時に例外をスローします。** 同じ `Name` を持つルートが 2 つ以上あると、サーバー起動時に `ArgumentException` が発生します(デフォルトで有効な [Router.CheckForRouteCollisions](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CheckForRouteCollisions.md) が有効な場合)。 - **ファイルサーバールートはプレフィックスルートです。** `Router.MapFileSystem` と `HttpFileServer.CreateServingRoute` は正規表現ルートではなく [PrefixRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.PrefixRoute.md) を作成し、`ForceTrailingSlash` の影響を受けなくなります。 - **`RouteMethod.Any` に `QUERY` が含まれます。** `MapAny` でマッピングされたルートは `QUERY` リクエストにも応答します。`RouteMethod.Any` の数値が変更されたため、これを保存または比較しているコードは再コンパイルしてください。 - **CORS プリフライトが `UseCors` を尊重します。** `UseCors = false` のルートに対して自動的に生成される `OPTIONS` 応答は CORS ヘッダーを送信しなくなり、`OPTIONS` 用に明示的にマッピングされたルートがリクエストを処理します。[preflight requests](https://docs.sisk-framework.org/ja/docs/features/cors.md#preflight-requests) を参照してください。 - **Cadente が不正なリクエストを拒否します。** 無効なリクエストメソッドやターゲット、サポートされていない `Transfer-Encoding` の値、矛盾する `Content-Length` ヘッダーは拒否され、8 KB を超えるヘッダーブロックには `431 Request Header Fields Too Large` が返されます。[Cadente request validation](https://docs.sisk-framework.org/ja/docs/cadente.md#request-validation) を参照してください。 ### 廃止されたパッケージ `Sisk.BasicAuth`、`Sisk.SslProxy`、`Sisk.ServiceProvider` はもはやメンテナンスされず、NuGet でも非推奨となっており、代替パッケージはありません。プロジェクトから削除してください。 - `Sisk.BasicAuth` を、`Authorization` ヘッダーを検証する独自の [request handler](https://docs.sisk-framework.org/ja/docs/fundamentals/request-handlers.md) に置き換えてください。 - `Sisk.SslProxy` を、[Cadente engine](https://docs.sisk-framework.org/ja/docs/cadente.md) のネイティブ SSL サポートに置き換えてください。 - `Sisk.ServiceProvider` を、`UsePortableConfiguration` を介して `Sisk.HttpServer` に組み込まれている [portable configuration](https://docs.sisk-framework.org/ja/docs/extensions/service-providers.md) に置き換えてください。 ### 新機能 - `Route.Match`、`RouteMatch`、`Route.IsStatic`、`Route.AllowRewrites` を使用した [Custom route matching](https://docs.sisk-framework.org/ja/docs/fundamentals/routing.md#custom-route-matching) と新しい [prefix routes](https://docs.sisk-framework.org/ja/docs/fundamentals/routing.md#prefix-routes)。 - `MapQuery`、`Route.Query`、`[RouteQuery]`、`Accept-Query` ヘッダーを使用した HTTP の [QUERY method](https://docs.sisk-framework.org/ja/docs/fundamentals/routing.md#query-method-routes) のサポート。 - `HttpContext.Interlocked` を介した [Atomic context values](https://docs.sisk-framework.org/ja/docs/fundamentals/requests.md#atomic-context-values)。 - リクエストバッグの値を出力する `%{$key}` [access log term](https://docs.sisk-framework.org/ja/docs/features/logging.md#access-log-formatting) と、[safe file writing helpers](https://docs.sisk-framework.org/ja/docs/features/logging.md#writing-files-safely) の `LogStream.SafeWriteToFile` と `LogStream.SafeAppendToFile`。 - Cadente において: パイプライン化されたキープアライブリクエスト、`WriteInlineContent` と `WriteInlineContentAsync`、および SSL ポートで受信したプレーン HTTP リクエストを HTTPS にリダイレクトする機能。 - 拡張機能において: [Sisk.Documenting](https://docs.sisk-framework.org/ja/docs/extensions/api-documentation.md) のパラメータ例と `LlmsApiExporter.FooterText`、[Sisk.ModelContextProtocol](https://docs.sisk-framework.org/ja/docs/extensions/mcp.md) のスキルと `2026-07-28` プロトコルリビジョン、そして Sisk.Monitoring ダッシュボードの HTTP トラフィック、レイテンシ、ルート、例外テレメトリ。 ### その他の動作変更 - `HttpListener` エンジンは各リクエストをスレッドプールにディスパッチするため、遅いリクエストが次の受け入れ接続を遅延させなくなりました。その `ContextAbortedToken` は例外をスローする代わりに `CancellationToken.None` を返します。 - リクエストの終了時やログ書き込み中にサーバーハンドラからスローされた例外は、HTTP エンジンにエスケープするのではなくエラーログに記録されます。[exceptions thrown by handlers](https://docs.sisk-framework.org/ja/docs/advanced/http-server-handlers.md#exceptions-thrown-by-handlers) を参照してください。 - 許可されたオリジンがリクエストに依存する場合、CORS 応答は `Vary: Origin` を送信します。 - `%ri` アクセスログ項目は、リクエスト開始時に解決されたクライアントアドレスを使用します。 - `Sisk.HttpServer` は .NET 8、.NET 9、.NET 10 を対象とします。`Sisk.Cadente` と `Sisk.Cadente.CoreEngine` は .NET 9 と .NET 10 を対象とし、デバッグシンボルを `.snupkg` パッケージとして公開します。 --- # Frequently Asked Questions Source: https://docs.sisk-framework.org/ja/docs/faq.html Sisk に関するよくある質問。 ## Sisk はオープンソースですか? 完全に。Sisk で使用されるすべてのソースコードは、[GitHub](https://github.com/sisk-http) で公開および頻繁に更新されています。 ## 貢献は受け付けますか? [Sisk の哲学](/) と互換性がある限り、すべての貢献は大歓迎です!貢献はコードに限りません。ドキュメント、テスト、翻訳、寄付、投稿など、さまざまな形で貢献できます。 ## Sisk は資金提供されていますか? いいえ。現在、Sisk を後援する組織やプロジェクトはありません。 ## Sisk を本番環境で使用できますか? 絶対に。プロジェクトは 3 年以上開発されており、商用アプリケーションでのテストが行われてきました。Sisk は、主要なインフラストラクチャとして商用プロジェクトで使用されています。 さまざまなシステムや環境での [デプロイ](https://docs.sisk-framework.org/ja/docs/deploying.md) 方法についてのガイドが書かれており、利用可能です。 ## Sisk には認証、監視、データベースサービスがありますか? いいえ。Sisk にはこれらのサービスはありません。Sisk は HTTP Web アプリケーションの開発フレームワークですが、まだ最小限のフレームワークであり、アプリケーションが動作するために必要なものだけを提供します。 好みの第三者ライブラリを使用して、必要なサービスをすべて実装できます。Sisk は、汎用性、柔軟性、すべてのものと連携するように設計されています。 ## なぜ Sisk を <フレームワーク> よりも使用するべきですか? わかりません。你が教えてください。 Sisk は、.NET の HTTP Web アプリケーションの一般的なシナリオを埋めるために作成されました。既存のプロジェクト、such as ASP.NET、はさまざまな問題を解決しますが、異なる偏見で解決します。大きいフレームワークとは異なり、Sisk では、ユーザーが何をしているのか、そして何を構築しているのかを知っている必要があります。Web 開発および HTTP プロトコルの基本的な概念は、Sisk を使用するために不可欠です。 Sisk は、ASP.NET Core よりも Node.js の Express に近いです。HTTP ロジックが必要なアプリケーションを作成できる、高レベルの抽象化を提供します。 ## Sisk を学ぶために何が必要ですか? 以下の基本的な知識が必要です: - Web 開発 (HTTP、Restful など) - .NET これら 2 つのトピックについての知識がある場合、Sisk で高度なアプリケーションを開発するために数時間を費やすことができます。 ## Sisk を使用して商用アプリケーションを開発できますか? 絶対に。 Sisk は MIT ライセンスの下で作成されており、Sisk を商用プロジェクトまたは非商用プロジェクトで、独自のライセンスを必要とせずに使用できます。 ただし、アプリケーション内で、使用されているオープンソース プロジェクトについての通知を表示し、Sisk が使用されていることを示すことを求めます。 --- # ルーティング Source: https://docs.sisk-framework.org/ja/docs/fundamentals/routing.html [Router](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.md) はサーバー構築の最初のステップです。URL とそのメソッドをサーバーが実行するアクションにマッピングするエンドポイントである [Route](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.md) オブジェクトを保持する役割を担います。各アクションはリクエストを受け取り、クライアントへレスポンスを返すことを担当します。 ルートはパス式(「パスパターン」)とリッスンできる HTTP メソッドの組み合わせです。サーバーにリクエストが送られると、受信したリクエストにマッチするルートを探し、そのルートのアクションを呼び出して結果のレスポンスをクライアントに返します。 Sisk ではルートの定義方法が複数あります。静的、動的、または自動スキャン、属性による定義、あるいは Router オブジェクトに直接定義できます。 ```cs Router mainRouter = new Router(); // GET / を以下のアクションにマッピング mainRouter.MapGet("/", request => { return new HttpResponse("Hello, world!"); }); ``` ルートが何ができるかを理解するには、リクエストが何ができるかを理解する必要があります。[HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) には必要な情報がすべて含まれています。Sisk には開発を高速化する追加機能も用意されています。 サーバーが受け取った各アクションについて、[RouteAction](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteAction.md) 型のデリゲートが呼び出されます。このデリゲートは、サーバーが受け取ったリクエストに関する情報をすべて保持した [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) を引数に持ちます。このデリゲートの戻り値は [HttpResponse](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpResponse.md) か、[暗黙的レスポンスタイプ](https://docs.sisk-framework.org/ja/docs/fundamentals/responses.md#implicit-response-types) を通じてマッピングできるオブジェクトでなければなりません。 ## ルートのマッチング HTTP サーバーがリクエストを受け取ると、Sisk はリクエストのパス式に合致するルートを検索します。式は常にルート側とリクエスト側のパスだけでテストされ、クエリ文字列は考慮されません。 パスとメソッドがリクエストと一致した最初のルートが処理を担当します。マッチするルートがない場合は [Router.NotFoundErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.NotFoundErrorHandler.md) のレスポンスがクライアントに返されます。パスは一致したが HTTP メソッドが不一致の場合は [Router.MethodNotAllowedErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MethodNotAllowedErrorHandler.md) のレスポンスが返されます。ルートのテスト順序は [マッチングの優先順位](#matching-precedence) に記載されています。 Sisk は静的ルート間の衝突可能性をチェックします。サーバー起動時に、パスパターンとメソッドが重複する静的ルートがないか検査し、見つかった場合は例外をスローします。同時に、2 つ以上のルートが同じ [Route.Name](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.Name.md) を共有している場合も `ArgumentException` がスローされます。これらのチェックはデフォルトで有効な [Router.CheckForRouteCollisions](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CheckForRouteCollisions.md) によって制御されます。 ### マッチングの優先順位 各ルートは **静的** か **動的** のいずれかで、[Route.IsStatic](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.IsStatic.md) によって示されます。 - **静的ルート** はパスパターンを厳密に遵守します(例: `/users/`)。`Map*` メソッド、`Route` インスタンス、ルート属性で作成されたルートはすべて静的です。 - **動的ルート** は独自の方法でパスを解釈し、パスパターンで表現できないパスにもマッチします。[RegexRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RegexRoute.md)、[PrefixRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.PrefixRoute.md)([ファイルサーバー](https://docs.sisk-framework.org/ja/docs/features/file-server.md) が使用)および [カスタムマッチングルート](#custom-route-matching) は動的です。 サーバー起動時に、ルーターはルートを一度だけ再配置し、すべての静的ルートがすべての動的ルートより先にテストされるようにします。各グループ内では登録順が保持されます。つまり、静的ルートは動的ルートより常に優先され、定義順に関係なく次のように動作します。 ```cs // 動的: "wwwroot" ディレクトリから任意の GET パスを提供 mainRouter.MapFileSystem("/", "wwwroot"); // 静的: ファイルサーバーの後に定義されても GET /api/status を処理 mainRouter.MapGet("/api/status", request => new HttpResponse("ok")); ``` 上記の例では、ファイルサーバーは静的ルートで処理されないすべての GET リクエストのフォールバックとして機能します。 2 つのグループは検証と変換の方法でも異なります。 | | 静的ルート | 動的ルート | |---|---|---| | マッチング順序 | 最初に、登録順 | すべての静的ルートの後、登録順 | | ルート衝突チェック | あり | なし | | [Router.Prefix](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.Prefix.md) が適用される | あり | なし | > [!WARNING] > 動的ルートは衝突チェックの対象になりません。Sisk はどのパスにマッチするかを把握できないためです。2 つの動的ルートが同じリクエストにマッチした場合、先に定義された方が勝ちます。例として、`/` にマッピングされたファイルサーバーが `/static` にマッピングされたファイルサーバーより先に定義されていると、2 番目は到達不能になります。 > [!IMPORTANT] > 1.7 以前のバージョンでは、ルートは登録順のみでテストされ、正規表現ルートが後に定義された静的ルートよりも優先されることがありました。正規表現ルートが静的ルートと同じパスにマッチするようなロジックに依存している場合は、静的ルートに移すか、静的パスパターンを絞り込んでください。 ### パスパターンでルートを作成する 新規アプリケーションでは `Map*` 系メソッドの使用を推奨します。呼び出し側で HTTP メソッドが明示でき、現在の `Router` API にマッチします。古い `SetRoute` 系メソッドは互換性ラッパーとして残っていますが、例示は `Map`、`MapGet`、`MapPost`、`MapPut`、`MapDelete`、`MapPatch`、`MapAny`、`MapOptions`、`MapHead` を使用してください。 ```cs // Map* 系メソッドはメソッド固有のルートを定義する標準的な方法です。 mainRouter.MapGet("/hey/", (request) => { string name = request.RouteParameters["name"].GetString(); return new HttpResponse($"Hello, {name}"); }); mainRouter.MapPost("/form", (request) => { var formData = request.GetFormContent(); return new HttpResponse(); // 空の 200 OK }); // Route インスタンスが必要なときは Map で受け取れます。 mainRouter.Map(Route.Get("/image.png", (request) => { var imageStream = File.OpenRead("image.png"); return new HttpResponse() { // StreamContent の内部 // 送信後にストリームが破棄されます Content = new StreamContent(imageStream) }; })); // 複数パラメータ mainRouter.MapGet("/hey//surname/", (request) => { string name = request.RouteParameters["name"].GetString(); string surname = request.RouteParameters["surname"].GetString(); return new HttpResponse($"Hello, {name} {surname}!"); }); ``` [RouteParameters](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RouteParameters.md) プロパティは、受信したリクエストのパス変数に関するすべての情報を保持します。 サーバーが受け取るすべてのパスは、パスパターンテストが実行される前に正規化されます。正規化ルールは次の通りです。 - 空のセグメントはすべて除去されます。例: `////foo//bar` → `/foo/bar`。 - パスのマッチングは **大文字小文字を区別** しますが、[Router.MatchRoutesIgnoreCase](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MatchRoutesIgnoreCase.md) が `true` に設定されている場合は例外です。 [Query](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Query.md) と [RouteParameters](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RouteParameters.md) は、各インデックスプロパティが非 null の [StringValue](https://docs.sisk-framework.org/api/Sisk.Core.Entity.StringValue.md) を返す [StringValueCollection](https://docs.sisk-framework.org/api/Sisk.Core.Entity.StringValueCollection.md) オブジェクトを返します。これにより、生の値を管理オブジェクトへ変換するオプション/モナドとして利用できます。 以下の例はルートパラメータ `"id"` を読み取り、`Guid` に変換します。パラメータが有効な Guid でない場合は例外がスローされ、サーバーが [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) を処理していなければ 500 エラーがクライアントに返されます。 ```cs mainRouter.MapGet("/user/", (request) => { Guid id = request.RouteParameters["id"].GetGuid(); return new HttpResponse($"User id: {id}"); }); ``` > [!NOTE] > パスの末尾の `/` はリクエスト側・ルート側ともに無視されます。つまり、`/index/page` と定義されていても、`/index/page/` でもアクセス可能です。 > > `[HttpServerConfiguration.ForceTrailingSlash](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ForceTrailingSlash.md)` を有効にすれば、URL が必ず `/` で終わるよう強制できます。 ### クラスインスタンスでルートを作成する 属性 `[RouteAttribute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteAttribute.md)` を使ってリフレクションで動的にルートを定義できます。この方法では、属性が付与されたクラスのメソッドが対象ルーターに自動的にマッピングされます。 メソッドをルートとして定義するには、`[RouteAttribute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteAttribute.md)`(または `[RouteGetAttribute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteGetAttribute.md)` など)でマークする必要があります。メソッドは static、インスタンス、public、private いずれでも構いません。インスタンスメソッドと static メソッドの両方をマッピングしたい場合は `MapInstance` を、型から static メソッドだけをマッピングしたい場合は `MapType` を使用します。 ```cs {title="Controller/MyController.cs"} public class MyController { // GET / にマッチ [RouteGet] HttpResponse Index(HttpRequest request) { HttpResponse res = new HttpResponse(); res.Content = new StringContent("Index!"); return res; } // static メソッドも対応 [RouteGet("/hello")] static HttpResponse Hello(HttpRequest request) { HttpResponse res = new HttpResponse(); res.Content = new StringContent("Hello world!"); return res; } } ``` 以下のコードは `MyController` のインスタンスを渡すことで、`Index` と `Hello` の両方をルートとして登録します。型だけを渡した場合は static メソッドのみが対象になります。 ```cs var myController = new MyController(); mainRouter.MapInstance(myController); ``` static メソッドだけを型からマッピングしたい場合は次のようにします。 ```cs mainRouter.MapType(); ``` Sisk 0.16 以降では AutoScan を有効にでき、`RouterModule` を実装したユーザー定義クラスを自動的に検索してルーターに関連付けます。AOT コンパイルではサポートされません。 ```cs mainRouter.AutoScanModules(); ``` 上記の指示は `ApiController` を実装するすべての型を検索しますが、**型自体は対象外**です。2 つのオプション引数は検索対象のアセンブリと検索方法を指定します。 ## 正規表現ルート デフォルトの HTTP パスマッチング方式ではなく、パスを正規表現として解釈するルートを定義できます。正規表現ルートは [RegexRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RegexRoute.md) クラスで実装され、[動的ルート](#matching-precedence) に分類されます。 ```cs mainRouter.Map(new RegexRoute(RouteMethod.Get, @"\/[a-z]+\/", request => { return new HttpResponse("hello, world"); })); ``` 正規表現パターンからキャプチャグループを取得し、[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}"); } } ``` > [!NOTE] > 正規表現は自動的にアンカーされません。`/uploads` のようなパターンは `/files/uploads/a` にもマッチします。全体パスにマッチさせるには `^` と `$` を使用してください。 > [!IMPORTANT] > Sisk 1.7 以降、`Route.UseRegex` プロパティは設定できなくなりました。廃止予定であり、ルートが [RegexRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RegexRoute.md) かどうかを示すだけです。`[Route(RouteMethod.Get, "...", UseRegex = true)]` や `[RegexRoute]` 属性は引き続き機能します。 `Route.UseRegex` を使って正規表現ルートを作成しているコードは、`RegexRoute` に置き換えてください。 ```cs // 1.7 以前 mainRouter.Map(new Route(RouteMethod.Get, @"^/items/(?\d+)$", action) { UseRegex = true }); // 1.7 以降 mainRouter.Map(new RegexRoute(RouteMethod.Get, @"^/items/(?\d+)$", action)); ``` ## プレフィックスルート [PrefixRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.PrefixRoute.md) は自身のパスとその下位のすべてのパスにマッチし、セグメント単位で比較します。これは [Router.MapFileSystem](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MapFileSystem.md) が使用するルートです。プレフィックスルートは [動的ルート](#matching-precedence) に分類されます。 ```cs // /docs、/docs/、/docs/any/nested/path にマッチし、/docsfoo にはマッチしません mainRouter.Map(new PrefixRoute(RouteMethod.Get, "/docs", null, request => { return new HttpResponse($"Requested {request.Path}"); }, null)); ``` ## カスタムルートマッチング [Route](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.md) を継承し、[Route.Match](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.Match.md) をオーバーライドすることで独自のマッチングルールを作成できます。メソッドはリクエストパスとマッチング中のルーターを受け取り、結果とオプションで取得したルートパラメータを含む [RouteMatch](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMatch.md) を返します。 ルーターは `Match` が成功した後に HTTP メソッドを検証するため、`Match` はパスだけをテストすべきです。これによりカスタムルートでも 405 Method Not Allowed と `OPTIONS` のレスポンスが正しく機能します。 ```cs {title="Routing/ExtensionRoute.cs"} // 任意の拡張子で終わるパスにマッチ(例: /reports/2024.csv) public sealed class ExtensionRoute : Route { public ExtensionRoute(string extension, RouteAction action) : base(RouteMethod.Get, extension, action) { } // このルートはパスパターンに従いません public override bool IsStatic => false; // パスはパターンではないため、末尾スラッシュ形へのリダイレクトは行えません public override bool AllowRewrites => false; public override RouteMatch Match(string requestPath, Router router) { StringComparison comparison = router.MatchRoutesIgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; if (!requestPath.EndsWith(Path, comparison)) return RouteMatch.NotMatched; var parameters = new NameValueCollection { ["file"] = requestPath[..^Path.Length] }; return new RouteMatch(true, parameters); } } ``` ```cs mainRouter.Map(new ExtensionRoute(".csv", request => { string file = request.RouteParameters["file"].GetString(); return new HttpResponse($"Exporting {file} as CSV"); })); ``` `Match` をオーバーライドする際の留意点: - ルートがパスパターンに厳密に従わない場合は `false` を返すよう [Route.IsStatic](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.IsStatic.md) をオーバーライドしてください。そうしないと、ルーターは衝突チェックや [Router.Prefix](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.Prefix.md) の適用時に `Path` をパスパターンとして扱い、誤検知や無効なパスが生じます。 - 末尾スラッシュ形へのリダイレクトが不可能な場合は `false` を返すよう [Route.AllowRewrites](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.AllowRewrites.md) をオーバーライドしてください。 - `Match` はルートに到達したすべてのリクエストで呼び出され、複数スレッドから同時に実行される可能性があります。マッチしない場合はできるだけ割り当てを避け、キャッシュ状態はスレッドセーフに保ちましょう。失敗時は [RouteMatch.NotMatched](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMatch.NotMatched.md) を返してください。 - `router` パラメータを通じて [Router.MatchRoutesIgnoreCase](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MatchRoutesIgnoreCase.md) を尊重してください。 ## ルートのプレフィックス付与 クラスやモジュール全体にプレフィックスを付与したい場合は、[RoutePrefix](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RoutePrefixAttribute.md) 属性を使用し、文字列としてプレフィックスを設定します。 以下は BREAD アーキテクチャ(Browse, Read, Edit, Add, Delete)を用いた例です。 ```cs {title="Controller/Api/UsersController.cs"} [RoutePrefix("/api/users")] public class UsersController { // GET /api/users [RouteGet] public async Task Browse() { ... } // GET /api/users/ [RouteGet("/")] public async Task Read() { ... } // PATCH /api/users/ [RoutePatch("/")] public async Task Edit() { ... } // POST /api/users [RoutePost] public async Task Add() { ... } // DELETE /api/users/ [RouteDelete("/")] public async Task Delete() { ... } } ``` 上記例では、`HttpResponse` パラメータは省略され、グローバルコンテキスト `[HttpContext.Current](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.Current.md)` を通じて使用されます。続くセクションで詳しく説明します。 ## リクエストパラメータなしのルート `[HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md)` パラメータなしでルートを定義できますが、リクエストコンテキストからリクエストやそのコンポーネントを取得可能です。以下はすべてのコントローラの基底クラスとして機能する `ControllerBase` の例で、`Request` プロパティで現在の `[HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md)` を取得できます。 ```cs {title="Controller/ControllerBase.cs"} public abstract class ControllerBase { // 現在のスレッドからリクエストを取得 public HttpRequest Request { get => HttpContext.Current.Request; } // 以下の行は、現在の HTTP セッションからデータベースを取得し、存在しなければ新規作成します public DbContext Database { get => HttpContext.Current.RequestBag.GetOrAdd(); } } ``` そして、すべての派生クラスでリクエストパラメータなしのルート構文を利用できます。 ```cs {title="Controller/UsersController.cs"} [RoutePrefix("/api/users")] public class UsersController : ControllerBase { [RoutePost] public async Task Create() { // 現在のリクエストから JSON データを取得 UserCreationDto? user = await Request.GetJsonContentAsync(); ... Database.Users.Add(user); return new HttpResponse(201); } } ``` 現在のコンテキストと依存性注入の詳細は、[依存性注入](https://docs.sisk-framework.org/ja/docs/features/instancing.md) チュートリアルをご参照ください。 ## 任意メソッドルート パスだけでマッチさせ、HTTP メソッドは無視するルートを定義できます。これにより、ルートコールバック内でメソッド検証を行うことが可能です。 ```cs // 任意の HTTP メソッドで / にマッチ mainRouter.MapAny("/", callbackFunction); ``` [RouteMethod.Any](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md) には `GET`、`POST`、`PUT`、`PATCH`、`DELETE`、`QUERY`、`HEAD`、`OPTIONS` が含まれます。 ## QUERY メソッドルート Sisk はリクエストボディにクエリを載せる安全かつ冪等な HTTP `QUERY` メソッドをサポートします。これらのルートは [Router.MapQuery](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MapQuery.md)、[Route.Query](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.Query.md)、[RouteMethod.Query](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md) または [RouteQuery](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteQueryAttribute.md) 属性で定義します。 ```cs mainRouter.MapQuery("/search", request => { string filter = request.Body; return new HttpResponse($"Searching for {filter}"); }); ``` ```cs {title="Controller/SearchController.cs"} public class SearchController { [RouteQuery("/products")] static HttpResponse SearchProducts(HttpRequest request) { return new HttpResponse($"Searching for {request.Body}"); } } ``` `QUERY` リクエストで受け入れ可能なメディアタイプを示す `Accept-Query` ヘッダーは、[HttpHeaderCollection.AcceptQuery](https://docs.sisk-framework.org/api/Sisk.Core.Entity.HttpHeaderCollection.AcceptQuery.md) および [HttpKnownHeaderNames.AcceptQuery](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpKnownHeaderNames.AcceptQuery.md) から取得できます。 ## 任意パスルート 任意パスルートは、HTTP サーバーが受け取った任意のパスに対してテストを行い、ルートメソッドが評価対象となります。`RouteMethod.Any` が指定され、かつパス式に [Route.AnyPath](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.AnyPath.md) が使用されている場合、このルートはサーバーへのすべてのリクエストをリッスンし、他のルートは定義できません。 ```cs // 以下のルートはすべての POST リクエストにマッチ mainRouter.Map(RouteMethod.Post, Route.AnyPath, callbackFunction); ``` ## 大文字小文字無視のルートマッチング 既定ではルートとリクエストの解釈は大文字小文字を区別します。無視させたい場合は次のオプションを有効にしてください。 ```cs mainRouter.MatchRoutesIgnoreCase = true; ``` これにより正規表現ルートでは `RegexOptions.IgnoreCase` が有効になり、プレフィックスルートでも大文字小文字を区別しない比較が行われます。 ## Not Found (404) コールバックハンドラ リクエストが既知のルートにマッチしなかったときのカスタムコールバックを作成できます。 ```cs mainRouter.NotFoundErrorHandler = () => { return new HttpResponse(404) { // v0.14 以降 Content = new HtmlContent("

Not found

") // それ以前のバージョン Content = new StringContent("

Not found

", Encoding.UTF8, "text/html") }; }; ``` ## Method not allowed (405) コールバックハンドラ リクエストがパスにはマッチしたがメソッドが一致しなかったときのカスタムコールバックも作成できます。 ```cs mainRouter.MethodNotAllowedErrorHandler = (context) => { return new HttpResponse(405) { Content = new StringContent($"Method not allowed for this route.") }; }; ``` ## エラーハンドリング リクエストライフサイクル(事前実行ハンドラ、ルーターアクション、事後実行ハンドラ、バリューハンドラ)内で例外がスローされることがあります。これらの例外は次の仕組みで管理されます。 - `[HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md)` が `true` の場合、例外は通常通りスローされ、Sisk では捕捉されません。例外が捕捉されないと HTTP サーバーが中断する可能性があります。 - `[HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md)` が `false` の場合、例外は捕捉され Sisk が処理します。その後、`Router.CallbackErrorHandler` が定義されていれば捕捉した例外とリクエストコンテキストで呼び出され、標準エラー出力へは転送されません。未定義の場合は標準エラー出力へ転送され、クライアントは HTTP 500 エラーを受け取ります。標準エラー出力が未定義の場合はエラーは黙って無視されます。 ※ `Router.CallbackErrorHandler` 内では、エラーログ、アクセスログ、またはその両方、あるいは無効化を設定でき、デフォルトのログ書き込み動作を変更できます。 ```csharp router.CallbackErrorHandler = (ex, ctx) => { ctx.LogMode = LogOutput.Both; // エラーをアクセスログとエラーログの両方に記録 } ``` ## 内部エラーハンドラ ルートコールバックがサーバー実行中にエラーをスローすることがあります。適切に処理しないと HTTP サーバー全体が停止する恐れがあります。ルーターには、ルートコールバックが失敗した際にサービス中断を防ぐコールバックが用意されています。 このメソッドは `[ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md)` が `false` に設定されている場合にのみ呼び出せます。 ```cs mainRouter.CallbackErrorHandler = (ex, context) => { return new HttpResponse(500) { Content = new StringContent($"Error: {ex.Message}") }; }; ``` --- # リクエストハンドリング Source: https://docs.sisk-framework.org/ja/docs/fundamentals/request-handlers.html リクエストハンドラ(ミドルウェアとも呼ばれます)は、ルーター上でリクエストが実行される前後に実行される関数です。ルート単位またはルーター単位で定義できます。 リクエストハンドラには 2 種類あります。 - **BeforeResponse**: ルーターアクションを呼び出す前にリクエストハンドラが実行されることを示します。 - **AfterResponse**: ルーターアクションを呼び出した後にリクエストハンドラが実行されることを示します。このコンテキストで HTTP レスポンスを送信すると、ルーターのアクションレスポンスが上書きされます。 両方のリクエストハンドラは、実際のルーターコールバック関数のレスポンスを上書きできます。なお、リクエストハンドラは認証やコンテンツの検証、情報の保存、ログ記録、レスポンス前後に実行できるその他の処理など、リクエストの検証に便利です。 ![](https://docs.sisk-framework.org/assets/img/requesthandlers1.png) このように、リクエストハンドラは実行を途中で中断し、サイクルが完了する前にレスポンスを返すことで、以降の処理をすべて破棄できます。 例: ユーザー認証リクエストハンドラが認証に失敗したとします。この場合、リクエストライフサイクルは継続できず、処理が停止します。もしこのハンドラが 2 番目の位置にある場合、3 番目以降は評価されません。 ![](https://docs.sisk-framework.org/assets/img/requesthandlers2.png) ## リクエストハンドラの作成 リクエストハンドラを作成するには、[IRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.md) インターフェイスを継承したクラスを次のように作ります。 ```cs {title="Middleware/AuthenticateUserRequestHandler.cs"} public class AuthenticateUserRequestHandler : IRequestHandler { public RequestHandlerExecutionMode ExecutionMode { get; init; } = RequestHandlerExecutionMode.BeforeResponse; public HttpResponse? Execute(HttpRequest request, HttpContext context) { if (request.Headers.Authorization != null) { // null を返すと、リクエストサイクルを継続できることを示します return null; } else { // HttpResponse オブジェクトを返すと、このレスポンスが隣接するレスポンスを上書きすることを示します return new HttpResponse(System.Net.HttpStatusCode.Unauthorized); } } } ``` 上の例では、リクエストに `Authorization` ヘッダーが存在すれば処理を続行し、次のリクエストハンドラまたはルーターコールバックが呼び出されます。`ExecutionMode` プロパティで **AfterResponse** として実行され、かつ非 null の値を返す場合は、ルーターのレスポンスが上書きされます。 リクエストハンドラが `null` を返すと、リクエストは継続され、次のオブジェクトが呼び出されるか、ルーターのレスポンスでサイクルが終了します。 > [!WARNING] > リクエストハンドラのインスタンスは一度作成され、処理するすべてのリクエスト(同時実行リクエストを含む)で共有されます。**リクエストハンドラのクラス自体に個別のリクエスト状態(現在のユーザー、解析済みトークン、タイマーなど)をフィールドやプロパティとして保持しないでください**。その値は他のリクエストに漏れます。代わりに `HttpContext` の [RequestBag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.RequestBag.md) にリクエスト単位の値を格納してください。 > > ```cs > public HttpResponse? Execute(HttpRequest request, HttpContext context) > { > // 誤り: this.currentUser = FindUser(request); > context.RequestBag.Set(FindUser(request)); > return null; > } > ``` 組み込みの [RequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RequestHandler.md) クラスを継承する場合は、`Next()` を返すことで意図を明示できます。 ```cs public class AuthenticateUserRequestHandler : RequestHandler { public override HttpResponse? Execute(HttpRequest request, HttpContext context) { if (request.Headers.Authorization is not null) return Next(); return new HttpResponse(System.Net.HttpStatusCode.Unauthorized); } } ``` I/O が必要なハンドラは、[AsyncRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.AsyncRequestHandler.md) を継承します。 ```cs public class LoadUserRequestHandler : AsyncRequestHandler { public override async Task ExecuteAsync(HttpRequest request, HttpContext context) { var user = await UserRepository.FindAsync(request.Headers.Authorization, request.DisconnectToken); if (user is null) return new HttpResponse(System.Net.HttpStatusCode.Unauthorized); request.Bag.Set(user); return Next(); } } ``` 小規模なインラインハンドラは `RequestHandler.Create` または `AsyncRequestHandler.Create` でも作成できます。 ```cs var requireJson = RequestHandler.Create((request, context) => { if (request.Headers.ContentType?.Contains("application/json") == true) return null; return new HttpResponse(System.Net.HttpStatusCode.UnsupportedMediaType); }); ``` ## 単一ルートにリクエストハンドラを関連付ける ルートに対して 1 つ以上のリクエストハンドラを定義できます。 ```cs {title="Router.cs"} mainRouter.Map(RouteMethod.Get, "/", IndexPage, new IRequestHandler[] { new AuthenticateUserRequestHandler(), // before request handler new ValidateJsonContentRequestHandler(), // before request handler // -- method IndexPage will be executed here new WriteToLogRequestHandler() // after request handler }); ``` または [Route](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Route.md) オブジェクトを作成して設定します。 ```cs {title="Router.cs"} Route indexRoute = Route.Get("/", IndexPage); indexRoute.RequestHandlers = new IRequestHandler[] { new AuthenticateUserRequestHandler() }; mainRouter.Map(indexRoute); ``` ## ルーターにリクエストハンドラを関連付ける すべてのルートで実行されるグローバルリクエストハンドラを定義できます。 ```cs {title="Router.cs"} mainRouter.GlobalRequestHandlers = new IRequestHandler[] { new AuthenticateUserRequestHandler() }; ``` ## 属性にリクエストハンドラを関連付ける メソッド属性とルート属性に併せて、リクエストハンドラを属性として定義できます。 ```cs {title="Controller/MyController.cs"} public class MyController { [RouteGet("/")] [RequestHandler] static HttpResponse Index(HttpRequest request) { return new HttpResponse() { Content = new StringContent("Hello world!") }; } } ``` ここではインスタンスではなくハンドラの型を指定する必要があります。これにより、ルーターのパーサーがハンドラをインスタンス化します。コンストラクタ引数は [ConstructorArguments](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RequestHandlerAttribute.ConstructorArguments.md) プロパティで渡せます。 例: ```cs {title="Controller/MyController.cs"} [RequestHandler("arg1", 123, ...)] public HttpResponse Index(HttpRequest request) { return res = new HttpResponse() { Content = new StringContent("Hello world!") }; } ``` 独自の属性を作成して RequestHandler を実装することも可能です。 ```cs {title="Middleware/Attributes/AuthenticateAttribute.cs"} public class AuthenticateAttribute : RequestHandlerAttribute { public AuthenticateAttribute() : base(typeof(AuthenticateUserRequestHandler), ConstructorArguments = new object?[] { "arg1", 123, ... }) { ; } } ``` 使用例: ```cs {title="Controller/MyController.cs"} [Authenticate] static HttpResponse Index(HttpRequest request) { return res = new HttpResponse() { Content = new StringContent("Hello world!") }; } ``` ## グローバルリクエストハンドラをバイパスする ルートにグローバルリクエストハンドラを設定した後、特定のルートでそのハンドラを無視できます。 ```cs {title="Router.cs"} var myRequestHandler = new AuthenticateUserRequestHandler(); mainRouter.GlobalRequestHandlers = new IRequestHandler[] { myRequestHandler }; Route publicRoute = Route.Get("/", IndexPage); publicRoute.Name = "My route"; publicRoute.BypassGlobalRequestHandlers = new IRequestHandler[] { myRequestHandler, // ok: 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] > リクエストハンドラをバイパスする場合、スキップしたいハンドラの **同じインスタンス** を使用しなければなりません。別のインスタンスを作成すると参照が変わり、グローバルリクエストハンドラはスキップされません。`GlobalRequestHandlers` と `BypassGlobalRequestHandlers` の両方で同一のハンドラ参照を使用することを忘れないでください。 --- # リクエスト Source: https://docs.sisk-framework.org/ja/docs/fundamentals/requests.html Requests は HTTP リクエストメッセージを表す構造体です。 [HttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.md) オブジェクトは、アプリケーション全体で HTTP メッセージを処理するための便利な機能を提供します。 HTTP リクエストは、メソッド、パス、バージョン、ヘッダー、ボディで構成されます。 このドキュメントでは、これらの要素を取得する方法を解説します。 ## リクエストメソッドの取得 受信したリクエストのメソッドを取得するには、Method プロパティを使用します: ```cs static HttpResponse Index(HttpRequest request) { HttpMethod requestMethod = request.Method; ... } ``` このプロパティは、リクエストのメソッドを表す [HttpMethod](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.httpmethod) オブジェクトを返します。 > [!NOTE] > ルートメソッドとは異なり、このプロパティは [RouteMethod.Any](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md) アイテムを提供しません。その代わりに、実際のリクエストメソッドを返します。 ## リクエスト URL コンポーネントの取得 リクエストの特定のプロパティを使用して、URL からさまざまなコンポーネントを取得できます。例として、次の URL を考えてみましょう。 ``` http://localhost:5000/user/login?email=foo@bar.com ``` | コンポーネント名 | 説明 | コンポーネント値 | | --- | --- | --- | | [Path](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Path.md) | リクエストのパスを取得します。 | `/user/login` | | [FullPath](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.FullPath.md) | リクエストのパスとクエリ文字列を取得します。 | `/user/login?email=foo@bar.com` | | [FullUrl](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.FullUrl.md) | リクエスト全体の URL 文字列を取得します。 | `http://localhost:5000/user/login?email=foo@bar.com` | | [Host](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Host.md) | リクエストのホストを取得します。 | `localhost` | | [Authority](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Authority.md) | リクエストのホストとポートを取得します。 | `localhost:5000` | | [QueryString](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.QueryString.md) | リクエストのクエリ文字列を取得します。 | `?email=foo@bar.com` | | [Query](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Query.md) | 名前付き値コレクションとしてリクエストのクエリを取得します。 | `{StringValueCollection object}` | | [IsSecure](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.IsSecure.md) | リクエストが SSL を使用しているかどうかを判定します(true = 使用、false = 未使用)。 | `false` | また、[HttpRequest.Uri](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Uri.md) プロパティを使用すれば、上記すべてを 1 つのオブジェクトで取得できます。 ## リクエストメタデータとキャンセル Sisk は各リクエストに運用メタデータも付与します。これらのプロパティは、ログ、トレース、ローカリゼーション、診断、長時間実行される操作に役立ちます。 | プロパティまたはメソッド | 用途 | | --- | --- | | [RequestId](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RequestId.md) | リクエストの一意な識別子です。[IncludeRequestIdHeader](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.IncludeRequestIdHeader.md) を有効にすると `X-Request-Id` ヘッダーとして返されます。 | | [RequestedAt](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RequestedAt.md) | Sisk がリクエストオブジェクトを作成した時点です。 | | [RemoteAddress](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RemoteAddress.md) | 接続から解決されたクライアントアドレス、または [ForwardingResolver](https://docs.sisk-framework.org/ja/docs/advanced/forwarding-resolvers.md) から取得されたアドレスです。 | | [Culture](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Culture.md) | `Accept-Language` から解決された最適なカルチャで、取得できない場合は現在のカルチャが使用されます。 | | [DisconnectToken](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.DisconnectToken.md) | クライアントが切断されたときに発行されるキャンセルトークンです(設定された HTTP エンジンがサポートしている場合)。 | | [Bag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Bag.md) | リクエストハンドラやルートアクション間で共有される型付きキー/バリュー ストアです。 | | [GetRawHttpRequest](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetRawHttpRequest.md) | 診断用のリクエストのテキスト表現です。 | ## リクエストボディの取得 リクエストには、フォームやファイル、API 取引などのボディが含まれることがあります。ボディは以下のプロパティから取得できます。 ```cs // リクエストのエンコーディングを使用して、リクエストボディを文字列として取得します string body = request.Body; // またはバイト配列として取得します byte[] bodyBytes = request.RawBody; // あるいはストリームとして取得できます。 Stream requestStream = request.GetRequestStream(); // 非同期でボディを読み取ることもできます Memory bodyMemory = await request.GetBodyContentsAsync(); ``` リクエストにボディが存在し、ロードされているかどうかは、[HasContents](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.HasContents.md) プロパティ(リクエストにコンテンツがあるかを判定)および [IsContentAvailable](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.IsContentAvailable.md) プロパティ(リモート側からコンテンツが完全に受信されたかを示す)で確認できます。 `GetRequestStream` でリクエストコンテンツを複数回読み取ることはできません。このメソッドで読み取った場合、`RawBody` と `Body` の値も利用できなくなります。リクエストストリームは、作成された HTTP セッションの終了時に自動的に破棄されるため、明示的に破棄する必要はありません。また、[HttpRequest.RequestEncoding](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RequestEncoding.md) プロパティを使用すれば、リクエストを手動でデコードする際の最適なエンコーディングを取得できます。 サーバーはリクエストコンテンツの読み取りに制限を設けており、これは [HttpRequest.Body](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Body.md) と [HttpRequest.RawBody](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Body.md) の両方に適用されます。これらのプロパティは、入力ストリーム全体を [HttpRequest.ContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.ContentLength.md) と同サイズのローカルバッファにコピーします。 リクエストコンテンツは [HttpServerConfiguration.MaximumContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.MaximumContentLength.md) によって制限されます。この設定は Sisk 1.7 以降デフォルトで 100 MB です(以前のバージョンでは `0`(無制限)でした)。制限は以下のように適用されます: - `Content-Length` が上限を超えているリクエストは、ルートが呼び出される前に `413 Content Too Large` 応答が返されます。 - 長さが宣言されていないチャンク化またはストリーミングされたボディは、読み取り中にチェックされます。上限を超えて [RawBody](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RawBody.md)、[Body](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Body.md)、または [GetRequestStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetRequestStream.md) が返すストリームを読み込むと、[InsufficientMemoryException](https://learn.microsoft.com/en-us/dotnet/api/system.insufficientmemoryexception) がスローされ、コードまたは [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) が処理しない限り `500 Internal Server Error` が返されます。 - 同じ上限が受信した [WebSocket](https://docs.sisk-framework.org/ja/docs/features/websockets.md) メッセージの最大サイズにも適用されます。 プロパティを `0` に設定すると、制限が解除されます: ```cs using var app = HttpServer.CreateBuilder() .UseConfiguration(config => config.MaximumContentLength = 0) .Build(); ``` 設定された制限がなくても、`Body` と `RawBody` は [Array.MaxLength](https://learn.microsoft.com/en-us/dotnet/api/system.array.maxlength)(約 2 GB)を超えるコンテンツをバッファできず、アクセスしようとすると例外がスローされます。ストリーミングを使用すれば、より大きなコンテンツを扱うことが可能です。 > [!NOTE] > Sisk が許可している場合でも、HTTP のセマンティクスに従ってアプリケーションを構築し、許可されていないメソッドでコンテンツを取得または提供しないことが常に推奨されます。[RFC 9110 "HTTP Semantics"](https://httpwg.org/spec/rfc9110.html) を参照してください。 ## JSON リクエストの読み取り JSON API では、`Body` を読み取って手動でデシリアライズする代わりに、組み込みの JSON ヘルパーを使用してください。これらは [System.Text.Json](https://learn.microsoft.com/en-us/dotnet/api/system.text.json) を使用し、デフォルトで [HttpRequest.DefaultJsonSerializerOptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.DefaultJsonSerializerOptions.md) が適用されます。 ```cs public record CreateUserRequest(string Name, string Email); router.MapPost("/users", (HttpRequest request) => { CreateUserRequest? body = request.GetJsonContent(); if (body is null) return new HttpResponse(System.Net.HttpStatusCode.BadRequest); return new HttpResponse(System.Net.HttpStatusCode.Created); }); ``` 非同期ルート内で使用するか、リクエストのキャンセルでデシリアライズを中止したい場合は、非同期オーバーロードを使用してください。 ```cs router.MapPost("/users", async (HttpRequest request) => { CreateUserRequest? body = await request.GetJsonContentAsync(request.DisconnectToken); if (body is null) return new HttpResponse(System.Net.HttpStatusCode.BadRequest); return new HttpResponse(System.Net.HttpStatusCode.Created); }); ``` 特定のエンドポイント向けにカスタムの [JsonSerializerOptions](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializeroptions) を渡すこともできます。 ```cs var options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { PropertyNameCaseInsensitive = true }; UserDto? user = request.GetJsonContent(options); ``` Native AOT やトリミングに敏感なアプリケーションでは、`JsonSerializerContext` が生成する `JsonTypeInfo` オーバーロードを使用してください。 ```cs [JsonSerializable(typeof(CreateUserRequest))] public partial class AppJsonSerializerContext : JsonSerializerContext { } CreateUserRequest? body = await request.GetJsonContentAsync( AppJsonSerializerContext.Default.CreateUserRequest, request.DisconnectToken); ``` JSON ヘルパーにも同じ一度だけ読み取るルールが適用されます。Sisk が `GetJsonContent`、`GetJsonContentAsync`、`Body`、または `RawBody` を通じてリクエストストリームを読み取った後は、`GetRequestStream()` で同じボディを再度取得することはできません。 ## リクエストコンテキストの取得 HTTP Context は、HTTP サーバー、ルート、ルーター、リクエストハンドラ情報を格納する Sisk 固有のオブジェクトです。これらのオブジェクトが整理しにくい環境で、情報を一元管理するために使用できます。 現在実行中の [HttpContext](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.md) は、静的メソッド `HttpContext.GetCurrentContext()` で取得できます。このメソッドは、現在のスレッドで処理中のリクエストのコンテキストを返します。 ```cs HttpContext context = HttpContext.GetCurrentContext(); ``` ### ログモード [HttpContext.LogMode](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.LogMode.md) プロパティを使用すると、現在のリクエストのロギング動作を制御できます。特定のリクエストに対してロギングを有効または無効にし、サーバーのデフォルト設定を上書きできます。 ```cs // Disable logging for this request context.LogMode = LogOutputMode.None; ``` ### リクエストバッグ [RequestBag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.RequestBag.md) オブジェクトは、リクエストハンドラから別のポイントへ渡され、最終的な宛先で利用できる情報を格納します。このオブジェクトは、ルートコールバックの後に実行されるリクエストハンドラでも使用できます。 > [!TIP] > このプロパティは [HttpRequest.Bag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Bag.md) プロパティからもアクセス可能です。 ```cs {title="Middleware/AuthenticateUserRequestHandler.cs"} public class AuthenticateUserRequestHandler : IRequestHandler { public string Identifier { get; init; } = Guid.NewGuid().ToString(); public RequestHandlerExecutionMode ExecutionMode { get; init; } = RequestHandlerExecutionMode.BeforeResponse; public HttpResponse? Execute(HttpRequest request, HttpContext context) { if (request.Headers.Authorization != null) { context.RequestBag.Add("AuthenticatedUser", new User("Bob")); return null; } else { return new HttpResponse(System.Net.HttpStatusCode.Unauthorized); } } } ``` 上記のリクエストハンドラは `AuthenticatedUser` をリクエストバッグに設定し、最終コールバックで後から取得できます。 ```cs {title="Controller/MyController.cs"} public class MyController { [RouteGet("/")] [RequestHandler] static HttpResponse Index(HttpRequest request) { User authUser = request.Context.RequestBag["AuthenticatedUser"]; return new HttpResponse() { Content = new StringContent($"Hello, {authUser.Name}!") }; } } ``` `Bag.Set()` と `Bag.Get()` ヘルパーメソッドを使用して、型シングルトンでオブジェクトを取得または設定できます。 `TypedValueDictionary` クラスは、より細かい制御のために `GetValue` と `SetValue` メソッドも提供します。 ```cs {title="Middleware/Authenticate.cs"} public class Authenticate : RequestHandler { public override HttpResponse? Execute(HttpRequest request, HttpContext context) { request.Bag.Set(authUser); } } ``` ```csharp {title="Controller/MyController.cs"} [RouteGet("/")] [RequestHandler] public static HttpResponse GetUser(HttpRequest request) { var user = request.Bag.Get(); ... } ``` ### 原子性コンテキスト値 リクエストバッグは同期されていません。同じリクエスト内で、ルートが開始した並列タスクなど、同時に数値が変更される可能性がある場合は、[HttpContext.Interlocked](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.Interlocked.md) を使用してください。これにより、リクエストバッグ内の名前付き値に対して原子的な操作を行う [HttpContextInterlocked](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.HttpContextInterlocked.md) オブジェクトが取得できます。 | メソッド | 説明 | | --- | --- | | `Add(name, value)` | `value` を既存の値に加算します。`name` が未設定の場合は `value` を保存します。 | | `Exchange(name, value)` | 保存されている値を置き換え、以前の値を返します。`name` が未設定の場合は `value` を返します。 | | `CompareExchange(name, value, comparand, notFound = -1)` | 保存された値が `comparand` と等しい場合にのみ `value` に置き換え、元の値を返します。`name` が未設定の場合は `notFound` を返します。 | | `Inspect(name)` | 保存された値を返します。`name` が未設定の場合は `null` を返します。 | リクエストバッグでは値は `double` として保存されます。`int` や `long` を受け取るオーバーロードは、内部で `double` に変換します。 ```cs router.MapPost("/import", async (HttpRequest request) => { string[] lines = request.Body.Split('\n'); await Parallel.ForEachAsync(lines, async (line, cancellation) => { if (await TryImportAsync(line, cancellation)) request.Context.Interlocked.Add("imported", 1); }); double imported = request.Context.Interlocked.Inspect("imported") ?? 0; return new HttpResponse($"{imported} line(s) imported"); }); ``` > [!NOTE] > これらの操作は他の `Interlocked` 呼び出しに対してのみ原子的です。`RequestBag` を直接介して同じキーを読み書きする場合、これらの操作とは同期されません。 ## フォームデータの取得 [StringKeyStoreCollection](https://docs.sisk-framework.org/api/Sisk.Core.Entity.StringKeyStoreCollection.md) を使用して、以下の例のようにフォームデータの値を取得できます。 ```cs {title="Controller/Auth.cs"} [RoutePost("/auth")] public HttpResponse Index(HttpRequest request) { var form = request.GetFormContent(); string? username = form["username"]; string? password = form["password"]; if (AttempLogin(username, password)) { ... } } ``` リクエストボディが大きい場合やキャンセルサポートが必要な場合は、非同期バージョンが便利です。 ```cs var form = await request.GetFormContentAsync(request.DisconnectToken); ``` ## マルチパートフォームデータの取得 Sisk の HTTP リクエストでは、ファイルやフォームフィールド、任意のバイナリコンテンツなど、アップロードされたマルチパートコンテンツを取得できます。 ```cs // 以下のメソッドは、リクエストの入力全体を // MultipartObject の配列に読み込みます var multipartFormDataObjects = request.GetMultipartFormContent(); foreach (MultipartObject uploadedObject in multipartFormDataObjects) { // マルチパートフォームデータで提供されたファイル名 // オブジェクトがファイルでない場合は null が返されます。 Console.WriteLine("File name : " + uploadedObject.Filename); // マルチパートフォームデータオブジェクトのフィールド名 Console.WriteLine("Field name : " + uploadedObject.Name); // マルチパートフォームデータのコンテンツ長 Console.WriteLine("Content length : " + uploadedObject.ContentLength); // 各ファイルヘッダーに基づいて画像形式を判定します // 既知のコンテンツタイプです。認識できない一般的なファイル形式の場合、 // 以下のメソッドは MultipartObjectCommonFormat.Unknown を返します Console.WriteLine("Common format : " + uploadedObject.GetCommonFileFormat()); } ``` ルートが非同期の場合は、[GetMultipartFormContentAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetMultipartFormContentAsync.md) を使用してください。 ```cs var multipartFormDataObjects = await request.GetMultipartFormContentAsync(request.DisconnectToken); ``` Sisk の [Multipart form objects](https://docs.sisk-framework.org/api/Sisk.Core.Entity.MultipartObject.md) とそのメソッド、プロパティ、機能の詳細は、こちらをご覧ください。 ## クライアント切断の検出 Sisk v1.15 以降、フレームワークは [HttpRequest.DisconnectToken](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.DisconnectToken.md) を通じてキャンセルトークンを提供します。設定された HTTP エンジンが切断検出をサポートしている場合、クライアント接続がレスポンス完了前に閉じられるとこのトークンがキャンセルされます。これにより、クライアントが結果を待っていない長時間実行される操作を停止できます。 ```csharp router.MapGet("/connect", async (HttpRequest req) => { // gets the disconnection token from the request var dc = req.DisconnectToken; await LongOperationAsync(dc); return new HttpResponse(); }); ``` このトークンはすべての HTTP エンジンで互換性があるわけではなく、各エンジンで実装が必要です。 デフォルトの Sisk エンジン(`System.Net.HttpListener` ベース)はクライアント切断検出をサポートしていません。デフォルトエンジンを使用している場合、`DisconnectToken` は `CancellationToken.None` となり、実質的にキャンセルできないトークンとして扱われます。 [Cadente engine](https://docs.sisk-framework.org/ja/docs/cadente.md) は `DisconnectToken` をサポートしています。ルートが切断検出付きのキャンセルに依存する場合は、Cadente もしくはこの機能を明示的に実装した他のエンジンを使用してください。サポートされたエンジンでも、キャンセルは協調的に行われます。トークンを非同期 API に渡し、長時間実行する処理内でトークンをチェックしてください。 ## サーバー送信イベント (Server-sent events) のサポート Sisk は [Server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) をサポートしており、チャンクをストリームとして送信し、サーバーとクライアント間の接続を維持できます。 [HttpRequest.GetEventSource](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetEventSource.md) メソッドを呼び出すと、HttpRequest がリスナ状態になります。この状態では、サーバーサイドイベントで送信されるパケットと重複するため、HTTP リクエストのコンテキストは HttpResponse を期待しません。 すべてのパケットを送信した後、コールバックは [Close](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequestEventSource.Close.md) メソッドを返す必要があります。これにより、サーバーに最終レスポンスが送信され、ストリーミングが終了したことが示されます。 送信されるすべてのパケットの総長さを予測できないため、`Content-Length` ヘッダーで接続の終了を判定することはできません。 ほとんどのブラウザのデフォルトでは、サーバーサイドイベントは GET 以外の HTTP ヘッダーやメソッドの送信をサポートしていません。そのため、特定のヘッダーが必要な event-source リクエストを扱うハンドラを使用する際は注意が必要です。 また、ほとんどのブラウザは、すべてのパケット受信後にクライアント側で [EventSource.close](https://developer.mozilla.org/en-US/docs/Web/API/EventSource/close) が呼び出されない場合、ストリームを再開します。これによりサーバー側で無限に処理が続く可能性があります。この問題を回避するため、イベントソースがすべてのパケット送信を完了したことを示す最終パケットを送るのが一般的です。 以下の例は、ブラウザが Server-sent events をサポートするサーバーと通信する方法を示しています。 ```html {title="sse-example.html"} フルーツ:
    ``` そして、クライアントへ順次メッセージを送信します。 ```cs {title="Controller/MyController.cs"} public class MyController { [RouteGet("/event-source")] public async Task ServerEventsResponse(HttpRequest request) { var serverEvents = await request.GetEventSourceAsync (); string[] fruits = new[] { "Apple", "Banana", "Watermelon", "Tomato" }; foreach (string fruit in fruits) { await serverEvents.SendAsync(fruit); await Task.Delay(1500); } return await serverEvents.CloseAsync(); } } ``` このコードを実行すると、以下のような結果が得られるはずです。 ## プロキシされた IP とホストの解決 Sisk はプロキシと併用できるため、クライアントからプロキシへの取引において IP アドレスがプロキシエンドポイントに置き換えられることがあります。 Sisk では、[forwarding resolvers](https://docs.sisk-framework.org/ja/docs/advanced/forwarding-resolvers.md) を使用して独自のリゾルバを定義できます。 ## ヘッダーエンコーディング ヘッダーのエンコーディングは、一部の実装で問題になることがあります。Windows では UTF-8 ヘッダーがサポートされていないため、ASCII が使用されます。Sisk には組み込みのエンコーディングコンバータがあり、誤ったエンコーディングのヘッダーをデコードするのに役立ちます。 この操作はコストが高く、デフォルトでは無効化されていますが、[HttpServerConfiguration.NormalizeHeadersEncodings](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.NormalizeHeadersEncodings.md) を有効にすることで使用可能になります。 --- # Responses Source: https://docs.sisk-framework.org/ja/docs/fundamentals/responses.html Responses は HTTP リクエストに対する HTTP レスポンスのオブジェクトを表します。サーバーはリソース、ページ、ドキュメント、ファイル、またはその他のオブジェクトへの要求の結果として、クライアントに送信します。 HTTP レスポンスはステータス、ヘッダー、コンテンツで構成されます。 このドキュメントでは、Sisk で HTTP レスポンスを設計する方法を解説します。 ## Setting an HTTP status HTTP ステータス一覧は HTTP/1.0 以来同じで、Sisk はすべてをサポートしています。 ```cs HttpResponse res = new HttpResponse(); res.Status = System.Net.HttpStatusCode.Accepted; // 202 ``` または Fluent Syntax を使用して: ```cs new HttpResponse() .WithStatus(200) // or .WithStatus(HttpStatusCode.Ok) // or .WithStatus(HttpStatusInformation.Ok); ``` 利用可能な `HttpStatusCode` の完全な一覧は[こちら](https://learn.microsoft.com/pt-br/dotnet/api/system.net.httpstatuscode)で確認できます。独自のステータスコードは [HttpStatusInformation](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpStatusInformation.md) 構造体を使用して指定することもできます。 ## Body and content-type Sisk は .NET のネイティブコンテンツオブジェクトをサポートしており、レスポンスのボディを送信できます。たとえば JSON レスポンスを送る場合は [StringContent](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.stringcontent) クラスを使用します。 ```cs HttpResponse res = new HttpResponse(); res.Content = new StringContent(myJson, Encoding.UTF8, "application/json"); ``` サーバーはヘッダーで明示的に `Content-Length` を定義していない限り、コンテンツから自動的に `Content-Length` を算出しようとします。サーバーがレスポンスコンテンツから暗黙的に `Content-Length` ヘッダーを取得できない場合、レスポンスは Chunked-Encoding で送信されます。 また、[StreamContent](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.streamcontent) を送信したり、メソッド [GetResponseStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetResponseStream.md) を使用してストリームでレスポンスを返すこともできます。 ## Response headers レスポンスで送信するヘッダーは追加、編集、削除が可能です。以下の例はクライアントへリダイレクトレスポンスを送る方法を示しています。 ```cs HttpResponse res = new HttpResponse(); res.Status = HttpStatusCode.Moved; res.Headers.Add(HttpKnownHeaderNames.Location, "/login"); ``` または Fluent Syntax を使用して: ```cs new HttpResponse(301) .WithHeader("Location", "/login"); ``` `HttpHeaderCollection` の [Add](https://docs.sisk-framework.org/api/Sisk.Core.Entity.HttpHeaderCollection.Add.md) メソッドは、既に送信されているヘッダーを変更せずにヘッダーを追加します。[Set](https://docs.sisk-framework.org/api/Sisk.Core.Entity.HttpHeaderCollection.Set.md) メソッドは同名のヘッダーを指定した値で置き換えます。`HttpHeaderCollection` のインデクサは内部的に Set メソッドを呼び出してヘッダーを置き換えます。 ヘッダー値は [GetHeaderValue](https://docs.sisk-framework.org/api/Sisk.Core.Entity.HttpHeaderCollection.GetHeaderValue.md) メソッドで取得できます。このメソッドはレスポンスヘッダーとコンテンツヘッダー(コンテンツが設定されている場合)の両方から値を取得するのに役立ちます。 ```cs // Returns the value of the "Content-Type" header, checking both response.Headers and response.Content.Headers string? contentType = response.GetHeaderValue("Content-Type"); ``` ## Sending cookies Sisk にはクライアント側のクッキー定義を簡素化するメソッドがあります。このメソッドで設定されたクッキーはすでに URL エンコードされており、RFC-6265 標準に準拠しています。 ```cs HttpResponse res = new HttpResponse(); res.SetCookie("cookie-name", "cookie-value"); ``` または Fluent Syntax を使用して: ```cs new HttpResponse(301) .WithCookie("cookie-name", "cookie-value", expiresAt: DateTime.Now.Add(TimeSpan.FromDays(7))); ``` 同じメソッドの[より完全なバージョン](https://docs.sisk-framework.org/api/Sisk.Core.Helpers.CookieHelper.SetCookie.md)も用意されています。 ## Chunked responses 大きなレスポンスを送信する場合は、転送エンコーディングを chunked に設定できます。 ```cs HttpResponse res = new HttpResponse(); res.SendChunked = true; ``` chunked-encoding を使用すると、`Content-Length` ヘッダーは自動的に省略されます。 ## Response stream レスポンスストリームは、レスポンスを分割して送信できる管理された方法です。`HttpResponse` オブジェクトを使用するより低レベルの操作で、ヘッダーとコンテンツを手動で送信し、最後に接続を閉じる必要があります。 この例はファイルの読み取り専用ストリームを開き、ストリームをレスポンス出力ストリームにコピーし、メモリにファイル全体をロードしません。中規模から大規模なファイルの配信に有用です。 ```cs // gets the response output stream using var fileStream = File.OpenRead("my-big-file.zip"); var responseStream = request.GetResponseStream(); // sets the response encoding to use chunked-encoding // also you shouldn't send content-length header when using // chunked encoding responseStream.SendChunked = true; responseStream.SetStatus(200); responseStream.SetHeader(HttpKnownHeaderNames.ContentType, contentType); // copies the file stream to the response output stream fileStream.CopyTo(responseStream.ResponseStream); // closes the stream return responseStream.Close(); ``` ## GZip, Deflate and Brotli compression Sisk では HTTP コンテンツを圧縮してレスポンスを送信できます。まず、`HttpContent` オブジェクトを以下のいずれかの圧縮クラスでラップし、圧縮されたレスポンスをクライアントに送ります。 ```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)), }; }); ``` ストリームでも同様の圧縮コンテンツを使用できます。 ```cs router.MapGet("/archive.zip", request => { // do not apply "using" here. the HttpServer will discard your content // after sending the response. var archive = File.OpenRead("/path/to/big-file.zip"); return new HttpResponse () { Content = new GZipContent(archive) } }); ``` `Content-Encoding` ヘッダーはこれらのコンテンツを使用すると自動的に設定されます。 ## Automatic compression `[EnableAutomaticResponseCompression](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.EnableAutomaticResponseCompression.md)` プロパティを使用すると、HTTP レスポンスを自動的に圧縮できます。このプロパティは、レスポンスが `[CompressedContent](https://docs.sisk-framework.org/api/Sisk.Core.Http.CompressedContent.md)` から継承されていない限り、ルーターからのレスポンスコンテンツをリクエストが受け入れ可能な圧縮コンテンツに自動的にラップします。 リクエストごとに選択される圧縮コンテンツは `Accept-Encoding` ヘッダーに従い、以下の順序で決定されます。 - [BrotliContent](https://docs.sisk-framework.org/api/Sisk.Core.Http.BrotliContent.md) (br) - [GZipContent](https://docs.sisk-framework.org/api/Sisk.Core.Http.GZipContent.md) (gzip) - [DeflateContent](https://docs.sisk-framework.org/api/Sisk.Core.Http.DeflateContent.md) (deflate) リクエストがこれらの圧縮方式のいずれかを受け入れることを示すと、レスポンスは自動的に圧縮されます。 ## Implicit response types `HttpResponse` 以外の戻り値型も使用できますが、ルーターに各オブジェクト型の取り扱い方法を設定する必要があります。 概念としては、常に参照型を返し、それを有効な `HttpResponse` オブジェクトに変換します。`HttpResponse` を返すルートは変換が行われません。 値型(構造体)は `[RouterCallback](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouterCallback.md)` と互換性がないため、ハンドラで使用するには `ValueResult` にラップする必要があります。 以下は `HttpResponse` を戻り値に使用しないルーターモジュールの例です。 ```cs [RoutePrefix("/users")] public class UsersController : RouterModule { public List Users = new List(); [RouteGet] public IEnumerable Index(HttpRequest request) { return Users.ToArray(); } [RouteGet("")] public User View(HttpRequest request) { int id = request.RouteParameters["id"].GetInteger(); User dUser = Users.First(u => u.Id == id); return dUser; } [RoutePost] public ValueResult Create(HttpRequest request) { User fromBody = request.GetJsonContent()!; Users.Add(fromBody); return true; } } ``` これにより、ルーター側で各オブジェクト型の処理方法を定義する必要があります。ハンドラの最初の引数は常にオブジェクトで、出力型は有効な `HttpResponse` でなければなりません。また、ルートの出力オブジェクトは `null` にすべきではありません。 `ValueResult` 型の場合、入力オブジェクトが `ValueResult` であることや `T` だけを示す必要はありません。`ValueResult` は元のコンポーネントから反映されたオブジェクトです。 型の関連付けは、ルーターコールバックから返されたオブジェクトの型と登録された型を比較するのではなく、ルーター結果の型が登録型に代入可能かどうかをチェックします。 `Object` 型のハンドラを登録すると、以前に検証されていないすべての型のフォールバックとして機能します。値ハンドラの挿入順序も重要で、`Object` ハンドラを登録すると他の型固有ハンドラが無視されます。順序を保証するため、常に具体的な値ハンドラを先に登録してください。 ```cs Router r = new Router(); r.MapInstance(new UsersController()); r.RegisterValueHandler(apiResult => { return new HttpResponse() { Status = apiResult.Success ? HttpStatusCode.OK : HttpStatusCode.BadRequest, Content = apiResult.GetHttpContent(), Headers = apiResult.GetHeaders() }; }); r.RegisterValueHandler(bvalue => { return new HttpResponse() { Status = bvalue ? HttpStatusCode.OK : HttpStatusCode.BadRequest }; }); r.RegisterValueHandler>(enumerableValue => { return new HttpResponse(string.Join("\n", enumerableValue)); }); // registering an value handler of object must be the last // value handler which will be used as an fallback r.RegisterValueHandler(fallback => { return new HttpResponse() { Status = HttpStatusCode.OK, Content = JsonContent.Create(fallback) }; }); ``` ## Deferred Actions リクエストがルーターに到達すると、まず [request handlers](https://docs.sisk-framework.org/ja/docs/fundamentals/request-handlers.md) を通過し、ルーターアクションで処理され、続いてポスト実行リクエストハンドラが実行されます。ルーターアクションの結果は値ハンドラに渡され、値ハンドラの結果がクライアントへのレスポンスとして送信されます。 このライフサイクルは非同期コンテキスト内で行われます。非同期コンテキストは、ハンドラ間やルーターアクション間でデータを共有するためにユーザーが `[HttpContext Bag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.md)` に追加できる変数を公開します。ルーターアクションの戻り値はこの非同期コンテキストに追加され、値ハンドラからアクセス可能です。 Deferred actions は、クライアントへのレスポンス配信後、同じ非同期コンテキスト内でサイクルの最後に必ず実行されるアクションです。これらは、ログ保存、データベース更新、メール送信など、レスポンス送信に必須でない長時間タスクの実行に利用できます。 例外は Deferred actions 内でも捕捉され、リクエストライフサイクルの他の場所でスローされた場合と同様に処理されます。違いはクライアントがすでにレスポンスを受け取っている点で、例外はデフォルトのエラーハンドリングで処理されます。 `[HttpContext.EnqueueDeferredAction](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.EnqueueDeferredAction.md)` メソッドでアクションの実行を遅延させます。このメソッドは、実行すべき非同期関数と、オプションで実行時間の上限を表すタイムアウトを受け取ります。タイムアウト内に完了しない場合はキャンセルされます。 ```csharp [RoutePost("/send-mail")] public HttpResponse SendMail(HttpRequest request) { string to = request.Query["to"].GetString(); string subject = request.Query["subject"].GetString(); string body = request.Query["body"].GetString(); if (string.IsNullOrWhiteSpace(to) || string.IsNullOrWhiteSpace(subject) || string.IsNullOrWhiteSpace(body)) { throw new ApiException("Missing required parameters."); } // schedules a long-running action that will be executed after sending the response to the client, but still within the same asynchronous context of the request request.Context.EnqueueDeferredAction(async (ct) => { await EmailService.SendEmailAsync(to, subject, body); }, timeout: TimeSpan.FromSeconds(30)); return new HttpResponse() { Status = 200, Content = new StringContent("Sending the email...") }; } ``` ## Note on enumerable objects and arrays `IEnumerable` を実装した暗黙的なレスポンスオブジェクトは、定義された値ハンドラを通す前に `ToArray()` メソッドでメモリ上に読み込まれます。この際、`IEnumerable` オブジェクトはオブジェクト配列に変換され、レスポンスコンバータは常に `Object[]` を受け取ります。 以下のシナリオを考えてみましょう。 ```csharp using var host = HttpServer.CreateBuilder(12300) .UseRouter(r => { r.RegisterValueHandler>(stringEnumerable => { return new HttpResponse("String array:\n" + string.Join("\n", stringEnumerable)); }); r.RegisterValueHandler>(stringEnumerable => { return new HttpResponse("Object array:\n" + string.Join("\n", stringEnumerable)); }); r.MapGet("/", request => { return (IEnumerable)["hello", "world"]; }); }) .Build(); ``` 上記例では、`IEnumerable` コンバータは **決して呼び出されません**。入力オブジェクトは常に `Object[]` となり、`IEnumerable` に変換できないためです。一方、`IEnumerable` を受け取るコンバータは入力を受け取ります。これはその型が互換性を持つためです。 列挙可能なオブジェクトの型そのものを扱う必要がある場合は、コレクション要素の型を取得するためにリフレクションを使用する必要があります。すべての列挙可能オブジェクト(リスト、配列、コレクション)は HTTP レスポンスコンバータによってオブジェクト配列に変換されます。 `IAsyncEnumerable` を実装した値は、`[ConvertIAsyncEnumerableIntoEnumerable](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ConvertIAsyncEnumerableIntoEnumerable.md)` プロパティが有効になっている場合、サーバーが自動的に処理します。これは `IEnumerable` と同様の動作で、デフォルトで `HttpServerConfiguration` に有効化されています。非同期列挙はブロッキング列挙子に変換され、さらに同期的なオブジェクト配列に変換されます。独自の値ハンドラや非同期シーケンス用のストリーミングレスポンス戦略を提供する場合にのみ、無効化してください。 --- # ロギング Source: https://docs.sisk-framework.org/ja/docs/features/logging.html Sisk を構成して、アクセスログとエラーログを自動的に書き込むことができます。ログのローテーション、拡張子、書き込み頻度を定義することも可能です。 [LogStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.md) クラスは、非同期でログを書き込み、await 可能な書き込みキューに保持する方法を提供します。`LogStream` クラスは `IAsyncDisposable` を実装しており、ストリームが閉じられる前に保留中のすべてのログが書き込まれることを保証します。 本稿では、アプリケーションのロギングを設定する方法を示します。 ## ファイルベースのアクセスログ ファイルへのログは、ファイルを開き、行テキストを書き込み、書き込んだ行ごとにファイルを閉じます。この手順は、ログの書き込み応答性を維持するために採用されています。 ```cs {title="Program.cs"} class Program { static async Task Main(string[] args) { using var app = HttpServer.CreateBuilder() .UseConfiguration(config => { config.AccessLogsStream = new LogStream("logs/access.log"); }) .Build(); ... await app.StartAsync(); } } ``` 上記のコードは、すべての受信リクエストを `logs/access.log` ファイルに書き込みます。ファイルが存在しない場合は自動的に作成されますが、フォルダーは自動で作成されません。`LogStream` クラスがフォルダーを自動的に作成するため、`logs/` ディレクトリを事前に作成する必要はありません。 ## ストリームベースのロギング コンストラクターに `TextWriter` オブジェクトを渡すことで、`Console.Out` などの `TextWriter` インスタンスにログファイルを書き込むことができます。 ```cs {title="Program.cs"} using var app = HttpServer.CreateBuilder() .UseConfiguration(config => { config.AccessLogsStream = new LogStream(Console.Out); }) .Build(); ``` ストリームベースのログに書き込まれるたびに、`TextWriter.Flush()` メソッドが呼び出されます。 ## アクセスログのフォーマット 事前定義された変数でアクセスログのフォーマットをカスタマイズできます。以下の行を例にしてください。 ```cs config.AccessLogsFormat = "%dd/%dmm/%dy %tH:%ti:%ts %tz %ls %ri %rs://%ra%rz%rq [%sc %sd] %lin -> %lou in %lmsms [%{user-agent}]"; ``` 次のようなメッセージが書き込まれます。 29/mar./2023 15:21:47 -0300 Executed ::1 http://localhost:5555/ [200 OK] 689B -> 707B in 84ms [Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/111.0.0.0 Safari/537.36] 以下の表に示すフォーマットでログファイルを整形できます。 | Value | What it represents | Example | |--------|--------------------|---------| | %dd | 月の日付 (2桁) | 05 | | %dmmm | 月の完全名 | July | | %dmm | 月の省略名 (3文字) | Jul | | %dm | 月番号 (2桁) | 07 | | %dy | 年 (4桁) | 2023 | | %th | 12時間制の時 | 03 | | %tH | 24時間制の時 (HH) | 15 | | %ti | 分 (2桁) | 30 | | %ts | 秒 (2桁) | 45 | | %tm | ミリ秒 (3桁) | 123 | | %tz | タイムゾーンオフセット (UTC の合計時間) | +03:00 | | %ri | リクエスト開始時に解決されたクライアントのリモート IP アドレス | 192.168.1.100 | | %rm | HTTP メソッド (大文字) | GET | | %rs | URI スキーム (http/https) | https | | %ra | URI 権限部 (ドメイン) | example.com | | %rh | リクエストのホスト | www.example.com | | %rp | リクエストのポート | 443 | | %rz | リクエストのパス | /path/to/resource | | %rq | クエリ文字列 | ?key=value&another=123 | | %sc | HTTP 応答ステータスコード | 200 | | %sd | HTTP 応答ステータスの説明 | OK | | %lin | リクエストの人間が読めるサイズ | 1.2 KB | | %linr | リクエストの生サイズ (バイト) | 1234 | | %lou | レスポンスの人間が読めるサイズ | 2.5 KB | | %lour | レスポンスの生サイズ (バイト) | 2560 | | %lms | 経過時間 (ミリ秒) | 120 | | %ls | 実行ステータス | Executed | | %{header-name} | リクエストの `header-name` ヘッダーを表す | `Mozilla/5.0 (platform; rv:gecko [...]` | | %{:header-name} | レスポンスの `header-name` ヘッダーを表す | `application/json` | | %{$key} | [HttpContext.RequestBag] に格納された `key` の値を `ToString()` で変換したものを表す。キーが設定されていない場合は空 | `42` | `HttpServerConfiguration.DefaultAccessLogFormat` を使用して、デフォルトのアクセスログフォーマットを利用することもできます。 `%{$key}` 項目を使用すると、リクエストハンドラやルートが独自の値をアクセスログに追加できます。 ```cs config.AccessLogsFormat = "%dd/%dmm/%dy %tH:%ti:%ts %rm %rz [%sc] user=%{$UserId}"; router.MapGet("/profile", request => { request.Bag["UserId"] = "42"; return new HttpResponse("ok"); }); ``` ## ログのローテーション HTTP サーバーを構成して、一定サイズに達したときにログファイルを圧縮された .gz ファイルにローテーションできます。サイズは定義したしきい値で定期的にチェックされます。 ```cs LogStream errorLog = new LogStream("logs/error.log") .ConfigureRotatingPolicy( maximumSize: 64 * SizeHelper.UnitMb, dueTime: TimeSpan.FromHours(6)); ``` 上記のコードは、6 時間ごとに `LogStream` のファイルが 64 MB の上限に達しているかを確認します。上限に達していれば、ファイルは .gz に圧縮され、`access.log` がクリーンアップされます。 この処理中は、ファイルへの書き込みはロックされ、圧縮とクリーンアップが完了するまで待機します。この期間に書き込まれた行はすべてキューに入れられ、圧縮完了を待ちます。 この機能はファイルベースの LogStream のみで動作します。 ## エラーロギング サーバーがデバッガーに例外を送出しない場合、エラーが存在すればログに転送されます。エラー書き込みは次のように構成できます。 ```cs config.ThrowExceptions = false; config.ErrorsLogsStream = new LogStream("error.log"); ``` このプロパティは、エラーがコールバックまたは [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) プロパティで捕捉されなかった場合にのみログに書き込みます。 サーバーが書き込むエラーは常に日時、リクエストヘッダー(ボディは除く)、エラートレース、内部例外トレース(存在する場合)を含みます。 ## その他のロギングインスタンス アプリケーションは 0 個または複数の LogStream を持つことができ、チャネル数に制限はありません。したがって、デフォルトの AccessLog や ErrorLog 以外のファイルにアプリケーションのログを出力することが可能です。 ```cs LogStream appMessages = new LogStream("messages.log"); appMessages.WriteLine("Application started at {0}", DateTime.Now); ``` ## ファイルの安全な書き込み ログストリーム以外のファイル(レポートや状態ダンプなど)については、[LogStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.md) が静的ヘルパーを提供し、欠落している親ディレクトリを作成し、同一プロセス内で同じファイルへの同時書き込みをシリアライズし、失敗時には例外を投げずに `false` を返します。 - [LogStream.SafeWriteToFile](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeWriteToFile.md) と [LogStream.SafeWriteToFileAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeWriteToFileAsync.md) はファイル内容を置き換えます。 - [LogStream.SafeAppendToFile](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeAppendToFile.md) と [LogStream.SafeAppendToFileAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.LogStream.SafeAppendToFileAsync.md) はファイルに追記します。 各メソッドは文字列または文字列配列を受け取り、`Encoding` を受け取るオーバーロードも用意されています。 ```cs bool written = await LogStream.SafeAppendToFileAsync("logs/audit.log", $"{DateTime.Now:s} user signed in\n"); if (!written) { Console.WriteLine("Could not write the audit entry."); } ``` ロックはプロセス単位です。他プロセスから同じファイルへの書き込みは調整されません。ディレクトリが存在することだけを確認したい場合は、[PathHelper.EnsureDirectoryExistance](https://docs.sisk-framework.org/api/Sisk.Core.Helpers.PathHelper.EnsureDirectoryExistance.md) または [PathHelper.EnsureFileDirectoryExistance](https://docs.sisk-framework.org/api/Sisk.Core.Helpers.PathHelper.EnsureFileDirectoryExistance.md) を使用してください。これらはディレクトリが既に存在すれば `true`、作成した場合は `false` を返します。 ## LogStream の拡張 `LogStream` クラスを拡張して、現在の Sisk ログエンジンと互換性のあるカスタムフォーマットを書き込むことができます。以下の例は Spectre.Console ライブラリを使用してコンソールにカラー付きメッセージを書き込む方法を示しています。 ```cs {title="CustomLogStream.cs"} public class CustomLogStream : LogStream { protected override void WriteLineInternal(string line) { base.WriteLineInternal($"[{DateTime.Now:g}] {line}"); } } ``` 各リクエスト/レスポンスごとにカスタムログを自動的に書き込む別の方法として、[HttpServerHandler](https://docs.sisk-framework.org/api/Sisk.Core.Http.Handlers.HttpServerHandler.md) を作成します。以下の例はやや完全な形です。リクエストとレスポンスの本文を JSON 形式でコンソールに出力します。リクエスト全体のデバッグに役立ちます。この例は ContextBag と HttpServerHandler を利用しています。 ```cs {title="Program.cs"} class Program { static async Task Main(string[] args) { var app = HttpServer.CreateBuilder(host => { host.UseListeningPort(5555); host.UseHandler(); }); app.Router.MapAny("/json", request => { return new HttpResponse() .WithContent(JsonContent.Create(new { method = request.Method.Method, path = request.Path, specialMessage = "Hello, world!!" })); }); await app.StartAsync(); } } ``` ```cs {title="JsonMessageHandler.cs"} class JsonMessageHandler : HttpServerHandler { protected override void OnHttpRequestOpen(HttpRequest request) { if (request.Method != HttpMethod.Get && request.Headers["Content-Type"]?.Contains("json", StringComparison.InvariantCultureIgnoreCase) == true) { // この時点で接続はオープンしており、クライアントはコンテンツが JSON であることを示すヘッダーを送信しています。 // 以下の行でコンテンツを読み取り、リクエストに保持させます。 // // リクエスト処理でコンテンツを読み取らない場合、GC がコンテンツを回収してしまい、クライアントへのレスポンス送信後にコンテンツが利用できなくなる可能性があります。 // _ = request.RawBody; // コンテキストにヒントを追加し、このリクエストが JSON ボディを持つことを示します request.Bag.Add("IsJsonRequest", true); } } protected override async void OnHttpRequestClose(HttpServerExecutionResult result) { string? requestJson = null, responseJson = null, responseMessage; if (result.Request.Bag.ContainsKey("IsJsonRequest")) { // CypherPotato.LightJson ライブラリを使用して JSON を整形します var content = result.Request.Body; requestJson = JsonValue.Deserialize(content, new JsonOptions() { WriteIndented = true }).ToString(); } if (result.Response is { } response) { var content = response.Content; responseMessage = $"{(int)response.Status} {HttpStatusInformation.GetStatusCodeDescription(response.Status)}"; if (content is HttpContent httpContent && // レスポンスが JSON かどうかをチェック httpContent.Headers.ContentType?.MediaType?.Contains("json", StringComparison.InvariantCultureIgnoreCase) == true) { string json = await httpContent.ReadAsStringAsync(); responseJson = JsonValue.Deserialize(json, new JsonOptions() { WriteIndented = true }).ToString(); } } else { // 内部サーバーの処理ステータスを取得 responseMessage = result.Status.ToString(); } StringBuilder outputMessage = new StringBuilder(); if (requestJson != null) { outputMessage.AppendLine("-----"); outputMessage.AppendLine($">>> {result.Request.Method} {result.Request.Path}"); if (requestJson is not null) outputMessage.AppendLine(requestJson); } outputMessage.AppendLine($"<<< {responseMessage}"); if (responseJson is not null) outputMessage.AppendLine(responseJson); outputMessage.AppendLine("-----"); await Console.Out.WriteLineAsync(outputMessage.ToString()); } } ``` --- # サーバー送信イベント Source: https://docs.sisk-framework.org/ja/docs/features/server-sent-events.html Sisk は、Server Sent Events を使用したメッセージ送信を標準でサポートしています。使い捨ておよび永続的な接続を作成でき、実行時に接続を取得して使用することができます。 この機能には、ブラウザーが課すいくつかの制限があります。たとえば、テキストメッセージのみ送信でき、接続を永続的に閉じることができません。サーバー側で接続が閉じられた場合、クライアントは 5 秒ごと(ブラウザーによっては 3 秒ごと)に再接続を試みます。 これらの接続は、クライアントが毎回情報を要求することなく、サーバーからクライアントへイベントを送信するのに便利です。 ## SSE 接続の作成 SSE 接続は通常の HTTP リクエストと同様に動作しますが、レスポンスを送信してすぐに接続を閉じるのではなく、メッセージを送信できるように接続を開いたままにします。 [HttpRequest.GetEventSource()](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetEventSource.md) メソッドを呼び出すと、SSE インスタンスが作成される間、リクエストは待機状態になります。 ```cs r.MapGet("/", (req) => { using var sse = req.GetEventSource(); sse.Send("Hello, world!"); return sse.Close(); }); ``` 上記のコードでは、SSE 接続を作成し、"Hello, world" メッセージを送信し、サーバー側から SSE 接続を閉じています。 > [!NOTE] > サーバー側の接続を閉じると、デフォルトではクライアントは再度接続しようとし、接続が再開されてメソッドが永遠に再実行されます。 > > 接続がサーバー側で閉じられた際に、クライアントが再接続しようとしないように、サーバーから終了メッセージを転送するのが一般的です。 ## ヘッダーの追加 ヘッダーを送信する必要がある場合は、メッセージを送信する前に [HttpRequestEventSource.AppendHeader](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.AppendHeader.md) メソッドを使用できます。 ```cs r.MapGet("/", (req) => { using var sse = req.GetEventSource(); sse.AppendHeader("Header-Key", "Header-value"); sse.Send("Hello!"); return sse.Close(); }); ``` ヘッダーはメッセージを送信する前に送信する必要があることに注意してください。 ## Wait-For-Fail 接続 接続は、クライアント側の切断が原因でサーバーがメッセージを送信できなくなると通常は終了します。この場合、接続は自動的に終了し、クラスのインスタンスは破棄されます。 再接続が行われても、クラスのインスタンスは前の接続に紐付いているため機能しません。状況によっては、後でこの接続が必要になることがあり、ルートのコールバックメソッドで管理したくない場合があります。 そのため、SSE 接続に識別子を付けて後で取得できるようにし、ルートのコールバック外でも使用できます。また、接続を [WaitForFail](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.WaitForFail.md) でマークすることで、ルートを終了させずに接続を自動的に終了させません。 `WaitForFail` 状態の SSE 接続は、切断による送信エラーまたは設定されたアイドル許容時間が経過するのを待ち、ルートが再開されて接続が閉じられます。 ```cs r.MapGet("/", (req) => { using var sse = req.GetEventSource("my-index-connection"); sse.WaitForFail(TimeSpan.FromSeconds(15)); // メッセージが無い状態で 15 秒待機し、接続を終了する前に待ちます return sse.Close(); }); ``` 上記のメソッドは接続を作成し、処理を行い、切断またはエラーを待ちます。 ```cs HttpRequestEventSource? evs = server.EventSources.GetByIdentifier("my-index-connection"); if (evs != null) { // 接続はまだ生きています evs.Send("Hello again!"); } ``` 上記のスニペットは新しく作成された接続を探し、存在すればメッセージを送信します。 識別されたすべてのアクティブなサーバー接続はコレクション [HttpServer.EventSources](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.EventSources.md) で利用可能です。このコレクションはアクティブで識別された接続のみを保持し、閉じられた接続はコレクションから削除されます。 > [!NOTE] > keep-alive には、Web プロキシや HTTP カーネル、ネットワークドライバーなど、制御できない形で Sisk に接続するコンポーネントが設定する制限があり、一定時間アイドル状態が続くと接続が閉じられることに注意が必要です。 > > したがって、定期的に ping を送信するか、接続が閉じられるまでの最大時間を延長して接続を開いたままにすることが重要です。次のセクションで定期的な ping の送信について詳しく説明します。 ## 接続 ping ポリシーの設定 Ping ポリシーは、クライアントに定期的なメッセージを自動的に送信する方法です。この機能により、サーバーは接続を無期限に開いたままにせず、クライアントが切断したことを検知できます。 ```cs [RouteGet("/sse")] public async Task Events(HttpRequest request) { using var sse = await request.GetEventSourceAsync("user-events"); sse.WithPing(ping => { ping.DataMessage = "ping-message"; ping.Interval = TimeSpan.FromSeconds(5); ping.Start(); }); await sse.WaitForFailAsync(TimeSpan.FromMinutes(10)); return await sse.CloseAsync(); } ``` 上記のコードでは、5 秒ごとに新しい ping メッセージがクライアントに送信されます。これにより TCP 接続が維持され、アイドル状態による切断を防止します。また、メッセージの送信に失敗した場合、接続は自動的に閉じられ、接続で使用されていたリソースが解放されます。 非同期ルートでは [SendAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.SendAsync.md) と [CloseAsync](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.CloseAsync.md) を使用してください。閉じる前にキューに入ったイベントを破棄する必要がある場合は [Cancel](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpRequestEventSource.Cancel.md) を呼び出します。 ## 接続のクエリ たとえばブロードキャストを行うために、接続識別子に対する述語を使用してアクティブな接続を検索できます。 ```cs HttpRequestEventSource[] evs = server.EventSources.Find(es => es.StartsWith("my-connection-")); foreach (HttpRequestEventSource e in evs) { e.Send("Broadcasting to all event sources that starts with 'my-connection-'"); } ``` また、[All](https://docs.sisk-framework.org/api/Sisk.Core.Http.Streams.HttpEventSourceCollection.All.md) メソッドを使用して、すべてのアクティブな SSE 接続を取得することもできます。 --- # Web ソケット Source: https://docs.sisk-framework.org/ja/docs/features/websockets.html Sisk は Web ソケットもサポートしており、クライアントとのメッセージの受信および送信が可能です。 この機能はほとんどのブラウザーで問題なく動作しますが、Sisk ではまだ実験的な段階です。バグを見つけた場合は、GitHub で報告してください。 ## メッセージの受信 WebSocket メッセージは順番に受信され、`ReceiveMessageAsync` によって処理されるまでキューに保持されます。このメソッドは、タイムアウトに達した場合、操作がキャンセルされた場合、またはクライアントが切断された場合にメッセージを返しません。 受信メッセージの最大サイズは [HttpServerConfiguration.MaximumContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.MaximumContentLength.md) で、Sisk 1.7 以降はデフォルトで 100 MB です。クライアントがそれより大きなメッセージを送信した場合、接続は `MessageTooBig` ステータスで閉じられ、`ReceiveMessageAsync` はメッセージを返しません。 読み取りと書き込みの操作は同時に一つしか実行できないため、`ReceiveMessageAsync` でメッセージを待機している間は、接続されたクライアントへ書き込むことはできません。 ```cs router.MapGet("/connect", async (HttpRequest req) => { using var ws = await req.GetWebSocketAsync(); while (await ws.ReceiveMessageAsync(timeout: TimeSpan.FromSeconds(30)) is { } receivedMessage) { string msgText = receivedMessage.GetString(); Console.WriteLine("Received message: " + msgText); await ws.SendAsync("Hello!"); } return await ws.CloseAsync(); }); ``` ## 永続的な接続 以下の例は、メッセージを受信し処理し、ソケットの使用を終了するまで、永続的な WebSocket 接続を利用する方法を示しています。 ```cs router.MapGet("/connect", async (HttpRequest req) => { using var ws = await req.GetWebSocketAsync(); WebSocketMessage? msg; askName: await ws.SendAsync("What is your name?"); msg = await ws.ReceiveMessageAsync(); if (msg is null) return await ws.CloseAsync(); string name = msg.GetString(); if (string.IsNullOrEmpty(name)) { await ws.SendAsync("Please, insert your name!"); goto askName; } askAge: await ws.SendAsync("And your age?"); msg = await ws.ReceiveMessageAsync(); if (msg is null) return await ws.CloseAsync(); if (!Int32.TryParse(msg?.GetString(), out int age)) { await ws.SendAsync("Please, insert an valid number"); goto askAge; } await ws.SendAsync($"You're {name}, and you are {age} old."); return await ws.CloseAsync(); }); ``` ## Ping ポリシー Server Side Events の Ping ポリシーと同様に、非アクティブ時でも TCP 接続を維持するために Ping ポリシーを設定することができます。 ```cs ws.PingPolicy.Start( dataMessage: "ping-message", interval: TimeSpan.FromSeconds(10)); ``` ## 管理された接続 WebSocket を受け入れる際に識別子を指定できます。識別されたソケットは [HttpServer.WebSockets](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.WebSockets.md) に登録され、サーバーはそれらを受け入れたルート以外からでもアクティブな接続を検索できるようになります。 ```cs router.MapGet("/connect/", async (HttpRequest req) => { string userId = req.RouteParameters["userId"].GetString(); using var ws = await req.GetWebSocketAsync(identifier: $"user:{userId}"); ws.State = userId; ws.PingPolicy.Start( dataMessage: "ping", interval: TimeSpan.FromSeconds(10)); while (await ws.ReceiveMessageAsync(TimeSpan.FromMinutes(5)) is { } message) { await ws.SendAsync("Received: " + message.GetString()); } return await ws.CloseAsync(); }); ``` アプリケーションの別の部分から、識別子または述語でコレクションを照会できます。 ```cs HttpWebSocket? socket = server.WebSockets.GetByIdentifier("user:42"); if (socket is { IsClosed: false }) { await socket.SendAsync("Your report is ready."); } foreach (HttpWebSocket activeSocket in server.WebSockets.Find(id => id.StartsWith("user:"))) { await activeSocket.SendAsync("Broadcast message"); } ``` `HttpWebSocket` は `Identifier`、`State`、`IsClosed`、`PingPolicy` を公開します。コレクションはさらに、サーバー管理の接続戦略のために `All()`、`Find(...)`、`GetByIdentifier(...)`、`ActiveConnections`、`DropAll()` を提供します。 --- # 捨てられる構文 Source: https://docs.sisk-framework.org/ja/docs/features/discard-syntax.html HTTPサーバーは、OAuth認証などのアクションからのコールバック要求を待ち受けるために使用でき、要求を受け取った後には捨てられる。この機能は、バックグラウンドアクションが必要だが、HTTPアプリケーションを設定したくない場合に便利です。 以下の例は、[CreateListener](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.CreateListener.md) を使用してポート5555でHTTPサーバーを作成し、次のコンテキストを待機する方法を示しています。 ```csharp using (var server = HttpServer.CreateListener(5555)) { // 次のHTTP要求を待機 var context = await server.WaitNextAsync(); Console.WriteLine($"要求されたパス: {context.Request.Path}"); } ``` [WaitNext](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.WaitNext.md) 関数は、完了した要求処理の次のコンテキストを待機します。この操作の結果が取得されると、サーバーはすでに要求を完全に処理し、クライアントに応答を送信しています。 --- # 依存性の注入 Source: https://docs.sisk-framework.org/ja/docs/features/instancing.html リクエストの有効期間中に存在するメンバーとインスタンス(たとえば、データベース接続、認証済みユーザー、またはセッショントークン)を指定することは一般的です。可能な方法の1つは、[HttpContext.RequestBag](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.md)を使用することです。これは、リクエストの有効期間中に存在する辞書を作成します。 この辞書は、[リクエストハンドラー](https://docs.sisk-framework.org/ja/docs/fundamentals/request-handlers.md)によってアクセスされ、リクエスト全体で変数を定義するために使用できます。たとえば、ユーザーを認証するリクエストハンドラーは、`HttpContext.RequestBag`内にユーザーを設定し、リクエストロジック内では、`HttpContext.RequestBag.Get()`を使用してこのユーザーを取得できます。 この辞書に定義されたオブジェクトは、リクエストのライフサイクルにスコープされます。リクエストの終了時に破棄されます。レスポンスの送信が必ずしもリクエストのライフサイクルの終了を定義するわけではありません。レスポンスの送信後に実行される[リクエストハンドラー](https://docs.sisk-framework.org/ja/docs/fundamentals/request-handlers.md)が実行されるとき、`RequestBag`オブジェクトはまだ存在し、破棄されていません。 以下は例です: ```csharp {title="RequestHandlers/AuthenticateUser.cs"} public class AuthenticateUser : IRequestHandler { public RequestHandlerExecutionMode ExecutionMode { get; init; } = RequestHandlerExecutionMode.BeforeResponse; public HttpResponse? Execute(HttpRequest request, HttpContext context) { User authenticatedUser = AuthenticateUser(request); context.RequestBag.Set(authenticatedUser); return null; // 次のリクエストハンドラーまたはリクエストロジックに進む } } ``` ```csharp {title="Controllers/HelloController.cs"} [RouteGet("/hello")] [RequestHandler] public HttpResponse SayHello(HttpRequest request) { var authenticatedUser = request.Bag.Get(); return new HttpResponse() { Content = new StringContent($"Hello {authenticatedUser.Name}!") }; } ``` これは、この操作の初期的な例です。`User`のインスタンスは、認証用のリクエストハンドラー内で作成されましたが、このリクエストハンドラーを使用するすべてのルートには、`HttpContext.RequestBag`のインスタンス内に`User`が存在することが保証されます。 `RequestBag`に事前に定義されていないインスタンスを取得するロジックを定義するには、[GetOrAdd](https://docs.sisk-framework.org/api/Sisk.Core.Entity.TypedValueDictionary.GetOrAdd.md)や[GetOrAddAsync](https://docs.sisk-framework.org/api/Sisk.Core.Entity.TypedValueDictionary.GetOrAddAsync.md)などのメソッドを使用できます。 バージョン1.3以降、静的プロパティ[HttpContext.Current](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.Current.md)が導入され、現在実行中のリクエストコンテキストの`HttpContext`にアクセスできるようになりました。これにより、`HttpContext`のメンバーをリクエストの外部に公開し、ルートオブジェクト内にインスタンスを定義できるようになりました。 以下の例では、リクエストコンテキストで一般的にアクセスされるメンバーを持つコントローラーを定義します。 ```csharp {title="Controllers/Controller.cs"} public abstract class Controller : RouterModule { // リクエストごとに既存のデータベースインスタンスを取得または作成する protected DbContext Database => HttpContext.Current.RequestBag.GetOrAdd(() => new DbContext()); // リポジトリの遅延ロードも一般的です protected IUserRepository Users => HttpContext.Current.RequestBag.GetOrAdd(() => new UserRepository(Database)); protected IBlogRepository Blogs => HttpContext.Current.RequestBag.GetOrAdd(() => new BlogRepository(Database)); protected IBlogPostRepository BlogPosts => HttpContext.Current.RequestBag.GetOrAdd(() => new BlogPostRepository(Database)); // 次の行は、リクエストバッグ内にユーザーが定義されていない場合に例外をスローします protected User AuthenticatedUser => => HttpContext.Current.RequestBag.Get(); // HttpRequestインスタンスの公開もサポートされています protected HttpRequest Request => HttpContext.Current.Request } ``` コントローラーから継承するタイプを定義します。 ```csharp {title="Controllers/PostsController.cs"} [RoutePrefix("/api/posts/{author}")] sealed class PostsController : Controller { protected Guid AuthorId => Request.RouteParameters["author"].GetInteger(); [RouteGet] public IAsyncEnumerable ListPosts() { return BlogPosts.GetPostsAsync(authorId: AuthorId); } [RouteGet("")] public async Task GetPost() { int postId = Request.RouteParameters["id"].GetInteger(); Post? post = await BlogPosts .FindPostAsync(post => post.Id == postId && post.AuthorId == AuthorId); return post; } } ``` 上記の例では、ルーターの戻り値を有効な[HttpResponse](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpResponse.md)に変換するために、[値ハンドラー](https://docs.sisk-framework.org/ja/docs/fundamentals/responses.md#implicit-response-types)をルーターに構成する必要があります。 メソッドに`HttpRequest request`引数がないことに注意してください。これは、バージョン1.3以降、ルーターがルーティングレスポンスの2種類のデリゲートをサポートしているためです。1つは、`HttpRequest`引数を受け取るデフォルトのデリゲートである[RouteAction](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteAction.md)で、もう1つは[ParameterlessRouteAction](https://docs.sisk-framework.org/api/Sisk.Core.Routing.ParameterlessRouteAction.md)です。`HttpRequest`オブジェクトは、静的な`HttpContext`の[Request](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpContext.Request.md)プロパティを介して、両方のデリゲートからアクセスできます。 上記の例では、`DbContext`という破棄可能なオブジェクトを定義し、HTTPセッション終了時に作成されたすべての`DbContext`インスタンスを破棄する必要があります。これを実現するには、2つの方法があります。1つは、ルーターのアクション後に実行される[リクエストハンドラー](https://docs.sisk-framework.org/ja/docs/fundamentals/request-handlers.md)を作成することです。もう1つは、カスタム[サーバーハンドラー](https://docs.sisk-framework.org/ja/docs/advanced/http-server-handlers.md)を使用することです。 最初の方法では、`OnSetup`メソッド内に直接インラインでリクエストハンドラーを作成できます。 ```csharp {title="Controllers/PostsController.cs"} public abstract class Controller : RouterModule { // ... protected override void OnSetup(Router parentRouter) { base.OnSetup(parentRouter); HasRequestHandler(RequestHandler.Create( execute: (req, ctx) => { // リクエストハンドラーのコンテキストで定義されたDbContextを取得し、破棄する ctx.RequestBag.GetOrDefault()?.Dispose(); return null; }, executionMode: RequestHandlerExecutionMode.AfterResponse)); } } ``` > [!TIP] > > Siskバージョン1.4以降、プロパティ[HttpServerConfiguration.DisposeDisposableContextValues](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.DisposeDisposableContextValues.md)が導入され、デフォルトで有効になりました。これは、HTTPセッションが閉じられたときにコンテキストバッグ内のすべての`IDisposable`値を破棄するかどうかを定義します。 上記の方法により、HTTPセッションが終了すると、`DbContext`が破棄されます。破棄が必要な他のメンバーについても同様に行うことができます。 2番目の方法では、HTTPセッションが終了すると`DbContext`を破棄するカスタム[サーバーハンドラー](https://docs.sisk-framework.org/ja/docs/advanced/http-server-handlers.md)を作成します。 ```csharp {title="Server/Handlers/ObjectDisposerHandler.cs"} public class ObjectDisposerHandler : HttpServerHandler { protected override void OnHttpRequestClose(HttpServerExecutionResult result) { result.Context.RequestBag.GetOrDefault()?.Dispose(); } } ``` そして、アプリビルダーで使用します。 ```csharp {title="Program.cs"} using var host = HttpServer.CreateBuilder() .UseHandler() .Build(); ``` これは、コードのクリーンアップを処理し、リクエストの依存関係を使用されるモジュールの種類によって分離し、ルーターの各アクション内でのコードの重複を減らす方法です。これは、ASP.NETなどのフレームワークで依存性の注入が使用されるのと似た方法です。 --- # コンテンツのストリーミング Source: https://docs.sisk-framework.org/ja/docs/features/content-streaming.html Sisk では、クライアントとの間でコンテンツのストリーミングを読み書きすることができます。この機能は、リクエストの生存期間中にコンテンツのシリアル化とデシリアル化のメモリ負荷を削減するために役立ちます。 ## リクエストコンテンツストリーム 小さなコンテンツは自動的に HTTP 接続バッファメモリに読み込まれ、[HttpRequest.Body](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.Body.md) と [HttpRequest.RawBody](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.RawBody.md) に迅速に読み込まれます。より大きなコンテンツの場合は、[HttpRequest.GetRequestStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetRequestStream.md) メソッドを使用して、リクエストコンテンツの読み取りストリームを取得できます。 [HttpRequest.GetMultipartFormContent](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetMultipartFormContent.md) メソッドは、リクエストの全コンテンツをメモリに読み込むため、大きなコンテンツを読み取るには適していないことに注意してください。 以下の例を考えてみましょう: ```csharp {title="Controller/UploadDocument.cs"} [RoutePost ( "/api/upload-document/" )] public async Task UploadDocument ( HttpRequest request ) { var fileName = request.RouteParameters [ "filename" ].GetString (); if (!request.HasContents) { // リクエストにコンテンツがありません return new HttpResponse ( HttpStatusInformation.BadRequest ); } var contentStream = request.GetRequestStream (); var outputFileName = Path.Combine ( AppDomain.CurrentDomain.BaseDirectory, "uploads", fileName ); using (var fs = File.Create ( outputFileName )) { await contentStream.CopyToAsync ( fs ); } return new HttpResponse () { Content = JsonContent.Create ( new { message = "ファイルが正常に送信されました。" } ) }; } ``` 上記の例では、`UploadDocument` メソッドはリクエストコンテンツを読み取り、コンテンツをファイルに保存します。`Stream.CopyToAsync` で使用される読み取りバッファ以外に、追加のメモリ割り当ては行われません。上記の例は、非常に大きなファイルの場合にメモリ割り当ての負担を削減し、アプリケーションのパフォーマンスを最適化するのに役立ちます。 良い実践は、ファイルの送信などの時間のかかる操作では、常に [CancellationToken](https://learn.microsoft.com/pt-br/dotnet/api/system.threading.cancellationtoken) を使用することです。これは、クライアントとサーバーの間のネットワーク速度に依存するためです。 CancellationToken での調整は、以下のように行うことができます: ```csharp {title="Controller/UploadDocument.cs"} // 以下のキャンセルトークンは、30 秒のタイムアウトに達した場合に例外をスローします。 CancellationTokenSource copyCancellation = new CancellationTokenSource ( delay: TimeSpan.FromSeconds ( 30 ) ); try { using (var fs = File.Create ( outputFileName )) { await contentStream.CopyToAsync ( fs, copyCancellation.Token ); } } catch (OperationCanceledException) { return new HttpResponse ( HttpStatusInformation.BadRequest ) { Content = JsonContent.Create ( new { Error = "アップロードが最大アップロード時間 (30 秒) を超えました。" } ) }; } ``` ## レスポンスコンテンツストリーム レスポンスコンテンツを送信することも可能です。現在、[HttpRequest.GetResponseStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetResponseStream.md) メソッドと、[StreamContent](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.streamcontent?view=net-9.0) 型のコンテンツを使用する、2 つの方法があります。 画像ファイルを提供するシナリオを考えてみましょう。以下のコードを使用できます: ```csharp {title="Controller/ImageController.cs"} [RouteGet ( "/api/profile-picture" )] public async Task UploadDocument ( HttpRequest request ) { // プロフィール画像を取得するための例メソッド var profilePictureFilename = "profile-picture.jpg"; byte[] profilePicture = await File.ReadAllBytesAsync ( profilePictureFilename ); return new HttpResponse () { Content = new ByteArrayContent ( profilePicture ), Headers = new () { ContentType = "image/jpeg", ContentDisposition = $"inline; filename={profilePictureFilename}" } }; } ``` 上記のメソッドは、画像コンテンツを読み取るたびにメモリ割り当てを行います。如果画像が大きい場合、これはパフォーマンスの問題を引き起こし、ピーク時にはメモリオーバーロードやサーバークラッシュの原因となる可能性があります。このような状況では、キャッシングは役立ちますが、問題を完全に解決することはできません。キャッシングは、毎回メモリ割り当ての負担を軽減するのに役立ちますが、大きなファイルの場合は十分ではありません。 ストリーミングを使用して画像を送信することが解決策となります。画像の全コンテンツを読み取るのではなく、ファイル上で読み取りストリームを作成し、クライアントに小さなバッファを使用してコピーします。 #### GetResponseStream メソッドを使用した送信 [HttpRequest.GetResponseStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpRequest.GetResponseStream.md) メソッドは、HTTP 応答のコンテンツフローが準備されるにつれて、HTTP 応答のチャンクを送信できるオブジェクトを作成します。このメソッドはより手動で、コンテンツを送信する前に、ステータス、ヘッダー、コンテンツサイズを定義する必要があります。 ```csharp {title="Controller/ImageController.cs"} [RouteGet ( "/api/profile-picture" )] public async Task UploadDocument ( HttpRequest request ) { var profilePictureFilename = "profile-picture.jpg"; // この形式の送信では、ステータスとヘッダーを事前に定義する必要があります。 var requestStreamManager = request.GetResponseStream (); requestStreamManager.SetStatus ( System.Net.HttpStatusCode.OK ); requestStreamManager.SetHeader ( HttpKnownHeaderNames.ContentType, "image/jpeg" ); requestStreamManager.SetHeader ( HttpKnownHeaderNames.ContentDisposition, $"inline; filename={profilePictureFilename}" ); using (var fs = File.OpenRead ( profilePictureFilename )) { // この形式の送信では、コンテンツサイズも事前に定義する必要があります。 requestStreamManager.SetContentLength ( fs.Length ); // コンテンツサイズがわからない場合は、チャンク化されたエンコードを使用してコンテンツを送信できます。 requestStreamManager.SendChunked = true; // その後、出力ストリームに書き込みます。 await fs.CopyToAsync ( requestStreamManager.ResponseStream ); } } ``` #### StreamContent を使用したコンテンツの送信 [StreamContent](https://learn.microsoft.com/pt-br/dotnet/api/system.net.http.streamcontent?view=net-9.0) クラスを使用すると、データソースからバイトストリームとしてコンテンツを送信できます。この形式の送信は、以前の要件を削除し、[圧縮エンコード](https://docs.sisk-framework.org/ja/docs/fundamentals/responses.md#gzip-deflate-and-brotli-compression) を使用してコンテンツサイズを削減することもできます。 ```csharp {title="Controller/ImageController.cs"} [RouteGet ( "/api/profile-picture" )] public HttpResponse UploadDocument ( HttpRequest request ) { var profilePictureFilename = "profile-picture.jpg"; return new HttpResponse () { Content = new StreamContent ( File.OpenRead ( profilePictureFilename ) ), Headers = new () { ContentType = "image/jpeg", ContentDisposition = $"inline; filename=\"{profilePictureFilename}\"" } }; } ``` > [!IMPORTANT] > > この種のコンテンツでは、ストリームを `using` ブロックで囲むことは避けてください。コンテンツフローが終了すると、HTTP サーバーによってコンテンツが自動的に破棄されます。エラーが発生した場合でも同様です。 --- # SiskでCORS(クロスオリジンリソース共有)を有効にする Source: https://docs.sisk-framework.org/ja/docs/features/cors.html Sisk には、サービスを公開する際に [Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS) の処理に役立つツールがあります。この機能は HTTP プロトコルの一部ではなく、W3C が定義した Web ブラウザ固有の機能です。このセキュリティ機構は、ウェブページがそのページを提供したドメインとは異なるドメインへリクエストを送ることを防止します。サービス提供者は特定のドメイン、あるいは単一のドメインに対してリソースへのアクセスを許可できます。 ## 同一オリジン リソースが「同一オリジン」とみなされるためには、リクエストに [Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Origin) ヘッダーが含まれている必要があります: ```http GET /api/users HTTP/1.1 Host: example.com Origin: http://example.com ... ``` そして、リモートサーバーは要求されたオリジンと同じ値を持つ [Access-Control-Allow-Origin](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) ヘッダーで応答しなければなりません: ```http HTTP/1.1 200 OK Access-Control-Allow-Origin: http://example.com ... ``` この検証は **明示的** です。ホスト、ポート、プロトコルは要求されたものと同一である必要があります。例を確認してください: - サーバーが `Access-Control-Allow-Origin` を `https://example.com` と返す場合: - `https://example.net` - ドメインが異なる。 - `http://example.com` - スキームが異なる。 - `http://example.com:5555` - ポートが異なる。 - `https://www.example.com` - ホストが異なる。 仕様では、リクエストとレスポンスの両方のヘッダーに対して構文のみが許容されます。URL のパスは無視されます。ポートはデフォルトポート(HTTP は 80、HTTPS は 443)の場合は省略されます。 ```http Origin: null Origin: :// Origin: ://: ``` ## CORS の有効化 標準で、[ListeningHost](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.md) 内に [CrossOriginResourceSharingHeaders](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.md) オブジェクトが用意されています。 サーバーの初期化時に CORS を設定できます: ```csharp static async Task Main(string[] args) { using var app = HttpServer.CreateBuilder() .UseCors(new CrossOriginResourceSharingHeaders( allowOrigin: "http://example.com", allowHeaders: ["Authorization"], exposeHeaders: ["Content-Type"])) .Build(); await app.StartAsync(); } ``` 上記のコードは **すべてのレスポンス** に対して以下のヘッダーを送信します: ```http HTTP/1.1 200 OK Access-Control-Allow-Origin: http://example.com Access-Control-Allow-Headers: Authorization Access-Control-Expose-Headers: Content-Type ``` これらのヘッダーは、エラーやリダイレクトを含む、ウェブクライアントへのすべてのレスポンスで送信する必要があります。 [CrossOriginResourceSharingHeaders](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.md) クラスには、[AllowOrigin](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigin.md) と [AllowOrigins](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigins.md) の 2 つの類似したプロパティがあることに気付くでしょう。1 つは単数形、もう 1 つは複数形です。 - **AllowOrigin** プロパティは静的です。指定したオリジンだけがすべてのレスポンスで送信されます。 - **AllowOrigins** プロパティは動的です。サーバーはリクエストのオリジンがこのリストに含まれているかを確認し、含まれていればそのオリジンに対するレスポンスで送信されます。 許可されたオリジンがリクエストに依存する場合、つまり **AllowOrigins** や以下で説明する [AutoAllowOrigin](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AutoAllowOrigin.md) の値を使用する場合、Sisk は `Vary: Origin` ヘッダーも送信し、キャッシュが別のオリジン用のレスポンスを再利用しないようにします。 ### ワイルドカードと自動ヘッダー 代わりに、レスポンスのオリジンにワイルドカード (`*`) を使用して、任意のオリジンからのリソースアクセスを許可することもできます。ただし、認証情報(認可ヘッダー)を含むリクエストに対してはこの値は使用できず、この操作は [エラーになります](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS/Errors/CORSNotSupportingCredentials)。 この問題は、[AllowOrigins](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigins.md) プロパティで許可するオリジンを明示的に列挙するか、[AllowOrigin](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigin.md) の値に [AutoAllowOrigin](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AutoAllowOrigin.md) 定数を使用することで回避できます。このマジックプロパティは、リクエストの `Origin` ヘッダーと同じ値で `Access-Control-Allow-Origin` ヘッダーを設定します。 `AllowOrigin` と同様の動作で、送信されたヘッダーに基づいて自動的に応答するために、[AutoFromRequestMethod](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AutoFromRequestMethod.md) と [AutoFromRequestHeaders](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AutoFromRequestHeaders.md) を使用することもできます。 ```csharp using var host = HttpServer.CreateBuilder() .UseCors(new CrossOriginResourceSharingHeaders( // リクエストの Origin ヘッダーに基づいて応答 allowOrigin: CrossOriginResourceSharingHeaders.AutoAllowOrigin, // Access-Control-Request-Method ヘッダーまたはリクエストメソッドに基づいて応答 allowMethods: [CrossOriginResourceSharingHeaders.AutoFromRequestMethod], // Access-Control-Request-Headers ヘッダーまたは送信されたヘッダーに基づいて応答 allowHeaders: [CrossOriginResourceSharingHeaders.AutoFromRequestHeaders], exposeHeaders: [HttpKnownHeaderNames.ContentType, "X-Authenticated-Account-Id"], allowCredentials: true)) .Build(); ``` ## CORS を適用する他の方法 [service providers](https://docs.sisk-framework.org/ja/docs/extensions/service-providers.md) を扱う場合、設定ファイルで定義された値を上書きできます: ```csharp static async Task Main(string[] args) { using var app = HttpServer.CreateBuilder() .UsePortableConfiguration(...) .UseCors(cors => { // 設定ファイルで定義されたオリジンを上書きします // file. cors.AllowOrigin = "http://example.com"; }) .Build(); await app.StartAsync(); } ``` ## 特定ルートでの CORS 無効化 `UseCors` プロパティはルートおよびすべてのルート属性で利用可能で、以下の例のように無効化できます: ```csharp [RoutePrefix("api/widgets")] public class WidgetController : Controller { // GET /api/widgets/colors [RouteGet("/colors", UseCors = false)] public IEnumerable GetWidgets() { return new[] { "Green widget", "Red widget" }; } } ``` ## レスポンス内の値の置き換え ルーターアクション内で値を明示的に置き換えたり削除したりできます: ```csharp [RoutePrefix("api/widgets")] public class WidgetController : Controller { public IEnumerable GetWidgets(HttpRequest request) { // Removes the Access-Control-Allow-Credentials header request.Context.OverrideHeaders.AccessControlAllowCredentials = string.Empty; // Replaces the Access-Control-Allow-Origin request.Context.OverrideHeaders.AccessControlAllowOrigin = "https://contorso.com"; return new[] { "Green widget", "Red widget" }; } } ``` ## プリフライトリクエスト プリフライトリクエストとは、クライアントが実際のリクエストの前に送信する [OPTIONS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/OPTIONS) メソッドのリクエストです。 `OPTIONS` メソッドのリクエストがルートのパスと一致しても、`OPTIONS` 用に明示的にルートが設定されていない場合、Sisk サーバーは `200 OK` と適用可能な CORS ヘッダーで応答し、その後クライアントは実際のリクエストを続行できます。 - ルートが [RouteMethod.Options](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md)(例: `MapOptions`)で明示的にマッピングされている場合、そのルートが自動応答ではなくリクエストを処理します。 - パスに一致するルートに `UseCors = false` が設定されている場合でも、自動応答は送信されますが CORS ヘッダーは含まれません。 > [!NOTE] > Sisk 1.7 以前では、自動プリフライト応答は常に CORS ヘッダーを含んでおり、`UseCors = false` のルートでも同様でした。 ## グローバルに CORS を無効化する これは実現できません。CORS を使用しないようにするには、設定しないでください。 --- # ファイルサーバー Source: https://docs.sisk-framework.org/ja/docs/features/file-server.html Sisk は `Sisk.Http.FileSystem` 名前空間を提供し、静的ファイルの配信、ディレクトリ一覧、ファイル変換のためのツールが含まれています。この機能により、ローカルディレクトリからファイルを配信でき、Range リクエスト(音声/動画ストリーミング)やカスタムファイル処理もサポートします。 ## Serving static files 静的ファイルを配信する最も簡単な方法は [Router.MapFileSystem](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MapFileSystem.md) です。このメソッドは URL プレフィックスをディスク上のディレクトリにマッピングします。 ```cs using Sisk.Core.Http; using Sisk.Core.Http.FileSystem; // サーバーのルートを現在のディレクトリにマッピングします mainRouter.MapFileSystem("/", Directory.GetCurrentDirectory()); // /assets を "public/assets" フォルダーにマッピングします mainRouter.MapFileSystem( "/assets", Path.Combine(Directory.GetCurrentDirectory(), "public", "assets")); ``` リクエストがルートプレフィックスに一致すると、`HttpFileServerHandler` は指定されたディレクトリ内のファイルを探します。見つかればそのファイルを配信し、見つからなければ 404 応答(アクセスが拒否された場合は 403)を返します。 `HttpFileServer.CreateServingRoute` は `Route` オブジェクトを明示的に作成したいときにまだ利用可能ですが、`MapFileSystem` がアプリケーションコードにとって最も直接的なオプションです。 両方のメソッドは [PrefixRoute](https://docs.sisk-framework.org/api/Sisk.Core.Routing.PrefixRoute.md)(Sisk 1.7 以前は正規表現ルート)を作成し、これは [dynamic route](https://docs.sisk-framework.org/ja/docs/fundamentals/routing.md#matching-precedence) です。静的ルートは常にそれより先にテストされるため、`/` にマッピングされたファイルサーバーは他のルートが処理しないパスのフォールバックとして機能します。ファイルサーバールートは [HttpServerConfiguration.ForceTrailingSlash](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ForceTrailingSlash.md) の影響を受けません。 ## HttpFileServerHandler ファイルの配信方法をより細かく制御したい場合は、`HttpFileServerHandler` を手動でインスタンス化して設定できます。 ```cs var fileHandler = new HttpFileServerHandler("/var/www/html"); // ディレクトリ一覧を有効にします(デフォルトでは無効) fileHandler.AllowDirectoryListing = true; // カスタムルートプレフィックスを設定します(リクエストパスからこの部分が除去されます) fileHandler.RoutePrefix = "/public"; // /public 以下にハンドラを登録します mainRouter.MapFileSystem("/public", fileHandler); ``` ### Configuration | プロパティ | 説明 | |---|---| | `RootDirectoryPath` | ファイルが提供されるルートディレクトリへの絶対パスまたは相対パス。 | | `RoutePrefix` | ファイル解決時にリクエストパスから除去されるルートプレフィックス。デフォルトは `/`。 | | `AllowDirectoryListing` | `true` に設定すると、ディレクトリが要求され、インデックスファイルが見つからない場合にディレクトリ一覧を有効にします。デフォルトは `false`。 | | `FileConverters` | 提供前にファイルを変換するために使用される `HttpFileServerFileConverter` のリスト。 | ## Directory Listing `AllowDirectoryListing` が有効で、ユーザーがディレクトリパスを要求した場合、Sisk はそのディレクトリの内容を一覧表示する HTML ページを生成します。 ディレクトリ一覧には以下が含まれます: - 親ディレクトリへのナビゲーション (`..`)。 - サブディレクトリの一覧。 - サイズと最終更新日を含むファイルの一覧。 ## File Converters ファイルコンバータを使用すると、特定のファイルタイプをインターセプトして別の方法で処理できます。たとえば、画像をトランスコードしたり、ファイルをオンザフライで圧縮したり、部分コンテンツ(Range リクエスト)でファイルを配信したりできます。 Sisk にはメディアストリーミング用の組み込みコンバータが 2 つ含まれています: - `HttpFileAudioConverter`: `.mp3`, `.ogg`, `.wav`, `.flac`, `.ogv` を処理します。 - `HttpFileVideoConverter`: `.webm`, `.avi`, `.mkv`, `.mpg`, `.mpeg`, `.wmv`, `.mov`, `.mp4` を処理します。 これらのコンバータは **HTTP Range Requests** をサポートし、クライアントが音声や動画ファイルをシークできるようにします。 ### Creating a custom converter カスタムファイルコンバータを作成するには、`HttpFileServerFileConverter` を継承し、`CanConvert` と `Convert` を実装します。 ```cs using Sisk.Core.Http; using Sisk.Core.Http.FileSystem; public class MyTextConverter : HttpFileServerFileConverter { public override bool CanConvert(FileInfo file) { // .txt ファイルにのみ適用 return file.Extension.Equals(".txt", StringComparison.OrdinalIgnoreCase); } public override HttpResponse Convert(FileInfo file, HttpRequest request) { string content = File.ReadAllText(file.FullName); // すべてのテキストコンテンツを大文字に変換 return new HttpResponse(200) { Content = new StringContent(content.ToUpper()) }; } } ``` 次に、ハンドラに追加します: ```cs var handler = new HttpFileServerHandler("./files"); handler.FileConverters.Add(new MyTextConverter()); ``` --- # モデルコンテキストプロトコル Source: https://docs.sisk-framework.org/ja/docs/extensions/mcp.html 大規模言語モデル(LLM)を使用してエージェントモデルにコンテキストを提供するアプリケーションを、[Sisk.ModelContextProtocol](https://www.nuget.org/packages/Sisk.ModelContextProtocol/) パッケージで構築することができます: dotnet add package Sisk.ModelContextProtocol --prerelease このパッケージは、[Streamable HTTP](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http) 上で動作し、単一のリクエスト/レスポンスメッセージをサポートする MCP サーバーを構築するための便利なクラスとメソッドを提供します。バージョン 1.7.0-rc1 以降、プロトコルバージョン `2026-07-28` のステートレスリクエストをサポートし、`2025-11-25`、`2025-06-18`、`2025-03-26` バージョンを使用するクライアント向けにレガシーな初期化フローを保持しています。ツールとスキルもサポートします。 > [!NOTE] > > 開始する前に、このパッケージは開発中であり、仕様に準拠しない動作を示す可能性があることに注意してください。開発中の内容やまだ動作しないものについては、[パッケージの詳細](https://github.com/sisk-http/core/tree/main/extensions/Sisk.ModelContextProtocol) をご覧ください。 ## MCP の開始方法 [McpProvider](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpProvider.md) クラスは MCP サーバーを定義するためのエントリーポイントです。これはシールドされたプロバイダーオブジェクトで、起動時に構成できます。Sisk アプリケーションは 1 つまたは複数の 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}")); })); ``` アプリケーションが 1 つの MCP プロバイダーだけを提供する場合は、ビルダーのシングルトンを使用できます: ```csharp static void Main(string[] args) { using var host = HttpServer.CreateBuilder() .UseMcp(mcp => { mcp.ServerName = "math-server"; mcp.ServerTitle = "Mathematics server"; mcp.Tools.Add(new McpTool( name: "math_sum", description: "Sums one or more numbers.", schema: JsonSchema.CreateObjectSchema( properties: new Dictionary() { { "numbers", JsonSchema.CreateArraySchema( itemsSchema: JsonSchema.CreateNumberSchema(), minItems: 1, description: "The numbers to sum.") } }, requiredProperties: ["numbers"]), executionHandler: async (McpToolContext context) => { var numbers = context.Arguments["numbers"].GetJsonArray().ToArray(); var sum = numbers.Sum(); return await Task.FromResult(McpToolResult.CreateText($"Sum result: {sum:N4}")); })); }) .UseRouter(router => { router.MapAny("/mcp", async (HttpRequest req) => { return await req.HandleMcpRequestAsync(); }); }) .Build(); host.Start(); } ``` エンドポイントは `GET` と `POST` の両方のリクエストを受け付ける必要があるため、`MapAny` が最もシンプルなルートマッピングです。`HandleMcpRequestAsync` は [HttpResponse](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpResponse.md) を返し、ルートはそれを返す必要があります。アプリ内で複数のプロバイダーが必要な場合は、シングルトンを使用せず、各ルートから直接 [McpProvider.HandleRequestAsync](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpProvider.HandleRequestAsync.md) を呼び出してください: ```csharp var mathProvider = new McpProvider("math-server", "Mathematics server", new Version(1, 0)); router.MapAny("/mcp/math", async request => { return await mathProvider.HandleRequestAsync(request); }); ``` ## 関数用 JSON スキーマの作成 [Sisk.ModelContextProtocol] ライブラリは JSON と JSON スキーマ操作のために [LightJson](https://github.com/CypherPotato/LightJson) のフォークを使用しています。この実装はさまざまなオブジェクト向けに流暢な JSON Schema ビルダーを提供します: - JsonSchema.CreateObjectSchema - JsonSchema.CreateArraySchema - JsonSchema.CreateBooleanSchema - JsonSchema.CreateNumberSchema - JsonSchema.CreateStringSchema - JsonSchema.Empty 例: ```csharp JsonSchema.CreateObjectSchema( properties: new Dictionary() { { "numbers", JsonSchema.CreateArraySchema( itemsSchema: JsonSchema.CreateNumberSchema(), minItems: 1, description: "The numbers to sum.") } }, requiredProperties: ["numbers"]); ``` 以下のスキーマが生成されます: ```json { "type": "object", "properties": { "numbers": { "type": "array", "items": { "type": "number" }, "minItems": 1, "description": "The numbers to sum." } }, "required": ["numbers"] } ``` ## 関数呼び出しの処理 [McpTool](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpTool.md) の `executionHandler` パラメーターで定義された関数は、呼び出し引数を含む JsonObject を提供し、流暢に読み取ることができます: ```csharp mcp.Tools.Add(new McpTool( name: "browser_do_action", description: "Run an browser action, such as scrolling, refreshing or navigating.", schema: JsonSchema.CreateObjectSchema( properties: new Dictionary() { { "action_name", JsonSchema.CreateStringSchema( enums: ["go_back", "refresh", "scroll_bottom", "scroll_top"], description: "The action name.") }, { "action_data", JsonSchema.CreateStringSchema( description: "Action parameter." ) } }, requiredProperties: ["action_name"]), executionHandler: async (McpToolContext context) => { // アクション名を読み取ります。null または明示的な文字列でない場合は例外がスローされます string actionName = context.Arguments["action_name"].GetString(); // action_data は必須ではないため、ここでは null になる可能性があります string? actionData = context.Arguments["action_data"].MaybeNull()?.GetString(); // actionName に基づいてブラウザアクションを処理します return await Task.FromResult( McpToolResult.CreateText($"Performed browser action: {actionName}")); })); ``` ツールの引数はハンドラが実行される前にスキーマに対して検証されます。検証に失敗した場合、プロバイダーはエラー結果を MCP クライアントに返し、ツールハンドラは呼び出されません。 ## 関数の結果 [McpToolResult](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpToolResult.md) オブジェクトは、ツールレスポンス用のコンテンツを作成するための 3 つのメソッドを提供します: - [CreateAudio(ReadOnlySpan, string)](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpToolResult.CreateAudio.md): MCP クライアント向けの音声ベースのレスポンスを作成します。 - [CreateImage(ReadOnlySpan, string)](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpToolResult.CreateImage.md): MCP クライアント向けの画像ベースのレスポンスを作成します。 - [CreateText(string)](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpToolResult.CreateText.md): MCP クライアント向けのテキストベースのレスポンス(デフォルト)を作成します。 さらに、複数の異なるコンテンツを単一の JSON ツールレスポンスに結合することが可能です: ```csharp mcp.Tools.Add(new McpTool( ... executionHandler: async (McpToolContext context) => { // 実際の作業をシミュレート byte[] browserScreenshot = await browser.ScreenshotAsync(); return McpToolResult.Combine( McpToolResult.CreateText("Heres the screenshot of the browser:"), McpToolResult.CreateImage(browserScreenshot, "image/png") ) })); ``` プロバイダーは現在、`server/discover`、レガシーな `initialize` と `ping` メソッド、`tools/list`、`tools/call`、`skills/list`、`skills/get`、`resources/list`、`resources/read`、および通知を処理します。サポートされていない JSON-RPC メソッドは JSON-RPC エラーレスポンスを返します。 ## スキルの公開 MCP プロバイダーは `io.modelcontextprotocol/skills` 拡張を通じて [Agent Skills](https://github.com/modelcontextprotocol/ext-skills) を公開することもできます。プロバイダーがリクエストの処理を開始する前に、[McpSkill](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpSkill.md) オブジェクトを [McpProvider.Skills](https://docs.sisk-framework.org/api/Sisk.ModelContextProtocol.McpProvider.Skills.md) に追加します: ```csharp mcp.Skills.Add(new McpSkill( path: "acme/refunds", frontmatter: new JsonObject { ["name"] = "refunds", ["description"] = "Follow the refund workflow." }, body: "# Refunds\nRead references/policy.md before using the refund tools.", files: new Dictionary { ["references/policy.md"] = File.ReadAllBytes("skills/refunds/references/policy.md") })); ``` `McpSkill` は frontmatter と Markdown 本文から `SKILL.md` ファイルを生成します。`path` の最後のセグメントは frontmatter の `name` フィールドと一致しなければなりません。スキルファイルはスキル作成時にコピーされ、プロバイダーによってディスクから読み取られることはありません。クライアントは `skills/list`、`skills/get`、`resources/read` を使用してスキルを一覧取得・読み取りし、`2026-07-28` プロトコルバージョンを使用します。制限や検証ルールについては、[パッケージの詳細](https://github.com/sisk-http/core/tree/main/extensions/Sisk.ModelContextProtocol) を参照してください。 ## 今後の作業 Model Context Protocol はエージェントモデルとそれらにコンテンツを提供するアプリケーション間の通信プロトコルです。新しいプロトコルであるため、仕様は非推奨項目や新機能、破壊的変更などで常に更新されるのが一般的です。 エージェントアプリケーションの構築を始める前に、[Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro) が解決する課題を理解することが重要です。 また、[Sisk.ModelContextProtocol](https://github.com/sisk-http/core/tree/main/extensions/Sisk.ModelContextProtocol) パッケージの仕様を読んで、進捗、ステータス、そして何ができるかを把握してください。 --- # JSON-RPC 拡張 Source: https://docs.sisk-framework.org/ja/docs/extensions/json-rpc.html Sisk には [JSON-RPC 2.0](https://www.jsonrpc.org/specification) API 用の実験的モジュールがあり、さらにシンプルなアプリケーションを作成できます。この拡張は JSON-RPC 2.0 のトランスポートインターフェースを厳密に実装し、HTTP GET、POST リクエストおよび Sisk の WebSocket によるトランスポートを提供します。 以下のコマンドで NuGet から拡張機能をインストールできます。実験的/ベータ版の場合は、Visual Studio でプレリリース パッケージを検索するオプションを有効にしてください。 ```bash dotnet add package Sisk.JsonRpc ``` ## トランスポートインターフェース JSON-RPC はステートレスで非同期のリモート手続き呼び出し (RPC) プロトコルで、データ通信に JSON を使用します。JSON-RPC のリクエストは通常 ID で識別され、レスポンスはリクエストで送信された同じ ID で返されます。すべてのリクエストがレスポンスを必要とするわけではなく、そういったものは「通知」と呼ばれます。 [JSON-RPC 2.0 仕様](https://www.jsonrpc.org/specification) はトランスポートの動作を詳細に説明しています。このトランスポートは使用場所に依存しません。Sisk は HTTP を介してこのプロトコルを実装し、[JSON-RPC over HTTP](https://www.jsonrpc.org/historical/json-rpc-over-http.html) の規格に従います。GET リクエストは部分的にサポートされ、POST リクエストは完全にサポートされます。WebSocket もサポートされ、非同期メッセージ通信を提供します。 JSON-RPC のリクエストは次のようになります: ```json { "jsonrpc": "2.0", "method": "Sum", "params": [1, 2, 4], "id": 1 } ``` 成功したレスポンスは次のようになります: ```json { "jsonrpc": "2.0", "result": 7, "id": 1 } ``` ## JSON-RPC メソッド 以下の例は Sisk を使用して JSON-RPC API を作成する方法を示しています。数学演算クラスがリモート操作を実行し、シリアライズされたレスポンスをクライアントに返します。 ```csharp {title="Program.cs"} using var app = HttpServer.CreateBuilder(port: 5555) .UseJsonRPC((sender, args) => { // WebMethod 属性が付与されたすべてのメソッドを JSON-RPC ハンドラに追加します args.Handler.Methods.AddMethodsFromType(new MathOperations()); // /service ルートをマッピングし、JSON-RPC の POST と GET リクエストを処理します args.Router.MapPost("/service", args.Handler.Transport.HttpPost); args.Router.MapGet("/service", args.Handler.Transport.HttpGet); // GET /ws で JSON-RPC WebSocket トランスポートをマッピングします args.Router.MapGet("/ws", args.Handler.Transport.WebSocket); }) .Build(); await app.StartAsync(); ``` ```csharp {title="MathOperations.cs"} public class MathOperations { [WebMethod] public float Sum(float a, float b) { return a + b; } [WebMethod] public double Sqrt(float a) { return Math.Sqrt(a); } } ``` 上記の例では `Sum` と `Sqrt` メソッドが JSON-RPC ハンドラにマッピングされ、`GET /service`、`POST /service`、`GET /ws` で利用可能になります。メソッド名は大文字小文字を区別しません。 メソッドパラメータは自動的にそれぞれの型へデシリアライズされます。名前付きパラメータを使用したリクエストもサポートされています。JSON のシリアライズは LightJson ライブラリが行います。型が正しくデシリアライズされない場合は、その型用の JSON コンバータを作成し、[JsonRpcHandler.JsonSerializerOptions](https://docs.sisk-framework.org/api/Sisk.JsonRPC.JsonRpcHandler.JsonSerializerOptions.md) に関連付けることができます。 メソッド内で JSON-RPC リクエストの `$.params` 生オブジェクトを直接取得することもできます。 ```csharp {title="MathOperations.cs"} [WebMethod] public float Sum(JsonArray|JsonObject @params) { ... } ``` これを行うには、`@params` がメソッドの **唯一** のパラメータであり、名前が正確に `params` である必要があります(C# ではこのパラメータ名をエスケープするために `@` が必要です)。 パラメータのデシリアライズは、名前付きオブジェクトでも位置指定配列でも行われます。例えば、以下のメソッドは両方のリクエストでリモート呼び出しできます。 ```csharp [WebMethod] public float AddUserToStore(string apiKey, User user, UserStore store) { ... } ``` 配列の場合、パラメータの順序を守る必要があります。 ```json { "jsonrpc": "2.0", "method": "AddUserToStore", "params": [ "1234567890", { "name": "John Doe", "email": "john@example.com" }, { "name": "My Store" } ], "id": 1 } ``` ## シリアライザのカスタマイズ JSON シリアライザは [JsonRpcHandler.JsonSerializerOptions](https://docs.sisk-framework.org/api/Sisk.JsonRPC.JsonRpcHandler.JsonSerializerOptions.md) プロパティでカスタマイズできます。このプロパティでは、メッセージのデシリアライズに JSON5 の使用を有効にできます。JSON-RPC 2.0 の規格ではありませんが、JSON5 は JSON の拡張で、より人間に読みやすく書きやすくなります。 ```csharp {title="Program.cs"} using var host = HttpServer.CreateBuilder ( 5556 ) .UseJsonRPC ( ( o, e ) => { // サニタイズされた名前比較子を使用します。この比較子は名前中の文字と数字のみを比較し、他の記号は無視します。例: // foo_bar10 == FooBar10 e.Handler.JsonSerializerOptions.PropertyNameComparer = new JsonSanitizedComparer (); // JSON インタプリタで JSON5 を有効にします。これを有効にしても、通常の JSON は引き続き使用可能です e.Handler.JsonSerializerOptions.SerializationFlags = LightJson.Serialization.JsonSerializationFlags.Json5; // POST /service ルートを JSON RPC ハンドラにマッピングします e.Router.MapPost ( "/service", e.Handler.Transport.HttpPost ); } ) .Build (); host.Start (); ``` --- # サービスプロバイダー Source: https://docs.sisk-framework.org/ja/docs/extensions/service-providers.html Service Providers は、ポータブルな構成ファイルを使用して Sisk アプリケーションをさまざまな環境へ移植する方法です。この機能により、サーバーのポートやパラメーター、その他のオプションを、環境ごとにアプリケーションコードを変更することなく変更できます。このモジュールは Sisk の構成構文に依存しており、`UsePortableConfiguration` メソッドを通じて構成できます。 構成プロバイダーは `IConfigurationProvider` で実装され、構成リーダーを提供し、任意の実装を受け取ることができます。デフォルトでは Sisk は JSON 構成リーダーを提供しますが、INI ファイル用のパッケージもあります。独自の構成プロバイダーを作成し、次のように登録することもできます。 ```csharp using var app = HttpServer.CreateBuilder() .UsePortableConfiguration(config => { config.WithConfigReader(); }) .Build(); ``` 前述のとおり、デフォルトのプロバイダーは JSON ファイルです。既定で検索されるファイル名は `service-config.json` で、実行プロセスのカレントディレクトリが検索対象となり、実行ファイルのディレクトリは対象外です。 ファイル名や Sisk が構成ファイルを探す場所を変更したい場合は、次のように指定できます。 ```csharp using Sisk.Core.Http; using Sisk.Core.Http.Hosting; using var app = HttpServer.CreateBuilder() .UsePortableConfiguration(config => { config.WithConfigFile("config.toml", createIfDontExists: true, lookupDirectories: ConfigurationFileLookupDirectory.CurrentDirectory | ConfigurationFileLookupDirectory.AppDirectory); }) .Build(); ``` 上記のコードは、実行プロセスのカレントディレクトリで `config.toml` を探します。見つからない場合は、実行ファイルが置かれているディレクトリを検索します。ファイルが存在しない場合、`createIfDontExists` パラメーターが尊重され、最後にテストされたパス(`lookupDirectories` に基づく)に内容のないファイルが作成され、コンソールにエラーが出力されてアプリケーションの初期化が中止されます。 > [!TIP] > > INI 構成リーダーと JSON 構成リーダーのソースコードを確認すると、`IConfigurationProvider` がどのように実装されているかが分かります。 ## JSON ファイルからの構成の読み取り デフォルトで、Sisk は JSON ファイルから構成を読み取る構成プロバイダーを提供します。このファイルは固定の構造に従い、以下のパラメーターで構成されます。 ```json { "Server": { "DefaultEncoding": "UTF-8", "ThrowExceptions": true, "IncludeRequestIdHeader": true }, "ListeningHost": { "Label": "My sisk application", "Ports": [ "http://localhost:80/", "https://localhost:443/", // 設定ファイルはコメントもサポートします ], "CrossOriginResourceSharingPolicy": { "AllowOrigin": "*", "AllowOrigins": [ "*" ], // 0.14 で新規追加 "AllowMethods": [ "*" ], "AllowHeaders": [ "*" ], "MaxAge": 3600 }, "Parameters": { "MySqlConnection": "server=localhost;user=root;" } } } ``` 構成ファイルから作成されたパラメーターは、サーバーコンストラクター内で次のように取得できます。 ```csharp using var app = HttpServer.CreateBuilder() .UsePortableConfiguration(config => { config.WithParameters(paramCollection => { string databaseConnection = paramCollection.GetValueOrThrow("MySqlConnection"); }); }) .Build(); ``` 各構成リーダーはサーバー初期化パラメーターを読み取る手段を提供します。一部のプロパティは、機密性の高い API データや API キーなど、構成ファイルではなくプロセス環境に設定すべきであることが示されています。 ## 構成ファイルの構造 JSON 構成ファイルは以下のプロパティで構成されます。
    プロパティ 必須 説明
    Server 必須 サーバー本体とその設定を表します。
    Server.AccessLogsStream 任意 デフォルトは console。アクセスログの出力先ストリームを指定します。ファイル名、null、または console が使用可能です。
    Server.ErrorsLogsStream 任意 デフォルトは null。エラーログの出力先ストリームを指定します。ファイル名、null、または console が使用可能です。
    Server.MaximumContentLength 任意 デフォルトは 0(無制限)。バイト単位で最大コンテンツ長を指定します。Server オブジェクトが存在しこのプロパティが省略された場合、値は 0(無制限)となり、HttpServerConfiguration.MaximumContentLength の 100 MB デフォルトを上書きします。
    Server.IncludeRequestIdHeader 任意 デフォルトは false。HTTP サーバーが X-Request-Id ヘッダーを送信するかどうかを指定します。
    Server.ThrowExceptions 任意 デフォルトは true。未処理例外をスローするかどうかを指定します。本番環境では false、デバッグ時は true に設定してください。
    ListeningHost 必須 サーバーのリスニングホストを表します。
    ListeningHost.Label 任意 アプリケーションのラベルを表します。
    ListeningHost.Ports 必須 文字列の配列で、ListeningPort の構文に一致します。
    ListeningHost.CrossOriginResourceSharingPolicy 任意 アプリケーションの CORS ヘッダーを設定します。
    ListeningHost.CrossOriginResourceSharingPolicy.AllowCredentials 任意 デフォルトは false。Allow-Credentials ヘッダーを指定します。
    ListeningHost.CrossOriginResourceSharingPolicy.ExposeHeaders 任意 デフォルトは null。文字列配列を期待し、Expose-Headers ヘッダーを指定します。
    ListeningHost.CrossOriginResourceSharingPolicy.AllowOrigin 任意 デフォルトは null。文字列を期待し、Allow-Origin ヘッダーを指定します。
    ListeningHost.CrossOriginResourceSharingPolicy.AllowOrigins 任意 デフォルトは null。文字列配列を期待し、複数の Allow-Origin ヘッダーを指定します。詳細は AllowOrigins を参照してください。
    ListeningHost.CrossOriginResourceSharingPolicy.AllowMethods 任意 デフォルトは null。文字列配列を期待し、Allow-Methods ヘッダーを指定します。
    ListeningHost.CrossOriginResourceSharingPolicy.AllowHeaders 任意 デフォルトは null。文字列配列を期待し、Allow-Headers ヘッダーを指定します。
    ListeningHost.CrossOriginResourceSharingPolicy.MaxAge 任意 デフォルトは null。整数を期待し、秒単位で Max-Age ヘッダーを指定します。
    ListeningHost.Parameters 任意 アプリケーションのセットアップメソッドに提供されるプロパティを指定します。
    --- # INI 設定プロバイダー Source: https://docs.sisk-framework.org/ja/docs/extensions/ini-configuration.html Sisk には JSON 以外の起動設定を取得する方法があります。実際、[IConfigurationReader](https://docs.sisk-framework.org/api/Sisk.Core.Http.Hosting.IConfigurationReader.md) を実装したパイプラインであれば、[PortableConfigurationBuilder.WithConfigurationPipeline](https://docs.sisk-framework.org/api/Sisk.Core.Http.Hosting.PortableConfigurationBuilder.md) と組み合わせて使用でき、任意のファイル形式からサーバー設定を読み取ることができます。 [Sisk.IniConfiguration](https://www.nuget.org/packages/Sisk.IniConfiguration/) パッケージは、一般的な構文エラーで例外をスローせず、シンプルな設定構文を持つストリームベースの INI ファイルリーダーを提供します。このパッケージは Sisk フレームワークの外部でも使用でき、効率的な INI ドキュメントリーダーが必要なプロジェクトに柔軟性を提供します。 ## インストール パッケージをインストールするには、次のコマンドから始めます。 ```bash $ dotnet add package Sisk.IniConfiguration ``` コアパッケージもインストールできます。このパッケージは INI の [IConfigurationReader](https://docs.sisk-framework.org/api/Sisk.Core.Http.Hosting.IConfigurationReader) や Sisk の依存関係を含まず、INI シリアライザーだけが含まれます: ```bash $ dotnet add package Sisk.IniConfiguration.Core ``` メインパッケージを使用すると、以下の例のようにコード内で利用できます。 ```cs class Program { static HttpServerHostContext Host = null!; static void Main(string[] args) { Host = HttpServer.CreateBuilder() .UsePortableConfiguration(config => { config.WithConfigFile("app.ini", createIfDontExists: true); // IniConfigurationReader 設定リーダーを使用します config.WithConfigurationPipeline(); }) .UseRouter(r => { r.MapGet("/", SayHello); }) .Build(); Host.Start(); } static HttpResponse SayHello(HttpRequest request) { string? name = Host.Parameters["name"] ?? "world"; return new HttpResponse($"Hello, {name}!"); } } ``` 上記のコードは、プロセスのカレントディレクトリ (CurrentDirectory) にある app.ini ファイルを探します。INI ファイルの例は以下の通りです: ```ini [Server] # 複数のリッスンアドレスをサポートしています Listen = http://localhost:5552/ Listen = http://localhost:5553/ ThrowExceptions = false AccessLogsStream = console [Cors] AllowMethods = GET, POST AllowHeaders = Content-Type, Authorization AllowOrigin = * [Parameters] Name = "Kanye West" ``` ## INI の方言と構文 現在の実装の方針: - プロパティ名とセクション名は **大文字小文字を区別しません**。 - プロパティ名と値は **トリム** されます(値が引用符で囲まれている場合を除く)。 - 値はシングルクオートまたはダブルクオートで囲むことができます。クオート内に改行を含めることも可能です。 - コメントは `#` と `;` で記述できます。また、**行末コメントも許可されています**。 - プロパティは複数の値を持つことができます。 詳細については、Sisk で使用されている INI パーサーの「方針」ドキュメントが [このドキュメント](https://github.com/sisk-http/archive/blob/master/ext/ini-reader-syntax.md) にあります。 以下の INI コードを例として使用します: ```ini One = 1 Value = this is an value Another value = "this value has an line break on it" ; 以下のコードは色を示しています [some section] Color = Red Color = Blue Color = Yellow ; yellow は使用しないでください ``` 次のように解析します: ```csharp // 文字列から INI テキストを解析する IniDocument doc = IniDocument.FromString(iniText); // 1つの値を取得する string? one = doc.Global.GetOne("one"); string? anotherValue = doc.Global.GetOne("another value"); // 複数の値を取得する string[]? colors = doc.GetSection("some section")?.GetMany("color"); ``` ## 設定パラメータ | セクションと名前 | 複数値を許可 | 説明 | | ---------------- | ------------ | ---- | | `Server.Listen` | はい | サーバーがリッスンするアドレス/ポート。 | | `Server.Encoding` | いいえ | サーバーのデフォルトエンコーディング。 | | `Server.MaximumContentLength` | いいえ | サーバーの最大コンテンツ長サイズ(バイト単位)。省略した場合、サーバーのデフォルト(Sisk 1.7 以降は 100 MB)が使用されます。制限なしにするには `0` を使用してください。 | | `Server.IncludeRequestIdHeader` | いいえ | HTTP サーバーが X-Request-Id ヘッダーを送信すべきかを指定します。 | | `Server.ThrowExceptions` | いいえ | 未処理例外をスローすべきかを指定します。 | | `Server.AccessLogsStream` | いいえ | アクセスログの出力ストリームを指定します。 | | `Server.ErrorsLogsStream` | いいえ | エラーログの出力ストリームを指定します。 | | `Cors.AllowMethods` | いいえ | CORS の Allow-Methods ヘッダー値を指定します。 | | `Cors.AllowHeaders` | いいえ | CORS の Allow-Headers ヘッダー値を指定します。 | | `Cors.AllowOrigins` | いいえ | カンマ区切りで複数の Allow-Origin ヘッダーを指定します。詳細は [AllowOrigins](https://docs.sisk-framework.org/api/Sisk.Core.Entity.CrossOriginResourceSharingHeaders.AllowOrigins.md) を参照してください。 | | `Cors.AllowOrigin` | いいえ | 単一の Allow-Origin ヘッダーを指定します。 | | `Cors.ExposeHeaders` | いいえ | CORS の Expose-Headers ヘッダー値を指定します。 | | `Cors.AllowCredentials` | いいえ | CORS の Allow-Credentials ヘッダー値を指定します。 | | `Cors.MaxAge` | いいえ | CORS の Max-Age ヘッダー値を指定します。 | --- # API ドキュメント Source: https://docs.sisk-framework.org/ja/docs/extensions/api-documentation.html `Sisk.Documenting` 拡張機能を使用すると、Sisk アプリケーションの API ドキュメントを自動的に生成できます。コード構造と属性を利用して包括的なドキュメントサイトを作成し、Open API(Swagger)形式へのエクスポートをサポートします。 > [!WARNING] > This package is published as a prerelease (1.7.0-rc3). Its behavior and API may be subject to change in future updates. NuGet から [Sisk.Documenting](https://www.nuget.org/packages/Sisk.Documenting/) パッケージをインストールします。ドキュメントを HTML ページとして提供するには、[Sisk.Documenting.Html](https://www.nuget.org/packages/Sisk.Documenting.Html/) もインストールしてください: ```sh dotnet add package Sisk.Documenting --prerelease dotnet add package Sisk.Documenting.Html --prerelease ``` ソースコードは [こちら](https://github.com/sisk-http/core/tree/main/extensions/Sisk.Documenting) で利用可能です。 `Sisk.Documenting` を使用するには、アプリケーションビルダーに登録し、ルートハンドラにドキュメント属性を付与する必要があります。 ### ドキュメント生成の登録 `HttpServerHostContextBuilder` の `UseApiDocumentation` 拡張メソッドを使用して、アプリケーションを提供する同じルーターから生成された API ドキュメントを公開します。 ```csharp using Sisk.Documenting; using Sisk.Documenting.Exporters; // ... host.UseApiDocumentation( context: new ApiGenerationContext() { ApplicationName = "My Application", ApplicationDescription = "Description of my application.", ApplicationVersion = "1.0.0" }, routerPath: "/api/docs", exporter: new OpenApiExporter() { ServerUrls = ["http://localhost:5555/"] }); ``` - **context**: アプリケーションの名前、説明、バージョンなどのメタデータを定義します。 - **routerPath**: ドキュメントのユーザーインターフェイス(または JSON)がアクセス可能になる URL パスです。 - **exporter**: ドキュメントのエクスポート方法を設定します。`OpenApiExporter` は Open API(Swagger)サポートを有効にします。 ### エンドポイントのドキュメント化 ルートハンドラメソッドに `[ApiEndpoint]` と `[ApiQueryParameter]` 属性を付与して、エンドポイントを記述できます。 ### `ApiEndpoint` `[ApiEndpoint]` 属性を使用すると、エンドポイントの説明を提供できます。 ```csharp [ApiEndpoint(Description = "Returns a greeting message.")] public HttpResponse Index(HttpRequest request) { ... } ``` ### `ApiQueryParameter` `[ApiQueryParameter]` 属性は、エンドポイントが受け取るクエリ文字列パラメータを文書化します。 ```csharp [ApiQueryParameter(name: "name", IsRequired = false, Description = "The name of the person to greet.", Type = "string")] public HttpResponse Index(HttpRequest request) { ... } ``` - **name**: クエリパラメータの名前です。 - **IsRequired**: パラメータが必須かどうかを指定します。 - **Description**: パラメータの人間が読める説明です。 - **Type**: 期待されるデータ型(例: "string", "int")です。 ### `ApiEndpoint` エンドポイントに一般的な情報を付与します。 * **Name** (string, required in constructor): API エンドポイントの名前です。 * **Description** (string): エンドポイントが何をするかの簡潔な説明です。 * **Group** (string): エンドポイントをグループ化するために使用します(例: コントローラやモジュール単位)。 * **InheritDescriptionFromXmlDocumentation** (bool, default: `true`): `true` の場合、`Description` が設定されていないときにメソッドの XML ドキュメントのサマリーを使用しようとします。 ### `ApiHeader` エンドポイントが期待または使用する特定の HTTP ヘッダーを文書化します。 * **HeaderName** (string, required in constructor): ヘッダーのキー(例: "Authorization")です。 * **Description** (string): ヘッダーの目的を説明します。 * **IsRequired** (bool): リクエストにおいてヘッダーが必須かどうかを示します。 ### `ApiParameter` エンドポイントの汎用パラメータを定義します。主にフォームフィールドや他の属性でカバーされないボディパラメータに使用されます。 * **Name** (string, required in constructor): パラメータの名前です。 * **TypeName** (string, required in constructor): パラメータのデータ型(例: "string", "int")です。 * **Description** (string): パラメータの説明です。 * **IsRequired** (bool): パラメータが必須かどうかを示します。 ### `ApiParameterExample` 特定の状況で文書化されたパラメータの例示値を追加します。この属性は同一メソッドに複数回適用できます。 * **ParameterName** (string, required in constructor): 例を適用するパラメータの名前です。 * **Title** (string, required in constructor): 例が示す状況を識別するタイトルです。 * **Description** (string): 例が適用される時期や理由です。 * **Example** (string): 例示値です。 * **ExampleLanguage** (string): 例示値のフォーマットに使用する言語(該当する場合)です。 ```csharp [ApiQueryParameter("status", Type = "string", Description = "Filters orders by status.")] [ApiParameterExample("status", "Pending orders", Example = "pending")] [ApiParameterExample("status", "Shipped orders", Example = "shipped", Description = "Orders that left the warehouse.")] ``` ### `ApiParametersFrom` 指定されたクラスまたは型のプロパティからパラメータのドキュメントを自動生成します。 * **Type** (Type, required in constructor): プロパティを反映させるクラスの `Type` です。 ### `ApiPathParameter` パス変数(例: `/users/{id}`)を文書化します。 * **Name** (string, required in constructor): パスパラメータの名前です。 * **Description** (string): パラメータが何を表すかを説明します。 * **Type** (string): 期待されるデータ型です。 ### `ApiQueryParameter` クエリ文字列パラメータ(例: `?page=1`)を文書化します。 * **Name** (string, required in constructor): クエリパラメータのキーです。 * **Description** (string): パラメータの説明です。 * **Type** (string): 期待されるデータ型です。 * **IsRequired** (bool): クエリパラメータが必須かどうかを示します。 ### `ApiRequest` 期待されるリクエストボディを記述します。 * **Description** (string, required in constructor): リクエストボディの説明です。 * **Example** (string): リクエストボディの例を含む生文字列です。 * **ExampleLanguage** (string): 例の言語(例: "json", "xml")です。 * **PayloadType** (Type): 設定した場合、構成されたコンテキストハンドラがサポートしていれば、この型から自動的に例とスキーマが生成されます。 ### `ApiResponse` エンドポイントからの可能なレスポンスを記述します。 * **StatusCode** (HttpStatusCode, required in constructor): 返される HTTP ステータスコード(例: `HttpStatusCode.OK`)です。 * **Description** (string): このレスポンスの条件を説明します。 * **Example** (string): レスポンスボディの例を含む生文字列です。 * **ExampleLanguage** (string): 例の言語です。 * **PayloadType** (Type): 設定した場合、構成されたコンテキストハンドラがサポートしていれば、この型から自動的に例とスキーマが生成されます。 ## タイプハンドラ タイプハンドラは、.NET の型(クラス、列挙型など)をドキュメント用の例に変換する役割を担います。データモデルに基づくリクエストおよびレスポンスボディの自動例生成に特に有用です。 これらのハンドラは `ApiGenerationContext` 内で設定します。 ```csharp using Sisk.Documenting.Content; var context = new ApiGenerationContext() { // ... BodyExampleTypeHandler = new JsonContentTypeHandler(), ParameterExampleTypeHandler = new JsonContentTypeHandler(), ContentSchemaTypeHandler = new JsonContentTypeHandler() }; ``` ### JsonContentTypeHandler `JsonContentTypeHandler` は組み込みハンドラで、JSON の例、パラメータ例、JSON スキーマを生成します。`IExampleBodyTypeHandler`、`IExampleParameterTypeHandler`、`IContentSchemaTypeHandler` を実装しています。 特定の `JsonSerializerOptions` や `IJsonTypeInfoResolver` を使用して、アプリケーションのシリアライズロジックに合わせてカスタマイズできます。 ```csharp var jsonHandler = new JsonContentTypeHandler(new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true }); context.BodyExampleTypeHandler = jsonHandler; context.ParameterExampleTypeHandler = jsonHandler; context.ContentSchemaTypeHandler = jsonHandler; ``` ### カスタムタイプハンドラ XML などの他形式をサポートしたり、例の生成方法をカスタマイズしたりするために、独自のハンドラを実装できます。 #### IExampleBodyTypeHandler このインターフェイスを実装して、リクエストおよびレスポンスタイプのボディ例を生成します。 ```csharp public class XmlExampleTypeHandler : IExampleBodyTypeHandler { public BodyExampleResult? GetBodyExampleForType(Type type) { // Generate XML string for the type string xmlContent = MyXmlGenerator.Generate(type); return new BodyExampleResult(xmlContent, "xml"); } } ``` #### IExampleParameterTypeHandler このインターフェイスを実装して、型から詳細なパラメータ記述を生成します(`[ApiParametersFrom]` で使用)。 ```csharp public class CustomParameterHandler : IExampleParameterTypeHandler { public ParameterExampleResult[] GetParameterExamplesForType(Type type) { var properties = type.GetProperties(); var examples = new List(); foreach (var prop in properties) { examples.Add(new ParameterExampleResult( name: prop.Name, typeName: prop.PropertyType.Name, isRequired: true, description: "Generated description" )); } return examples.ToArray(); } } ``` ## エクスポーター エクスポーターは、収集された API ドキュメントメタデータを、他のツールが利用できる、またはユーザーに表示できる特定の形式に変換する役割を担います。 ### OpenApiExporter デフォルトのエクスポーターは `OpenApiExporter` で、[OpenAPI Specification 3.0.0](https://spec.openapis.org/oas/v3.0.0) に従った JSON ファイルを生成します。 ```csharp new OpenApiExporter() { OpenApiVersion = "3.0.0", ServerUrls = new[] { "http://localhost:5555" }, Contact = new OpenApiContact() { Name = "Support", Email = "support@example.com", Url = "https://example.com/support" }, License = new OpenApiLicense() { Name = "MIT", Url = "https://opensource.org/licenses/MIT" }, TermsOfService = "https://example.com/terms" } ``` ### LlmsApiExporter `LlmsApiExporter` は [llms.txt](https://llmstxt.org/) スタイルのプレーンテキストドキュメントを生成し、言語モデルが読むことを想定しています。例で使用する `Hostname`、文書の冒頭に書き込まれる `HeaderText`、末尾に書き込まれる `FooterText` を設定できます。 ```csharp new LlmsApiExporter( hostname: "https://api.example.com", headerText: "Public API of the Example service.", footerText: "Contact support@example.com for an API key.") ``` ### カスタムエクスポーターの作成 `IApiDocumentationExporter` インターフェイスを実装することで、独自のエクスポーターを作成できます。これにより、Markdown、HTML、Postman Collection など、任意の形式でドキュメントを出力できます。 インターフェイスは単一のメソッド `ExportDocumentationContent` の実装を要求します。 ```csharp using Sisk.Core.Http; using Sisk.Documenting; public class MyCustomExporter : IApiDocumentationExporter { public HttpContent ExportDocumentationContent(ApiDocumentation documentation) { // 1. ドキュメントオブジェクトを処理する var sb = new StringBuilder(); sb.AppendLine($"# {documentation.ApplicationName}"); foreach(var endpoint in documentation.Endpoints) { sb.AppendLine($"## {endpoint.Method} {endpoint.Path}"); sb.AppendLine(endpoint.Description); } // 2. 内容を HttpContent として返す return new StringContent(sb.ToString(), Encoding.UTF8, "text/markdown"); } } ``` Then, simply use it in your configuration: ```csharp host.UseApiDocumentation( // ... exporter: new MyCustomExporter() ); ``` ### 完全な例 以下は、`Sisk.Documenting` を設定し、シンプルなコントローラをドキュメント化する完全な例です。 ```csharp using Sisk.Core.Entity; using Sisk.Core.Http; using Sisk.Core.Routing; using Sisk.Documenting; using Sisk.Documenting.Annotations; using Sisk.Documenting.Exporters; using var host = HttpServer.CreateBuilder(5555) .UseCors(CrossOriginResourceSharingHeaders.CreatePublicContext()) .UseApiDocumentation( context: new ApiGenerationContext() { ApplicationName = "My application", ApplicationDescription = "It greets someone." }, routerPath: "/api/docs", exporter: new OpenApiExporter() { ServerUrls = ["http://localhost:5555/"] }) .UseRouter(router => { router.MapInstance(new MyController()); }) .Build(); await host.StartAsync(); class MyController { [RouteGet] [ApiEndpoint(Description = "Returns a greeting message.")] [ApiQueryParameter(name: "name", IsRequired = false, Description = "The name of the person to greet.", Type = "string")] public HttpResponse Index(HttpRequest request) { string? name = request.Query["name"].MaybeNullOrEmpty() ?? "world"; return new HttpResponse($"Hello, {name}!"); } } ``` この例では、`/api/docs` にアクセスすると、"My application" API の生成されたドキュメントが提供され、`GET /` エンドポイントとその `name` パラメータが記述されます。 --- # ベーシック認証(廃止) Source: https://docs.sisk-framework.org/ja/docs/extensions/basic-auth.html > [!CAUTION] > **廃止されました。使用しないでください。** Sisk.BasicAuth パッケージはメンテナンスされておらず、NuGet でも非推奨となっており、代替パッケージはありません。`BasicAuthenticateRequestHandler` は標準的な HTTP クライアントが送信する Base64 認証情報をデコードしないため、認証されたリクエストは拒否されます。代わりに、[リクエストハンドラ](https://docs.sisk-framework.org/ja/docs/fundamentals/request-handlers.md) を自作し、[HttpHeaderCollection.Authorization](https://docs.sisk-framework.org/api/Sisk.Core.Entity.HttpHeaderCollection.Authorization.md) を読み取ってベーシック認証を実装してください。このページは歴史的参照用に残しています。 Basic Auth パッケージは、非常に少ない設定と手間で Sisk アプリケーションにベーシック認証スキームを処理できるリクエストハンドラを追加します。ベーシック HTTP 認証は、ユーザー ID とパスワードによる最小限の入力形式でリクエストを認証し、セッションはクライアント側で完全に管理され、認証トークンやアクセストークンは使用しません。 ![ベーシック認証](https://docs.sisk-framework.org/assets/img/basic-auth.svg) ベーシック認証スキームの詳細は、[MDN 仕様](https://developer.mozilla.org/pt-BR/docs/Web/HTTP/Authentication) を参照してください。 ## インストール まず、プロジェクトに Sisk.BasicAuth パッケージをインストールします。 > dotnet add package Sisk.BasicAuth インストール方法の詳細は、[NuGet リポジトリ](https://www.nuget.org/packages/Sisk.BasicAuth/0.15.0) をご覧ください。 ## 認証ハンドラの作成 認証スキームはモジュール全体または個々のルート単位で制御できます。まず、最初のベーシック認証ハンドラを書いてみましょう。 以下の例では、データベースへの接続を行い、ユーザーの存在とパスワードの有効性を確認した後、ユーザーをコンテキストバッグに格納します。 ```cs public class UserAuthHandler : BasicAuthenticateRequestHandler { public UserAuthHandler() : base() { Realm = "To enter this page, please, inform your credentials."; } public override HttpResponse? OnValidating(BasicAuthenticationCredentials credentials, HttpContext context) { DbContext db = new DbContext(); // この場合、メールアドレスをユーザーIDフィールドとして使用しているので、 // メールアドレスでユーザーを検索します。 User? user = db.Users.FirstOrDefault(u => u.Email == credentials.UserId); if (user == null) { return base.CreateUnauthorizedResponse("Sorry! No user was found by this email."); } // 資格情報のパスワードがこのユーザーに対して有効か検証します。 if (!user.ValidatePassword(credentials.Password)) { return base.CreateUnauthorizedResponse("Invalid credentials."); } // ログインしたユーザーを HttpContext に追加します // そして実行を続行します context.Bag.Add("loggedUser", user); return null; } } ``` このリクエストハンドラをルートまたはクラスに関連付けるだけです。 ```cs public class UsersController { [RouteGet("/")] [RequestHandler(typeof(UserAuthHandler))] public string Index(HttpRequest request) { User loggedUser = request.Bag.Get(); return $"Hello, {loggedUser.Name}!"; } } ``` または、[RouterModule](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouterModule.md) クラスを使用します。 ```cs public class UsersController : RouterModule { public ClientModule() { // このクラス内のすべてのルートは // UserAuthHandler によって処理されます。 base.HasRequestHandler(new UserAuthHandler()); } [RouteGet("/")] public string Index(HttpRequest request) { User loggedUser = request.Bag.Get(); return $"Hello, {loggedUser.Name}!"; } } ``` ## 備考 ベーシック認証の主な責務はクライアント側で実行されます。ストレージ、キャッシュ制御、暗号化はすべてクライアント側でローカルに処理され、サーバーは認証情報を受け取り、アクセスが許可されるかどうかを検証するだけです。 この方式は、クライアントに大きな責任が課せられるため、最も安全な方法とは言えません。クレデンシャルの追跡や保護が困難になる可能性があります。また、パスワードは固有の暗号化が施されていないため、SSL などの安全な接続環境で送信することが必須です。リクエストヘッダーが一瞬でも傍受されれば、ユーザーの認証情報が漏洩する恐れがあります。 本番環境のアプリケーションでは、より堅牢な認証ソリューションを採用し、汎用コンポーネントの過度な使用は避けてください。プロジェクトの要件に合わないコンポーネントは、セキュリティリスクを招く可能性があります。 --- # SSL プロキシ(廃止) Source: https://docs.sisk-framework.org/ja/docs/extensions/ssl-proxy.html > [!CAUTION] > **廃止されました。使用しないでください。** Sisk.SslProxy パッケージはもはやメンテナンスされておらず、NuGet で非推奨となっています。代替パッケージはありません。現在の Cadente API に対してコンパイルできません。HTTPS を提供するには、[Cadente engine](https://docs.sisk-framework.org/ja/docs/cadente.md) のネイティブ SSL サポートまたは [Working with SSL](https://docs.sisk-framework.org/ja/docs/ssl.md) のいずれかのオプションを使用してください。このページは歴史的参照のために残されています。 Sisk SSL Proxy は、Sisk の [ListeningHost](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.md) に HTTPS 接続を提供し、HTTPS メッセージを安全でない HTTP コンテキストへルーティングするモジュールです。このモジュールは、SSL をサポートしない [HttpListener](https://learn.microsoft.com/pt-br/dotnet/api/system.net.httplistener?view=net-8.0) を使用して実行するサービスに対して SSL 接続を提供するために作られました。 プロキシは同じアプリケーション内で実行され、HTTP/1.1 メッセージをリッスンし、同じプロトコルで Sisk に転送します。現在、この機能は非常に実験的であり、製品環境で使用するには不安定な可能性があります。 現時点では、SslProxy は keep-alive、チャンク化エンコーディング、WebSocket など、ほぼすべての HTTP/1.1 機能をサポートしています。SSL プロキシへのオープン接続の場合、ターゲットサーバーへの TCP 接続が作成され、プロキシは確立された接続へ転送されます。 SslProxy は次のように HttpServer.CreateBuilder と組み合わせて使用できます: ```csharp using var app = HttpServer.CreateBuilder(port: 5555) .UseRouter(r => { r.MapGet("/", request => { return new HttpResponse("Hello, world!"); }); }) // プロジェクトに SSL を追加 .UseSsl( sslListeningPort: 5567, new X509Certificate2(@".\ssl.pfx", password: "12345") ) .Build(); app.Start(); ``` プロキシ用に有効な SSL 証明書を提供する必要があります。ブラウザーが証明書を受け入れるようにするため、証明書をオペレーティングシステムにインポートして正しく機能するようにしてください。 --- # 手動(高度)設定 Source: https://docs.sisk-framework.org/ja/docs/advanced/manual-setup.html 手動セットアップは、サーバーの構成要素を自分で組み立てる必要がある場合に使用します。たとえば、1 つのプロセスが複数のホスト、ポート、ルーター、またはカスタムサーバー構成を公開しなければならない場合です。ほとんどのアプリケーションでは、ビルダー API の方が短く、優先すべきです。手動セットアップは、`Router`、1 つ以上の `ListeningHost` オブジェクト、`HttpServerConfiguration`、最終的な `HttpServer` の 4 つのコア要素を直接制御したいときに便利です。 まず、リクエスト/レスポンスの概念を理解する必要があります。これは非常にシンプルです。すべてのリクエストに対してレスポンスが必要です。Sisk もこの原則に従っています。ステータスコードとヘッダーを指定した、HTML の「Hello, World!」メッセージで応答するメソッドを作成しましょう。 ```csharp // Program.cs using Sisk.Core.Http; using Sisk.Core.Routing; static HttpResponse IndexPage(HttpRequest request) { HttpResponse indexResponse = new HttpResponse { Status = System.Net.HttpStatusCode.OK, Content = new HtmlContent(@"

    Hello, world!

    ") }; return indexResponse; } ``` 次のステップは、このメソッドを HTTP ルートに関連付けることです。 ## ルーター ルーターはリクエストルートの抽象化であり、サービスのリクエストとレスポンスの橋渡しを行います。ルーターはサービスのルート、関数、エラーを管理します。 ルーターは複数のルートを持つことができ、各ルートは関数の実行、ページの提供、サーバーからのリソース提供など、パスに対して異なる操作を実行できます。 最初のルーターを作成し、`IndexPage` メソッドをインデックスパスに関連付けてみましょう。 ```csharp Router mainRouter = new Router(); mainRouter.MapGet("/", IndexPage); ``` これでルーターはリクエストを受け取り、レスポンスを送信できるようになりました。ただし、`mainRouter` はホストやサーバーに結び付けられていないため、単体では機能しません。次のステップは `ListeningHost` を作成することです。 ## リスニングホストとポート [ListeningHost](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.md) はルーターと同じルーター用の複数のリスニングポートをホストできます。[ListeningPort](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningPort.md) は HTTP サーバーがリッスンするプレフィックスです。 ここでは、ルーターに対して 2 つのエンドポイントを指す `ListeningHost` を作成します。 ```csharp ListeningHost myHost = new ListeningHost { Router = mainRouter, Ports = new ListeningPort[] { new ListeningPort("http://localhost:5000/") } }; ``` これで HTTP サーバーは指定されたエンドポイントでリッスンし、リクエストをルーターに転送します。 ## サーバー構成 サーバー構成は HTTP サーバー自体の動作の大部分を担当します。この構成では、`ListeningHosts` をサーバーに関連付けることができます。 ```csharp HttpServerConfiguration config = new HttpServerConfiguration(); config.ListeningHosts.Add(myHost); // Add our ListeningHost to this server configuration ``` 一般的なサーバー構成オプション: | プロパティ | デフォルト | 使用シーン | 備考 | | --- | --- | --- | --- | | [RemoteRequestsAction](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.RemoteRequestsAction.md) | `RequestListenAction.Accept` | サービスは、信頼できるリバースプロキシ経由でない限り、ローカル以外のリクエストを拒否すべきです。 | `Drop` に設定するのは、デプロイトポロジーが明確な場合のみです。 | | [IncludeRequestIdHeader](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.IncludeRequestIdHeader.md) | `false` | クライアントまたはプロキシが `X-Request-Id` 応答ヘッダーに Sisk のリクエスト ID を必要とする場合。 | `HttpRequest.RequestId` を含むログと組み合わせて使用してください。 | | [IdleConnectionTimeout](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.IdleConnectionTimeout.md) | `120` 秒 | アイデル状態の Keep-Alive 接続は、いずれかの時点で閉じるべきです。 | これは HTTP エンジンによって適用されます。 | | [NormalizeHeadersEncodings](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.NormalizeHeadersEncodings.md) | `false` | エンコーディングが一致しないヘッダーを受信した場合。 | 処理コストがかかります。必要なければ無効のままにしてください。 | | [SendSiskHeader](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.SendSiskHeader.md) | `true` | `X-Powered-By` Sisk ヘッダーを隠すか表示したい場合。 | 本番環境でヘッダー方針を厳格にする場合は無効にしてください。 | | [OptionsLogMode](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.OptionsLogMode.md) | `LogOutput.Both` | 自動 `OPTIONS` 処理で生成されるログを削減またはリダイレクトしたい場合。 | ルートと同じログモードの値を使用します。 | | [AsyncRequestProcessing](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.AsyncRequestProcessing.md) | `true` | 診断のために決定的な単一リクエスト処理が必要な場合。 | 無効にするとスループットが制限されます。 | | [DisposeDisposableContextValues](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.DisposeDisposableContextValues.md) | `true` | `IDisposable` を実装するリクエストバッグの値は自動的に破棄すべきです。 | 所有権が他で管理されていない限り有効にしておいてください。 | | [ConvertIAsyncEnumerableIntoEnumerable](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ConvertIAsyncEnumerableIntoEnumerable.md) | `true` | 値ハンドラは非同期列挙可能オブジェクトをブロッキング列挙可能値として受け取るべきです。 | 独自の非同期ストリーム処理を実装する場合は無効にしてください。 | | [KeepAlive](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.KeepAlive.md) | `true` | レスポンス後も接続を再利用可能にすべきです。 | 永続接続を適切に処理できないクライアントや中間サーバーの場合は無効にしてください。 | | [ForceTrailingSlash](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ForceTrailingSlash.md) | `false` | GET ルートは末尾スラッシュ付き URL にリダイレクトすべきです。 | 正規表現以外のルートにのみ適用されます。 | | [MaximumContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.MaximumContentLength.md) | `100 MB` | リクエストボディや WebSocket メッセージに別のサイズ上限が必要な場合。 | Sisk 1.7 以前のデフォルトは `0`(無制限)でした。上限を解除するには `0` に設定してください。 | | [EnableAutomaticResponseCompression](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.EnableAutomaticResponseCompression.md) | `false` | クライアントがサポートしている場合、レスポンスを自動的に圧縮すべきです。 | 既存の `CompressedContent` 応答は再度圧縮されません。 | 次に、HTTP サーバーを作成します。 ```csharp HttpServer server = new HttpServer(config); server.Start(); // Starts the server Console.ReadKey(); // Prevents the application from exiting ``` これで実行ファイルをビルドし、次のコマンドで HTTP サーバーを起動できます。 ```bash dotnet watch ``` 実行時にブラウザを開きサーバーパスへアクセスすると、以下のように表示されます。 --- # リクエストライフサイクル Source: https://docs.sisk-framework.org/ja/docs/advanced/request-lifecycle.html 以下では、HTTP リクエストの例を通して、リクエストの全ライフサイクルについて説明します。 - **リクエストの受信:** 各リクエストは、リクエスト自体とクライアントに配信されるレスポンスの間に HTTP コンテキストを作成します。このコンテキストは Sisk に組み込まれたリスナーから取得され、[HttpListener](https://learn.microsoft.com/en-us/dotnet/api/system.net.httplistener?view=net-9.0)、[Kestrel](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel?view=aspnetcore-9.0)、または[Cadente](https://blog.sisk-framework.org/posts/2025-01-29-cadente-experiment/) のいずれかになります。 - 外部リクエストの検証: [HttpServerConfiguration.RemoteRequestsAction](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.RemoteRequestsAction.md) の検証がリクエストに対して行われます。 - リクエストが外部でプロパティが `Drop` の場合、クライアントへのレスポンスなしで接続が閉じられ、`HttpServerExecutionStatus = RemoteRequestDropped` が設定されます。 - フォワーディングリゾルバの構成: [ForwardingResolver](https://docs.sisk-framework.org/ja/docs/advanced/forwarding-resolvers.md) が構成されている場合、リクエスト元ホストの [OnResolveRequestHost](https://docs.sisk-framework.org/api/Sisk.Core.Http.ForwardingResolver.OnResolveRequestHost.md) メソッドが呼び出されます。 - DNS マッチング: ホストが解決され、複数の [ListeningHost](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.md) が構成されている場合、サーバーはリクエストに対応するホストを探します。 - 一致する ListeningHost がない場合、クライアントに 400 Bad Request が返され、HTTP コンテキストには `HttpServerExecutionStatus = DnsUnknownHost` が設定されます。 - ListeningHost が一致するが、その [Router](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.Router.md) がまだ初期化されていない場合、クライアントに 503 Service Unavailable が返され、HTTP コンテキストには `HttpServerExecutionStatus = ListeningHostNotReady` が設定されます。 - ルータのバインディング: 対応する ListeningHost のルータが受信した HTTP サーバーに関連付けられます。 - ルータが既に別の HTTP サーバーに関連付けられている場合、ルータはサーバーの構成リソースを積極的に使用するため許可されず、`InvalidOperationException` がスローされます。これは HTTP サーバーの初期化時にのみ発生し、HTTP コンテキストの作成時には発生しません。 - ヘッダーの事前定義: - 設定されている場合、レスポンスに `X-Request-Id` ヘッダーを事前に設定します。 - 設定されている場合、レスポンスに `X-Powered-By` ヘッダーを事前に設定します。 - コンテンツサイズの検証: リクエストコンテンツが [HttpServerConfiguration.MaximumContentLength](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.MaximumContentLength.md) 未満かどうかを、サイズが 0 より大きい場合にのみ検証します(デフォルトは 100 MB)。 - リクエストが設定された `Content-Length` を超える場合、クライアントに 413 Payload Too Large が返され、HTTP コンテキストには `HttpServerExecutionStatus = ContentTooLarge` が設定されます。 - `OnHttpRequestOpen` イベントが、構成されたすべての HTTP サーバーハンドラに対して呼び出されます。 - **アクションのルーティング:** サーバーは受信したリクエストに対してルータを呼び出します。 - ルータがリクエストに一致するルートを見つけない場合: - [Router.NotFoundErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.NotFoundErrorHandler.md) プロパティが構成されている場合、アクションが呼び出され、そのレスポンスが HTTP クライアントに転送されます。 - 前者が null の場合、デフォルトの 404 Not Found がクライアントに返されます。 - ルータが一致するルートを見つけたが、ルートのメソッドがリクエストのメソッドと一致しない場合: - [Router.MethodNotAllowedErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.MethodNotAllowedErrorHandler.md) プロパティが構成されている場合、アクションが呼び出され、そのレスポンスが HTTP クライアントに転送されます。 - 前者が null の場合、デフォルトの 405 Method Not Allowed がクライアントに返されます。 - リクエストが `OPTIONS` メソッドの場合: - ルートがリクエストメソッドに一致しない(ルートのメソッドが明示的に [RouteMethod.Options](https://docs.sisk-framework.org/api/Sisk.Core.Routing.RouteMethod.md) でない)場合に限り、ルータはクライアントに 200 Ok を返します。 - [HttpServerConfiguration.ForceTrailingSlash](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ForceTrailingSlash.md) プロパティが有効で、マッチしたルートが正規表現でなく、リクエストパスが `/` で終わらず、リクエストメソッドが `GET` の場合: - パスとクエリを同じ場所に `/` を付加した形で `Location` ヘッダーに設定した 307 Temporary Redirect がクライアントに返されます。 - `OnContextBagCreated` イベントが、構成されたすべての HTTP サーバーハンドラに対して呼び出されます。 - `BeforeResponse` フラグが付いたすべてのグローバル [IRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.md) インスタンスが実行されます。 - ハンドラが非 null のレスポンスを返した場合、そのレスポンスが HTTP クライアントに転送され、コンテキストは閉じられます。 - このステップでエラーがスローされ、[HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) が無効な場合: - [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) プロパティが有効であれば呼び出され、その結果のレスポンスがクライアントに返されます。 - 前者が未定義の場合、空のレスポンスがサーバーに返され、スローされた例外の種類に応じたレスポンス(通常は 500 Internal Server Error)が転送されます。 - ルートで定義され、`BeforeResponse` フラグが付いたすべての [IRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.md) インスタンスが実行されます。 - ハンドラが非 null のレスポンスを返した場合、そのレスポンスが HTTP クライアントに転送され、コンテキストは閉じられます。 - このステップでエラーがスローされ、[HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) が無効な場合: - [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) プロパティが有効であれば呼び出され、その結果のレスポンスがクライアントに返されます。 - 前者が未定義の場合、空のレスポンスがサーバーに返され、例外の種類に応じたレスポンス(通常は 500 Internal Server Error)が転送されます。 - ルータのアクションが呼び出され、HTTP レスポンスに変換されます。 - このステップでエラーがスローされ、[HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) が無効な場合: - [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) プロパティが有効であれば呼び出され、その結果のレスポンスがクライアントに返されます。 - 前者が未定義の場合、空のレスポンスがサーバーに返され、例外の種類に応じたレスポンス(通常は 500 Internal Server Error)が転送されます。 - `AfterResponse` フラグが付いたすべてのグローバル [IRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.md) インスタンスが実行されます。 - ハンドラが非 null のレスポンスを返した場合、そのレスポンスが前のレスポンスを置き換え、直ちに HTTP クライアントに転送されます。 - このステップでエラーがスローされ、[HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) が無効な場合: - [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) プロパティが有効であれば呼び出され、その結果のレスポンスがクライアントに返されます。 - 前者が未定義の場合、空のレスポンスがサーバーに返され、例外の種類に応じたレスポンス(通常は 500 Internal Server Error)が転送されます。 - ルートで定義され、`AfterResponse` フラグが付いたすべての [IRequestHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.IRequestHandler.md) インスタンスが実行されます。 - ハンドラが非 null のレスポンスを返した場合、そのレスポンスが前のレスポンスを置き換え、直ちに HTTP クライアントに転送されます。 - このステップでエラーがスローされ、[HttpServerConfiguration.ThrowExceptions](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ThrowExceptions.md) が無効な場合: - [Router.CallbackErrorHandler](https://docs.sisk-framework.org/api/Sisk.Core.Routing.Router.CallbackErrorHandler.md) プロパティが有効であれば呼び出され、その結果のレスポンスがクライアントに返されます。 - 前者が未定義の場合、空のレスポンスがサーバーに返され、例外の種類に応じたレスポンス(通常は 500 Internal Server Error)が転送されます。 - **レスポンスの処理:** レスポンスが準備できたら、サーバーはクライアントへ送信するためにそれを準備します。 - 現在の [ListeningHost.CrossOriginResourceSharingPolicy](https://docs.sisk-framework.org/api/Sisk.Core.Http.ListeningHost.CrossOriginResourceSharingPolicy.md) に設定された内容に従って、レスポンスに Cross-Origin Resource Sharing (CORS) ヘッダーが設定されます。 - レスポンスのステータスコードとヘッダーがクライアントに送信されます。 - レスポンスコンテンツがクライアントに送信されます: - レスポンスコンテンツが [ByteArrayContent](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.bytearraycontent) の派生クラスである場合、レスポンスバイトが直接レスポンス出力ストリームにコピーされます。 - 前者に該当しない場合、レスポンスはストリームにシリアライズされ、レスポンス出力ストリームにコピーされます。 - ストリームが閉じられ、レスポンスコンテンツは破棄されます。 - [HttpServerConfiguration.DisposeDisposableContextValues](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.DisposeDisposableContextValues.md) が有効な場合、リクエストコンテキストで定義された [IDisposable] を継承するすべてのオブジェクトが破棄されます。 - `OnHttpRequestClose` イベントが、構成されたすべての HTTP サーバーハンドラに対して呼び出されます。 - サーバーで例外がスローされた場合、`OnException` イベントが、構成されたすべての HTTP サーバーハンドラに対して呼び出されます。 - ルートがアクセスログを許可し、[HttpServerConfiguration.AccessLogsStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.AccessLogsStream.md) が null でない場合、ログ出力にログ行が書き込まれます。 - ルートがエラーログを許可し、例外が発生し、[HttpServerConfiguration.ErrorsLogsStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ErrorsLogsStream.md) が null でない場合、エラーログ出力にログ行が書き込まれます。 - サーバーが [HttpServer.WaitNext](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.WaitNext.md) でリクエストを待機している場合、ミューテックスが解放され、コンテキストがユーザーに利用可能になります。 --- # フォワーディングリゾルバ Source: https://docs.sisk-framework.org/ja/docs/advanced/forwarding-resolvers.html フォワーディングリゾルバは、リクエスト、プロキシ、CDN、ロードバランサーを通じてクライアントを識別する情報をデコードするのに役立つヘルパーです。Sisk サービスがリバースプロキシまたはフォワードプロキシを介して実行される場合、クライアントの IP アドレス、ホスト、プロトコルは元のリクエストとは異なることがあります。これは、あるサービスから別のサービスへ転送されるためです。この Sisk の機能により、リクエストを処理する前にこの情報を制御・解決できます。これらのプロキシは通常、クライアントを識別するための有用なヘッダーを提供します。 現在、[ForwardingResolver](https://docs.sisk-framework.org/api/Sisk.Core.Http.ForwardingResolver.md) クラスを使用すると、クライアントの IP アドレス、ホスト、使用された HTTP プロトコルを解決することが可能です。Sisk のバージョン 1.0 以降、サーバーはサービスごとに異なるセキュリティ上の理由から、これらのヘッダーをデコードする標準実装を提供しなくなりました。 たとえば、`X-Forwarded-For` ヘッダーにはリクエストを転送した IP アドレスの情報が含まれます。このヘッダーはプロキシが情報のチェーンを最終サービスへ渡すために使用され、使用されたすべてのプロキシの IP とクライアントの実際のアドレスが含まれます。問題は、クライアントのリモート IP を特定するのが難しいことがあり、このヘッダーを識別するための具体的なルールが存在しない点です。以下のヘッダーに関するドキュメントを必ずお読みください。 - `X-Forwarded-For` ヘッダーについては[こちら](https://developer.mozilla.org/en-US/docs/jp/Web/HTTP/Headers/X-Forwarded-For#security_and_privacy_concerns)をご参照ください。 - `X-Forwarded-Host` ヘッダーについては[こちら](https://developer.mozilla.org/en-US/docs/jp/Web/HTTP/Headers/X-Forwarded-Host)をご参照ください。 - `X-Forwarded-Proto` ヘッダーについては[こちら](https://developer.mozilla.org/en-US/docs/jp/Web/HTTP/Headers/X-Forwarded-Proto)をご参照ください。 ## ForwardingResolver クラス このクラスには、各サービスに最適な実装を可能にする 3 つの仮想メソッドが用意されています。各メソッドは、プロキシを介したリクエストから情報を解決する役割を担い、クライアントの IP アドレス、リクエストのホスト、使用されたセキュリティプロトコルを取得します。デフォルトでは、Sisk はヘッダーを解決せず、元のリクエストに含まれる情報を常に使用します。 以下の例は、この実装の使用方法を示しています。この例では `X-Forwarded-For` ヘッダーを使ってクライアントの IP を解決し、リクエストに複数の IP が転送されている場合はエラーをスローします。 > [!IMPORTANT] > 本例を本番コードで使用しないでください。実装が使用に適切かどうか必ず確認し、実装前にヘッダーのドキュメントを読んでください。 ```cs class Program { static void Main(string[] args) { using var host = HttpServer.CreateBuilder() .UseForwardingResolver() .UseListeningPort(5555) .Build(); host.Router.MapAny(Route.AnyPath, request => new HttpResponse("Hello, world!!!")); host.Start(); } class Resolver : ForwardingResolver { public override IPAddress OnResolveClientAddress(HttpRequest request, IPEndPoint connectingEndpoint) { string? forwardedFor = request.Headers.XForwardedFor; if (forwardedFor is null) { throw new Exception("The X-Forwarded-For header is missing."); } string[] ipAddresses = forwardedFor.Split(','); if (ipAddresses.Length != 1) { throw new Exception("Too many addresses in the X-Forwarded-For header."); } return IPAddress.Parse(ipAddresses[0]); } } } ``` --- # HTTP サーバーハンドラ Source: https://docs.sisk-framework.org/ja/docs/advanced/http-server-handlers.html Sisk バージョン 0.16 では、`HttpServerHandler` クラスを導入しました。このクラスは、Sisk の全体的な動作を拡張し、Http リクエスト、ルーター、コンテキストバッグなどの追加イベントハンドラを提供することを目的としています。 このクラスは、HTTP サーバー全体および個々のリクエストのライフタイム中に発生するイベントを集中管理します。Http プロトコルにはセッションが存在しないため、リクエスト間で情報を保持することはできません。現在の Sisk では、セッション、コンテキスト、データベース接続、その他の便利なプロバイダーを実装できるようにしています。 各イベントがいつトリガーされ、何のためにあるかについては、[this page](https://docs.sisk-framework.org/api/Sisk.Core.Http.Handlers.HttpServerHandler.md) を参照してください。また、リクエストで何が起こり、どこでイベントが発火するかを理解するために、[lifecycle of an HTTP request](https://docs.sisk-framework.org/ja/docs/advanced/request-lifecycle.md) もご覧ください。HTTP サーバーは複数のハンドラを同時に使用できます。各イベント呼び出しは同期的であり、すべてのハンドラが実行・完了するまで、リクエストまたはコンテキストごとに現在のスレッドがブロックされます。 RequestHandlers とは異なり、これらは特定のルートグループや個別のルートに適用できません。代わりに、HTTP サーバー全体に適用されます。Http Server Handler 内で条件を設定することも可能です。さらに、各 `HttpServerHandler` のシングルトンはすべての Sisk アプリケーションで定義されるため、`HttpServerHandler` ごとにインスタンスは 1 つだけです。 HttpServerHandler を使用する実用的な例として、リクエストの終了時にデータベース接続を自動的に破棄する方法があります。 ```cs // DatabaseConnectionHandler.cs public class DatabaseConnectionHandler : HttpServerHandler { protected override void OnHttpRequestClose(HttpServerExecutionResult result) { var requestBag = result.Request.Context.RequestBag; // リクエストが DbContext を定義しているか確認する // そのコンテキストバッグ内で if (requestBag.IsSet()) { var db = requestBag.Get(); db.Dispose(); } } } public static class DatabaseConnectionHandlerExtensions { public static DbContext GetDbContext(this HttpRequest request) { return request.Bag.GetOrAdd(() => new DbContext()); } } ``` 上記のコードでは、`GetDbContext` 拡張メソッドにより、`HttpRequest` オブジェクトから直接接続コンテキストを作成できるようになります。未破棄の接続はデータベース操作時に問題を引き起こす可能性があるため、`OnHttpRequestClose` で確実に終了させます。 Http サーバーにハンドラを登録するには、ビルダー内または直接 [HttpServer.RegisterHandler](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServer.RegisterHandler.md) を使用します。 ```cs // Program.cs class Program { static void Main(string[] args) { using var app = HttpServer.CreateBuilder() .UseHandler() .Build(); app.Router.MapInstance(new UserController()); app.Start(); } } ``` これにより、`UsersController` クラスは次のようにデータベースコンテキストを利用できます。 ```cs // UserController.cs [RoutePrefix("/users")] public class UserController : ApiController { [RouteGet()] public async Task List(HttpRequest request) { var db = request.GetDbContext(); var users = db.Users.ToArray(); return JsonOk(users); } [RouteGet("")] public async Task View(HttpRequest request) { var db = request.GetDbContext(); int userId = request.RouteParameters["id"].GetInteger(); var user = db.Users.FirstOrDefault(u => u.Id == userId); return JsonOk(user); } [RoutePost] public async Task Create(HttpRequest request) { var db = request.GetDbContext(); var user = await request.GetJsonContentAsync(); ArgumentNullException.ThrowIfNull(user); db.Users.Add(user); await db.SaveChangesAsync(); return JsonMessage("User added."); } } ``` 上記のコードは、`RouterController` から継承された `ApiController` に組み込まれている `JsonOk` や `JsonMessage` といったメソッドを使用しています。 ```cs // ApiController.cs public class ApiController : RouterModule { public HttpResponse JsonOk(object value) { return new HttpResponse(200) .WithContent(JsonContent.Create(value, null, new JsonSerializerOptions() { PropertyNameCaseInsensitive = true })); } public HttpResponse JsonMessage(string message, int statusCode = 200) { return new HttpResponse(statusCode) .WithContent(JsonContent.Create(new { Message = message })); } } ``` ## ハンドラでスローされる例外 Sisk 1.7 以降、サーバーハンドラでスローされた例外は以下のように処理されます。 - `OnServerStarting`、`OnServerStarted`、`OnSetupRouter` でスローされた例外はサーバーの起動を中止します。例外は `InvalidOperationException` でラップされ、元の例外は `InnerException` に格納されます。 - その他のイベントでスローされた例外は [ErrorsLogsStream](https://docs.sisk-framework.org/api/Sisk.Core.Http.HttpServerConfiguration.ErrorsLogsStream.md) に書き込まれ、`ThrowExceptions` が有効化されていない限り残りのハンドラは引き続き呼び出されます。`ThrowExceptions` が有効な場合、例外は `InvalidOperationException` でラップされます。 - `OnHttpRequestClose` や `OnException`、またはアクセスログ・エラーログのフォーマット中にスローされた例外は、`ThrowExceptions` が有効でも HTTP エンジンに伝播せず、`ErrorsLogsStream` に書き込まれます。過去のバージョンでは、これらの例外がプロセスを終了させたり、接続を開いたままにしたりすることがありました。 開発者はこのクラスを使用してセッション、コンテキスト、データベース接続を実装できます。提示したコードは、DatabaseConnectionHandler を用いた実用的な例で、リクエスト終了時にデータベース接続を自動的に破棄する方法を示しています。 ハンドラはサーバー設定時に登録するだけで簡単に統合でき、HttpServerHandler クラスはリソース管理と Sisk の HTTP アプリケーションにおける動作拡張のための強力なツールセットを提供します。 --- # サーバーあたり複数のリスニングホスト Source: https://docs.sisk-framework.org/ja/docs/advanced/multi-host-setup.html Sisk Framework は常にサーバーあたり複数のホストの使用をサポートしており、つまり単一の HTTP サーバーが複数のポートでリッスンでき、各ポートはそれぞれ独自のルーターとサービスを実行します。 このように、Sisk を使用すると単一の HTTP サーバー上で責務を分離し、サービスを管理することが容易になります。以下の例は、異なるポートでリッスンする 2 つの ListeningHost を作成し、異なるルーターとアクションを持たせる方法を示しています。 [アプリを手動で作成する](https://docs.sisk-framework.org/ja/docs/advanced/manual-setup.md) を参照してください。 ```cs static void Main(string[] args) { // 2 つのリスニングホストを作成し、それぞれが独自のルーターを持ち // 各自のポートでリッスンします // ListeningHost hostA = new ListeningHost(); hostA.Ports = [new ListeningPort(12000)]; hostA.Router = new Router(); hostA.Router.MapGet("/", request => new HttpResponse().WithContent("Hello from the host A!")); ListeningHost hostB = new ListeningHost(); hostB.Ports = [new ListeningPort(12001)]; hostB.Router = new Router(); hostB.Router.MapGet("/", request => new HttpResponse().WithContent("Hello from the host B!")); // サーバー構成を作成し、両方のリスニングホストを追加します // それにリスニングホストを設定します // HttpServerConfiguration configuration = new HttpServerConfiguration(); configuration.ListeningHosts.Add(hostA); configuration.ListeningHosts.Add(hostB); // 指定された構成を使用する HTTP サーバーを作成します // HttpServer server = new HttpServer(configuration); // サーバーを開始します server.Start(); Console.WriteLine("Try to reach host A in {0}", server.ListeningPrefixes[0]); Console.WriteLine("Try to reach host B in {0}", server.ListeningPrefixes[1]); Thread.Sleep(-1); } ``` --- # HTTPサーバーエンジン Source: https://docs.sisk-framework.org/ja/docs/advanced/server-engines.html Sisk Frameworkは複数のパッケージに分割されており、主なパッケージ(Sisk.HttpServer)は基本的なHTTPサーバーを含んでいません。デフォルトでは、[HttpListener](https://learn.microsoft.com/en-us/dotnet/api/system.net.httplistener?view=net-9.0)がSiskの主なエンジンとして使用され、低レベルのサーバーの役割を果たします。 HTTPエンジンは、Siskが提供するアプリケーション層の下の層を果たします。この層は、接続管理、メッセージのシリアライズとデシリアライズ、メッセージキューの制御、およびマシンのソケットとの通信を担当します。 [HttpServerEngine](https://docs.sisk-framework.org/api/Sisk.Core.Http.Engine.HttpServerEngine.md)クラスは、ルーティング、SSE、ミドルウェアなど、Siskで使用するためのHTTPエンジンの必要な機能を実装するAPIを公開します。これらの機能は、HTTPエンジンの責任ではなく、HTTPエンジンを基盤として実行するサブセットのライブラリの責任です。 この抽象化により、Siskを他のHTTPエンジン(.NETで書かれたものやそうでないもの)で使用できるように移植することができます。たとえば、Kestrelなどです。現在、Siskはネイティブの.NET [HttpListener](https://learn.microsoft.com/en-us/dotnet/api/system.net.httplistener?view=net-9.0)の抽象化をデフォルトとして使用しています。このデフォルトの抽象化は、いくつかの特定の問題を引き起こします。たとえば、異なるプラットフォームでの未指定の動作(HttpListenerにはWindows用と他のプラットフォーム用の実装が別々にある)、SSLのサポートの欠如、Windows以外でのパフォーマンスがあまり良くないことなどです。 Sisk用のHTTPエンジンとして、C#で書かれた高性能サーバーの実験的な実装も利用可能です。[Cadente](https://github.com/sisk-http/core/tree/main/cadente)プロジェクトと呼ばれます。これは、Siskと組み合わせて使用できるマネージドサーバーの実験です。 ## Sisk用のHTTPエンジンの実装 既存のHTTPサーバーとSiskの間の接続ブリッジを作成することで、[HttpServerEngine](https://docs.sisk-framework.org/api/Sisk.Core.Http.Engine.HttpServerEngine.md)クラスを拡張することができます。このクラスに加えて、コンテキスト、リクエスト、レスポンスの抽象化も実装する必要があります。 完了した抽象化の例は、[GitHub](https://github.com/sisk-http/core/blob/main/src/Http/Engine/HttpListenerAbstractEngine.cs)で参照できます。以下のようになります。 ```csharp /// /// のを使用した実装を提供します。 /// public sealed class HttpListenerAbstractEngine : HttpServerEngine { private HttpListener _listener; private static Lazy shared = new Lazy ( () => new HttpListenerAbstractEngine () ); /// /// クラスの共有インスタンスを取得します。 /// public static HttpListenerAbstractEngine Shared => shared.Value; /// /// クラスの新しいインスタンスを初期化します。 /// public HttpListenerAbstractEngine () { _listener = new HttpListener { IgnoreWriteExceptions = true }; } /// public override TimeSpan IdleConnectionTimeout { get => _listener.TimeoutManager.IdleConnection; set => _listener.TimeoutManager.IdleConnection = value; } // ... } ``` ## イベントループの選択 HTTPエンジンの作成中に、サーバーはリクエストを待ち受けるループで実行され、各リクエストを処理するコンテキストを別々のスレッドで作成します。したがって、[HttpServerEngineContextEventLoopMechanism](https://docs.sisk-framework.org/api/Sisk.Core.Http.Engine.HttpServerEngineContextEventLoopMechanism.md)を選択する必要があります。 - `InlineAsynchronousGetContext` イベントループは線形です。HTTPコンテキストの処理は非同期ループで発生します。 - `UnboundAsynchronousGetContext` イベントループは、`BeginGetContext`と`EndGetContext`メソッドを介して伝達されます。 ```csharp public override HttpServerEngineContextEventLoopMechanism EventLoopMechanism => HttpServerEngineContextEventLoopMechanism.UnboundAsynchronousGetContext; ``` 両方のイベントループを実装する必要はありません。HTTPエンジンに最も適したものを選択してください。 ## テスト HTTPエンジンをリンクした後、Siskのすべての機能が他のエンジンを使用して同じ動作を示すことを確認するためにテストを実行することが重要です。**非常に重要**なことは、Siskの動作が異なるHTTPエンジンで同じであることを確認することです。 テストリポジトリは、[GitHub](https://github.com/sisk-http/core/tree/main/tests)で参照できます。