diff --git a/.github/ISSUE_TEMPLATE/quality.md b/.github/ISSUE_TEMPLATE/quality.md
index ef0a76c47..febaefe55 100644
--- a/.github/ISSUE_TEMPLATE/quality.md
+++ b/.github/ISSUE_TEMPLATE/quality.md
@@ -36,7 +36,7 @@ labels: quality improvement
.\2_RunAllTests.ps1
.\3_SmokeTest.ps1
-合格の目安は 全ステップ OK / 8-8 差分 0 / 29-29。
+合格の目安は 全ステップ OK / 8-8 差分 0 / 30-30。
-->
## 利用者への影響
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 93bd39f6c..9496259b4 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -25,7 +25,7 @@
.\2_RunAllTests.ps1
.\3_SmokeTest.ps1
-合格の目安は 全ステップ OK / 8-8 差分 0 / 29-29。
+合格の目安は 全ステップ OK / 8-8 差分 0 / 30-30。
文書だけの変更なら「不要」と書いてください。
-->
diff --git a/root/programs/BUILDING.md b/root/programs/BUILDING.md
index 1282f679a..708fd480a 100644
--- a/root/programs/BUILDING.md
+++ b/root/programs/BUILDING.md
@@ -738,7 +738,7 @@ VS 18 のある環境では従来どおり `18.0` になるため、**挙動は
### 実測(run 30984111639 : 全ステップ成功)
> **当時の件数での記録。** その後、単体テストは 8 ケース(#520)、
-> 疎通は 29 件(#528、#546、#566、#570、#571、#578)である。**時間の目安として読むこと。**
+> 疎通は 30 件(#528、#546、#566、#570、#571、#578、#580)である。**時間の目安として読むこと。**
### 警告の内訳を見る(`-WarnDetail`、#571)
diff --git a/root/programs/CHEATSHEET.md b/root/programs/CHEATSHEET.md
index b76f9fe55..afed4c7b1 100644
--- a/root/programs/CHEATSHEET.md
+++ b/root/programs/CHEATSHEET.md
@@ -26,7 +26,7 @@ cd root\programs
|---|---|---|
| ビルド | 全ステップ OK | [`BUILDING.md`](BUILDING.md) |
| 単体テスト | 8/8 OK、差分 0 | [`TESTING.md`](TESTING.md) |
-| 疎通 | 29/29 OK | [`SMOKETEST.md`](SMOKETEST.md) |
+| 疎通 | 30/30 OK | [`SMOKETEST.md`](SMOKETEST.md) |
**既定は C# 側。VB 側は `-Lang` で回す**(3 節)。
diff --git a/root/programs/CONFIGURATION.md b/root/programs/CONFIGURATION.md
index b00222b20..27e7e9340 100644
--- a/root/programs/CONFIGURATION.md
+++ b/root/programs/CONFIGURATION.md
@@ -557,7 +557,7 @@ cd root\programs
| 見たいもの | 場所 |
|---|---|
| net10.0 の Web(本番向けの切り替えを含む) | `CS/Samples4NetCore/Backend/MVC_Sample/MVC_Sample/appsettings.json` |
-| 環境変数・`%変数%`・環境別ファイルの実運用 | `CS/Samples4NetCore/Docker/`([`README.md`](CS/Samples4NetCore/Docker/README.md)) |
+| 環境変数・`%変数%`・環境別ファイルの実運用 | `CS/Samples4NetCore/Docker/`(対象ごとに分かれる。[`MVC_Sample`](CS/Samples4NetCore/Docker/MVC_Sample/README.md) / [`ASPNETWebService`](CS/Samples4NetCore/Docker/ASPNETWebService/README.md)) |
| net48 の Web(`machineKey` / 転送ヘッダの注記) | `CS/Samples/WebApp_sample/MVC_Sample/MVC_Sample/Web.config` |
| 相対パスで自己完結させる書き方 | `CS/Samples4NetCore/Legacy/Bat_sample/*/appsettings.json` |
| 領域ごとの事情 | 各 `ANALYSIS.md`([Frameworks](CS/Frameworks/ANALYSIS.md) / [Samples](CS/Samples/ANALYSIS.md) / [Samples4NetCore](CS/Samples4NetCore/ANALYSIS.md)) |
diff --git a/root/programs/CS/Samples4NetCore/ANALYSIS.md b/root/programs/CS/Samples4NetCore/ANALYSIS.md
index 2cad73021..1fb4200ad 100644
--- a/root/programs/CS/Samples4NetCore/ANALYSIS.md
+++ b/root/programs/CS/Samples4NetCore/ANALYSIS.md
@@ -17,7 +17,7 @@ Open棟梁の **.NET (Core) 系サンプル**。4 つの区画に分かれてお
| **`Backend/`** | `MVC_Sample`(ASP.NET Core MVC) | **現行の推奨サンプル。** ここが本命 |
| **`Frontend/`** | README のみ | 別リポジトリ `FrontendTemplates` へ移動済み |
| **`Legacy/`** | `2CS_sample` / `Bat_sample` / `CLI_sample` / `WS_sample` | `../Samples`(net48)を .NET へ移植した**独立コピー**。名前どおり「レガシー」扱い |
-| **`Docker/`** | `Backend/MVC_Sample` を Linux コンテナで動かす一式 | サンプルのコードは持たない。**手順と設計は [`Docker/README.md`](Docker/README.md)**(#548) |
+| **`Docker/`** | `Backend/` のサンプルを Linux コンテナで動かす一式 | **対象ごとに分かれる**(`MVC_Sample` / `ASPNETWebService`)。サンプルのコードは持たない。**手順と設計は各 `README.md`**(#548、#582) |
**全プロジェクトが SDK 形式 csproj / `net10.0`(GUI は `net10.0-windows7.0`)。**
@@ -364,10 +364,17 @@ net48 側はコードから変えられない(`Request.IsSecureConnection` は
**IIS + ARR の URL Rewrite で `HTTPS` サーバ変数を立てる**
(`Samples/WebApp_sample/MVC_Sample` の `Web.config` にコメントで記載)。
-### 5-3. コンテナで動かす場合は `Docker/` を見る(#548)
+### 5-3. コンテナで動かす場合は `Docker/` を見る(#548、#582)
-`Backend/MVC_Sample` を Linux コンテナで動かす一式が `Docker/` にある。
-**手順・設計・落とし穴は [`Docker/README.md`](Docker/README.md) が一次情報**で、ここには書かない。
+`Backend/` のサンプルを Linux コンテナで動かす一式が `Docker/` にある。
+**対象ごとにフォルダが分かれる。**
+
+| 対象 | 一式 |
+|---|---|
+| `Backend/MVC_Sample` | [`Docker/MVC_Sample/`](Docker/MVC_Sample/README.md) |
+| `Backend/ASPNETWebService` | [`Docker/ASPNETWebService/`](Docker/ASPNETWebService/README.md) |
+
+**手順・設計・落とし穴は各 `README.md` が一次情報**で、ここには書かない。
上の 5-1 / 5-2 で書いた `%OT_RESOURCE_ROOT%` と 3 つの切り替えを、実際に使っている
(`appsettings.Container.json` + `docker-compose.yml` の環境変数)。
diff --git a/root/programs/CS/Samples4NetCore/Backend/ASPNETWebService/ASPNETWebService/ASPNETWebService.csproj b/root/programs/CS/Samples4NetCore/Backend/ASPNETWebService/ASPNETWebService/ASPNETWebService.csproj
index c64500833..5d27fe10e 100644
--- a/root/programs/CS/Samples4NetCore/Backend/ASPNETWebService/ASPNETWebService/ASPNETWebService.csproj
+++ b/root/programs/CS/Samples4NetCore/Backend/ASPNETWebService/ASPNETWebService/ASPNETWebService.csproj
@@ -7,6 +7,10 @@
+
+
+
diff --git a/root/programs/CS/Samples4NetCore/Backend/ASPNETWebService/ASPNETWebService/Startup.cs b/root/programs/CS/Samples4NetCore/Backend/ASPNETWebService/ASPNETWebService/Startup.cs
index b773ddb32..b22ef5f5d 100644
--- a/root/programs/CS/Samples4NetCore/Backend/ASPNETWebService/ASPNETWebService/Startup.cs
+++ b/root/programs/CS/Samples4NetCore/Backend/ASPNETWebService/ASPNETWebService/Startup.cs
@@ -15,23 +15,91 @@
//* 日時 更新者 内容
//* ---------- ---------------- -------------------------------------------------
//* 20xx/xx/xx XX XX XXXX
+//* 2026/08/21 玄人 幸道 OpenAPI(IDL)のドキュメント生成に対応(#580)
+//* 2026/08/21 玄人 幸道 MVC_Sample と同じ構成に揃えた(#582)。
+//* 環境を移しても動く形(#541)と、転送ヘッダ(#549)。
+//* **Cookie を使わないため、Cookie 由来の設定は
+//* コメントアウトで残す**(必要になったときの手本)。
//**********************************************************************************
+// <MVC_Sample との関係>(#582)
+//
+// **この Startup.cs は Backend/MVC_Sample/MVC_Sample/Startup.cs と
+// diff が取れる形にしてある。** 骨格を揃え、
+// Resource Server で使わないものはコメントアウトで残している。
+//
+// Resource Server は **OAuth2 の Bearer ヘッダ認証**であり、
+// Cookie 認証・セッション・静的ファイル・Razor を使わない。
+// そのため次は無効にしてある。**消していないのは、
+// 利用者がこのテンプレートから Cookie 認証へ広げるときの手本になるためである。**
+//
+// UseCookiePolicy / CookiePolicyOptions / UseSession / AddSession
+// AddDistributedMemoryCache / AddAuthentication + AddCookie
+// AddDataProtection(CookieSecurePolicy / DataProtectionKeyPath)
+// UseStaticFiles / MapRazorPages
+//
+// 逆に、こちらにしか無いのは CORS と OpenAPI である。
+
+// <設定で切り替えるもの>(#541)
+//
+// 本番でだけ有効にしたい設定を「コメントアウトして置いておく」と、
+// 環境を移すときにソースを書き換えることになる。
+// appsettings.json(および環境変数)で切り替えられるようにしてある。
+//
+// | キー(appSettings) | 既定 | on にすると |
+// |----------------------------------|--------|-------------|
+// | UseHttpsRedirection | off | HTTP を HTTPS へリダイレクトする |
+// | CookieSecurePolicy | (空) | always で Cookie に Secure 属性を必ず付ける |
+// | DataProtectionKeyPath | (空) | データ保護の鍵を、指定フォルダに永続化する |
+// | UseForwardedHeaders | off | X-Forwarded-Proto / -For を取り込む(#549) |
+// | ForwardedHeadersKnownProxies | (空) | 信用する前段のアドレス(空=範囲を制限しない) |
+//
+// **既定値は、いずれも従来どおりの動作**である。
+//
+// **CookieSecurePolicy と DataProtectionKeyPath は、この Resource Server では効かない。**
+// Cookie を使わないため。表に残してあるのは MVC_Sample と揃えるためで、
+// Cookie 認証を足すときに合わせて有効化する(#582)。
+//
+// <環境変数で上書きできる>
+//
+// Host.CreateDefaultBuilder が環境変数を構成に含めるため、
+// appsettings.json の値は環境変数で上書きできる。区切りは「__」(下線 2 つ)。
+//
+// appSettings__UseHttpsRedirection=on
+// appSettings__FxXMLSPDefinition=/app/files/resource/Xml/SPDefinition.xml
+// connectionStrings__ConnectionString_SQL=...
+//
+// **FxContainerization は要らない。** あちらは「接頭辞なしのキー名」で
+// 環境変数を読む別の仕組みで、ON にしたときだけ効く。
+
+using System;
+using System.IO;
+using System.Net;
+
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.HttpsPolicy;
+using Microsoft.AspNetCore.CookiePolicy;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.DataProtection;
+
+using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Caching.Memory;
-// using Microsoft.AspNetCore.Mvc.Cors.Internal;
+//using Microsoft.AspNetCore.Mvc.Cors.Internal;
using Touryo.Infrastructure.Framework.StdMigration;
using Touryo.Infrastructure.Public.Util;
+
namespace ASPNETWebService
{
///
@@ -70,26 +138,163 @@ public Startup(IConfiguration configuration)
///
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
+ // 転送ヘッダの取り込み(#549)
+ //
+ // **リバース プロキシで TLS を終端すると、アプリから見た接続は HTTP になる。**
+ // 利用者のブラウザは HTTPS で繋いでいるのに Request.IsHttps は false のままで、
+ // Secure 属性が付かない(#536 でフレームワークが立てるようにした分が効かない)。
+ //
+ // 前段が付ける X-Forwarded-Proto を取り込むと、IsHttps が正しくなる。
+ //
+ // **必ずパイプラインの先頭に置く。**
+ // 後ろに置くと、それより前のミドルウェア(UseHttpsRedirection など)が
+ // 取り込み前のスキームを見てしまう。
+ //
+ // 既定は off。素の HTTP で動かす開発環境では、転送ヘッダを
+ // 誰でも付けられる(=クライアントが詐称できる)ため。
+ //
+ // **Cookie を使わない Resource Server でも要る。**(#582)
+ // HTTPS へのリダイレクト判定と、ログに残る接続元アドレスが変わる。
+ if (Startup.IsOn("UseForwardedHeaders"))
+ {
+ ForwardedHeadersOptions options = new ForwardedHeadersOptions()
+ {
+ ForwardedHeaders = ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedFor
+ };
+
+ // 信用する前段を指定する。
+ //
+ // **既定ではループバックからの転送しか信用しない。**
+ // コンテナや Kubernetes では前段が別アドレスになるため、
+ // **指定しないとヘッダが黙って捨てられ、何も起きない。**
+ // 「on にしたのに直らない」の原因はほぼこれである。
+ //
+ // ForwardedHeadersKnownProxies に、前段のアドレスをカンマ区切りで書く。
+ string knownProxies = Startup.GetValue("ForwardedHeadersKnownProxies");
+
+ if (string.IsNullOrEmpty(knownProxies))
+ {
+ // **前段を特定できない場合(コンテナ等)は、範囲の制限を外す。**
+ // KnownIPNetworks / KnownProxies を空にすると、
+ // 「どこからの転送でも信用する」という意味になる。
+ // (KnownNetworks は .NET 10 で非推奨。KnownIPNetworks を使う)
+ //
+ // **アプリが前段を経由せず直接叩ける状態では使わないこと。**
+ // クライアントが X-Forwarded-Proto を詐称でき、
+ // HTTP で来ているのに HTTPS だと判断させられる。
+ // 前段だけが到達できるネットワークに閉じてから使う。
+ options.KnownIPNetworks.Clear();
+ options.KnownProxies.Clear();
+ }
+ else
+ {
+ options.KnownIPNetworks.Clear();
+ options.KnownProxies.Clear();
+
+ foreach (string ip in knownProxies.Split(','))
+ {
+ string trimmed = ip.Trim();
+
+ if (!string.IsNullOrEmpty(trimmed))
+ {
+ options.KnownProxies.Add(IPAddress.Parse(trimmed));
+ }
+ }
+ }
+
+ app.UseForwardedHeaders(options);
+ }
+
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
+ // **画面を持たないため、エラー画面へは飛ばさない。**(#582)
+ // MVC_Sample は app.UseExceptionHandler("/Home/Error") を使う。
+ // WebAPI では、例外はフレームワーク(MyBaseAsyncApiController)が
+ // 捕捉して JSON で返す。
+ //app.UseExceptionHandler("/Home/Error");
+
// The default HSTS value is 30 days.
// You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
- //app.UseHttpsRedirection();
+ }
+
+ // HTTPS へのリダイレクト(#541)
+ //
+ // **既定は off。** 平文 HTTP で動かす環境(疎通確認や、
+ // TLS を前段のリバース プロキシで終端する構成)でリダイレクトすると、
+ // 到達できなくなるため。
+ // TLS を自分で終端するなら on にする。
+ //
+ // **on にするだけでは足りない。リダイレクト先のポートも要る。**
+ // 決められないと、ミドルウェアは警告を出すだけで素通りする
+ // (見落としやすい。有効にしたのに HTTP のまま通ってしまう)。
+ //
+ // warn: ...HttpsRedirectionMiddleware[3]
+ // Failed to determine the https port for redirect.
+ //
+ // ポートは、次のいずれかで決まる。
+ // ・https の URL を Kestrel にバインドする(--urls に https://... を含める)
+ // ・環境変数 ASPNETCORE_HTTPS_PORT=443(単数)
+ // ・環境変数 HTTPS_PORT=443
+ //
+ // **ASPNETCORE_HTTPS_PORTS(複数)では決まらない。**
+ // あちらは Kestrel が「どのポートで待ち受けるか」を決めるもので、
+ // このミドルウェアが読むのは単数形の方である。紛らわしいので注意。
+ if (Startup.IsOn("UseHttpsRedirection"))
+ {
+ app.UseHttpsRedirection();
}
// HttpContextのマイグレーション用
app._UseHttpContextAccessor();
+ // **静的ファイルは持たない。**(#582)
+ // WebAPI なので /wwwroot が無い。
+ //app.UseStaticFiles();
+
+ // **Cookie を使わない。**(#582)
+ // Cookie ポリシーは ConfigureServices 側ごとコメントアウトしてある。
+ //app.UseCookiePolicy();
+
+ // **セッションを使わない。**(#582)
+ // Bearer トークンで都度認証するため、サーバ側に状態を持たない。
+ //app.UseSession(new SessionOptions()
+ //{
+ // IdleTimeout = TimeSpan.FromMinutes(30), // ここで調整
+ // IOTimeout = TimeSpan.FromSeconds(30),
+ // Cookie = new CookieBuilder()
+ // {
+ // HttpOnly = true,
+ // Name = "ws_session",
+ // Path = "/",
+ // SameSite = SameSiteMode.Strict,
+ // SecurePolicy = CookieSecurePolicy.SameAsRequest
+ // }
+ //});
+
+ // Routing
app.UseRouting();
+ // Identity
+ // Identityではなく、CookieAuthentication
+ //
+ // **Resource Server は Bearer ヘッダ認証である。**(#582)
+ // 認証は MyBaseAsyncApiController の属性
+ // (EnumHttpAuthHeader.Bearer)が行うため、
+ // ここでミドルウェアを挟まない。
//app.UseAuthentication();
//app.UseAuthorization();
+ // CORS(**こちらにしか無い**)
+ //
+ // ブラウザから直接叩ける Resource Server として公開するため。
+ // **AllowCredentials は付けない。**
+ // AllowAnyOrigin とは同時に指定できず、
+ // 資格情報を送らない構成として一貫させている。
app.UseCors( //認証・認可の後ろ
builder => builder
.AllowAnyOrigin()
@@ -98,9 +303,18 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
//.AllowCredentials());
+ // Routingの設定
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
+
+ // OpenAPI(IDL)を返す(#580)
+ // **開発時だけに絞っていない。**
+ // Resource Server のテンプレートとして、
+ // 利用側が IDL を引けることに意味があるため。
+ endpoints.MapOpenApi();
+
+ //endpoints.MapRazorPages();
});
}
@@ -117,6 +331,40 @@ public void ConfigureServices(IServiceCollection services)
// HttpContextのマイグレーション用
services._AddHttpContextAccessor();
+ // Cookie ポリシー(#541)
+ //
+ // **Cookie を使わないため無効にしてある。**(#582)
+ // Cookie 認証を足すときは、ここと Configure 側の
+ // app.UseCookiePolicy() を合わせて有効にする。
+ //services.Configure(options =>
+ //{
+ // options.HttpOnly = HttpOnlyPolicy.Always;
+ // options.MinimumSameSitePolicy = SameSiteMode.Strict;
+ //
+ // // Cookie の Secure 属性(既定は空=各 Cookie の設定に従う)
+ // //
+ // // TLS で公開するなら always にする。
+ // // 平文 HTTP の環境で always にすると、**Cookie が送られず
+ // // ログインできなくなる**ので、既定では変えない。
+ // if (Startup.GetValue("CookieSecurePolicy").ToLower() == "always")
+ // {
+ // options.Secure = CookieSecurePolicy.Always;
+ // }
+ //});
+
+ // Sessionのモード
+ //
+ // **セッションを使わないため無効にしてある。**(#582)
+ // Bearer トークンで都度認証する。
+ // セッションを足すなら、AddDistributedMemoryCache が
+ // プロセス内に持つ点(#541)に注意すること。
+ //services.AddDistributedMemoryCache(); // 開発用
+ //services.AddDistributedSqlServerCache(); // Microsoft.Extensions.Caching.SqlServer
+ //services.AddStackExchangeRedisCache(); // Microsoft.Extensions.Caching.StackExchangeRedis
+
+ // Sessionを使用する。
+ //services.AddSession();
+
// Core 3.0のテンプレートではUseMvcの
// 代わりにこれらを使用するようになった。
services
@@ -125,6 +373,101 @@ public void ConfigureServices(IServiceCollection services)
//services.AddControllersWithViews(); // MVC & WebAPI
// services.AddRazorPages(); // Razor Page
+
+ // OpenAPI(IDL)のドキュメント生成(#580)
+ //
+ // **.NET 9 以降は標準で入っている。** Swashbuckle は要らない。
+ // 既定では /openapi/v1.json で返る。
+ services.AddOpenApi();
+
+ #region Add Frameworks
+
+ // 一般的な Webアプリでは、
+ // EF, Identity, MVC などのミドルウェア サービスを登録する。
+ // ミドルウェアの実行順序は、IStartupFilter の登録順に設定される。
+
+ // EF
+ //services.AddDbContext(options =>
+ // options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
+
+ // Identity
+ //services.AddIdentity()
+ // .AddEntityFrameworkStores()
+ // .AddDefaultTokenProviders();
+
+ // Add application services.
+ //services.AddTransient();
+ //services.AddTransient();
+
+ // **AddMvc は要らない。**(#582)
+ // ビューを持たないため、上の AddControllers で足りる。
+ //services.AddMvc();
+
+ // Forms認証
+ //
+ // **Resource Server は Bearer ヘッダ認証である。**(#582)
+ // Cookie 認証を足すときに有効にする。
+ //services.AddAuthentication(options =>
+ //{
+ // options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ // options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ // options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
+ //})
+ //.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
+ //{
+ // options.LoginPath = new PathString("/Home/Login");
+ // options.AccessDeniedPath = new PathString(GetConfigParameter.GetConfigValue("FxErrorScreenPath"));
+ // options.ReturnUrlParameter = "ReturnUrl";
+ // options.ExpireTimeSpan = TimeSpan.FromHours(1);
+ // options.SlidingExpiration = true;
+ // options.Cookie.HttpOnly = true;
+ //});
+
+ // データ保護の鍵の置き場所(#541)
+ //
+ // 認証 Cookie・セッション・Antiforgery トークンは、この鍵で保護されている。
+ // **鍵が変わると、既存の Cookie が読めなくなり、ログインし直しになる。**
+ //
+ // **この Resource Server では、いずれも使っていない。**(#582)
+ // Cookie 認証やセッションを足すときに、合わせて有効にする。
+ // コンテナでは**必ず要る**(作り直すたびに鍵が変わるため)。
+ //string keyPath = Startup.GetValue("DataProtectionKeyPath");
+ //
+ //if (!string.IsNullOrEmpty(keyPath))
+ //{
+ // services.AddDataProtection()
+ // .PersistKeysToFileSystem(new DirectoryInfo(keyPath))
+ // // **アプリケーション名も固定する。**
+ // // 既定ではコンテンツ ルートのパスから決まるため、
+ // // 鍵を共有していても**配置先のパスが違うと復号できない**。
+ // .SetApplicationName("ASPNETWebService");
+ //}
+
+ #endregion
+ }
+
+ #endregion
+
+ #region 設定の読み取り
+
+ /// appSettings の値を取得する(無ければ空文字)
+ /// キー
+ /// 値
+ ///
+ /// 環境変数 appSettings__<キー> でも上書きできる(#541)。
+ /// Host.CreateDefaultBuilder が環境変数を構成に含めるため。
+ ///
+ private static string GetValue(string key)
+ {
+ return GetConfigParameter.GetConfigValue(key) ?? "";
+ }
+
+ /// appSettings の値が on かを判定する
+ /// キー
+ /// on なら true
+ private static bool IsOn(string key)
+ {
+ return Startup.GetValue(key).ToLower() == "on";
}
#endregion
diff --git a/root/programs/CS/Samples4NetCore/Docker/.dockerignore b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/.dockerignore
similarity index 100%
rename from root/programs/CS/Samples4NetCore/Docker/.dockerignore
rename to root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/.dockerignore
diff --git a/root/programs/CS/Samples4NetCore/Docker/.gitignore b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/.gitignore
similarity index 100%
rename from root/programs/CS/Samples4NetCore/Docker/.gitignore
rename to root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/.gitignore
diff --git a/root/programs/CS/Samples4NetCore/Docker/0_SetupCert.ps1 b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/0_SetupCert.ps1
similarity index 100%
rename from root/programs/CS/Samples4NetCore/Docker/0_SetupCert.ps1
rename to root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/0_SetupCert.ps1
diff --git a/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/1_PublishAndUp.bat b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/1_PublishAndUp.bat
new file mode 100644
index 000000000..8ef2e9cfa
--- /dev/null
+++ b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/1_PublishAndUp.bat
@@ -0,0 +1,66 @@
+@echo off
+rem NOTE: keep this file pure ASCII (root/programs/CODING.md section 4).
+rem Japanese explanations are in README.md.
+rem
+rem Host-build type: publish on the host, then copy into the runtime image.
+rem See README.md and #582.
+
+setlocal
+pushd "%~dp0"
+
+set SLN_DIR=..\..\Backend\ASPNETWebService\ASPNETWebService
+set FX_DLL=..\..\..\Frameworks\Infrastructure\Build_netcore100\net10.0\OpenTouryo.Framework.dll
+
+echo === 1/4 : checking prerequisites ===
+
+if not exist "%FX_DLL%" (
+ echo [ERROR] Framework is not built: %FX_DLL%
+ echo Build the framework first ^(root\programs\1_BuildAll.ps1^).
+ goto :fail
+)
+
+if not exist ".\https\aspnetapp.pem" (
+ echo [ERROR] .\https\aspnetapp.pem not found.
+ echo Run 0_SetupCert.ps1 first.
+ goto :fail
+)
+
+docker network inspect common_link >nul 2>&1
+if errorlevel 1 (
+ echo [ERROR] docker network "common_link" not found.
+ echo Start LocalServicesOnDocker first ^(it owns the network and the DB^).
+ echo https://github.com/NetDevInfraWGinOSSConsortium/LocalServicesOnDocker
+ goto :fail
+)
+
+echo === 2/4 : dotnet publish ===
+if exist ".\publish" rmdir /s /q ".\publish"
+dotnet publish "%SLN_DIR%\ASPNETWebService.csproj" -c Release -o ".\publish"
+if errorlevel 1 goto :fail
+
+if not exist ".\publish\ASPNETWebService.dll" (
+ echo [ERROR] publish output not found: .\publish\ASPNETWebService.dll
+ goto :fail
+)
+
+echo === 3/4 : docker compose up --build ===
+docker compose up --build -d
+if errorlevel 1 goto :fail
+
+echo === 4/4 : done ===
+echo HTTP : http://localhost:8090 ^(redirects to HTTPS^)
+echo HTTPS : https://localhost:8091
+echo IDL : https://localhost:8091/openapi/v1.json
+echo.
+echo logs : docker compose logs -f
+echo stop : 2_Down.bat
+popd
+endlocal
+exit /b 0
+
+:fail
+echo.
+echo [FAILED]
+popd
+endlocal
+exit /b 1
diff --git a/root/programs/CS/Samples4NetCore/Docker/2_Down.bat b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/2_Down.bat
similarity index 100%
rename from root/programs/CS/Samples4NetCore/Docker/2_Down.bat
rename to root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/2_Down.bat
diff --git a/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/3_Test.ps1 b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/3_Test.ps1
new file mode 100644
index 000000000..6d22513bc
--- /dev/null
+++ b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/3_Test.ps1
@@ -0,0 +1,291 @@
+#Requires -Version 5.1
+<#
+.SYNOPSIS
+ コンテナで動いている ASPNETWebService を確かめる(#582)。
+
+.DESCRIPTION
+ **この Resource Server には画面が無い。**
+ `MVC_Sample` はブラウザで開けば動作が分かるが、こちらは WebAPI だけなので、
+ 確かめる手段(クライアント)が要る。それがこのスクリプトである。
+
+ `1_PublishAndUp.bat` でコンテナを起動した状態で実行する。
+
+ <見るもの>
+
+ 1. HTTP → HTTPS のリダイレクト(appsettings.Container.json の UseHttpsRedirection=on)
+ 2. OpenAPI(IDL)が仕様として読める形で返ること(#580)
+ 3. WebAPI が応答すること(DB を使わないもの/使うもの)
+
+ **どれも「200 が返る」だけでは足りない。**
+ リダイレクト先・IDL の中身・応答の JSON まで見る。
+
+ <自己署名証明書>
+
+ `0_SetupCert.ps1` が作る開発用証明書は自己署名なので、検証を通さないと繋がらない。
+ **このスクリプトの中だけで**検証を無効にしている(プロセス全体には残さない)。
+
+.PARAMETER HttpPort
+ HTTP のポート。既定は 8090(docker-compose.yml のホスト側)。
+
+.PARAMETER HttpsPort
+ HTTPS のポート。既定は 8091。
+
+.EXAMPLE
+ powershell -NoProfile -ExecutionPolicy Bypass -File .\3_Test.ps1
+
+.NOTES
+ 作成者 :玄人 幸道
+ 更新履歴 :
+ 日時 更新者 内容
+ ---------- ---------------- -------------------------------------------------
+ 2026/08/21 玄人 幸道 新規作成(#582)
+#>
+[CmdletBinding()]
+param(
+ [int]$HttpPort = 8090,
+ [int]$HttpsPort = 8091
+)
+
+$ErrorActionPreference = "Stop"
+
+$httpBase = "http://localhost:$HttpPort"
+$httpsBase = "https://localhost:$HttpsPort"
+
+$script:ng = 0
+
+function Write-Result([string]$name, [bool]$ok, [string]$detail)
+{
+ if ($ok)
+ {
+ Write-Host (" OK {0,-34} {1}" -f $name, $detail) -ForegroundColor Green
+ }
+ else
+ {
+ Write-Host (" NG {0,-34} {1}" -f $name, $detail) -ForegroundColor Red
+ $script:ng++
+ }
+}
+
+# ------------------------------------------------------------------
+# 自己署名証明書を通す
+# ------------------------------------------------------------------
+# **5.1 と 7 で書き方が違う。**
+# 7 は Invoke-WebRequest -SkipCertificateCheck が使えるが、5.1 には無い。
+# 5.1 は ServicePointManager のコールバックで通す。
+[Net.ServicePointManager]::SecurityProtocol =
+ [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
+
+$isPS7 = $PSVersionTable.PSVersion.Major -ge 6
+
+if (-not $isPS7)
+{
+ Add-Type @"
+using System.Net;
+using System.Security.Cryptography.X509Certificates;
+public class SmokeCertPolicy : ICertificatePolicy {
+ public bool CheckValidationResult(ServicePoint sp, X509Certificate cert, WebRequest req, int problem) {
+ return true;
+ }
+}
+"@
+ [Net.ServicePointManager]::CertificatePolicy = New-Object SmokeCertPolicy
+}
+
+function Invoke-Api
+{
+ param([string]$Uri, [string]$Method = "GET", [switch]$NoRedirect)
+
+ $a = @{ Uri = $Uri; Method = $Method; UseBasicParsing = $true; TimeoutSec = 30 }
+ if ($NoRedirect) { $a["MaximumRedirection"] = 0 }
+ if ($isPS7) { $a["SkipCertificateCheck"] = $true }
+
+ try
+ {
+ return @{ Ok = $true; Res = (Invoke-WebRequest @a) }
+ }
+ catch
+ {
+ # **リダイレクトは例外になることがある。** 応答が取れれば、そこから読む。
+ $r = $_.Exception.Response
+ if ($null -ne $r) { return @{ Ok = $false; Res = $r; Ex = $_ } }
+ return @{ Ok = $false; Res = $null; Ex = $_ }
+ }
+}
+
+Write-Host ""
+Write-Host "============================================"
+Write-Host " ASPNETWebService (Docker) の確認"
+Write-Host "============================================"
+Write-Host (" HTTP : {0}" -f $httpBase)
+Write-Host (" HTTPS : {0}" -f $httpsBase)
+Write-Host ""
+
+# ------------------------------------------------------------------
+# 1. HTTP → HTTPS のリダイレクト
+# ------------------------------------------------------------------
+# **リダイレクト先まで見る。** 3xx が返るだけでは、どこへ飛ぶか分からない。
+$r = Invoke-Api ($httpBase + "/openapi/v1.json") -NoRedirect
+
+$status = 0
+$location = ""
+
+if ($null -ne $r.Res)
+{
+ $status = [int]$r.Res.StatusCode
+ try { $location = [string]$r.Res.Headers["Location"] } catch { }
+ if (-not $location -and $r.Res.PSObject.Properties.Name -contains "Headers")
+ {
+ try { $location = [string]$r.Res.Headers.Location } catch { }
+ }
+}
+
+if ($status -lt 300 -or $status -ge 400 -or $location -notlike "https://*")
+{
+ Write-Result "HTTP -> HTTPS リダイレクト" $false ("status={0} location={1}" -f $status, $location)
+}
+elseif ($location -notlike ("*:{0}/*" -f $HttpsPort))
+{
+ # **飛び先のポートまで見る。**(#582)
+ # 3xx が返り https で始まるだけでは足りない。
+ # コンテナ内のポート(8081)へ飛ばしていて、ホストから辿れなかった。
+ # UseHttpsRedirection は ASPNETCORE_HTTPS_PORT(**単数形**)を読む。
+ Write-Result "HTTP -> HTTPS リダイレクト" $false `
+ ("飛び先のポートが違う(ホストから辿れない) : {0}" -f $location)
+}
+else
+{
+ # **実際に辿れることまで確かめる。**
+ $f = Invoke-Api $location
+
+ if ($f.Ok)
+ {
+ Write-Result "HTTP -> HTTPS リダイレクト" $true `
+ ("{0} -> {1} (辿れる)" -f $status, $location)
+ }
+ else
+ {
+ Write-Result "HTTP -> HTTPS リダイレクト" $false `
+ ("飛び先へ辿れない : {0}" -f $location)
+ }
+}
+
+# ------------------------------------------------------------------
+# 2. OpenAPI(IDL)
+# ------------------------------------------------------------------
+# **200 が返るだけでは足りない。** 壊れた文書でも 200 は返る。
+$r = Invoke-Api ($httpsBase + "/openapi/v1.json")
+
+if (-not $r.Ok)
+{
+ Write-Result "OpenAPI (IDL)" $false ("取得できない : " + $r.Ex.Exception.Message)
+}
+else
+{
+ $doc = $null
+ try { $doc = $r.Res.Content | ConvertFrom-Json } catch { }
+
+ if ($null -eq $doc)
+ {
+ Write-Result "OpenAPI (IDL)" $false "JSON として読めない"
+ }
+ elseif (-not $doc.openapi)
+ {
+ Write-Result "OpenAPI (IDL)" $false "openapi の版が無い"
+ }
+ else
+ {
+ $paths = @($doc.paths.PSObject.Properties.Name)
+
+ # **代表的な API が名前で載っていること。**
+ # 件数だけだと、コントローラが入れ替わっても気づけない。
+ $want = @("/api/Json/Select", "/api/BatchUpdate/BatchUpdate")
+ $miss = @($want | Where-Object { $paths -notcontains $_ })
+
+ if ($miss.Count -gt 0)
+ {
+ Write-Result "OpenAPI (IDL)" $false ("載っていない : " + ($miss -join ", "))
+ }
+ else
+ {
+ Write-Result "OpenAPI (IDL)" $true ("openapi {0} / paths {1} 件" -f $doc.openapi, $paths.Count)
+ }
+ }
+}
+
+# ------------------------------------------------------------------
+# 3. WebAPI(DB を使わない)
+# ------------------------------------------------------------------
+# **まず DB に依らないもので、アプリが動いていることを確かめる。**
+# ここが通れば、次が失敗したときに「DB 側の問題」と切り分けられる。
+#
+# **メソッドは IDL のとおり GET。**(#582)
+# 最初 POST で叩いて 405 になった。**IDL に書いてある**(paths./api/Json/test.get)。
+$r = Invoke-Api ($httpsBase + "/api/Json/test") -Method GET
+
+if (-not $r.Ok)
+{
+ Write-Result "WebAPI /api/Json/test" $false ("応答が無い : " + $r.Ex.Exception.Message)
+}
+else
+{
+ $body = $r.Res.Content
+ Write-Result "WebAPI /api/Json/test" $true ("{0} / {1} バイト" -f [int]$r.Res.StatusCode, $body.Length)
+}
+
+# ------------------------------------------------------------------
+# 4. WebAPI(DB を使う)
+# ------------------------------------------------------------------
+# **リソース(Sql / Xml)とデータベース接続まで通ることを見る。**
+# コンテナ側の %OT_RESOURCE_ROOT% と connectionStrings が効いているかの確認になる。
+$r = Invoke-Api ($httpsBase + "/api/BatchUpdate/SelectCount") -Method POST
+
+if (-not $r.Ok)
+{
+ Write-Result "WebAPI /api/BatchUpdate/SelectCount" $false ("応答が無い : " + $r.Ex.Exception.Message)
+}
+else
+{
+ $body = $r.Res.Content
+ $ok = $false
+ $detail = $body
+
+ try
+ {
+ $j = $body | ConvertFrom-Json
+
+ # 件数が返ること(0 以上の数値)
+ if ($null -ne $j)
+ {
+ $props = @($j.PSObject.Properties.Name)
+ $detail = ("{0} / {1}" -f [int]$r.Res.StatusCode, ($props -join ", "))
+ $ok = $true
+ }
+ }
+ catch
+ {
+ $detail = "JSON として読めない : " + $body
+ }
+
+ Write-Result "WebAPI /api/BatchUpdate/SelectCount" $ok $detail
+}
+
+# ------------------------------------------------------------------
+# 判定
+# ------------------------------------------------------------------
+Write-Host ""
+Write-Host "============================================"
+
+if ($script:ng -eq 0)
+{
+ Write-Host " すべて OK です。" -ForegroundColor Green
+ Write-Host "============================================"
+ Write-Host ""
+ exit 0
+}
+
+Write-Host (" **{0} 件が NG**" -f $script:ng) -ForegroundColor Red
+Write-Host "============================================"
+Write-Host ""
+Write-Host " コンテナのログ : docker compose logs -f"
+Write-Host ""
+exit 1
diff --git a/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/Dockerfile b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/Dockerfile
new file mode 100644
index 000000000..a776460d0
--- /dev/null
+++ b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/Dockerfile
@@ -0,0 +1,47 @@
+# ホストビルド型の Dockerfile(#582)
+#
+# ホスト側で `dotnet publish` した成果物を、ランタイム イメージへ置くだけ。
+# **コンテナ内ではビルドしない。**
+#
+# 理由:ASPNETWebService.csproj はフレームワークを
+# ..\Frameworks\Infrastructure\Build_netcore100\net10.0\*.dll
+# というビルド済み DLL への相対参照で持っている。
+# Frameworks を先にビルドしないと restore すら通らないため、
+# コンテナ内でビルドするならビルド コンテキストを root/programs/CS 全体へ
+# 広げることになる(Samples4NetCore/ANALYSIS.md 2-1)。
+#
+# publish は 1_PublishAndUp.bat が行う。
+#
+# **MVC_Sample/Dockerfile と diff が取れる形にしてある。**
+# 違うのは ENTRYPOINT の DLL 名だけである。
+
+FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
+
+WORKDIR /app
+
+# publish 成果物
+COPY ./publish/ ./
+
+# コンテナ用の設定(appsettings.json を上書きする。ASPNETCORE_ENVIRONMENT=Container で効く)
+COPY ./appsettings.Container.json ./
+# コンテナ用の log4net 定義(出力先を %env{OT_LOG_ROOT})
+COPY ./conf/ ./conf/
+
+# ログと鍵の置き場所を、非 root ユーザで書けるようにしておく。
+#
+# **名前付きボリュームは、初回にイメージ側の所有者と権限を引き継ぐ。**
+# ここで chown しておかないと、マウント後に root 所有のままとなり、
+# USER で切り替えた非 root ユーザが書けない。
+#
+# **keys は、この Resource Server では使っていない。**(#582)
+# Cookie 認証やセッションを足すときに要る(Startup.cs のコメント参照)ため、
+# 置き場所だけ MVC_Sample と揃えてある。
+RUN mkdir -p /app/logs /app/keys && chown -R $APP_UID /app/logs /app/keys
+
+# 非 root で動かす(ベース イメージが用意している app ユーザ)
+USER $APP_UID
+
+EXPOSE 8080
+EXPOSE 8081
+
+ENTRYPOINT ["dotnet", "ASPNETWebService.dll"]
diff --git a/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/README.md b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/README.md
new file mode 100644
index 000000000..c0a7d3161
--- /dev/null
+++ b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/README.md
@@ -0,0 +1,118 @@
+# ASPNETWebService を Docker で動かす
+
+`Samples4NetCore/Backend/ASPNETWebService`(ResourceServerTemplate、net10.0)を
+Linux コンテナで動かす(#582)。
+
+**構成は [`../MVC_Sample/README.md`](../MVC_Sample/README.md) と同じである。**
+方式(ホストビルド型)・設定の置き方・証明書の扱い・本番へ持って行くときの注意は、
+**あちらが一次情報**なので、ここには書かない。
+
+ここに書くのは**この Resource Server だけの事情**である。
+
+---
+
+## 前提
+
+`MVC_Sample` と同じ。DB は
+[LocalServicesOnDocker](https://github.com/NetDevInfraWGinOSSConsortium/LocalServicesOnDocker)
+が提供するものに相乗りするので、**先に起動しておくこと。**
+
+## 手順
+
+```powershell
+# 1. HTTPS の開発用証明書を作る(初回のみ)
+powershell -NoProfile -ExecutionPolicy Bypass -File .\0_SetupCert.ps1
+
+# 2. publish してコンテナを起動する
+.\1_PublishAndUp.bat
+
+# 3. 動いているか確かめる(**画面が無いので、これがクライアント**)
+powershell -NoProfile -ExecutionPolicy Bypass -File .\3_Test.ps1
+
+# 4. 止める
+.\2_Down.bat
+```
+
+```
+HTTP : http://localhost:8090 (HTTPS へリダイレクト)
+HTTPS : https://localhost:8091
+IDL : https://localhost:8091/openapi/v1.json
+```
+
+---
+
+## `MVC_Sample` との違い
+
+### ① ホスト側のポートが `8090` / `8091`
+
+**`MVC_Sample`(8080 / 8081)と同時に起動できるようにしてある。**
+Resource Server を MVC から呼ぶ構成を、そのまま試せる。
+
+コンテナ内の待ち受けは両者とも `8080` / `8081` のままである
+(`ASPNETCORE_HTTP_PORTS` / `ASPNETCORE_HTTPS_PORTS`)。
+**変えているのは `docker-compose.yml` の `ports` の左側だけ。**
+
+### ② Cookie を使わない
+
+**OAuth2 の Resource Server であり、認証は `Authorization: Bearer` ヘッダである。**
+Cookie 認証・セッションを持たないため、次は設定していない。
+
+| | `MVC_Sample` | こちら |
+|---|---|---|
+| `CookieSecurePolicy` | `always` | **書かない**(効かない) |
+| `DataProtectionKeyPath` | `/app/keys` | **書かない**(同上) |
+
+`Startup.cs` 側もコメントアウトで残してある。
+**消していないのは、Cookie 認証へ広げるときの手本になるためである。**
+
+> **`/app/keys` のボリュームだけは用意してある。**
+> 今は使わないが、Cookie 認証を足した時点で**必ず要る**
+> (コンテナを作り直すたびに鍵が変わり、認証 Cookie が失効するため)。
+> そのとき `docker-compose.yml` を書き換えずに済む。
+
+### ③ 画面が無い
+
+静的ファイル(`/wwwroot`)も Razor も持たない。
+ブラウザで確認するなら **IDL(`/openapi/v1.json`)** を開く。
+
+---
+
+## 動作の確認は `3_Test.ps1` で行う
+
+**画面が無いため、確かめる手段(クライアント)が要る。** それがこのスクリプトである。
+
+```
+OK HTTP -> HTTPS リダイレクト 307 -> https://localhost:8091/... (辿れる)
+OK OpenAPI (IDL) openapi 3.1.1 / paths 16 件
+OK WebAPI /api/Json/test 200
+OK WebAPI /api/BatchUpdate/SelectCount 200 / count
+```
+
+**どれも「200 が返る」だけでは足りない。**
+
+- リダイレクトは**飛び先のポートと、実際に辿れるか**まで見る
+- IDL は `openapi` の版と、**代表的な API が名前で載っているか**まで見る
+ (疎通テストの判定も同じ。[`SMOKETEST.md`](../../../../SMOKETEST.md) 3 節)
+- `SelectCount` は **DB まで通る**ので、リソースと接続文字列の確認になる
+
+> **リダイレクトの判定は、最初「3xx かつ https で始まる」だけだった。**
+> それでは飛び先がコンテナ内のポート(8081)でも OK になり、
+> **実際にホストから辿れない状態を見逃していた**(下記)。
+
+### 踏んだところ : リダイレクト先はホスト側のポートで与える
+
+`UseHttpsRedirection` が読むのは **単数形の `ASPNETCORE_HTTPS_PORT`** である
+(複数形の `ASPNETCORE_HTTPS_PORTS` は Kestrel の待ち受け。`Startup.cs` のコメント)。
+
+**ここにコンテナ内の 8081 を与えると、ホストから辿れない URL へ飛ばす。**
+`ports` で `8091:8081` に付け替えているため。
+`docker-compose.yml` で `ASPNETCORE_HTTPS_PORT=8091` を与えている。
+
+**`MVC_Sample` は 1:1(8080/8081)なので、この指定が要らない。**
+
+---
+
+## 対象外
+
+**`Samples/WS_sample/ASPNETWebService`(net48)は対象外。**
+クラシック ASP.NET であり、Linux コンテナでは動かない。
diff --git a/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/appsettings.Container.json b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/appsettings.Container.json
new file mode 100644
index 000000000..ec816dc16
--- /dev/null
+++ b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/appsettings.Container.json
@@ -0,0 +1,48 @@
+// コンテナで動かすときの設定(#582)
+//
+// **ASPNETCORE_ENVIRONMENT=Container のときだけ読まれ、ここに書いたキーだけを
+// 上書きする**(丸ごと差し替えではない)。さらに環境変数の方が強い。
+//
+// appsettings.json → appsettings.Container.json → 環境変数
+//
+// 仕組みは CONFIGURATION.md 3〜5 節。%OT_RESOURCE_ROOT% の値は docker-compose.yml で与える。
+// このサンプル固有の事情は README.md。
+{
+ "appSettings": {
+
+ // フレームワークの使用するパラメータ - start
+ //
+ // **フォルダ名は Xml(XML ではない)。** Linux では綴りが違うと開けない。
+ // appsettings.json 側は "C:/root/files/resource/XML/..." だが、
+ // **Windows は大文字小文字を区別しないので、あちらは XML のままでも開ける。**
+ "FxXMLSPDefinition": "%OT_RESOURCE_ROOT%/Xml/SPDefinition.xml",
+ "FxXMLMSGDefinition": "%OT_RESOURCE_ROOT%/Xml/MSGDefinition.xml",
+ "FxXMLTCDefinition": "%OT_RESOURCE_ROOT%/Xml/TCDefinition.xml",
+ "FxXMLTMInProcessDefinition": "%OT_RESOURCE_ROOT%/Xml/TMInProcessDefinition.xml",
+ // フレームワークの使用するパラメータ - end
+
+ // 共通部品の使用するパラメータ - start
+ //
+ // log4net の定義はコンテナ専用のものを使う(出力先が違うため。README.md 参照)。
+ "FxLog4NetConfFile": "/app/conf/SampleLogConf.xml",
+ // 共通部品の使用するパラメータ - end
+
+ // 本番向けの切り替え - start
+ //
+ // HTTPS で公開するので、**本番と同じ設定のまま動かせる**。
+ "UseHttpsRedirection": "on",
+
+ // **CookieSecurePolicy と DataProtectionKeyPath は書いていない。**(#582)
+ // この Resource Server は Bearer ヘッダ認証で、Cookie もセッションも持たない。
+ // Startup.cs 側もコメントアウトしてあるため、書いても効かない。
+ // Cookie 認証を足すときに、Startup.cs と合わせて有効にする。
+ //"CookieSecurePolicy": "always",
+ //"DataProtectionKeyPath": "/app/keys",
+ // 本番向けの切り替え - end
+
+ // アプリケーションの使用するパラメータ - start
+ "SpRp_RsaCerFilePath": "%OT_RESOURCE_ROOT%/X509/SHA256RSA_Server.cer",
+ "SqlTextFilePath": "%OT_RESOURCE_ROOT%/Sql"
+ // アプリケーションの使用するパラメータ - end
+ }
+}
diff --git a/root/programs/CS/Samples4NetCore/Docker/conf/SampleLogConf.xml b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/conf/SampleLogConf.xml
similarity index 100%
rename from root/programs/CS/Samples4NetCore/Docker/conf/SampleLogConf.xml
rename to root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/conf/SampleLogConf.xml
diff --git a/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/docker-compose.yml b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/docker-compose.yml
new file mode 100644
index 000000000..9b097793e
--- /dev/null
+++ b/root/programs/CS/Samples4NetCore/Docker/ASPNETWebService/docker-compose.yml
@@ -0,0 +1,96 @@
+# ASPNETWebService(ResourceServerTemplate、net10.0)を Docker で動かす(#582)
+#
+# 前提:DB は LocalServicesOnDocker が提供する。先に起動しておくこと。
+# https://github.com/NetDevInfraWGinOSSConsortium/LocalServicesOnDocker
+# あちらが作る common_link に相乗りし、サービス名 sqlserver で届く。
+#
+# <値をどこに書くか>
+# ・配置に依らない「コンテナならこう」 → appsettings.Container.json
+# ・配置ごとに変わる値・秘密 → ここ(environment)
+# 読み込み順は appsettings.json → appsettings.Container.json → 環境変数で、
+# **後が勝つ**ので、ここに書けば JSON より優先される。
+#
+# <MVC_Sample との違い>(#582)
+# **ホスト側のポートを 8090 / 8091 にしてある。**
+# MVC_Sample(8080 / 8081)と**同時に起動できる**ようにするため。
+# コンテナ内の待ち受けは両者とも 8080 / 8081 のままでよい。
+
+services:
+ ws-sample:
+ image: opentouryo-ws-sample
+ build:
+ context: .
+ dockerfile: Dockerfile
+ ports:
+ # **左がホスト側。** MVC_Sample と重ならない番号にしてある。
+ - "8090:8080"
+ - "8091:8081"
+ environment:
+ # Container 環境として起動する(appsettings.Container.json が読まれる)。
+ # **Development にはしない。** 開発者例外ページが出てしまうため。
+ - ASPNETCORE_ENVIRONMENT=Container
+
+ # 待ち受けポート
+ - ASPNETCORE_HTTP_PORTS=8080
+ - ASPNETCORE_HTTPS_PORTS=8081
+
+ # HTTPS へのリダイレクト先のポート(#582)
+ #
+ # **単数形。** 複数形の ASPNETCORE_HTTPS_PORTS は
+ # 「Kestrel がどこで待ち受けるか」であって、
+ # UseHttpsRedirection が読むのはこちらである(Startup.cs のコメント)。
+ #
+ # **ホスト側の番号を与える。**
+ # コンテナ内の 8081 を書くと、ホストから辿れない URL へ飛ばしてしまう
+ # (ports で 8091:8081 に付け替えているため)。
+ # MVC_Sample は 1:1 なので、この指定が要らない。
+ - ASPNETCORE_HTTPS_PORT=8091
+
+ # 証明書(PEM + 秘密鍵。**パスワードを持たない**)
+ - Kestrel__Certificates__Default__Path=/https/aspnetapp.pem
+ - Kestrel__Certificates__Default__KeyPath=/https/aspnetapp.key
+
+ # リソースの置き場所
+ #
+ # Open棟梁は設定値の中の %変数% を自前で展開する
+ # (ResourceLoader.ResolveFilePath → StringVariableOperator)。
+ # Windows の書式に依存しない実装なので、Linux コンテナでも効く。
+ - OT_RESOURCE_ROOT=/app/files/resource
+ # ログの出力先(**リソースは ro でマウントするので、ログは別の場所へ**)
+ - OT_LOG_ROOT=/app/logs
+
+ # 接続文字列
+ #
+ # **Data Source は localhost ではなく sqlserver。**
+ # コンテナから見た localhost は自分自身であり、DB には届かない。
+ # パスワードを直に書いているのは、すぐ動かせるサンプルだからである。
+ # 実際の環境では secrets やシークレット ストアから渡すこと(README 参照)。
+ - connectionStrings__ConnectionString_SQL=Data Source=sqlserver;Initial Catalog=Northwind;User ID=sa;Password=seigi@123;Encrypt=false;
+
+ volumes:
+ # 定義ファイル・SQL・証明書(読み取り専用)
+ - ../../../../../files/resource:/app/files/resource:ro
+ # HTTPS の開発用証明書(0_SetupCert.ps1 が作る。読み取り専用)
+ - ./https:/https:ro
+ # ログ(書き込む)
+ - ws-sample-logs:/app/logs
+ # データ保護の鍵
+ #
+ # **この Resource Server では、まだ使っていない。**(#582)
+ # Bearer ヘッダ認証で、Cookie もセッションも持たないため。
+ # Cookie 認証を足すときに効いてくる(Startup.cs のコメント参照)ので、
+ # MVC_Sample と揃えて用意してある。
+ - ws-sample-keys:/app/keys
+
+ networks:
+ - common_link
+
+volumes:
+ ws-sample-logs:
+ ws-sample-keys:
+
+networks:
+ # LocalServicesOnDocker が作るネットワークに相乗りする。
+ # 先に向こうを起動しておくこと(無いと compose がエラーになる)。
+ common_link:
+ external: true
diff --git a/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/.dockerignore b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/.dockerignore
new file mode 100644
index 000000000..45fac3788
--- /dev/null
+++ b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/.dockerignore
@@ -0,0 +1,12 @@
+# ビルド コンテキストから除くもの(#548)
+#
+# **https/ は必ず除く。** 秘密鍵をイメージに焼かないため。
+# 証明書は実行時にボリュームでマウントする(docker-compose.yml)。
+https/
+
+# 手順・スクリプトはイメージに要らない
+README.md
+*.bat
+*.ps1
+.gitignore
+.dockerignore
diff --git a/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/.gitignore b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/.gitignore
new file mode 100644
index 000000000..7be6e4754
--- /dev/null
+++ b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/.gitignore
@@ -0,0 +1,6 @@
+# dotnet publish の成果物(1_PublishAndUp.bat が作る)
+publish/
+
+# HTTPS の開発用証明書と秘密鍵(0_SetupCert.ps1 が作る)
+# **秘密鍵をリポジトリに入れない。**
+https/
diff --git a/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/0_SetupCert.ps1 b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/0_SetupCert.ps1
new file mode 100644
index 000000000..137f537d4
--- /dev/null
+++ b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/0_SetupCert.ps1
@@ -0,0 +1,104 @@
+<#
+.SYNOPSIS
+ HTTPS の開発用証明書を、PEM 形式で書き出す(#548)。
+
+.DESCRIPTION
+ <パスワードを持たせない>
+
+ 参照元(ASPNETMVCOnDocker)は PFX + .env の CERT_PASSWORD を使うが、
+ ここでは PEM + 秘密鍵ファイルにして、**パスワードという管理対象を無くす**。
+
+ ・.env が要らない(平文パスワードのファイルを作らない)
+ ・compose での ${CERT_PASSWORD} 展開が要らない
+ ・初回に決めるパスワードが無い
+
+ 守る対象は localhost 限定・自己署名・1 年で失効する開発用証明書であり、
+ 本番の TLS は前段のリバース プロキシが終端する想定である(README 参照)。
+ PFX + パスワードにしても、結局 PFX とパスワードの両方がホストに置かれるので、
+ 実質の防御力は変わらない。守るのはファイルの権限である。
+
+ <出力>
+ .\https\aspnetapp.pem 証明書
+ .\https\aspnetapp.key 秘密鍵(**リポジトリには含めない。.gitignore 済み**)
+
+.PARAMETER SkipTrust
+ 証明書を信頼済みにする手順(dotnet dev-certs https --trust)を省く。
+ ブラウザで警告が出てもよい場合や、確認ダイアログを出したくない場合に指定する。
+
+.EXAMPLE
+ powershell -NoProfile -ExecutionPolicy Bypass -File .\0_SetupCert.ps1
+
+.NOTES
+ Windows PowerShell 5.1 / PowerShell 7 の両方で動く。
+#>
+param(
+ [switch]$SkipTrust
+)
+
+$ErrorActionPreference = "Stop"
+
+$httpsDir = Join-Path $PSScriptRoot "https"
+$pemPath = Join-Path $httpsDir "aspnetapp.pem"
+$keyPath = Join-Path $httpsDir "aspnetapp.key"
+
+Write-Host "============================================"
+Write-Host " HTTPS 開発用証明書のセットアップ(PEM)"
+Write-Host "============================================"
+Write-Host ""
+
+if (-not (Test-Path $httpsDir))
+{
+ New-Item -ItemType Directory -Path $httpsDir | Out-Null
+}
+
+# ------------------------------------------------------------------
+# PEM 形式で書き出す
+# ------------------------------------------------------------------
+# -np(--no-password)を付けるので、パスワードの入力は無い。
+Write-Host "[1/2] 証明書を PEM で書き出す ..."
+dotnet dev-certs https --format Pem -ep $pemPath -np
+
+if ($LASTEXITCODE -ne 0)
+{
+ Write-Host "[エラー] 証明書の書き出しに失敗しました。" -ForegroundColor Red
+ exit 1
+}
+
+if (-not (Test-Path $pemPath) -or -not (Test-Path $keyPath))
+{
+ # 終了コードだけでは判断しない(生成物の存在も確認する)。
+ Write-Host "[エラー] 出力が見つかりません : $pemPath / $keyPath" -ForegroundColor Red
+ exit 1
+}
+
+# ------------------------------------------------------------------
+# 信頼済みにする
+# ------------------------------------------------------------------
+if ($SkipTrust)
+{
+ Write-Host "[2/2] 信頼設定は省略しました(-SkipTrust)。"
+ Write-Host " ブラウザで証明書の警告が出ます。"
+}
+else
+{
+ Write-Host "[2/2] 証明書を信頼済みにする ..."
+ Write-Host " **Windows のセキュリティ警告が出たら「はい」を選ぶこと。**"
+
+ dotnet dev-certs https --trust
+
+ if ($LASTEXITCODE -ne 0)
+ {
+ # 信頼設定は失敗しても起動はできる(ブラウザで警告が出るだけ)。
+ Write-Host "[警告] 信頼設定に失敗しました。手動での信頼が要ります。" -ForegroundColor Yellow
+ }
+}
+
+Write-Host ""
+Write-Host "============================================"
+Write-Host " 完了"
+Write-Host "============================================"
+Write-Host " 証明書 : $pemPath"
+Write-Host " 秘密鍵 : $keyPath"
+Write-Host ""
+Write-Host " 次は 1_PublishAndUp.bat を実行してください。"
+Write-Host ""
diff --git a/root/programs/CS/Samples4NetCore/Docker/1_PublishAndUp.bat b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/1_PublishAndUp.bat
similarity index 91%
rename from root/programs/CS/Samples4NetCore/Docker/1_PublishAndUp.bat
rename to root/programs/CS/Samples4NetCore/Docker/MVC_Sample/1_PublishAndUp.bat
index 529282d04..761e0ed84 100644
--- a/root/programs/CS/Samples4NetCore/Docker/1_PublishAndUp.bat
+++ b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/1_PublishAndUp.bat
@@ -8,8 +8,8 @@ rem See README.md and #548.
setlocal
pushd "%~dp0"
-set SLN_DIR=..\Backend\MVC_Sample\MVC_Sample
-set FX_DLL=..\..\Frameworks\Infrastructure\Build_netcore100\net10.0\OpenTouryo.Framework.dll
+set SLN_DIR=..\..\Backend\MVC_Sample\MVC_Sample
+set FX_DLL=..\..\..\Frameworks\Infrastructure\Build_netcore100\net10.0\OpenTouryo.Framework.dll
echo === 1/4 : checking prerequisites ===
diff --git a/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/2_Down.bat b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/2_Down.bat
new file mode 100644
index 000000000..9b54086f9
--- /dev/null
+++ b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/2_Down.bat
@@ -0,0 +1,17 @@
+@echo off
+rem NOTE: keep this file pure ASCII (root/programs/CODING.md section 4).
+rem Japanese explanations are in README.md.
+rem
+rem Stops and removes the container.
+rem Named volumes (logs / data protection keys) are kept on purpose:
+rem removing the keys invalidates auth cookies and sessions.
+rem Use "docker compose down -v" if you really want to drop them.
+
+setlocal
+pushd "%~dp0"
+
+docker compose down
+set RC=%ERRORLEVEL%
+
+popd
+endlocal & exit /b %RC%
diff --git a/root/programs/CS/Samples4NetCore/Docker/Dockerfile b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/Dockerfile
similarity index 100%
rename from root/programs/CS/Samples4NetCore/Docker/Dockerfile
rename to root/programs/CS/Samples4NetCore/Docker/MVC_Sample/Dockerfile
diff --git a/root/programs/CS/Samples4NetCore/Docker/README.md b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/README.md
similarity index 100%
rename from root/programs/CS/Samples4NetCore/Docker/README.md
rename to root/programs/CS/Samples4NetCore/Docker/MVC_Sample/README.md
diff --git a/root/programs/CS/Samples4NetCore/Docker/appsettings.Container.json b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/appsettings.Container.json
similarity index 100%
rename from root/programs/CS/Samples4NetCore/Docker/appsettings.Container.json
rename to root/programs/CS/Samples4NetCore/Docker/MVC_Sample/appsettings.Container.json
diff --git a/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/conf/SampleLogConf.xml b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/conf/SampleLogConf.xml
new file mode 100644
index 000000000..f0d122cdb
--- /dev/null
+++ b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/conf/SampleLogConf.xml
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/root/programs/CS/Samples4NetCore/Docker/docker-compose.yml b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/docker-compose.yml
similarity index 98%
rename from root/programs/CS/Samples4NetCore/Docker/docker-compose.yml
rename to root/programs/CS/Samples4NetCore/Docker/MVC_Sample/docker-compose.yml
index bab5bdea6..0b74add9f 100644
--- a/root/programs/CS/Samples4NetCore/Docker/docker-compose.yml
+++ b/root/programs/CS/Samples4NetCore/Docker/MVC_Sample/docker-compose.yml
@@ -51,7 +51,7 @@ services:
volumes:
# 定義ファイル・SQL・証明書(読み取り専用)
- - ../../../../files/resource:/app/files/resource:ro
+ - ../../../../../files/resource:/app/files/resource:ro
# HTTPS の開発用証明書(0_SetupCert.ps1 が作る。読み取り専用)
- ./https:/https:ro
# ログ(書き込む)
diff --git a/root/programs/CS/Samples4NetCore/Frontend/README.md b/root/programs/CS/Samples4NetCore/Frontend/README.md
deleted file mode 100644
index 62099a5b5..000000000
--- a/root/programs/CS/Samples4NetCore/Frontend/README.md
+++ /dev/null
@@ -1,4 +0,0 @@
-Moved to the following repository.
-
-- OpenTouryoProject/FrontendTemplates
-https://github.com/OpenTouryoProject/FrontendTemplates/
diff --git a/root/programs/RELEASE.md b/root/programs/RELEASE.md
index d8c2147fd..eeed35f7f 100644
--- a/root/programs/RELEASE.md
+++ b/root/programs/RELEASE.md
@@ -185,7 +185,7 @@ cd root\programs
- [ ] **`1_BuildAll.ps1` のエラーが「既知の 1 件」だけである**
… `-SkipClean` は**使わない**。前回の成果物が残っていると通ったように見える
- [ ] **`2_RunAllTests.ps1` が終了コード 0**(8 ケース)
-- [ ] **`3_SmokeTest.ps1` が終了コード 0**(29 件)
+- [ ] **`3_SmokeTest.ps1` が終了コード 0**(30 件)
> **`1_BuildAll.ps1` は現状ここで終了コード 1 になる。**
> `WSClnt_sample (net48)` の ClickOnce 署名エラー(`MSB3482`)が残るため。
@@ -209,7 +209,7 @@ cd root\programs
|---|---|
| `1_BuildAll.ps1`(31 ステップ) | 5.8 分 |
| `2_RunAllTests.ps1`(8 ケース) | 1.3 分 |
-| `3_SmokeTest.ps1`(29 件) | 7.4 分 |
+| `3_SmokeTest.ps1`(30 件) | 7.4 分 |
### VB 側は、この 3 本に含めない
diff --git a/root/programs/SMOKETEST.md b/root/programs/SMOKETEST.md
index 5db82ff6e..912f68731 100644
--- a/root/programs/SMOKETEST.md
+++ b/root/programs/SMOKETEST.md
@@ -81,7 +81,7 @@ cd root\programs
---
-## 3. 対象(29 件)
+## 3. 対象(30 件)
**基盤系を先に置く。**(#571)
環境やツールが壊れていれば、アプリケーションの検証を待たずに分かる。
@@ -170,52 +170,32 @@ NG : /OUTPUT "C:\temp\out" ← \ が消える
**配置の成功だけでは足りない。** プロキシを無視して直結しても成功するため、
`Test-ProxyUsed` でログを見る。マニフェストと ZIP の両方が通っていることまで確認する。
-プロキシは `st_Proxy.ps1`(`TcpListener` の最小実装)。
-`HttpListener` は使えない。**プロキシへの要求は要求行が絶対 URI**
-(`GET http://host:port/path HTTP/1.1`)で来るため、
-プレフィクス登録で受ける `HttpListener` では扱えない。
+**`.NET Framework` がループバック宛を迂回する件と、その回避(実在しないホスト名)は
+通信制御と同じである。** 理由は下記「宛先に実在しないホスト名を使っている理由」を見ること。
+こちらでは別名を `deploy.smoketest` とし、プロキシ(`-MapHost`)が `localhost` へ繋ぎ替える。
-##### 踏んだ落とし穴
+##### 通信制御とは別に立てている理由
-**① `.NET Framework` は localhost 宛のプロキシを無条件にバイパスする。**
+**対象が別プロセスの EXE だからである。**
+通信制御(3 節)はオリジンもプロキシも `TestTransmission` の中に立てて 1 プロセスに閉じているが、
+`DeployZipPackWithHTTP` は外部の EXE なので、**プロキシも外に立てる**必要がある。
-```
-BypassProxyOnLocal(既定) False
-IsBypassed(localhost) **True** ← 設定に関わらず
-IsBypassed(example.com) False
-```
-
-配信が `localhost` にある限り net48 では経路を確かめられない。
-`hosts` は書き換えず、**別名(`deploy.smoketest`)を使い、
-プロキシ側で `localhost` に繋ぎ替える**(`-MapHost`)。
-
-**② `Host` ヘッダも繋ぎ替える。** IIS Express は `Host` が `localhost` でない要求を受け付けない。
+`st_Proxy.ps1`(`TcpListener`)を `Start-Process` で待ち受けさせる。
+**`HttpListener` は使えない。** プロキシへの要求は要求行が**絶対 URI**
+(`GET http://host:port/path HTTP/1.1`)で来るため、プレフィクス登録では受けられない。
-**③ 応答は長さに従って読む。** 「上流が閉じるまで読む」では**本体を持たない HEAD で止まる。**
+##### 外に立てたことで要ったこと
-**④ 起動待ちは「ポートに繋がるか」では足りない。**
-前回のプロキシが待ち受けを握ったままだと接続は成功し、
-新しいプロセスは死んでいるのに起動したと誤認する。**自分のログに `[start]` が出るまで待つ。**
+| | 内容 |
+|---|---|
+| `Host` ヘッダの繋ぎ替え | IIS Express は `Host` が `localhost` でない要求を受け付けない |
+| 応答は長さに従って読む | 「閉じるまで読む」では**本体を持たない HEAD で止まる** |
+| 起動待ちは**自分のログで見る** | ポートに繋がるかだけでは、**前回のプロセスが握っていても素通り**する |
> **同じ理由で `Stop-DeployWeb` も直した。**(#578)
> 残った `iisexpress` は 51084 を握り続け、**前回の web フォルダを配り続ける。**
-> 実測では net48 の配置が `deploy_core` のマニフェストを読み、
-> そちらへ展開していた。**配置は成功するため異常が出ず、判定だけが 0 件になる。**
-
-
-```
-/ZIPGEN /TOPONLY → root.zip(ルート直下だけ、書庫内ルート無し)
-/ZIPGEN /ROOTINZIP aaa → aaa.zip (フォルダごと、書庫内ルート = aaa)
-```
-
-**両方のモードを通る。** GUI のチェック ボックス
-(「個別のフォルダ圧縮」/「ルート フォルダからの圧縮」)に対応する分岐である。
-
-`Sample/FormAppRoot` が配布前の姿なので、**配置結果を MD5 で突き合わせられる**(21 ファイル)。
-
-**ここだけ ZIP 部品(`ZipperV2` / `UnZipperV2`)を通る。**
-単体テスト(`TestCode/TestZipV2.cs`)は部品の振る舞いを見るが、
-配布フロー全体を通すのはこの 2 件だけ。
+> 実測では net48 の配置が `deploy_core` のマニフェストを読み、そちらへ展開していた。
+> **配置は成功するため異常が出ず、判定だけが 0 件になる。**
#### マニュフェストの MD5 は計算し直して突き合わせる
@@ -344,6 +324,16 @@ net48 : http://127.0.0.1:51090/ IsBypassed=True ← プロキシを使わ
**テスト用プロキシが名前を解決せず、必ずオリジンへ繋ぎ替える**ようにしてある。
`hosts` ファイルの編集(管理者権限)も、ファイアウォールへの露出も要らない。
+> **`DeployZipPackWithHTTP` のプロキシ経由テストも同じ手を使っている。**(#578)
+> 別名は `deploy.smoketest`、プロキシは `st_Proxy.ps1`(51089)で、
+> `localhost:51084` の配信へ繋ぎ替える。
+> **迂回の理由と回避策はここが一次情報**であり、あちら側には書かない。
+>
+> ただし**外部の EXE が対象**なので、プロキシは別プロセスとして立てる
+> (通信制御は `TestTransmission` の中に閉じている)。
+> そのぶん `Host` ヘッダの繋ぎ替え、応答の長さ管理、起動待ちの判定が要る
+> (「DeployZipPackWithHTTP の CUI モード」を見ること)。
+
#### 対象外のオプション
| | 理由 |
@@ -449,6 +439,42 @@ JSON をまたいだことにならないため、HTTP 越しに送って戻す
> 部分一致にしていたところ、**IIS Express の 500.19 が返す HTML に "test" が含まれ、
> 疎通が OK と表示された。**「通ったこと」は正しさの証拠にならない。
+### ResourceServer の OpenAPI(IDL)(1 件)
+
+| 対象 | 判定 |
+|---|---|
+| `OpenAPI (net10.0)` | `/openapi/v1.json` が**仕様として読める形で返る** |
+
+**.NET 9 以降、ASP.NET Core は標準で OpenAPI ドキュメントを生成する。**
+`Swashbuckle` は要らない(`Microsoft.AspNetCore.OpenApi`)。
+
+```csharp
+services.AddOpenApi(); // ConfigureServices
+endpoints.MapOpenApi(); // UseEndpoints
+```
+
+**200 が返るだけでは足りない。** ドキュメントとして壊れていても 200 は返る。
+
+- `openapi` の版が入っていること
+- `paths` が空でないこと
+- **代表的な API が名前で載っていること**(`/api/Json/Select` と `/api/BatchUpdate/BatchUpdate`)
+- その `post` が定義されていること
+
+**数ではなく名前で見る。** 件数だけだと、コントローラが入れ替わっても気づけない。
+
+#### net48 側は対象外
+
+[`Samples/WS_sample/ASPNETWebService`](CS/Samples/WS_sample/ASPNETWebService) は
+**クラシック ASP.NET(`v4.8` / 非 SDK)**であり、`Microsoft.AspNetCore.OpenApi` は使えない。
+`Swashbuckle`(ASP.NET Web API 2 向け)は入手できるが 2016 年で更新が止まっており、
+生成されるのも Swagger 2.0 である。**古い依存を増やしてまで揃えない**(#580)。
+
+#### `Microsoft.OpenApi` は版を明示している
+
+`Microsoft.AspNetCore.OpenApi` 10.0.5 が引く `Microsoft.OpenApi` 2.0.0 は、
+**高深刻度の脆弱性**([GHSA-v5pm-xwqc-g5wc](https://github.com/advisories/GHSA-v5pm-xwqc-g5wc))に該当する。
+修正は 2.7.5 で、**推移的依存のままでは上がらない**ため csproj に明示した(#574 と同じ手)。
+
### Web アプリ(3 件)
| 対象 | ホスト | 認証の実装 | 判定 |
@@ -503,10 +529,13 @@ WinForms / WPF 系は、リリース チェックリスト(段階 4)の**手
`ConnectionString_SQL` を読む。ここで別途ハードコードすると追随できなくなるため
- **IIS Express** がインストールされていること(net48 の Web アプリ)
- **ASP.NET 状態サービスが開始されている**こと(net48 の Web アプリ)
-- **ポート 51081 - 51086、51090 - 51092 が空いている**こと
+- **ポート 51081 - 51093 が空いている**こと
- 51081 - 51083 … Web アプリ(net48 の 2 つと net10.0)
- 51084 … `DeployZipPackWithHTTP` の配信
- 51085 - 51086 … VB 側の Web アプリ(`-Lang VB`。10 節)
+ - 51087 - 51088 … `TestWebAPIClient` の相手(net48 / net10.0)
+ - **51089 … `DeployZipPackWithHTTP` のプロキシ(3 節)**
+ - **51093 … ResourceServer の OpenAPI(3 節)**
- **51090 - 51092 … 通信制御のオリジン・プロキシ・オリジン(TLS)(3 節)**
> **GitHub Actions でも実行している。** 前提の揃え方(SQL Server の導入と Northwind の
@@ -687,7 +716,7 @@ MVC_Sample (net10.0) OK ログイン後 /Crud1/Index = 200
全対象 OK
```
-全 29 件(ビルド 10 バッチ + 疎通 29 件)で **約 8 分**。
+全 30 件(ビルド 10 バッチ + 疎通 30 件)で **約 8 分**。
**#571 の統合で、13.9 分から半減した。**
同じ WebAPI を相手にしながらホストを 2 回起動していたのをやめたため。
@@ -823,7 +852,7 @@ $client.Connect("localhost", $port)
```powershell
.\3_SmokeTest.ps1 -Lang VB # VB のみ(6 件)
-.\3_SmokeTest.ps1 -Lang Both # C# 29 件 + VB 6 件
+.\3_SmokeTest.ps1 -Lang Both # C# 30 件 + VB 6 件
```
**既定に VB を含めない。** リリース時の検証([`RELEASE.md`](RELEASE.md) 3 節)は
diff --git a/root/programs/st_Flow.ps1 b/root/programs/st_Flow.ps1
index c6f8c8cd2..b4c29a9b3 100644
--- a/root/programs/st_Flow.ps1
+++ b/root/programs/st_Flow.ps1
@@ -93,3 +93,35 @@ $webFormsFlow = {
return @{ Ok = $true; Detail = "ログイン後 menu.aspx = 200" }
}
+# ResourceServer の OpenAPI(IDL): /openapi/v1.json が仕様として読める形で返ること(#580)
+#
+# **200 が返るだけでは足りない。**
+# ドキュメントとして壊れていても 200 は返る。
+# 版・パス・スキーマまで見て、初めて「IDL になっている」と言える。
+$openApiFlow = {
+ param($base)
+
+ $r = Invoke-Http "$base/openapi/v1.json"
+ if ($r.Status -ne 200) { return @{ Ok = $false; Detail = "GET /openapi/v1.json = $($r.Status)" } }
+
+ try { $doc = $r.Content | ConvertFrom-Json }
+ catch { return @{ Ok = $false; Detail = "JSON として読めない" } }
+
+ if (-not $doc.openapi) { return @{ Ok = $false; Detail = "openapi の版が無い" } }
+
+ $paths = @($doc.paths.PSObject.Properties.Name)
+ if ($paths.Count -eq 0) { return @{ Ok = $false; Detail = "paths が空" } }
+
+ # **代表的な API が載っていること。**
+ # コントローラを足しても気づけるよう、数ではなく名前で見る。
+ foreach ($p in @("/api/Json/Select", "/api/BatchUpdate/BatchUpdate"))
+ {
+ if ($paths -notcontains $p) { return @{ Ok = $false; Detail = "$p が載っていない" } }
+ }
+
+ # POST の要求本体にスキーマが付いていること(型情報が落ちていないか)
+ $op = $doc.paths.'/api/BatchUpdate/BatchUpdate'.post
+ if (-not $op) { return @{ Ok = $false; Detail = "/api/BatchUpdate/BatchUpdate に post が無い" } }
+
+ return @{ Ok = $true; Detail = ("openapi {0} / paths {1} 件" -f $doc.openapi, $paths.Count) }
+}
diff --git a/root/programs/st_Targets.ps1 b/root/programs/st_Targets.ps1
index 23a9d3ae8..fbfaf126c 100644
--- a/root/programs/st_Targets.ps1
+++ b/root/programs/st_Targets.ps1
@@ -214,6 +214,17 @@ $targetsCS = @(
Verify = { Stop-ApiWeb; return $true }
}
+ # ResourceServer の OpenAPI(IDL)(#580)
+ # **.NET 9 以降の ASP.NET Core は標準で OpenAPI を生成する。**
+ # Swashbuckle は要らない。net48 側(Samples/WS_sample)は
+ # クラシック ASP.NET のため対象外。
+ @{
+ Name = "OpenAPI (net10.0)"; Bat = "6_Build_WSSrvCore_sample.bat"
+ Kind = "Web"; WebHost = "Kestrel"; Port = 51093
+ Exe = "Samples4NetCore\Backend\ASPNETWebService\ASPNETWebService\bin\Debug\net10.0\ASPNETWebService.dll"
+ Flow = $openApiFlow
+ }
+
# --- Web アプリ ---
@{
Name = "WebForms_Sample (net48)"; Bat = "10_Build_WebApp_sample.bat"
diff --git a/root/programs/st_Utility.ps1 b/root/programs/st_Utility.ps1
index b6971db47..1359ce208 100644
--- a/root/programs/st_Utility.ps1
+++ b/root/programs/st_Utility.ps1
@@ -473,7 +473,10 @@ $verifyDeployCore = { $r = Test-Deploy "core"; Stop-DeployWeb; return $r }
# **#575 で HttpWebRequest を HttpClient へ移した経路の確認。**
# HttpClientHandler では Proxy = null が「使わない」にならず
# UseProxy = false が要る、という差があったため、実際に通して確かめる。
-$deployProxyPort = 51085
+# **51085 は VB 側の Web アプリが使う。**(SMOKETEST.md 9 節)
+# 51081-51086 は Web アプリ、51087/51088 は TestWebAPIClient、
+# 51090-51092 は通信制御のオリジン・プロキシ。空いているのは 51089。
+$deployProxyPort = 51089
$deployProxyUser = "pxuser"
$deployProxyPwd = "pxpass"