diff --git a/root/files/resource/Test/nw/Option (for example).txt b/root/files/resource/Test/nw/Option (for example).txt
index e4bba714c..2ec05dcbc 100644
--- a/root/files/resource/Test/nw/Option (for example).txt
+++ b/root/files/resource/Test/nw/Option (for example).txt
@@ -1 +1 @@
-UserName=administrator;Password=seigi;Domain=Domain;ProxyUrl=http://xxxxxxxx:8080;PUserName=PUserName;PPassword=PPassword;PDomain=PDomain;UserAgent=UserAgent;ConnGroupName=ConnGroupName;Compression=true;CertFile=C:\certnew.cer;CertPassword=CertPassword;
\ No newline at end of file
+UserName=administrator;Password=seigi;Domain=Domain;ProxyUrl=http://xxxxxxxx:8080;PUserName=PUserName;PPassword=PPassword;PDomain=PDomain;UserAgent=UserAgent;Compression=true;CertFile=C:\certnew.cer;CertPassword=CertPassword;
\ No newline at end of file
diff --git a/root/files/resource/Xml/TMProtocolDefinition.xml b/root/files/resource/Xml/TMProtocolDefinition.xml
index 785a1d198..f5707fc1d 100644
--- a/root/files/resource/Xml/TMProtocolDefinition.xml
+++ b/root/files/resource/Xml/TMProtocolDefinition.xml
@@ -1,6 +1,6 @@
+
@@ -8,11 +8,12 @@
-
]>
-
+
+
@@ -37,8 +38,8 @@
-
-
+
+
\ No newline at end of file
diff --git a/root/files/resource/Xml/TMProtocolDefinition2.xml b/root/files/resource/Xml/TMProtocolDefinition2.xml
index b2315a221..34ebaed99 100644
--- a/root/files/resource/Xml/TMProtocolDefinition2.xml
+++ b/root/files/resource/Xml/TMProtocolDefinition2.xml
@@ -1,6 +1,6 @@
+
@@ -8,11 +8,12 @@
-
]>
-
+
+
@@ -37,8 +38,8 @@
-
-
+
+
\ No newline at end of file
diff --git a/root/programs/0_RunAll.ps1 b/root/programs/0_RunAll.ps1
index 9f5017ff0..6c10012ff 100644
--- a/root/programs/0_RunAll.ps1
+++ b/root/programs/0_RunAll.ps1
@@ -13,6 +13,10 @@ param(
# UseLang = $true のものにだけ -Lang を渡す。
# 2_RunAllTests.ps1 に渡さないのは、VB 側にテスト プロジェクトが無く(#542)、
# 単体テストが CS の Frameworks\Tests に集約されているため。
+
+# まとめの整形。Format-Table は 5.1 で全角の桁を数えないため、自前で揃える。
+. (Join-Path $PSScriptRoot "SummaryTable.ps1")
+
$scripts = @(
@{ Name = "1_BuildAll.ps1"; UseLang = $true }
@{ Name = "2_RunAllTests.ps1"; UseLang = $false }
@@ -41,7 +45,9 @@ foreach ($s in $scripts)
# --- 結果のまとめ ---
Write-Host ""
Write-Host "================ 全体のまとめ ================"
-$results | Format-Table -AutoSize | Out-String | Write-Host
+Write-Host ""
+Write-SummaryTable $results
+Write-Host ""
# 1_BuildAll.ps1 は既知の署名エラー(MSB3482)で 1 になることがある。
# 終了コードだけで判断せず、エラー一覧の内容を確認すること(RELEASE.md 3 節)。
diff --git a/root/programs/1_BuildAll.ps1 b/root/programs/1_BuildAll.ps1
index 79c67489b..0a2f17e84 100644
--- a/root/programs/1_BuildAll.ps1
+++ b/root/programs/1_BuildAll.ps1
@@ -82,6 +82,9 @@ param(
# ビルド バッチはそれぞれの直下にあるため、ステップごとにそこへ移動して呼ぶ。
New-Item -ItemType Directory -Force $OutputDir | Out-Null
+# サマリの整形。Format-Table は 5.1 で全角の桁を数えないため、自前で揃える。
+. (Join-Path $PSScriptRoot "SummaryTable.ps1")
+
# ------------------------------------------------------------------
# コンソールのコード ページを先に UTF-8 にしておく
# ------------------------------------------------------------------
@@ -391,7 +394,9 @@ $total.Stop()
# ------------------------------------------------------------------
Write-Host ""
Write-Host "================ サマリ ================"
-$results | Format-Table -AutoSize | Out-String | Write-Host
+Write-Host ""
+Write-SummaryTable $results
+Write-Host ""
Write-Host (" 所要時間 : {0:N1} 分" -f $total.Elapsed.TotalMinutes)
Write-Host (" ログ : {0}" -f $OutputDir)
diff --git a/root/programs/2_RunAllTests.ps1 b/root/programs/2_RunAllTests.ps1
index b23792343..429ecb4b0 100644
--- a/root/programs/2_RunAllTests.ps1
+++ b/root/programs/2_RunAllTests.ps1
@@ -83,6 +83,9 @@ $testsRoot = Join-Path $csRoot "Frameworks\Tests"
$repoRoot = (& git -C $testsRoot rev-parse --show-toplevel)
New-Item -ItemType Directory -Force $OutputDir | Out-Null
+# サマリの整形。Format-Table は 5.1 で全角の桁を数えないため、自前で揃える。
+. (Join-Path $PSScriptRoot "SummaryTable.ps1")
+
# ------------------------------------------------------------------
# コンソールのコード ページを先に UTF-8 にしておく
# ------------------------------------------------------------------
@@ -314,7 +317,9 @@ foreach ($t in $tests)
# ------------------------------------------------------------------
Write-Host ""
Write-Host "================ サマリ ================"
-$results | Format-Table -AutoSize | Out-String | Write-Host
+Write-Host ""
+Write-SummaryTable $results
+Write-Host ""
Write-Host (" 期待値・ログ : {0}" -f $OutputDir)
Write-Host " 実測値 : ワーキング ツリーの Result*.txt(git diff で確認できます)"
diff --git a/root/programs/3_SmokeTest.ps1 b/root/programs/3_SmokeTest.ps1
index a6e1e00cd..d3bf07e61 100644
--- a/root/programs/3_SmokeTest.ps1
+++ b/root/programs/3_SmokeTest.ps1
@@ -105,6 +105,9 @@ $configRoot = if ($Lang -eq "VB") { Join-Path $PSScriptRoot "VB" } else { $csRoo
New-Item -ItemType Directory -Force $OutputDir | Out-Null
+# サマリの整形。Format-Table は 5.1 で全角の桁を数えないため、自前で揃える。
+. (Join-Path $PSScriptRoot "SummaryTable.ps1")
+
# 対象に与える stdin(空)。
# Start-Process -RedirectStandardInput は実在するファイルを要求するため、先に作る。
# これが無いと Console.ReadKey() が本当にキー入力を待つ。
@@ -818,6 +821,26 @@ $targetsCS = @(
Pre = $startDeployWebCore; Verify = $verifyDeployCore
}
+ # --- 通信制御の接続オプション(#546) ---
+ #
+ # CallController の接続オプション(ProxyUrl / UserName / UserAgent / Compression 等)が、
+ # 実際の HTTP 要求に反映されているかを見る。
+ #
+ # <外部環境が要らない>
+ # オリジンとプロキシを、テスト側が TcpListener で自前に立てる。
+ # 1 プロセスに閉じているので、起動・停止の面倒を見る必要がない。
+ #
+ # <net48 だけ>
+ # ASP.NET WebAPI の経路が .NET Framework 限定である(BinarySerialize を使うため)。
+ #
+ # <判定>
+ # 対象側が項目ごとに OK / NG を出し、末尾に件数を出す。
+ @{
+ Name = "TestTransmission (net48)"; Bat = "y_Build_TestTransmission.bat"
+ Exe = "Frameworks\Tests\TestTransmission\net48\bin\Debug\TestTransmissionFx.exe"
+ Expect = 'NG : 0 件'
+ }
+
# --- Web アプリ ---
@{
Name = "MVC_Sample (net48)"; Bat = "10_Build_WebApp_sample.bat"
@@ -1019,16 +1042,24 @@ if (-not $SkipBuild)
{
# ビルドする単位は「フォルダ + バッチ」。同じバッチ名が CS と VB の
# 両方にあるため(5_Build_Bat_sample.bat 等)、バッチ名だけでは一意化できない。
+ #
+ # 並びは「C# の対象 → VB の前提 → VB の対象」とする。
+ # 前提を先頭に置くと、-Lang Both で VB の準備が C# の対象より先に流れ、
+ # 1_BuildAll.ps1(CS → VB の順)と見た目が揃わない。依存関係は
+ # 「VB の前提が VB の対象より前」だけなので、この並びで足りる。
$builds = @()
- # VB を含むなら、前提のビルドを先に通す。
- if (@($selected | Where-Object { $_.Dir -eq "VB" }).Count -gt 0)
+ $builds += @($selected | Where-Object { $_.Dir -ne "VB" } |
+ ForEach-Object { @{ Dir = $_.Dir; Bat = $_.Bat; Note = "" } })
+
+ $vb = @($selected | Where-Object { $_.Dir -eq "VB" })
+ if ($vb.Count -gt 0)
{
- $builds += $prerequisitesVB
+ $builds += @($prerequisitesVB |
+ ForEach-Object { @{ Dir = $_.Dir; Bat = $_.Bat; Note = "(VB の前提)" } })
+ $builds += @($vb | ForEach-Object { @{ Dir = $_.Dir; Bat = $_.Bat; Note = "" } })
}
- $builds += @($selected | ForEach-Object { @{ Dir = $_.Dir; Bat = $_.Bat } })
-
$done = @{}
foreach ($b in $builds)
{
@@ -1036,7 +1067,7 @@ if (-not $SkipBuild)
if ($done.ContainsKey($key)) { continue }
$done[$key] = $true
- Write-Host ("=== ビルド : {0} ===" -f $key) -ForegroundColor Cyan
+ Write-Host ("=== ビルド : {0}{1} ===" -f $key, $b.Note) -ForegroundColor Cyan
$sw = [Diagnostics.Stopwatch]::StartNew()
[void](Invoke-BuildBat $b.Dir $b.Bat)
$sw.Stop()
@@ -1288,7 +1319,9 @@ foreach ($t in $selected)
# ------------------------------------------------------------------
Write-Host ""
Write-Host "================ サマリ ================"
-$results | Format-Table -AutoSize -Wrap | Out-String | Write-Host
+Write-Host ""
+Write-SummaryTable $results
+Write-Host ""
Write-Host (" ログ : {0}" -f $OutputDir)
$ng = @($results | Where-Object { $_.結果 -ne "OK" })
diff --git a/root/programs/BUILDING.md b/root/programs/BUILDING.md
index 8628ae720..62d254fa6 100644
--- a/root/programs/BUILDING.md
+++ b/root/programs/BUILDING.md
@@ -738,7 +738,7 @@ VS 18 のある環境では従来どおり `18.0` になるため、**挙動は
### 実測(run 30984111639 : 全ステップ成功)
> **当時の件数での記録。** その後、単体テストは 8 ケース(#520)、
-> 疎通は 22 件(#528)に増えている。**時間の目安として読むこと。**
+> 疎通は 23 件(#528、#546)に増えている。**時間の目安として読むこと。**
| ステップ | CI | ローカル |
|---|---|---|
diff --git a/root/programs/CHEATSHEET.md b/root/programs/CHEATSHEET.md
index 69d694049..bb5a55e6c 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) |
-| 疎通 | 22/22 OK | [`SMOKETEST.md`](SMOKETEST.md) |
+| 疎通 | 23/23 OK | [`SMOKETEST.md`](SMOKETEST.md) |
**既定は C# 側。VB 側は `-Lang` で回す**(3 節)。
diff --git a/root/programs/CODING.md b/root/programs/CODING.md
index 7bc232a76..6d0de9f28 100644
--- a/root/programs/CODING.md
+++ b/root/programs/CODING.md
@@ -217,6 +217,7 @@ sakura -GREPMODE -GKEY="ビルドに" -GFOLDER="." -GFILE="*.log" -GOPT=P -GCODE
| 同じファイルなのに差分が出る | `Get-Content` の既定エンコードが 5.1 は ANSI、7 は UTF-8 | **`-Encoding UTF8`** を明示する |
| HTTP が常に失敗(状態コード `-1`) | `-SkipHttpErrorCheck` は **7 以降にしかない** | バージョンを見て付け外しする |
| 実行中に画面がクリアされ、それまでの結果が消える | 子プロセスの `chcp 65001` はコンソール全体に影響する | スクリプト冒頭で先に切り替える |
+| 表の見出し・罫線・データがずれる | 5.1 の `Format-Table` は**桁数ではなく文字数**で幅を決める(全角は 1 文字で 2 桁) | `SummaryTable.ps1` の `Write-SummaryTable` を使う |
```powershell
# 7 専用の引数は、バージョンを見て付け外しする
@@ -230,8 +231,16 @@ if ([Console]::OutputEncoding.CodePage -ne 65001)
}
```
+```powershell
+# 集計表は Format-Table ではなく、桁数を自前で数える整形を使う
+. (Join-Path $PSScriptRoot "SummaryTable.ps1")
+Write-SummaryTable $results
+```
+
- **`.bat` は「非 ASCII を含むときだけ」BOM 付き**(8.4)だが、
**`.ps1` は非 ASCII を含むなら必ず BOM 付き**。5.1 が既定で ANSI として読むため。
+- **要素 1 個の配列は、返した時点でスカラーに展開される。** そのまま `[0]` を取ると
+ **文字列の 1 文字目**になる。関数の戻り値を添字で使うなら `@()` で受けること。
- 5.1 の `[Console]::OutputEncoding` は**起動時の値のまま**で、実行中にコード ページが
変わっても追随しない。同じ画面で 2 回目を実行したときに化ける原因になる。
- **変更したら 5.1 でも実行して確かめること。**
diff --git a/root/programs/CS/Frameworks/ANALYSIS.md b/root/programs/CS/Frameworks/ANALYSIS.md
index bd56b0017..1b982b007 100644
--- a/root/programs/CS/Frameworks/ANALYSIS.md
+++ b/root/programs/CS/Frameworks/ANALYSIS.md
@@ -62,6 +62,7 @@
| `TestDataAccess` | データ アクセスのテスト(#520)。`TestCode` と同じ構成だが**DB に接続する**。`/MODE` で対象 DBMS を切り替える(既定は SQL Server のみ) | [`README.md`](Tests/TestDataAccess/README.md) |
| `TestLog` | ログ出力確認(log4net/NLog、1〜3) | — |
| `TestBatch` | バッチ起動確認 | — |
+| `TestTransmission` | 通信制御の接続オプション(#546)。**オリジンとプロキシを自前で立て**、`CallController` から呼んで、要求に反映されているかを見る。**期待値ファイルを持たず、自分で合否を出す**(疎通テストから呼ばれる) | [`SMOKETEST.md`](../../SMOKETEST.md) 3 節 |
| `EncAndDecUtil` / `EncAndDecUtilCUI` | 暗号・署名ユーティリティの GUI/CUI 確認 | — |
**`TestCode` と `TestDataAccess` は専用の `README.md` を持つ。**
diff --git a/root/programs/CS/Frameworks/Infrastructure/Framework/Transmission/CallController.cs b/root/programs/CS/Frameworks/Infrastructure/Framework/Transmission/CallController.cs
index 3906260b1..1f60063c6 100644
--- a/root/programs/CS/Frameworks/Infrastructure/Framework/Transmission/CallController.cs
+++ b/root/programs/CS/Frameworks/Infrastructure/Framework/Transmission/CallController.cs
@@ -52,7 +52,7 @@
//* 2012/12/14 西野 大介 WCF-HTTP対応
//* 2012/12/17 西野 大介 WCF-TCP/IP対応
//* 2013/01/22 西野 大介 WCF-TCP/IP:Channelの解放(Close、Dispose)を明示
-//* 2014/11/14 Sandeep-san Implemented WCF communication options(i.e, Client, proxy credentials and Certificate)
+//* 2014/11/14 Sandeep-san Implemented WCF communication options
//* 2017/02/14 西野 大介 Invokeの非同期バージョン(InvokeAsync)を追加
//* 2017/02/28 西野 大介 ExceptionDispatchInfoを取り入れた。
//* 2017/08/18 西野 大介 ASP.NET WebAPI(JSON)対応
@@ -69,6 +69,8 @@
//* ・マルチスレッド・クライアントから利用不可。
//* ・Critical Section内で利用する。
//* ・Concurrent Collectionでプールを作成する。
+//* 2026/08/13 玄人 幸道 .NET Core版で未対応のprotocolを、nullではなく
+//* FrameworkExceptionで返すようにした(#543)。
//**********************************************************************************
using System;
@@ -104,33 +106,7 @@
using Touryo.Infrastructure.Public.IO;
using Touryo.Infrastructure.Public.Str;
-
-#if (NETSTD || NETCOREAPP)
-#else
-#region 自動生成されたReference.csのヘッダー部分
-
-//------------------------------------------------------------------------------
-//
-// このコードはツールによって生成されました。
-// ランタイム バージョン:2.0.50727.3082
-//
-// このファイルへの変更は、以下の状況下で不正な動作の原因になったり、
-// コードが再生成されるときに損失したりします。
-//
-//------------------------------------------------------------------------------
-
-//
-// このソース コードは Microsoft.VSDesigner、バージョン 2.0.50727.3082 によって自動生成されました。
-//
-using System.Diagnostics;
-using System.ComponentModel;
-
-using System.Web.Services;
-using System.Web.Services.Protocols;
-using System.Xml.Serialization;
-
-#endregion
-#endif
+//using Org.BouncyCastle.Ocsp;
#pragma warning disable 1591
@@ -189,54 +165,6 @@ public CookieContainer CookieContainer
#region Web参照と関連プロパティ
-#if (NETSTD || NETCOREAPP)
-#else
- #region WCF HTTP WebサービスのWeb参照
- /*
- /// WCF HTTP WebサービスのWeb参照
- private WCFHTTPSvcForFxClient WCF_HTTP;
-
- #region プロパティ直接公開
-
- /// WCF HTTP WebサービスのEndPointConfigName
- /// 既定値は「Transmission.WCFHTTPSvcForFx」となっている。
- public string WCF_HTTP_EndPointConfigName
- {
- set;
- get;
- }
-
- /// To get/set Binding Configuration of WCF HTTP
- public string WCF_HTTP_BindingConfiguration
- {
- set;
- get;
- }
-
- #endregion
- */
- #endregion
-
- #region WCF TCP/IPサービス
-
- /// WCF TCP/IPサービス
- private IWCFTCPSvcForFx WCF_TCPIP;
-
- #region プロパティ直接公開
-
- /// WCF TCP/IPサービスのEndPointConfigName
- /// 既定値は「Touryo.Infrastructure.Framework.Business.WCFTCPSvcForFx」となっている。
- public string WCF_TCPIP_EndPointConfigName
- {
- set;
- get;
- }
-
- #endregion
-
- #endregion
-#endif
-
#region ASPNETWebAPI → HttpClient
#if (NETSTD || NETCOREAPP)
@@ -303,12 +231,6 @@ public CallController(object context)
{
// コンテキスト情報
this._context = context;
-#if (NETSTD || NETCOREAPP)
-#else
- // EndPointConfigName(の既定値
- //this.WCF_HTTP_EndPointConfigName = FxLiteral.WCF_HTTP_ENDPOINT_CONFIGNAME;
- this.WCF_TCPIP_EndPointConfigName = FxLiteral.WCF_TCPIP_ENDPOINT_CONFIGNAME;
-#endif
}
#endregion
@@ -389,7 +311,26 @@ public object Invoke(string serviceName, object parameterValue)
#if (NETSTD || NETCOREAPP)
else
{
- return null; // FxEnumを潰したので、ココには来ない想定
+ // .NET Core 版が実装するのは InProcess だけで、
+ // FxEnum.TmProtocol からも 2 - 5 を落としてある。
+ //
+ // <null を返さない理由>(#543)
+ // ProtocolNameService は protocol 属性の値を検査せず、
+ // XML に書かれた文字列をそのまま返す。このため、
+ // protocol="4" のような定義があれば**ここに到達する**。
+ // 実際、配布している TMProtocolDefinition.xml には
+ // 4 と 5 の定義が含まれている。
+ //
+ // null を返すと呼び出し側で NullReferenceException になるだけで、
+ // 原因が設定の誤りであることに辿り着けない。
+ // .NET Framework 版と同じ例外を投げ、どの protocol が
+ // 受け付けられなかったかを示す。
+
+ // エラーをスロー
+ throw new FrameworkException(
+ FrameworkExceptionMessage.PRT_NAMESERVICE_XML_FORMAT_ERROR[0],
+ String.Format(FrameworkExceptionMessage.PRT_NAMESERVICE_XML_FORMAT_ERROR[1],
+ String.Format(FrameworkExceptionMessage.PRT_NAMESERVICE_XML_FORMAT_ERROR_prt2, protocol, serviceName)));
}
#else
else
@@ -450,284 +391,11 @@ public object Invoke(string serviceName, object parameterValue)
#region サービス呼び出し
- #region コメントアウト
- /*
- if (protocol == ((int)FxEnum.TmProtocol.AspNetWs).ToString())
- {
- #region WS-I Basic Profile v1.1、IIS + ASP.NET
-
- // 都度newしても接続はプールされているので、オーバーヘッドは少ない。
- Reference reference = new Reference();
-
- #region URL、タイムアウト
-
- // URL
- reference.Url = url;
- // タイムアウト
- if (0 <= timeout)
- {
- reference.Timeout = timeout * 1000;
- }
-
- #endregion
-
- #region その他(固定)
-
- // 実行アカウントでのWindows認証の有効・無効(Default:false)。
- reference.UseDefaultCredentials = true; // 有効
-
- // 事前認証の有効・無効(Default:false)。
- reference.PreAuthenticate = false; // 無効
- // → 認証後の要求でも「WWW-authenticate HTTP」ヘッダーを
- // ・ 送信する場合 :true
- // ・ それ以外の場合:false
-
- // Webサービスを要求するときに使用されるEncoding(Default:UTF-8)。
- reference.RequestEncoding = Encoding.GetEncoding(CustomEncode.UTF_8); // #36-この行IFの仕様として「UTF-8」前提で考える。
-
- // Cookieの有効化(Default:null)
- reference.CookieContainer = this.CookieContainer;
-
- // Redirectを受け付けるか、受け付けないか(Default:false)
- reference.AllowAutoRedirect = false; // 受け付けない
-
- // SOAP プロトコルのバージョン(Default:Soap11)
- // WR.SoapProtocolVersion // 変更しない;
-
- // NTLM認証で、接続共有の有効・無効(Default:false)
- // WR.UnsafeAuthenticatedConnectionSharing // 変更しない;
-
- // Componentを格納しているIContainer
- // WR.Container // 変更しない;
-
- // ComponentのISite
- // WR.Site // 変更しない;
-
- #endregion
-
- #region WASのクライアント認証のセキュリティ資格情報
-
- NetworkCredential nwcWAS = this.CreateCredentials(props);
- if (nwcWAS != null)
- {
- reference.Credentials = nwcWAS;
- }
-
- #endregion
-
- #region プロキシ経由の要求を行うためのプロキシ情報
-
- WebProxy proxy = this.CreateProxy(props);
- if (proxy != null)
- {
- reference.Proxy = proxy;
- }
-
- #endregion
-
- #region クライアント証明書、HTTP圧縮、エージェント ヘッダ、接続グループ.etc
-
- #region クライアント証明書(#z-このregion)
-
- X509Certificate2 x509 = this.CreateX509Certificate(props);
- if (x509 != null)
- {
- reference.ClientCertificates.Add(x509);
- }
-
- #endregion
-
- #region HTTP圧縮の有効・無効(Default:false)
-
- if (!props.ContainsKey(FxLiteral.TRANSMISSION_HTTP_PROP_ENABLEDE_COMPRESSION))// Dic化でnullチェック変更
- {
- // XML定義:キーが無い
- }
- else
- {
- if (string.IsNullOrEmpty(props[FxLiteral.TRANSMISSION_HTTP_PROP_ENABLEDE_COMPRESSION]))
- {
- // XML定義:null or 空文字列
- }
- else
- {
- // XML定義:あり
-
- bool compress;
-
- if (Boolean.TryParse(props[FxLiteral.TRANSMISSION_HTTP_PROP_ENABLEDE_COMPRESSION], out compress))
- {
- // 書式正常
- reference.EnableDecompression = compress;
- }
- else
- {
- // パラメータ・エラー(書式不正)
- throw new FrameworkException(
- FrameworkExceptionMessage.ERROR_IN_WRITING_OF_FX_SWITCH2[0],
- String.Format(FrameworkExceptionMessage.ERROR_IN_WRITING_OF_FX_SWITCH2[1],
- FxLiteral.TRANSMISSION_HTTP_PROP_ENABLEDE_COMPRESSION
- + "=" + props[FxLiteral.TRANSMISSION_HTTP_PROP_ENABLEDE_COMPRESSION]));
- }
- }
- }
-
- #endregion
-
- #region ユーザ エージェント ヘッダ値
-
- // (Default:MS Web Services Client Protocol number、numberは、CLRのver)
- if (!props.ContainsKey(FxLiteral.TRANSMISSION_HTTP_PROP_USER_AGENT))// Dic化でnullチェック変更
- {
- // XML定義:キーが無い
- }
- else
- {
- if (string.IsNullOrEmpty(props[FxLiteral.TRANSMISSION_HTTP_PROP_USER_AGENT]))
- {
- // XML定義:null or 空文字列
- }
- else
- {
- // XML定義:あり
- reference.UserAgent = props[FxLiteral.TRANSMISSION_HTTP_PROP_USER_AGENT]; // #34-この行
- }
- }
-
- #endregion
-
- #region 接続グループ(Default:Empty)
-
- if (!props.ContainsKey(FxLiteral.TRANSMISSION_HTTP_PROP_CONNECTION_GROUP_NAME))// Dic化でnullチェック変更
- {
- // XML定義:キーが無い
- }
- else
- {
- if (string.IsNullOrEmpty(props[FxLiteral.TRANSMISSION_HTTP_PROP_CONNECTION_GROUP_NAME]))
- {
- // XML定義:null or 空文字列
- }
- else
- {
- // XML定義:あり
- reference.ConnectionGroupName = props[FxLiteral.TRANSMISSION_HTTP_PROP_CONNECTION_GROUP_NAME];
- }
- }
-
- #endregion
-
- #endregion
-
- // 同期呼び出しで実行
- ret = reference.DotNETOnlineWS(
- serviceName, ref contextObject,
- parameterValueObject, out returnValueObject); // #x-この行
-
- #endregion
- }
- */
- /*
- else if (protocol == ((int)FxEnum.TmProtocol.WCF_HTTP).ToString())
- {
- #region WCF : basicHTTPBinding、wsHTTPBinding
-
- // 都度newしても接続はプールされているので、オーバーヘッドは少ない(と思われる)。
- this.WCF_HTTP = new WCFHTTPSvcForFxClient(this.WCF_HTTP_EndPointConfigName, url);
-
- #region WASのクライアント認証のセキュリティ資格情報 for WCF
-
- NetworkCredential nwcWAS = this.CreateCredentials(props);
- if (nwcWAS != null)
- {
- this.WCF_HTTP.ClientCredentials.Windows.ClientCredential = nwcWAS;
- }
-
- #endregion
-
- #region プロキシ経由の要求を行うためのプロキシ情報
-
- WebProxy proxy = this.CreateProxy(props);
- if (proxy != null)
- {
- WebRequest.DefaultWebProxy = proxy;
- }
-
- #endregion
-
- #region クライアント証明書 CERTIFICATE
-
- X509Certificate2 x509 = this.CreateX509Certificate(props);
- if (x509 != null)
- {
- this.WCF_HTTP.ClientCredentials.ClientCertificate.Certificate = x509;
- }
-
- #endregion
-
- ret = this.WCF_HTTP.DotNETOnlineWS(
- serviceName, ref contextObject,
- out returnValueObject, parameterValueObject);
-
- #endregion
- }
- */
- #endregion
-
//else
if (protocol == ((int)FxEnum.TmProtocol.WCF_TCPIP).ToString())
{
#region WCF : netTCPBinding
-
- // 都度newしても接続はプールされているので、オーバーヘッドは少ない(と思われる)。
- ChannelFactory factory = new ChannelFactory(
- this.WCF_TCPIP_EndPointConfigName, new EndpointAddress(url));
-
- try
- {
- #region Specifying WCF options: netTCPBinding
-
- #region WASのクライアント認証のセキュリティ資格情報 for WCF
-
- NetworkCredential nwcWAS = this.CreateCredentials(props);
- if (nwcWAS != null)
- {
- factory.Credentials.Windows.ClientCredential = nwcWAS;
- }
-
- #endregion
-
- #region クライアント証明書 CERTIFICATE
-
- X509Certificate2 x509 = this.CreateX509Certificate(props);
- if (x509 != null)
- {
- factory.Credentials.ClientCertificate.Certificate = x509;
- }
-
- #endregion
-
- this.WCF_TCPIP = factory.CreateChannel();
-
- #endregion
-
- // 同期呼び出しで実行
- ret = this.WCF_TCPIP.DotNETOnlineTCP(
- serviceName, ref contextObject,
- parameterValueObject, out returnValueObject);
- }
- finally
- {
- // Close、Disposeを実行する。
- // http://geekswithblogs.net/SoftwareDoneRight/archive/2008/05/23/clean-up-wcf-clients--the-right-way.aspx
-
- if (this.WCF_TCPIP != null)
- {
- ((IClientChannel)this.WCF_TCPIP).Close();
- ((IDisposable)this.WCF_TCPIP).Dispose();
- this.WCF_TCPIP = null;
- }
- }
+
#endregion
}
@@ -830,6 +498,84 @@ public object Invoke(string serviceName, object parameterValue)
#endregion
+#if (NETSTD || NETCOREAPP)
+#else
+ #region WCF_TCPIP
+ /// WCF TCPIP
+ /// string
+ /// string
+ /// int
+ /// Dictionary(string, string)
+ /// byte[]
+ /// byte[]
+ /// byte[]
+ /// エラー情報のバイト配列
+ private byte[] WCF_TCPIP(
+ string serviceName, string url, int timeout, Dictionary props,
+ byte[] contextObject, byte[] parameterValueObject, out byte[] returnValueObject)
+ {
+ // WCF TCP/IPサービス
+ IWCFTCPSvcForFx WCF_TCPIP = null;
+
+ // エラー情報のバイト配列
+ byte[] ret = null;
+
+ // 都度newしても接続はプールされているので、オーバーヘッドは少ない(と思われる)。
+ ChannelFactory factory = new ChannelFactory(
+ FxLiteral.WCF_TCPIP_ENDPOINT_CONFIGNAME, new EndpointAddress(url));
+
+ try
+ {
+ #region Specifying WCF options: netTCPBinding
+
+ #region WASのクライアント認証のセキュリティ資格情報 for WCF
+
+ NetworkCredential nwcWAS = this.CreateCredentials(props);
+ if (nwcWAS != null)
+ {
+ factory.Credentials.Windows.ClientCredential = nwcWAS;
+ }
+
+ #endregion
+
+ #region クライアント証明書 CERTIFICATE
+
+ X509Certificate2 x509 = this.CreateX509Certificate(props);
+ if (x509 != null)
+ {
+ factory.Credentials.ClientCertificate.Certificate = x509;
+ }
+
+ #endregion
+
+ WCF_TCPIP = factory.CreateChannel();
+
+ #endregion
+
+ // 同期呼び出しで実行
+ ret = WCF_TCPIP.DotNETOnlineTCP(
+ serviceName, ref contextObject,
+ parameterValueObject, out returnValueObject);
+ }
+ finally
+ {
+ // Close、Disposeを実行する。
+ // http://geekswithblogs.net/SoftwareDoneRight/archive/2008/05/23/clean-up-wcf-clients--the-right-way.aspx
+
+ if (WCF_TCPIP != null)
+ {
+ ((IClientChannel)WCF_TCPIP).Close();
+ ((IDisposable)WCF_TCPIP).Dispose();
+ WCF_TCPIP = null;
+ }
+ }
+
+ return ret;
+ }
+
+ #endregion
+#endif
+
#region ASPNETWebAPI
///
/// ASP.NET WebAPI (JSON-RPC)
@@ -1290,350 +1036,6 @@ private X509Certificate2 CreateX509Certificate(Dictionary props)
}
#endregion
-
- #region インナークラス(プロキシ類)
-
-#if (NETSTD || NETCOREAPP)
-#else
- #region WS-I Basic Profile v1.1、IIS + ASP.NET(#y-このregion)
-
- //------------------------------------------------------------------------------
- //
- // このコードはツールによって生成されました。
- // ランタイム バージョン:2.0.50727.3643
- //
- // このファイルへの変更は、以下の状況下で不正な動作の原因になったり、
- // コードが再生成されるときに損失したりします。
- //
- //------------------------------------------------------------------------------
-
- //
- // このソース コードは Microsoft.VSDesigner、バージョン 2.0.50727.3643 によって自動生成されました。
- //
-
- //[MS12-074] Windows XP および Windows Server 2003 用の
- // .NET Framework 2.0 Service Pack 2 のセキュリティ更新プログラム (2012 年 11 月 13 日) について
- //http://support.microsoft.com/kb/2729450/ja
-
- /// 自動生成したWeb参照(インナークラス化)
- /// コンストラクタ・非同期関係の処理をコメントアウト
- [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "2.0.50727.3053")]
- [System.Diagnostics.DebuggerStepThroughAttribute()]
- [System.ComponentModel.DesignerCategoryAttribute("code")]
- [System.Web.Services.WebServiceBindingAttribute(Name = "ServiceForFxSoap", Namespace = FxLiteral.WS_NAME_SPACE)]
- public partial class Reference : System.Web.Services.Protocols.SoapHttpClientProtocol
- {
- #region コンストラクタ・非同期関係の処理(コメントアウト)
-
- //private System.Threading.SendOrPostCallback DotNETOnlineWSOperationCompleted;
- //private bool useDefaultCredentialsSetExplicitly;
-
- //public Reference()
- //{
- // //this.Url = global::WindowsApplication1.Properties.Settings.Default.WindowsApplication1_localhost_ServiceForFx;
- // if ((this.IsLocalFileSystemWebService(this.Url) == true))
- // {
- // this.UseDefaultCredentials = true;
- // this.useDefaultCredentialsSetExplicitly = false;
- // }
- // else
- // {
- // this.useDefaultCredentialsSetExplicitly = true;
- // }
- //}
-
- //public new string Url
- //{
- // get
- // {
- // return base.Url;
- // }
- // set
- // {
- // if ((((this.IsLocalFileSystemWebService(base.Url) == true)
- // && (this.useDefaultCredentialsSetExplicitly == false))
- // && (this.IsLocalFileSystemWebService(value) == false)))
- // {
- // base.UseDefaultCredentials = false;
- // }
- // base.Url = value;
- // }
- //}
-
- //public new bool UseDefaultCredentials
- //{
- // get
- // {
- // return base.UseDefaultCredentials;
- // }
- // set
- // {
- // base.UseDefaultCredentials = value;
- // this.useDefaultCredentialsSetExplicitly = true;
- // }
- //}
-
- //public event DotNETOnlineWSCompletedEventHandler DotNETOnlineWSCompleted;
-
- #endregion
-
- #region Webメソッド:DotNETOnlineWS
-
- /// Webメソッド:DotNETOnlineWS
- /// サービス名
- /// コンテキスト
- /// 引数
- /// 戻り値
- /// 返すべきエラーの情報
- /// 値は全て.NETオブジェクトをバイナリシリアライズしたバイト配列データ
- [System.Web.Services.Protocols.SoapDocumentMethodAttribute(
- FxLiteral.WS_NAME_SPACE + FxLiteral.WS_METHOD_NAME_DOTNET_ONLINE,
- RequestNamespace = FxLiteral.WS_NAME_SPACE,
- ResponseNamespace = FxLiteral.WS_NAME_SPACE,
- Use = System.Web.Services.Description.SoapBindingUse.Literal,
- ParameterStyle = System.Web.Services.Protocols.SoapParameterStyle.Wrapped)]
- [return: System.Xml.Serialization.XmlElementAttribute(DataType = "base64Binary")]
- public byte[] DotNETOnlineWS(
- string serviceName,
- [System.Xml.Serialization.XmlElementAttribute(DataType = "base64Binary")] ref byte[] contextObject,
- [System.Xml.Serialization.XmlElementAttribute(DataType = "base64Binary")] byte[] parameterValueObject,
- [System.Xml.Serialization.XmlElementAttribute(DataType = "base64Binary")] out byte[] returnValueObject)
- {
- object[] results = this.Invoke(FxLiteral.WS_METHOD_NAME_DOTNET_ONLINE,
- new object[] { serviceName, contextObject, parameterValueObject });
-
- contextObject = ((byte[])(results[1]));
- returnValueObject = ((byte[])(results[2]));
- return ((byte[])(results[0]));
- }
-
- #endregion
-
- #region 非同期関係の処理(コメントアウト)
-
- /////
- //public void DotNETOnlineWSAsync(string serviceName, byte[] contextObject, byte[] parameterValueObject)
- //{
- // this.DotNETOnlineWSAsync(serviceName, contextObject, parameterValueObject, null);
- //}
-
- /////
- //public void DotNETOnlineWSAsync(string serviceName, byte[] contextObject, byte[] parameterValueObject, object userState)
- //{
- // if ((this.DotNETOnlineWSOperationCompleted == null))
- // {
- // this.DotNETOnlineWSOperationCompleted = new System.Threading.SendOrPostCallback(this.OnDotNETOnlineWSOperationCompleted);
- // }
- // this.InvokeAsync("DotNETOnlineWS", new object[] {
- // serviceName,
- // contextObject,
- // parameterValueObject}, this.DotNETOnlineWSOperationCompleted, userState);
- //}
-
- //private void OnDotNETOnlineWSOperationCompleted(object arg)
- //{
- // if ((this.DotNETOnlineWSCompleted != null))
- // {
- // System.Web.Services.Protocols.InvokeCompletedEventArgs invokeArgs = ((System.Web.Services.Protocols.InvokeCompletedEventArgs)(arg));
- // this.DotNETOnlineWSCompleted(this, new DotNETOnlineWSCompletedEventArgs(invokeArgs.Results, invokeArgs.Error, invokeArgs.Cancelled, invokeArgs.UserState));
- // }
- //}
-
- /////
- //public new void CancelAsync(object userState)
- //{
- // base.CancelAsync(userState);
- //}
-
- //private bool IsLocalFileSystemWebService(string url)
- //{
- // if (((url == null)
- // || (url == string.Empty)))
- // {
- // return false;
- // }
- // System.Uri wsUri = new System.Uri(url);
- // if (((wsUri.Port >= 1024)
- // && (string.Compare(wsUri.Host, "localHost", System.StringComparison.OrdinalIgnoreCase) == 0)))
- // {
- // return true;
- // }
- // return false;
- //}
-
- #endregion
- }
-
- #region 非同期関係の処理(コメントアウト)
-
- /////
- //[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "2.0.50727.3053")]
- //public delegate void DotNETOnlineWSCompletedEventHandler(object sender, DotNETOnlineWSCompletedEventArgs e);
-
- /////
- //[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "2.0.50727.3053")]
- //[System.Diagnostics.DebuggerStepThroughAttribute()]
- //[System.ComponentModel.DesignerCategoryAttribute("code")]
- //public partial class DotNETOnlineWSCompletedEventArgs : System.ComponentModel.AsyncCompletedEventArgs
- //{
-
- // private object[] results;
-
- // internal DotNETOnlineWSCompletedEventArgs(object[] results, System.Exception exception, bool cancelled, object userState)
- // :
- // base(exception, cancelled, userState)
- // {
- // this.results = results;
- // }
-
- // ///
- // public byte[] Result
- // {
- // get
- // {
- // this.RaiseExceptionIfNecessary();
- // return ((byte[])(this.results[0]));
- // }
- // }
-
- // ///
- // public byte[] contextObject
- // {
- // get
- // {
- // this.RaiseExceptionIfNecessary();
- // return ((byte[])(this.results[1]));
- // }
- // }
-
- // ///
- // public byte[] returnValueObject
- // {
- // get
- // {
- // this.RaiseExceptionIfNecessary();
- // return ((byte[])(this.results[2]));
- // }
- // }
- //}
-
- #endregion
-
- #region 旧処理
-
- //[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Web.Services", "2.0.50727.3053")]
- //[System.Diagnostics.DebuggerStepThroughAttribute()]
- //[System.ComponentModel.DesignerCategoryAttribute("code")]
- //[System.Web.Services.WebServiceBindingAttribute(Name = "ServiceSoap", Namespace = FxLiteral.WS_NAME_SPACE)]
- //private class Reference : System.Web.Services.Protocols.SoapHttpClientProtocol
- //{
- // /// Webサービス呼び出しのメソッド
- // /// インナークラス
- // [System.Web.Services.Protocols.SoapDocumentMethodAttribute(
- // FxLiteral.WS_NAME_SPACE + FxLiteral.WS_METHOD_NAME_DOTNET_ONLINE,
- // RequestNamespace = FxLiteral.WS_NAME_SPACE,
- // ResponseNamespace = FxLiteral.WS_NAME_SPACE,
- // Use = System.Web.Services.Description.SoapBindingUse.Literal,
- // ParameterStyle = System.Web.Services.Protocols.SoapParameterStyle.Wrapped
- // )]
- // public byte[] DotNETOnlineWS(
- // string serviceName, ref byte[] contextObject,
- // byte[] parameterValueObject, out byte[] returnValueObject)
- // {
- // object[] results = this.Invoke(FxLiteral.WS_METHOD_NAME_DOTNET_ONLINE,
- // new object[] { serviceName, contextObject, parameterValueObject});
-
- // contextObject = ((byte[])(results[1]));
- // returnValueObject = ((byte[])(results[2]));
- // return ((byte[])(results[0]));
- // }
- //}
-
- #endregion
-
- #endregion
-
- #region WCF HTTP Webサービス : basicHTTPBinding、wsHTTPBinding
-
- //------------------------------------------------------------------------------
- //
- // このコードはツールによって生成されました。
- // ランタイム バージョン:4.0.30319.296
- //
- // このファイルへの変更は、以下の状況下で不正な動作の原因になったり、
- // コードが再生成されるときに損失したりします。
- //
- //------------------------------------------------------------------------------
-
- [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
- [System.ServiceModel.ServiceContractAttribute(ConfigurationName = FxLiteral.SERVICE_CONTRACT_ATTRIBUTE_CONFIGURATION_NAME)]
- public interface IWCFHTTPSvcForFx
- {
- [System.ServiceModel.OperationContractAttribute(
- Action = "http://tempuri.org/IWCFHTTPSvcForFx/DotNETOnlineWS",
- ReplyAction = "http://tempuri.org/IWCFHTTPSvcForFx/DotNETOnlineWSResponse")]
- byte[] DotNETOnlineWS(
- string serviceName,
- ref byte[] contextObject,
- out byte[] returnValueObject,
- byte[] parameterValueObject);
- }
-
- [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
- public interface IWCFHTTPSvcForFxChannel : IWCFHTTPSvcForFx, IClientChannel
- {
- }
-
- [System.Diagnostics.DebuggerStepThroughAttribute()]
- [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")]
- public partial class WCFHTTPSvcForFxClient : ClientBase, IWCFHTTPSvcForFx
- {
- public WCFHTTPSvcForFxClient()
- {
- }
-
- public WCFHTTPSvcForFxClient(string endpointConfigurationName) :
- base(endpointConfigurationName)
- {
- }
-
- public WCFHTTPSvcForFxClient(string endpointConfigurationName, string remoteAddress) :
- base(endpointConfigurationName, remoteAddress)
- {
- }
-
- public WCFHTTPSvcForFxClient(
- string endpointConfigurationName,
- System.ServiceModel.EndpointAddress remoteAddress) :
- base(endpointConfigurationName, remoteAddress)
- {
- }
-
- public WCFHTTPSvcForFxClient(
- Binding binding,
- System.ServiceModel.EndpointAddress remoteAddress) :
- base(binding, remoteAddress)
- {
- }
-
- public byte[] DotNETOnlineWS(
- string serviceName,
- ref byte[] contextObject,
- out byte[] returnValueObject,
- byte[] parameterValueObject)
- {
- return base.Channel.DotNETOnlineWS(
- serviceName,
- ref contextObject,
- out returnValueObject,
- parameterValueObject);
- }
- }
-
- #endregion
-#endif
-
- #endregion
}
}
diff --git a/root/programs/CS/Frameworks/Infrastructure/Framework/Util/FxLiteral.cs b/root/programs/CS/Frameworks/Infrastructure/Framework/Util/FxLiteral.cs
index 9c77170e4..269a926f4 100644
--- a/root/programs/CS/Frameworks/Infrastructure/Framework/Util/FxLiteral.cs
+++ b/root/programs/CS/Frameworks/Infrastructure/Framework/Util/FxLiteral.cs
@@ -549,6 +549,28 @@ public const string WS_METHOD_DESCRIPTION_WEB_SERVICE_BRIDGE
public const string TRANSMISSION_HTTP_PROP_USER_AGENT = "USERAGENT";
/// 接続グループ
+ ///
+ /// **現在は効果が無い。**(#546)
+ ///
+ /// <何のための値だったか>
+ /// HttpWebRequest.ConnectionGroupName に渡し、ServicePoint
+ /// (scheme+host+port)の接続プールを、さらに分割するための名前だった。
+ /// NTLM / Kerberos やクライアント証明書は**接続に対して**認証が確立するため、
+ /// ユーザ A で認証済みの接続をユーザ B が再利用しないよう仕切る必要があった。
+ ///
+ /// <なぜ効かなくなったか>
+ /// 通信が HttpClient に移り、**設定する口が無くなった**。
+ /// HttpClientHandler にも WebRequestHandler にも相当するプロパティは無い。
+ ///
+ /// <目的は別の形で満たされている>
+ /// HttpClient では、接続プールの単位が HttpClientHandler そのものである。
+ /// CallController は**サービス名ごとにハンドラと HttpClient をプール**しており
+ /// (_handlerCD / _httpClientCD)、資格情報もサービス名ごとの Prop で決まるため、
+ /// 接続が混ざることはない。
+ ///
+ /// このため、定数は互換のために残すが、値を書いても読まれない。
+ /// 定義例(root/files/resource/Test/nw)からは外してある。
+ ///
public const string TRANSMISSION_HTTP_PROP_CONNECTION_GROUP_NAME = "CONNGROUPNAME";
#endregion
diff --git a/root/programs/CS/Frameworks/Infrastructure/Public/Dto/DTColumn.cs b/root/programs/CS/Frameworks/Infrastructure/Public/Dto/DTColumn.cs
index 22c2ed708..332c6367d 100644
--- a/root/programs/CS/Frameworks/Infrastructure/Public/Dto/DTColumn.cs
+++ b/root/programs/CS/Frameworks/Infrastructure/Public/Dto/DTColumn.cs
@@ -29,9 +29,11 @@
//* ---------- ---------------- -------------------------------------------------
//* 2010/03/xx 西野 大介 新規作成
//* 2011/10/09 西野 大介 国際化対応
+//* 2026/08/14 玄人 幸道 AutoCastのカルチャを固定した(#544)。
//**********************************************************************************
using System;
+using System.Globalization;
using System.Text.RegularExpressions;
namespace Touryo.Infrastructure.Public.Dto
@@ -165,10 +167,21 @@ public static bool CheckType(object o, DTType dtType)
/// 変換後の値
public static object AutoCast(DTType dtType, object o)
{
+ // **カルチャを固定する。**(#544)
+ //
+ // 既定のカルチャで解釈すると、小数点が「,」になる環境で書いた値を
+ // 別の環境で読んだときに、**例外にならずに値が変わる**
+ // ("1234,56" を ja-JP で読むと 123456 になる)。
+ // 書き出し側(CustomMarshaler.StringFromPrimitivetype)も固定してある。
+ //
+ // 「.」を小数点に使うカルチャ(ja-JP / en-US など)では、
+ // 解釈は従来と同じである。
+ IFormatProvider provider = CultureInfo.InvariantCulture;
+
switch (dtType)
{
case DTType.Boolean:
- return Convert.ToBoolean(o);
+ return Convert.ToBoolean(o, provider);
case DTType.ByteArray:
// バイト配列の自動変換はサポートしない
@@ -176,28 +189,28 @@ public static object AutoCast(DTType dtType, object o)
"It is a data type which is not supporting automatic conversion (System.Byte[]). ");
case DTType.Char:
- return Convert.ToChar(o);
+ return Convert.ToChar(o, provider);
case DTType.DateTime:
- return Convert.ToDateTime(o);
+ return Convert.ToDateTime(o, provider);
case DTType.Decimal:
- return Convert.ToDecimal(o);
+ return Convert.ToDecimal(o, provider);
case DTType.Double:
- return Convert.ToDouble(o);
+ return Convert.ToDouble(o, provider);
case DTType.Int16:
- return Convert.ToInt16(o);
+ return Convert.ToInt16(o, provider);
case DTType.Int32:
- return Convert.ToInt32(o);
+ return Convert.ToInt32(o, provider);
case DTType.Int64:
- return Convert.ToInt64(o);
+ return Convert.ToInt64(o, provider);
case DTType.Single:
- return Convert.ToSingle(o);
+ return Convert.ToSingle(o, provider);
case DTType.String:
// 文字列の自動変換はサポートしない
@@ -289,6 +302,192 @@ public static DTType StringToEnum(string strType)
#endregion
+ #region 値と文字列の相互変換
+
+ // <なぜここに置くか>(#544)
+ // 以前は Util.CustomMarshaler にあったが、
+ // ・CustomMarshaler の本体はアンマネージ構造体の相互運用(Marshal クラス)で、
+ // DTType の変換とは別の責務だった(同じ「マーシャリング」という語が
+ // 2 つの意味で使われていた)
+ // ・Util から Dto への参照が、この 2 メソッドのためだけに生じていた
+ // (Dto → Util の参照と合わせて循環していた)
+ // ・対になる AutoCast / CheckType / EnumToString は、もともとここにある
+ // という理由で、DTType の変換をこのクラスに集約した。
+ //
+ // CustomMarshaler 側には、当面は転送するだけのメソッドを残してある。
+
+ /// プリミティブな値を文字列にする
+ /// 値
+ /// 行単位で区切る形式向けに、改行を退避するか
+ /// 文字列(値が null なら null)
+ ///
+ /// **カルチャに依存しない形にする。**
+ /// 機械の間で持ち回る形式を作るためのメソッドで、
+ /// 既定のカルチャで書くと、小数点が「,」になる環境で書いた値を
+ /// 別の環境で読んだときに、**例外にならずに値が変わる**(#544)。
+ ///
+ public static string StringFromPrimitivetype(object primitiveType, bool checkType)
+ {
+ string convertedString = null;
+
+ if (primitiveType != null)
+ {
+ if (DTColumn.CheckType(primitiveType, DTType.DateTime))
+ {
+ // ISO 8601(ラウンドトリップ書式)で書き出す。
+ //
+ // <旧書式(yyyy/M/d-H:m:s.fff)から変えた理由>(#544)
+ // ・他システムと JSON をやり取りできない(ISO 8601 が事実上の標準)
+ // ・ミリ秒より細かい値と DateTimeKind が落ちる
+ // ・カルチャに依存しない形であることが、書式からは読み取れない
+ //
+ // 読み込み(PrimitivetypeFromString)は旧書式も受け付けるため、
+ // 過去に書き出したデータはそのまま読める。
+ convertedString = ((DateTime)primitiveType)
+ .ToString("o", CultureInfo.InvariantCulture);
+ }
+ else if (DTColumn.CheckType(primitiveType, DTType.ByteArray))
+ {
+ convertedString = Convert.ToBase64String((byte[])primitiveType);
+ }
+ else if (DTColumn.CheckType(primitiveType, DTType.String))
+ {
+ if (checkType == true)
+ {
+ // 行単位で区切る形式(DTTables.Save)向けに、改行を退避する。
+ //
+ // 「\r」を「\rrnr:」、「\n」を「\rrnn:」に置き換えると、
+ // WriteLine / ReadLine を通したときに
+ // … 元の文字列の前半
+ // rnr: 元の文字列の後半
+ // のように分かれ、DTTables.Load 側で連結して元に戻せる。
+ //
+ // <以前は、置換が効いていなかった>(#544)
+ // 3 行とも primitiveType.ToString() から書き始めており、
+ // 前の置換結果を捨てていたため、最後の 1 行だけが効いていた。
+ // その結果、改行を含む文字列は Load 側で
+ // **改行以降が捨てられていた**(例外は出ない)。
+ convertedString = primitiveType.ToString()
+ .Replace("\r", "\rrnr:")
+ .Replace("\n", "\rrnn:");
+ }
+ else
+ {
+ convertedString = primitiveType.ToString();
+ }
+ }
+ else if (DTColumn.CheckType(primitiveType, DTType.Double))
+ {
+ // 「R」は最短のラウンドトリップ書式。
+ //
+ // <既定の書式では往復しない>(#544)
+ // .NET Framework の既定は有効桁 15 桁で、
+ // ランダムな Double の 94% が元の値に戻らない。
+ // Double.MaxValue に至っては、読み戻しで OverflowException になる
+ // (15 桁への丸めで、表現できる上限を超えるため)。
+ // .NET (Core) 3.0 以降の既定は最短ラウンドトリップだが、
+ // 両者で挙動が割れるため、書式を明示して揃える。
+ convertedString = ((double)primitiveType).ToString("R", CultureInfo.InvariantCulture);
+ }
+ else if (DTColumn.CheckType(primitiveType, DTType.Single))
+ {
+ // 理由は Double と同じ(Single は 68% が往復しなかった)。
+ convertedString = ((float)primitiveType).ToString("R", CultureInfo.InvariantCulture);
+ }
+ else
+ {
+ // 「.」を小数点に使うカルチャ(ja-JP / en-US など)では、
+ // 出力は従来と同じである。
+ convertedString = Convert.ToString(primitiveType, CultureInfo.InvariantCulture);
+ }
+ }
+
+ return convertedString;
+ }
+
+ /// 文字列をプリミティブな値に戻す
+ /// 列の型
+ /// 文字列
+ /// 値(文字列が null なら null)
+ public static object PrimitivetypeFromString(DTType colType, string cellString)
+ {
+ object convertedPrimitiveType = null;
+
+ if (cellString != null)
+ {
+ // ByteArray
+ if (colType == DTType.ByteArray)
+ {
+ convertedPrimitiveType = Convert.FromBase64String(cellString);
+ }
+ // DateTime
+ else if (colType == DTType.DateTime)
+ {
+ convertedPrimitiveType = DTColumn.StringToDateTime(cellString);
+ }
+ // String
+ else if (colType == DTType.String)
+ {
+ // 変換の必要が無いので、そのまま返す。
+ //
+ // <以前は例外になっていた>(#544)
+ // AutoCast に落ちており、AutoCast は String を
+ // 「自動変換の対象外」として例外を投げる。このため
+ // 呼び出し側が手前で String を分岐して避けていた。
+ // 本メソッド単体では使えない状態だったので、ここで受ける。
+ convertedPrimitiveType = cellString;
+ }
+ else
+ {
+ convertedPrimitiveType = DTColumn.AutoCast(colType, cellString);
+ }
+ }
+
+ return convertedPrimitiveType;
+ }
+
+ ///
+ /// 文字列を DateTime に戻す(ISO 8601 と旧書式の両方を受け付ける)。
+ ///
+ /// セルの文字列
+ /// DateTime
+ ///
+ /// StringFromPrimitivetype は ISO 8601 で書き出すが、
+ /// それ以前に書き出したデータ(yyyy/M/d-H:m:s.fff)も読めるようにしてある(#544)。
+ /// 旧書式を落とすと、永続化・退避した既存データが読めなくなるため。
+ ///
+ private static DateTime StringToDateTime(string cellString)
+ {
+ // ISO 8601(ラウンドトリップ書式)
+ //
+ // RoundtripKind を指定して、DateTimeKind(Utc / Local / Unspecified)を保つ。
+ // 指定しないと、末尾に Z が付いた値が Local へ変換されてしまう。
+ DateTime iso;
+ if (DateTime.TryParseExact(cellString, "o",
+ CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out iso))
+ {
+ return iso;
+ }
+
+ // 旧書式(yyyy/M/d-H:m:s.fff)
+ //
+ // 「-」で日付部と時刻部を分けている。ISO 8601 は日付部にも「-」を含むため、
+ // 上の ISO の判定を必ず先に行うこと。
+ string ymd = cellString.Split('-')[0];
+ string hmsf = cellString.Split('-')[1];
+
+ return new DateTime(
+ int.Parse(ymd.Split('/')[0]),
+ int.Parse(ymd.Split('/')[1]),
+ int.Parse(ymd.Split('/')[2]),
+ int.Parse(hmsf.Split(':')[0]),
+ int.Parse(hmsf.Split(':')[1]),
+ int.Parse(hmsf.Split(':')[2].Split('.')[0]),
+ int.Parse(hmsf.Split(':')[2].Split('.')[1]));
+ }
+
+ #endregion
+
#endregion
}
}
diff --git a/root/programs/CS/Frameworks/Infrastructure/Public/Dto/DTRow.cs b/root/programs/CS/Frameworks/Infrastructure/Public/Dto/DTRow.cs
index 34bf827e6..f8c8027c9 100644
--- a/root/programs/CS/Frameworks/Infrastructure/Public/Dto/DTRow.cs
+++ b/root/programs/CS/Frameworks/Infrastructure/Public/Dto/DTRow.cs
@@ -34,6 +34,7 @@
//* 2010/11/30 前川 祐介 Added、Deletedは、Modifiedに変更しない
//* 2011/09/06 前川 祐介 共通化、同値が設定された場合、RowStateを変更しない
//* 2011/10/09 西野 大介 国際化対応
+//* 2026/08/14 玄人 幸道 DataRowStateをDTRowStateに改名(#544)。
//**********************************************************************************
using System;
@@ -55,7 +56,7 @@ public class DTRow : IEnumerable
private List
[FlagsAttribute]
- public enum DataRowState
+ public enum DTRowState
{
///
/// 行が、どの行リストにも属さない状態
diff --git a/root/programs/CS/Frameworks/Infrastructure/Public/Dto/DTRows.cs b/root/programs/CS/Frameworks/Infrastructure/Public/Dto/DTRows.cs
index 9e226c629..919ca8f86 100644
--- a/root/programs/CS/Frameworks/Infrastructure/Public/Dto/DTRows.cs
+++ b/root/programs/CS/Frameworks/Infrastructure/Public/Dto/DTRows.cs
@@ -34,6 +34,7 @@
//* DeleteFromList、Find、ToDataSource
//* 2010/11/11 前川 祐介 Silverlight対応(ジェネリック)
//* 2011/10/09 西野 大介 国際化対応
+//* 2026/08/14 玄人 幸道 DataRowStateをDTRowStateに改名(#544)。
//**********************************************************************************
using System;
@@ -92,7 +93,7 @@ public DTRow AddNew()
DTRow dr = new DTRow(this._cols);
// 行ステータスを "Added"(追加された行) に変更
- dr.RowState = DataRowState.Added;
+ dr.RowState = DTRowState.Added;
this._rows.Add(dr);
@@ -114,7 +115,7 @@ public void Add(DTRow dr)
this._rows.Add(dr);
// 行ステータスを "Added"(追加された行) に変更
- dr.RowState = DataRowState.Added;
+ dr.RowState = DTRowState.Added;
// 行数をインクリメント
this._tblStat.RowsCount++;
@@ -133,7 +134,7 @@ public void Delete(int index)
if (0 < this._rows.Count)
{
// 行ステータスを "Deleted"(削除された行) に変更
- ((DTRow)this._rows[index]).RowState = DataRowState.Deleted;
+ ((DTRow)this._rows[index]).RowState = DTRowState.Deleted;
this._tblStat.RowsCount--;
}
@@ -200,9 +201,9 @@ public IEnumerator GetEnumerator()
/// 行ステータス
/// 指定したステータスの行
/// 行ステータスは論理和演算で複数条件の指定が可能
- public DTRows Find(DataRowState rowState)
+ public DTRows Find(DTRowState rowState)
{
- DataRowState workState; // 行ステータスの退避用
+ DTRowState workState; // 行ステータスの退避用
// List型として、指定したステータスの行を取得する
@@ -253,10 +254,10 @@ public DTRows ToDataSource()
{
// 削除された行を除外して返す
return this.Find(
- DataRowState.Added
- | DataRowState.Detached
- | DataRowState.Modified
- | DataRowState.Unchanged);
+ DTRowState.Added
+ | DTRowState.Detached
+ | DTRowState.Modified
+ | DTRowState.Unchanged);
}
#endregion
diff --git a/root/programs/CS/Frameworks/Infrastructure/Public/Dto/DTTable.cs b/root/programs/CS/Frameworks/Infrastructure/Public/Dto/DTTable.cs
index 9a5daf4af..d2732d048 100644
--- a/root/programs/CS/Frameworks/Infrastructure/Public/Dto/DTTable.cs
+++ b/root/programs/CS/Frameworks/Infrastructure/Public/Dto/DTTable.cs
@@ -27,16 +27,16 @@
//*
//* 日時 更新者 内容
//* ---------- ---------------- -------------------------------------------------
-//* 2010/03/xx 西野 大介 新規作成
-//* 2010/11/11 前川 祐介 メソッド追加
+//* 2010/03/xx 西野 大介 新規作成
+//* 2010/11/11 前川 祐介 メソッド追加
//* 一覧更新処理対応
-//* ・AcceptChanges、GetChanges
+//* ・AcceptChanges、GetChanges
//* Datatable対応
-//* ・ToDataTable、FromDataTable
-//* ・ConvertDTTypeToType、ConvertTypeToDTType
-//* 2011/10/09 西野 大介 国際化対応
-//* 2015/03/05 Supragyan Created SavejqGridJson method for saving datatable data to JQGrid.
-//* 2015/06/08 Supragyan Modified SavejqGridJson method for saving datatable data to JQGrid.
+//* ・ToDataTable、FromDataTable
+//* ・ConvertDTTypeToType、ConvertTypeToDTType
+//* 2011/10/09 西野 大介 国際化対応
+//* 2026/08/14 玄人 幸道 ToDataTableで行ステータスを保つようにした(#544)。
+//* 2026/08/14 玄人 幸道 DataRowStateをDTRowStateに改名(#544)。
//**********************************************************************************
using System;
@@ -76,7 +76,7 @@ public class DTTable
/// 確認専用(変更されないように)
public string TableName
{
- get
+ get
{
return this._tblName;
}
@@ -116,7 +116,7 @@ public DTTable(string tblName)
// コンバートのことを考え棟梁部品は使用しない。)
Regex rgx = new Regex("^[a-zA-Z0-9_-]+$");
Match mch = rgx.Match(tblName);
-
+
if (mch.Success)
{
// 表名が正しい
@@ -144,11 +144,22 @@ public DTTable(string tblName)
/// DTTableをSystem.Data.DataTableに変換する
///
/// 変換後のSystem.Data.DataTable
+ ///
+ /// 行ステータス(RowState)を保って変換する(#544)。
+ ///
+ /// <以前は失われていた>
+ /// 変換の前後で AcceptChanges を呼んでいたため、全行が Unchanged になり、
+ /// Deleted の行は消えていた。FromDataTable が行ステータスを復元しているのに、
+ /// 戻すと落ちる、という非対称な状態だった。
+ /// これでは受け取った側で Added / Modified / Deleted の振り分けができない。
+ ///
+ /// <Modified の元の値は復元されない>
+ /// DTRow は現在値と行ステータスだけを持ち、変更前の値を持たない。
+ /// このため DataRowVersion.Original には現在値が入る。
+ /// 区分(Added / Modified / Deleted)の判別を目的とした変換である。
+ ///
public DataTable ToDataTable()
{
- // DataTable 変換前に、変更を確定させる
- this.AcceptChanges();
-
// テーブル定義
DataTable dt = new DataTable(this.TableName);
@@ -160,25 +171,66 @@ public DataTable ToDataTable()
}
// 値追加
- DataRow dr;
+ //
+ // Detached(どの行リストにも属さない)は DataTable では表せないため、
+ // ここでは追加しない。行ステータスを戻すとき、位置がずれないよう
+ // DTRow との対応を控えておく。
+ List added = new List();
+
foreach (DTRow row in this.Rows)
{
+ if (row.RowState == DTRowState.Detached) { continue; }
+
// 行を新規作成
- dr = dt.NewRow();
+ DataRow dr = dt.NewRow();
// 各列ごとに値を追加
foreach (DTColumn col in this.Cols)
{
- dr[col.ColName] = row[col.ColName];
+ object value = row[col.ColName];
+
+ // **null は DBNull に置き換える。**(#544)
+ // DTRow は値なしを null で持つが、DataRow は null を受け付けず
+ // 「Column を null に設定できません。DBNull を使用してください。」
+ // と ArgumentException を投げる。
+ // 文字列の列は通ってしまうため、値型の列で初めて落ちる。
+ dr[col.ColName] = (value == null) ? DBNull.Value : value;
}
// 行をテーブルに追加
dt.Rows.Add(dr);
+ added.Add(row);
}
- // 値を確定させておく
+ // いったん確定させ、全行を Unchanged にする。
+ // SetAdded / SetModified は Unchanged の行にしか使えないため、この順である。
dt.AcceptChanges();
+ // 行ステータスを復元する
+ for (int i = 0; i < added.Count; i++)
+ {
+ switch (added[i].RowState)
+ {
+ case DTRowState.Added:
+ dt.Rows[i].SetAdded();
+ break;
+
+ case DTRowState.Modified:
+ dt.Rows[i].SetModified();
+ break;
+
+ case DTRowState.Deleted:
+ // Delete しても行は Rows に残る(RowState が Deleted になる)ため、
+ // 以降の添字はずれない。
+ dt.Rows[i].Delete();
+ break;
+
+ default:
+ // Unchanged は、AcceptChanges 済みの状態がそのまま当てはまる。
+ break;
+ }
+ }
+
// DataTable を返す
return dt;
}
@@ -224,23 +276,23 @@ public static DTTable FromDataTable(DataTable table)
// 行ステータスを復元
if (row.RowState == System.Data.DataRowState.Detached)
{
- dr.RowState = DataRowState.Detached;
+ dr.RowState = DTRowState.Detached;
}
else if (row.RowState == System.Data.DataRowState.Added)
{
- dr.RowState = DataRowState.Added;
+ dr.RowState = DTRowState.Added;
}
else if (row.RowState == System.Data.DataRowState.Modified)
{
- dr.RowState = DataRowState.Modified;
+ dr.RowState = DTRowState.Modified;
}
else if (row.RowState == System.Data.DataRowState.Deleted)
{
- dr.RowState = DataRowState.Deleted;
+ dr.RowState = DTRowState.Deleted;
}
else
{
- dr.RowState = DataRowState.Unchanged;
+ dr.RowState = DTRowState.Unchanged;
}
}
@@ -373,9 +425,9 @@ private static DTType ConvertTypeToDTType(Type type)
public DTRows GetChanges()
{
return this._rows.Find(
- DataRowState.Added
- | DataRowState.Deleted
- | DataRowState.Modified);
+ DTRowState.Added
+ | DTRowState.Deleted
+ | DTRowState.Modified);
}
///
@@ -387,7 +439,7 @@ public void AcceptChanges()
while (i < this.Rows.Count)
{
- if (this.Rows[i].RowState == DataRowState.Deleted)
+ if (this.Rows[i].RowState == DTRowState.Deleted)
{
// 行リストから明示的に削除する
this.Rows.DeleteFromList(i);
@@ -395,94 +447,10 @@ public void AcceptChanges()
else
{
// 行ステータスをUnchangedに変え、行の値を確定させる
- this.Rows[i].RowState = DataRowState.Unchanged;
+ this.Rows[i].RowState = DTRowState.Unchanged;
i++;
}
}
}
-
-
- ///
- /// SavejqGridJson method for converting datatable data to JQGrid data by passing total count,rows of current page.
- ///
- /// DataTable data
- /// Total count
- /// current page number
- /// Rows count
- /// returns JQGrid data
- public JQGridDataClass SavejqGridJson(DataTable data, int totalCount, string page, string rows)
- {
- int intRows = Math.Min(int.Parse(rows), totalCount);
- int intPage = int.Parse(page);
-
- // jqGrid に渡すデータを格納
- JQGridDataClass jqGridData = new JQGridDataClass();
- jqGridData.page = page;
- jqGridData.total = (int)Math.Ceiling((double)totalCount / (double)intRows);
-
- // jqGrid の各セルに表示するデータを格納
- jqGridData.rows = new List();
- for (int rIndex = 0; rIndex < intRows; rIndex++)
- {
- // ページなどを考慮し、DataTable から取得する行インデックスを取得
- if ((rIndex + 1) > data.Rows.Count)
- {
- // 行インデックスが、実際の行数を超えた場合は、ループを抜ける
- // (最終ページの行数が端数の場合)
- break;
- }
-
- // jqGrid に表示する、1 行分のデータを格納
- JQGridDataRowClass subData = new JQGridDataRowClass();
- subData.id = data.Rows[rIndex][0].ToString();
- subData.cell = new List();
-
- for (int cIndex = 0; cIndex < data.Columns.Count; cIndex++)
- {
- // 列のデータを、DataTable から取得
- subData.cell.Add(data.Rows[rIndex][cIndex].ToString());
- }
- jqGridData.rows.Add(subData);
- }
- // jqGrid にデータを返す
- return jqGridData;
- }
-
- ///
- /// jqGrid に渡すデータを格納するクラス
- ///
- public class JQGridDataClass
- {
- ///
- /// クエリの現在のページ
- ///
- public string page;
-
- ///
- /// クエリの総ページ数
- ///
- public int total;
-
- ///
- /// 実際のデータを含むリスト
- ///
- public List rows;
- }
-
- ///
- /// jqGrid に表示する、1 行分のデータを格納するクラス
- ///
- public class JQGridDataRowClass
- {
- ///
- /// 列の固有 ID
- ///
- public string id;
-
- ///
- /// 列のデータを含むリスト
- ///
- public List cell;
- }
}
}
diff --git a/root/programs/CS/Frameworks/Infrastructure/Public/Dto/DTTables.cs b/root/programs/CS/Frameworks/Infrastructure/Public/Dto/DTTables.cs
index 6f293c3ec..825bd75bb 100644
--- a/root/programs/CS/Frameworks/Infrastructure/Public/Dto/DTTables.cs
+++ b/root/programs/CS/Frameworks/Infrastructure/Public/Dto/DTTables.cs
@@ -1,4 +1,4 @@
-//**********************************************************************************
+//**********************************************************************************
//* Copyright (C) 2007,2016 Hitachi Solutions,Ltd.
//**********************************************************************************
@@ -27,20 +27,19 @@
//*
//* 日時 更新者 内容
//* ---------- ---------------- -------------------------------------------------
-//* 2010/03/xx 西野 大介 新規作成
-//* 2010/11/11 前川 祐介 一覧更新処理対応(行ステータス)
-//* 2010/11/11 前川 祐介 Silverlight対応(ジェネリック)
-//* 2011/10/09 西野 大介 国際化対応
-//* 2011/11/21 西野 大介 マーシャリングのサポート メソッドを追加
-//* 2014/09/05 Rituparna Added TableRecords class ,SaveJson and LoadJson Method
-//* 2014/09/09 Rituparna Modified TableRecords class ,SaveJson and LoadJson Method
-//* 2015/01/15 Supragyan Added StringFromPrimitivetypes and PrimitivetypeFromString method
-//* 2015/01/15 Supragyan Modified SaveJson,LoadJson,Save,Load method by implementing
-//* StringFromPrimitivetypes and PrimitivetypeFromString method
-//* 2015/03/20 Sai Modifed varaiable '_tbls' access specifier to Private instead of Public
+//* 2010/03/xx 西野 大介 新規作成
+//* 2010/11/11 前川 祐介 一覧更新処理対応(行ステータス)
+//* 2010/11/11 前川 祐介 Silverlight対応(ジェネリック)
+//* 2011/10/09 西野 大介 国際化対応
+//* 2011/11/21 西野 大介 マーシャリングのサポート メソッドを追加
+//* 2026/08/14 玄人 幸道 SaveJson / LoadJson を追加(#544)。
+//* 2026/08/14 玄人 幸道 値と文字列の相互変換の呼び先をDTColumnに変更(#544)。
+//* 2026/08/14 玄人 幸道 DataSetとの相互変換を追加(#544)。
+//* 2026/08/14 玄人 幸道 DataRowStateをDTRowStateに改名(#544)。
//**********************************************************************************
using System;
+using System.Data;
using System.IO;
using System.Text;
using System.Collections;
@@ -48,26 +47,11 @@
using Newtonsoft.Json;
-using Touryo.Infrastructure.Public.Util;
-
namespace Touryo.Infrastructure.Public.Dto
{
/// 表コレクション
public class DTTables : IEnumerable
{
- ///
- /// Added the class to save the records in Json Format
- ///
- public class TableRecords
- {
- /// tbl
- public string tbl { get; set; }
- /// col
- public Dictionary col { get; set; }
- /// row
- public ArrayList row { get; set; }
- }
-
#region インスタンス変数
/// 表を保持するList
@@ -156,68 +140,48 @@ public IEnumerator GetEnumerator()
#endregion
- #region SaveJson
-
- ///
- /// SaveJson Method(To save records in Json Format)
- ///
- /// string
- public string SaveJson()
+ #region DataSetとの相互変換
+
+ // DTTable の ToDataTable / FromDataTable を、表の数だけ回すだけである。
+ // DTTables が DataSet に対応する層なので、ここに置く(#544)。
+ //
+ // <移らないもの>
+ // ・DataSet の名前(DataSetName)… DTTables は名前を持たない
+ // ・リレーション(Relations)と制約(Constraints)… 同上
+ // 移すのは「表・列・行・値・行ステータス」だけである。
+
+ /// System.Data.DataSetをDTTablesに変換する
+ /// 変換元のSystem.Data.DataSet
+ /// 変換後のDTTables
+ ///
+ /// **同じ名前の表が複数あると例外になる。**
+ /// DTTables は表名でも引けるようにするため、名前の重複を許さない。
+ ///
+ public static DTTables FromDataSet(DataSet ds)
{
- TableRecords tableRecords = null;
-
- List lstTableRecords = new List();
+ DTTables dtts = new DTTables();
- int tblNo = -1;
- foreach (DTTable dt in this._tbls)
+ foreach (DataTable dt in ds.Tables)
{
- tblNo++;
-
- tableRecords = new TableRecords();
-
- tableRecords.tbl = dt.TableName;
-
- tableRecords.col = new Dictionary();
- foreach (DTColumn col in dt.Cols)
- {
- tableRecords.col.Add(col.ColName, DTColumn.EnumToString(col.ColType));
- }
-
- tableRecords.row = new ArrayList();
- Dictionary rowDetails = null;
- int colNo = 0;
- int rowStateFlag = 0;
+ dtts.Add(DTTable.FromDataTable(dt));
+ }
- foreach (DTRow dr in dt.Rows)
- {
- rowDetails = new Dictionary();
- rowStateFlag = 1;
- foreach (DTColumn col in dt.Cols)
- {
- colNo = colNo + 1;
- object colValue;
- colValue = dr[col.ColName];
+ return dtts;
+ }
- // add values to rowdetails based on colvalue.
- string strTemp = CustomMarshaler.StringFromPrimitivetype(colValue, false);
- rowDetails.Add(col.ColName, strTemp);
+ /// DTTablesをSystem.Data.DataSetに変換する
+ /// 変換後のSystem.Data.DataSet
+ /// 行ステータス(RowState)は保たれる。詳細は DTTable.ToDataTable。
+ public DataSet ToDataSet()
+ {
+ DataSet ds = new DataSet();
- // adding rowState in rowDeatils.
- if (rowStateFlag == 1 && colNo == dt.Cols.Count)
- {
- rowDetails.Add("rowstate", (int)dr.RowState);
- rowStateFlag = 0;
- colNo = 0;
- }
- }
- tableRecords.row.Add(rowDetails);
- }
- lstTableRecords.Add(tableRecords);
+ foreach (DTTable dt in this._tbls)
+ {
+ ds.Tables.Add(dt.ToDataTable());
}
- //converting the list into json format and return that string value
- string json = JsonConvert.SerializeObject(lstTableRecords);
- return json;
+ return ds;
}
#endregion
@@ -266,13 +230,24 @@ public void Save(TextWriter tw)
// 列番号のインクリメント
colNo++;
- string strTemp = CustomMarshaler.StringFromPrimitivetype(o, true);
+ string strTemp = DTColumn.StringFromPrimitivetype(o, true);
+
+ // **null は "null" と書く。**(#544)
+ //
+ // Load 側には元から「celString == "null" なら値を設定しない」
+ // という受け口があるのに、こちらが書いていなかった。
+ // null は空文字として書かれ、読み込み時に
+ // Convert.ToDecimal("") などが FormatException を投げていた。
+ //
+ // <値が文字列の "null" だった場合>
+ // 読み戻すと null になる。行単位・文字列だけの形式なので、
+ // ここは区別できない。値を厳密に往復させたいときは
+ // JSON 版(SaveJson / LoadJson)を使うこと。
+ // あちらは JSON の null をそのまま使うため、曖昧さが無い。
tw.WriteLine(
"cel:"
+ tblNo.ToString() + "," + rowNo.ToString() + "," + colNo.ToString() +
- ":" + strTemp);
-
-
+ ":" + (strTemp == null ? "null" : strTemp));
}
// 行ステータス
@@ -283,100 +258,6 @@ public void Save(TextWriter tw)
}
}
- ///
- /// LoadJson Method(To Load records from Json format)
- ///
- /// StreamReader
- public void LoadJson(StreamReader sr)
- {
- List lstTableRecords = new List();
- string json = sr.ReadToEnd();
- lstTableRecords = JsonConvert.DeserializeObject>(json);
-
- DTTable tbl = null;
- DTColumn col = null;
- DTRow row = null;
- int bkColIndex = 0;
- for (int i = 0; i < lstTableRecords.Count; i++)
- {
- string tblName = lstTableRecords[i].tbl;
-
- //add the DTTable present in lstTableRecords into tbl
- tbl = new DTTable(tblName);
-
- // add the tbl into DTTables
- this.Add(tbl);
-
- Dictionary tempCol = lstTableRecords[i].col;
-
- foreach (string key in tempCol.Keys)
- {
- string colName = key;
- string colType = tempCol[key];
-
- //add the colName and colValue into DTColumn
- col = new DTColumn(colName, DTColumn.StringToEnum(colType));
-
- // add the col into tbl
- tbl.Cols.Add(col);
- }
-
- ArrayList tempRow = lstTableRecords[i].row;
- for (int j = 0; j < lstTableRecords[i].row.Count; j++)
- {
- //deserialize the first value inside row of lstTableRecords
- object rowJson = JsonConvert.DeserializeObject(lstTableRecords[i].row[j].ToString());
-
- //Convert the deserialized value into dictionary
- Dictionary rowDetails = JsonConvert.DeserializeObject>(rowJson.ToString());
-
- int colIndex = 0;
- if (colIndex == 0)
- {
- // add new row
- row = tbl.Rows.AddNew();
- }
- else
- {
- // do nothing
- }
- foreach (var key in rowDetails)
- {
- string colName = key.Key;
- object colValue = rowDetails[colName];
-
- // getting the values and adding it to rows
- if (colName != "rowstate")
- {
- col = (DTColumn)tbl.Cols.ColsInfo[colIndex];
-
- if (colValue == null)
- {
- row[colIndex] = null;
- }
- else if (col.ColType == DTType.String)
- {
- // そのまま
- row[colIndex] = colValue;
- // インデックスを退避
- bkColIndex = colIndex;
- }
- else
- {
- object primitiveData = CustomMarshaler.PrimitivetypeFromString(col.ColType, colValue.ToString());
- row[colIndex] = primitiveData;
- }
- colIndex = colIndex + 1;
- }
- else
- {
- row.RowState = (DataRowState)(int.Parse)(colValue.ToString());
- }
- }
- }
- }
- }
-
/// テキストからロードする
/// 任意のTextReader
public void Load(TextReader tr)
@@ -436,7 +317,7 @@ public void Load(TextReader tr)
case "row:":
// 行ステータス
- row.RowState = (DataRowState)int.Parse(line.Substring(4));
+ row.RowState = (DTRowState)int.Parse(line.Substring(4));
break;
@@ -459,25 +340,24 @@ public void Load(TextReader tr)
// 継続行
}
+ // セルに値を設定
if (celString == "null")
{
// row[colIndex] = null;
}
else
{
+ // 列情報
col = (DTColumn)tbl.Cols.ColsInfo[colIndex];
+
+ // String もそのまま返るようになったため、分岐は要らない(#544)
+ row[colIndex] = DTColumn.PrimitivetypeFromString(col.ColType, celString);
+
if (col.ColType == DTType.String)
{
- // そのまま
- row[colIndex] = celString;
- // インデックスを退避
+ // 改行の継続行(rnr: / rnn:)を連結する先として、インデックスを退避
bkColIndex = colIndex;
}
- else
- {
- object primitiveData = CustomMarshaler.PrimitivetypeFromString(col.ColType, celString.ToString());
- row[colIndex] = primitiveData;
- }
}
break;
@@ -513,7 +393,191 @@ public void Load(TextReader tr)
#endregion
+ #region セーブ&ロード(JSON化)
+
+ #region 中間生成物(JSONとの相互変換に使うクラス)
+
+ // <なぜ DTTables を直接シリアライズしないのか>(#544)
+ // DTTables / DTTable / DTRow は内部にインデックスのマップや行数の
+ // カウンタを持ち、そのままでは JSON として素直な形にならない。
+ // 間に単純なクラスを挟み、JSON の構造をこちらで決める。
+ //
+ // <列と行を「配列」で持つ理由>
+ // ・JSON オブジェクトのキー順は、本来は無保証。
+ // 列を Dictionary で持つと、行の値と列の対応が順序に依存して危うい。
+ // ・行ステータスをセルと同じ階層に置くと、
+ // 「rowstate」という名前の列があったときに壊れる。
+ // このため列も行のセルも配列にし、行ステータスはセルの外に出してある。
+ //
+ // <ASP.NET Core から使うとき>
+ // このクラス群はプレーンなクラスと List だけで出来ているため、
+ // コントローラの戻り値にすれば System.Text.Json でもそのまま直列化できる。
+ // ToJsonObject / FromJsonObject はそのために公開している。
+
+ /// JSONとの相互変換に使う中間生成物(表コレクション)
+ public class JsonTables
+ {
+ /// 表
+ public List tbls { get; set; }
+ }
+
+ /// JSONとの相互変換に使う中間生成物(表)
+ public class JsonTable
+ {
+ /// 表名
+ public string tbl { get; set; }
+
+ /// 列(順序に意味があるため配列)
+ public List cols { get; set; }
+
+ /// 行
+ public List rows { get; set; }
+ }
+
+ /// JSONとの相互変換に使う中間生成物(列)
+ public class JsonColumn
+ {
+ /// 列名
+ public string name { get; set; }
+
+ /// 列の型(DTTypeの文字列表現)
+ public string type { get; set; }
+ }
+
+ /// JSONとの相互変換に使う中間生成物(行)
+ public class JsonRow
+ {
+ /// セル(列と同じ順序。nullはnullのまま)
+ public List cels { get; set; }
+
+ /// 行ステータス(DTRowStateの数値)
+ public int state { get; set; }
+ }
+
+ #endregion
+
+ #region 中間生成物との相互変換
+
+ /// 中間生成物に変換する
+ /// JsonTables
+ public JsonTables ToJsonObject()
+ {
+ JsonTables jTbls = new JsonTables();
+ jTbls.tbls = new List();
+
+ foreach (DTTable dt in this._tbls)
+ {
+ JsonTable jTbl = new JsonTable();
+ jTbl.tbl = dt.TableName;
+
+ // 列情報
+ jTbl.cols = new List();
+ foreach (DTColumn col in dt.Cols)
+ {
+ JsonColumn jCol = new JsonColumn();
+ jCol.name = col.ColName;
+ jCol.type = DTColumn.EnumToString(col.ColType);
+ jTbl.cols.Add(jCol);
+ }
+
+ // 行のセル
+ jTbl.rows = new List();
+ foreach (DTRow dr in dt.Rows)
+ {
+ JsonRow jRow = new JsonRow();
+ jRow.cels = new List();
+
+ foreach (object o in dr)
+ {
+ // テキスト版と違い、改行のエスケープは行わない(false)。
+ // 行単位で区切る形式ではなく、エスケープは JSON 側の仕事のため。
+ jRow.cels.Add(DTColumn.StringFromPrimitivetype(o, false));
+ }
+
+ jRow.state = (int)dr.RowState;
+ jTbl.rows.Add(jRow);
+ }
+
+ jTbls.tbls.Add(jTbl);
+ }
+
+ return jTbls;
+ }
+
+ /// 中間生成物から復元する
+ /// JsonTables
+ public void FromJsonObject(JsonTables jTbls)
+ {
+ // 初期化
+ this._tbls = new List();
+ this._tblsNameIndexMap = new Dictionary();
+
+ if (jTbls == null || jTbls.tbls == null) { return; }
+
+ foreach (JsonTable jTbl in jTbls.tbls)
+ {
+ // 表を生成して追加
+ DTTable tbl = new DTTable(jTbl.tbl);
+ this.Add(tbl);
+
+ // 列情報
+ if (jTbl.cols != null)
+ {
+ foreach (JsonColumn jCol in jTbl.cols)
+ {
+ tbl.Cols.Add(new DTColumn(jCol.name, DTColumn.StringToEnum(jCol.type)));
+ }
+ }
+
+ if (jTbl.rows == null) { continue; }
+
+ // 行のセル
+ foreach (JsonRow jRow in jTbl.rows)
+ {
+ // AddNew は行ステータスを Added にするため、値を入れ終えてから戻す。
+ DTRow row = tbl.Rows.AddNew();
+
+ if (jRow.cels != null)
+ {
+ for (int i = 0; i < jRow.cels.Count; i++)
+ {
+ string cel = jRow.cels[i];
+
+ // null は設定しない(DTRow の初期値のままにする)。
+ if (cel == null) { continue; }
+
+ DTColumn col = (DTColumn)tbl.Cols.ColsInfo[i];
+ row[i] = DTColumn.PrimitivetypeFromString(col.ColType, cel);
+ }
+ }
+
+ // 行ステータス(値を設定すると Modified になるため、最後に戻す)
+ row.RowState = (DTRowState)jRow.state;
+ }
+ }
+ }
+
+ #endregion
+
+ #region セーブ&ロード
+ /// JSONとしてセーブする
+ /// 任意のTextWriter
+ public void SaveJson(TextWriter tw)
+ {
+ tw.Write(JsonConvert.SerializeObject(this.ToJsonObject()));
+ }
+
+ /// JSONからロードする
+ /// 任意のTextReader
+ public void LoadJson(TextReader tr)
+ {
+ this.FromJsonObject(JsonConvert.DeserializeObject(tr.ReadToEnd()));
+ }
+
+ #endregion
+
+ #endregion
#region マーシャリングのサポート メソッド
@@ -555,6 +619,44 @@ public static DTTables StringToDTTables(string str)
}
}
+ ///
+ /// 汎用DTOのマーシャル処理(JSON)
+ ///
+ public static string DTTablesToJson(DTTables dtts)
+ {
+ StringBuilder sb = new StringBuilder();
+ StringWriter sw = new StringWriter(sb);
+
+ try
+ {
+ dtts.SaveJson(sw);
+ return sb.ToString();
+ }
+ finally
+ {
+ sw.Close();
+ }
+ }
+
+ ///
+ /// 汎用DTOのアンマーシャル処理(JSON)
+ ///
+ public static DTTables JsonToDTTables(string str)
+ {
+ StringReader sr = new StringReader(str);
+
+ try
+ {
+ DTTables dtts = new DTTables();
+ dtts.LoadJson(sr);
+ return dtts;
+ }
+ finally
+ {
+ sr.Close();
+ }
+ }
+
#endregion
}
}
diff --git a/root/programs/CS/Frameworks/Infrastructure/Public/Public_net48.csproj b/root/programs/CS/Frameworks/Infrastructure/Public/Public_net48.csproj
index 86525ad1e..e8d1b70a7 100644
--- a/root/programs/CS/Frameworks/Infrastructure/Public/Public_net48.csproj
+++ b/root/programs/CS/Frameworks/Infrastructure/Public/Public_net48.csproj
@@ -85,7 +85,7 @@
-
+
diff --git a/root/programs/CS/Frameworks/Infrastructure/Public/Util/CustomMarshaler.cs b/root/programs/CS/Frameworks/Infrastructure/Public/Util/CustomMarshaler.cs
index 558937c45..c1f390ffe 100644
--- a/root/programs/CS/Frameworks/Infrastructure/Public/Util/CustomMarshaler.cs
+++ b/root/programs/CS/Frameworks/Infrastructure/Public/Util/CustomMarshaler.cs
@@ -30,6 +30,8 @@
//* 2010/11/19 西野 大介 新規作成
//* 2015/01/15 Supragyan Added StringFromPrimitivetype method to convert premitive data to string.
//* 2015/01/15 Supragyan Added PrimitivetypeFromString method to convert string to premitive data.
+//* 2026/08/14 玄人 幸道 StringFromPrimitivetype / PrimitivetypeFromString を
+//* DTColumn へ移動し、ここには転送のみ残した(#544)。
//**********************************************************************************
using System;
@@ -209,7 +211,23 @@ public static byte[] StructuresToBytes(object[] cStructures, int count)
#endregion
- #region StringFromPrimitivetype
+ #region 値と文字列の相互変換(DTColumnへ移動)
+
+ // <なぜ移したか>(#544)
+ // 本クラスの本体はアンマネージ構造体の相互運用(Marshal クラス)で、
+ // DTType の変換とは責務が違う。同じ「マーシャリング」という語が
+ // 2 つの意味で使われていたため、同居していた。
+ //
+ // また、この 2 メソッドのためだけに Util から Dto を参照しており、
+ // Dto → Util の参照と合わせて循環していた。
+ //
+ // 対になる DTColumn.AutoCast / CheckType / EnumToString は
+ // もともと Dto 側にあるため、そちらへ集約した。
+ //
+ // <ここに残してある理由>
+ // 公開している API のため、いきなり消すと利用者のビルドが壊れる。
+ // 当面は転送するだけのメソッドを残す。
+ // これを削除すると、Util から Dto への参照も無くなる。
///
/// To converts all premitive data to string data.
@@ -217,98 +235,23 @@ public static byte[] StructuresToBytes(object[] cStructures, int count)
/// Column data type
/// Check sting Type data
/// returns String data
+ [Obsolete("DTColumn.StringFromPrimitivetype へ移動しました。")]
public static string StringFromPrimitivetype(object primitiveType, bool checkType)
{
- string convertedString = null;
-
- if (primitiveType != null)
- {
- if (DTColumn.CheckType(primitiveType, DTType.DateTime))
- {
- DateTime dateTime = (DateTime)primitiveType;
- convertedString += dateTime.Year + "/";
- convertedString += dateTime.Month + "/";
- convertedString += dateTime.Day + "-";
-
- convertedString += dateTime.Hour + ":";
- convertedString += dateTime.Minute + ":";
- convertedString += dateTime.Second + ".";
- convertedString += dateTime.Millisecond;
- }
- else if (DTColumn.CheckType(primitiveType, DTType.ByteArray))
- {
- convertedString = Convert.ToBase64String((byte[])primitiveType);
- }
- else if (DTColumn.CheckType(primitiveType, DTType.String))
- {
- if (checkType == true)
- {
- convertedString = primitiveType.ToString().Replace("\r", "\rrnr:");
- convertedString = primitiveType.ToString().Replace("\n", "\rrnn:");
-
- convertedString = primitiveType.ToString().Replace("\r", "\r\n");
- }
- else
- {
- convertedString = primitiveType.ToString();
- }
- }
- else
- {
- convertedString = primitiveType.ToString();
- }
-
- }
- return convertedString;
+ return DTColumn.StringFromPrimitivetype(primitiveType, checkType);
}
- #endregion
-
- #region PrimitivetypeFromString
-
///
/// To converts all string data to premitive Type by colType and cellString.
///
/// Column data type
/// Cell data
/// return Premitive type object
+ [Obsolete("DTColumn.PrimitivetypeFromString へ移動しました。")]
public static object PrimitivetypeFromString(DTType colType, string cellString)
{
- object convertedPrimitiveType = null;
- if (cellString != null)
- {
- // ByteArray
- if (colType == DTType.ByteArray)
- {
- byte[] cellByte = Convert.FromBase64String(cellString);
- convertedPrimitiveType = cellByte;
- }
- // DateTime
- else if (colType == DTType.DateTime)
- {
- string ymd = cellString.Split('-')[0];
- string hmsf = cellString.Split('-')[1];
-
- DateTime cellDttm = new DateTime(
- int.Parse(ymd.Split('/')[0]),
- int.Parse(ymd.Split('/')[1]),
- int.Parse(ymd.Split('/')[2]),
- int.Parse(hmsf.Split(':')[0]),
- int.Parse(hmsf.Split(':')[1]),
- int.Parse(hmsf.Split(':')[2].Split('.')[0]),
- int.Parse(hmsf.Split(':')[2].Split('.')[1]));
-
- convertedPrimitiveType = cellDttm;
- }
- else
- {
- convertedPrimitiveType = DTColumn.AutoCast(colType, cellString.ToString());
- }
- }
-
- return convertedPrimitiveType;
+ return DTColumn.PrimitivetypeFromString(colType, cellString);
}
-
#endregion
}
diff --git a/root/programs/CS/Frameworks/Tests/TestCode/Result48.txt b/root/programs/CS/Frameworks/Tests/TestCode/Result48.txt
index 55d78db7e..21c473f5c 100644
--- a/root/programs/CS/Frameworks/Tests/TestCode/Result48.txt
+++ b/root/programs/CS/Frameworks/Tests/TestCode/Result48.txt
@@ -70,11 +70,11 @@ log4net: Setting Collection Property [AddFilter] to object [log4net.Filter.Level
log4net: Created Appender [OPERATION2]
log4net: Adding appender named [OPERATION2] to logger [OPERATION].
log4net: Hierarchy Threshold []
-[2026/08/08 10:15:45,904],[DEBUG],[1],LogIF.DebugLog("ACCESS");
-[2026/08/08 10:15:45,912],[INFO ],[1],LogIF.InfoLog("ACCESS");
-[2026/08/08 10:15:45,913],[WARN ],[1],LogIF.WarnLog("ACCESS");
-[2026/08/08 10:15:45,913],[ERROR],[1],LogIF.ErrorLog("ACCESS");
-[2026/08/08 10:15:45,913],[FATAL],[1],LogIF.FatalLog("ACCESS");
+[2026/08/14 02:49:43,862],[DEBUG],[1],LogIF.DebugLog("ACCESS");
+[2026/08/14 02:49:43,881],[INFO ],[1],LogIF.InfoLog("ACCESS");
+[2026/08/14 02:49:43,882],[WARN ],[1],LogIF.WarnLog("ACCESS");
+[2026/08/14 02:49:43,882],[ERROR],[1],LogIF.ErrorLog("ACCESS");
+[2026/08/14 02:49:43,882],[FATAL],[1],LogIF.FatalLog("ACCESS");
----------------------------------------------------------------------------------------------------
GetMessage: ~メッセージID:I0001に対応する記述(正常系)~
GetMessage: ~メッセージID:E0001に対応する記述(異常系)~
@@ -2490,6 +2490,74 @@ DTTable
行 : 5, a
行 : 6, b
----------------------------------------------------------------------------------------------------
+DataSet との JSON 往復
+[元の DataSet]
+表の数 : 2
+表名 : TestTypes
+列 : Id(Int32), Name(String), Price(Decimal), Rate(Double), Ordered(DateTime), Flag(Boolean), Blob(Byte[])
+行 : Id=1, Name="あいう", Price=1234.56, Rate=0.30000000000000004, Ordered=2026-08-14T12:34:56.7890000, Flag=True, Blob=byte[3]:00-01-FF, RowState=Added
+行 : Id=2, Name="", Price=-0.01, Rate=1.7976931348623157E+308, Ordered=1977-04-24T00:00:00.0000000, Flag=False, Blob=byte[0]:, RowState=Added
+行 : Id=3, Name="あ\r\nい", Price=0, Rate=0, Ordered=2000-01-01T00:00:00.0000000, Flag=False, Blob=byte[1]:01, RowState=Added
+行 : Id=4, Name=, Price=, Rate=, Ordered=, Flag=, Blob=, RowState=Added
+表名 : TestOdd
+列 : state(String), cels(Int32)
+行 : state="わな", cels=7, RowState=Added
+[JSON]
+{"tbls":[{"tbl":"TestTypes","cols":[{"name":"Id","type":"Int32"},{"name":"Name","type":"String"},{"name":"Price","type":"Decimal"},{"name":"Rate","type":"Double"},{"name":"Ordered","type":"DateTime"},{"name":"Flag","type":"Boolean"},{"name":"Blob","type":"ByteArray"}],"rows":[{"cels":["1","あいう","1234.56","0.30000000000000004","2026-08-14T12:34:56.7890000","True","AAH/"],"state":2},{"cels":["2","","-0.01","1.7976931348623157E+308","1977-04-24T00:00:00.0000000","False",""],"state":2},{"cels":["3","あ\r\nい","0","0","2000-01-01T00:00:00.0000000","False","AQ=="],"state":2},{"cels":["4",null,null,null,null,null,null],"state":2}]},{"tbl":"TestOdd","cols":[{"name":"state","type":"String"},{"name":"cels","type":"Int32"}],"rows":[{"cels":["わな","7"],"state":2}]}]}
+[JSONとの往復(DTTables)]
+表の数 : 2
+表名で取得 : TestOdd
+表名 : TestTypes
+列 : Id(Int32), Name(String), Price(Decimal), Rate(Double), Ordered(DateTime), Flag(Boolean), Blob(ByteArray)
+行 : Id=1, Name="あいう", Price=1234.56, Rate=0.30000000000000004, Ordered=2026-08-14T12:34:56.7890000, Flag=True, Blob=byte[3]:00-01-FF, RowState=Added
+行 : Id=2, Name="", Price=-0.01, Rate=1.7976931348623157E+308, Ordered=1977-04-24T00:00:00.0000000, Flag=False, Blob=byte[0]:, RowState=Added
+行 : Id=3, Name="あ\r\nい", Price=0, Rate=0, Ordered=2000-01-01T00:00:00.0000000, Flag=False, Blob=byte[1]:01, RowState=Added
+行 : Id=4, Name=(null), Price=(null), Rate=(null), Ordered=(null), Flag=(null), Blob=(null), RowState=Added
+表名 : TestOdd
+列 : state(String), cels(Int32)
+行 : state="わな", cels=7, RowState=Added
+[DataSet へ戻す]
+表の数 : 2
+表名 : TestTypes
+列 : Id(Int32), Name(String), Price(Decimal), Rate(Double), Ordered(DateTime), Flag(Boolean), Blob(Byte[])
+行 : Id=1, Name="あいう", Price=1234.56, Rate=0.30000000000000004, Ordered=2026-08-14T12:34:56.7890000, Flag=True, Blob=byte[3]:00-01-FF, RowState=Added
+行 : Id=2, Name="", Price=-0.01, Rate=1.7976931348623157E+308, Ordered=1977-04-24T00:00:00.0000000, Flag=False, Blob=byte[0]:, RowState=Added
+行 : Id=3, Name="あ\r\nい", Price=0, Rate=0, Ordered=2000-01-01T00:00:00.0000000, Flag=False, Blob=byte[1]:01, RowState=Added
+行 : Id=4, Name=, Price=, Rate=, Ordered=, Flag=, Blob=, RowState=Added
+表名 : TestOdd
+列 : state(String), cels(Int32)
+行 : state="わな", cels=7, RowState=Added
+往復 : TestTypes 行1 全列一致
+往復 : TestTypes 行2 全列一致
+往復 : TestTypes 行3 全列一致
+往復 : TestTypes 行4 全列一致
+往復 : TestOdd 行1 全列一致
+[テキストとの往復]
+往復 : TestTypes 行1 全列一致
+往復 : TestTypes 行2 全列一致
+往復 : TestTypes 行3 全列一致
+往復 : TestTypes 行4 全列一致
+往復 : TestOdd 行1 全列一致
+----------------------------------------------------------------------------------------------------
+行ステータスの往復
+[元の DataTable]
+行数 : 4
+行 : Id=1, Note="そのまま", RowState=Unchanged
+行 : Id=2, Note="変更後", RowState=Modified
+行 : Id=3, Note="削除される", RowState=Deleted
+行 : Id=4, Note="追加", RowState=Added
+[JSON 往復後の DTTable]
+行 : 1, そのまま, RowState=Unchanged
+行 : 2, 変更後, RowState=Modified
+行 : 3, 削除される, RowState=Deleted
+行 : 4, 追加, RowState=Added
+[ToDataTable 後の DataTable]
+行数 : 4
+行 : Id=1, Note="そのまま", RowState=Unchanged
+行 : Id=2, Note="変更後", RowState=Modified
+行 : Id=3, Note="削除される", RowState=Deleted
+行 : Id=4, Note="追加", RowState=Added
+----------------------------------------------------------------------------------------------------
ObjectInspector.Inspect : 基本の型
[null]
null
diff --git a/root/programs/CS/Frameworks/Tests/TestCode/ResultCore100.txt b/root/programs/CS/Frameworks/Tests/TestCode/ResultCore100.txt
index 13a8c76f0..cca14a9d0 100644
--- a/root/programs/CS/Frameworks/Tests/TestCode/ResultCore100.txt
+++ b/root/programs/CS/Frameworks/Tests/TestCode/ResultCore100.txt
@@ -70,11 +70,11 @@ log4net: Setting Collection Property [AddFilter] to object [log4net.Filter.Level
log4net: Created Appender [OPERATION2]
log4net: Adding appender named [OPERATION2] to logger [OPERATION].
log4net: Hierarchy Threshold []
-[2026/08/08 10:15:49,809],[DEBUG],[2],LogIF.DebugLog("ACCESS");
-[2026/08/08 10:15:49,818],[INFO ],[2],LogIF.InfoLog("ACCESS");
-[2026/08/08 10:15:49,819],[WARN ],[2],LogIF.WarnLog("ACCESS");
-[2026/08/08 10:15:49,819],[ERROR],[2],LogIF.ErrorLog("ACCESS");
-[2026/08/08 10:15:49,819],[FATAL],[2],LogIF.FatalLog("ACCESS");
+[2026/08/14 02:49:51,630],[DEBUG],[2],LogIF.DebugLog("ACCESS");
+[2026/08/14 02:49:51,659],[INFO ],[2],LogIF.InfoLog("ACCESS");
+[2026/08/14 02:49:51,661],[WARN ],[2],LogIF.WarnLog("ACCESS");
+[2026/08/14 02:49:51,662],[ERROR],[2],LogIF.ErrorLog("ACCESS");
+[2026/08/14 02:49:51,662],[FATAL],[2],LogIF.FatalLog("ACCESS");
----------------------------------------------------------------------------------------------------
GetMessage: ~メッセージID:I0001に対応する記述(正常系)~
GetMessage: ~メッセージID:E0001に対応する記述(異常系)~
@@ -2509,6 +2509,74 @@ DTTable
行 : 5, a
行 : 6, b
----------------------------------------------------------------------------------------------------
+DataSet との JSON 往復
+[元の DataSet]
+表の数 : 2
+表名 : TestTypes
+列 : Id(Int32), Name(String), Price(Decimal), Rate(Double), Ordered(DateTime), Flag(Boolean), Blob(Byte[])
+行 : Id=1, Name="あいう", Price=1234.56, Rate=0.30000000000000004, Ordered=2026-08-14T12:34:56.7890000, Flag=True, Blob=byte[3]:00-01-FF, RowState=Added
+行 : Id=2, Name="", Price=-0.01, Rate=1.7976931348623157E+308, Ordered=1977-04-24T00:00:00.0000000, Flag=False, Blob=byte[0]:, RowState=Added
+行 : Id=3, Name="あ\r\nい", Price=0, Rate=0, Ordered=2000-01-01T00:00:00.0000000, Flag=False, Blob=byte[1]:01, RowState=Added
+行 : Id=4, Name=, Price=, Rate=, Ordered=, Flag=, Blob=, RowState=Added
+表名 : TestOdd
+列 : state(String), cels(Int32)
+行 : state="わな", cels=7, RowState=Added
+[JSON]
+{"tbls":[{"tbl":"TestTypes","cols":[{"name":"Id","type":"Int32"},{"name":"Name","type":"String"},{"name":"Price","type":"Decimal"},{"name":"Rate","type":"Double"},{"name":"Ordered","type":"DateTime"},{"name":"Flag","type":"Boolean"},{"name":"Blob","type":"ByteArray"}],"rows":[{"cels":["1","あいう","1234.56","0.30000000000000004","2026-08-14T12:34:56.7890000","True","AAH/"],"state":2},{"cels":["2","","-0.01","1.7976931348623157E+308","1977-04-24T00:00:00.0000000","False",""],"state":2},{"cels":["3","あ\r\nい","0","0","2000-01-01T00:00:00.0000000","False","AQ=="],"state":2},{"cels":["4",null,null,null,null,null,null],"state":2}]},{"tbl":"TestOdd","cols":[{"name":"state","type":"String"},{"name":"cels","type":"Int32"}],"rows":[{"cels":["わな","7"],"state":2}]}]}
+[JSONとの往復(DTTables)]
+表の数 : 2
+表名で取得 : TestOdd
+表名 : TestTypes
+列 : Id(Int32), Name(String), Price(Decimal), Rate(Double), Ordered(DateTime), Flag(Boolean), Blob(ByteArray)
+行 : Id=1, Name="あいう", Price=1234.56, Rate=0.30000000000000004, Ordered=2026-08-14T12:34:56.7890000, Flag=True, Blob=byte[3]:00-01-FF, RowState=Added
+行 : Id=2, Name="", Price=-0.01, Rate=1.7976931348623157E+308, Ordered=1977-04-24T00:00:00.0000000, Flag=False, Blob=byte[0]:, RowState=Added
+行 : Id=3, Name="あ\r\nい", Price=0, Rate=0, Ordered=2000-01-01T00:00:00.0000000, Flag=False, Blob=byte[1]:01, RowState=Added
+行 : Id=4, Name=(null), Price=(null), Rate=(null), Ordered=(null), Flag=(null), Blob=(null), RowState=Added
+表名 : TestOdd
+列 : state(String), cels(Int32)
+行 : state="わな", cels=7, RowState=Added
+[DataSet へ戻す]
+表の数 : 2
+表名 : TestTypes
+列 : Id(Int32), Name(String), Price(Decimal), Rate(Double), Ordered(DateTime), Flag(Boolean), Blob(Byte[])
+行 : Id=1, Name="あいう", Price=1234.56, Rate=0.30000000000000004, Ordered=2026-08-14T12:34:56.7890000, Flag=True, Blob=byte[3]:00-01-FF, RowState=Added
+行 : Id=2, Name="", Price=-0.01, Rate=1.7976931348623157E+308, Ordered=1977-04-24T00:00:00.0000000, Flag=False, Blob=byte[0]:, RowState=Added
+行 : Id=3, Name="あ\r\nい", Price=0, Rate=0, Ordered=2000-01-01T00:00:00.0000000, Flag=False, Blob=byte[1]:01, RowState=Added
+行 : Id=4, Name=, Price=, Rate=, Ordered=, Flag=, Blob=, RowState=Added
+表名 : TestOdd
+列 : state(String), cels(Int32)
+行 : state="わな", cels=7, RowState=Added
+往復 : TestTypes 行1 全列一致
+往復 : TestTypes 行2 全列一致
+往復 : TestTypes 行3 全列一致
+往復 : TestTypes 行4 全列一致
+往復 : TestOdd 行1 全列一致
+[テキストとの往復]
+往復 : TestTypes 行1 全列一致
+往復 : TestTypes 行2 全列一致
+往復 : TestTypes 行3 全列一致
+往復 : TestTypes 行4 全列一致
+往復 : TestOdd 行1 全列一致
+----------------------------------------------------------------------------------------------------
+行ステータスの往復
+[元の DataTable]
+行数 : 4
+行 : Id=1, Note="そのまま", RowState=Unchanged
+行 : Id=2, Note="変更後", RowState=Modified
+行 : Id=3, Note="削除される", RowState=Deleted
+行 : Id=4, Note="追加", RowState=Added
+[JSON 往復後の DTTable]
+行 : 1, そのまま, RowState=Unchanged
+行 : 2, 変更後, RowState=Modified
+行 : 3, 削除される, RowState=Deleted
+行 : 4, 追加, RowState=Added
+[ToDataTable 後の DataTable]
+行数 : 4
+行 : Id=1, Note="そのまま", RowState=Unchanged
+行 : Id=2, Note="変更後", RowState=Modified
+行 : Id=3, Note="削除される", RowState=Deleted
+行 : Id=4, Note="追加", RowState=Added
+----------------------------------------------------------------------------------------------------
ObjectInspector.Inspect : 基本の型
[null]
null
diff --git a/root/programs/CS/Frameworks/Tests/TestCode/TestDto.cs b/root/programs/CS/Frameworks/Tests/TestCode/TestDto.cs
index 7f055a6fc..6f3d29ba3 100644
--- a/root/programs/CS/Frameworks/Tests/TestCode/TestDto.cs
+++ b/root/programs/CS/Frameworks/Tests/TestCode/TestDto.cs
@@ -24,11 +24,13 @@
//* 日時 更新者 内容
//* ---------- ---------------- -------------------------------------------------
//* 2026/08/06 玄人 幸道 新規作成(#522)
+//* 2026/08/14 玄人 幸道 JSONの往復と、行ステータスの往復を追加(#544)
//**********************************************************************************
using System;
using System.Collections.Generic;
using System.Data;
+using System.Globalization;
using Touryo.Infrastructure.Public.Diagnostics;
using Touryo.Infrastructure.Public.Dto;
@@ -67,6 +69,14 @@ public static void Root()
MyDebug.OutputDebugAndConsole("----------------------------------------------------------------------------------------------------");
TestDto.TestDTTable();
+
+ MyDebug.OutputDebugAndConsole("----------------------------------------------------------------------------------------------------");
+
+ TestDto.TestDTTablesJson();
+
+ MyDebug.OutputDebugAndConsole("----------------------------------------------------------------------------------------------------");
+
+ TestDto.TestRowState();
}
#endregion
@@ -252,6 +262,185 @@ private static void TestDTTable()
TestDto.OutputDTTable(restored[0]);
}
+ /// DataSet との JSON 往復のテスト
+ ///
+ /// SaveJson / LoadJson と、DataSet との相互変換を確認する(#544)。
+ ///
+ /// DataSet → DTTables → JSON → DTTables → DataSet
+ ///
+ /// <値を型ごとに見る理由>
+ /// JSON 上の値は DTColumn が文字列にしたもので、型ごとに書式が違う
+ /// (DateTime は ISO 8601、ByteArray は Base64)。
+ /// **列の型は JSON 側に持っており、読み戻しでそれを使う。**
+ /// ここが崩れると、値が入っているのに型が変わる、という壊れ方をする。
+ ///
+ /// <Double を「二進で割り切れない値」で見る理由>
+ /// 書式を指定しないと、.NET Framework 側は有効桁 15 桁に丸めてしまい、
+ /// 0.1 + 0.2 が 0.3 になって**元の値に戻らない**(#544)。
+ /// ラウンドトリップ書式を使っているので、両者で同じ文字列になる。
+ ///
+ /// <改行を含む文字列と null を入れている>
+ /// 改行は JSON 側のエスケープに任せており、崩れると読み戻しで
+ /// **例外にならずに文字が欠ける**(テキスト版で実際に起きていた)。
+ /// null は JSON でも null になり、読み戻しで値を設定しない経路を通る。
+ ///
+ /// <表を 2 つにしている>
+ /// DTTables は表のコレクションなので、複数表と表名での取得を通す。
+ /// 2 つ目は、列名を JSON のキー(state / cels)とわざと重ねてある。
+ ///
+ private static void TestDTTablesJson()
+ {
+ MyDebug.OutputDebugAndConsole("DataSet との JSON 往復");
+
+ DataSet ds = new DataSet("TestDataSet");
+ ds.Tables.Add(TestDto.MakeTypedTable());
+ ds.Tables.Add(TestDto.MakeOddTable());
+
+ MyDebug.OutputDebugAndConsole("[元の DataSet]");
+ TestDto.OutputDataSet(ds);
+
+ DTTables dtts = DTTables.FromDataSet(ds);
+
+ // JSON へ
+ string json = DTTables.DTTablesToJson(dtts);
+ MyDebug.OutputDebugAndConsole("[JSON]");
+ MyDebug.OutputDebugAndConsole(json);
+
+ // JSON から
+ DTTables restored = DTTables.JsonToDTTables(json);
+
+ MyDebug.OutputDebugAndConsole("[JSONとの往復(DTTables)]");
+ MyDebug.OutputDebugAndConsole("表の数 : " + restored.Count);
+
+ // 表名でも引けること(インデクサ)
+ MyDebug.OutputDebugAndConsole("表名で取得 : " + restored["TestOdd"].TableName);
+
+ foreach (DTTable dtt in restored)
+ {
+ TestDto.OutputDTTableAll(dtt);
+ }
+
+ // DataSet へ戻す
+ MyDebug.OutputDebugAndConsole("[DataSet へ戻す]");
+ DataSet jsonBack = restored.ToDataSet();
+ TestDto.OutputDataSet(jsonBack);
+
+ // **値の表示だけに頼らない。**
+ // CompareResult.ps1 は 16 文字以上の英数字の並びを に潰すため、
+ // ラウンドトリップ書式の Double(17 桁)は期待値の上で読めなくなる。
+ // 元の DataSet と突き合わせた結果を、明示的に出す。
+ TestDto.OutputRoundTripResult(ds, jsonBack);
+
+ // テキスト版でも同じものが往復すること。
+ //
+ // こちらは改行を「\rrnr:」「\rrnn:」に退避して行を分け、
+ // 読み込み側で連結して戻す。以前は退避が効いておらず、
+ // **改行以降が捨てられていた**(#544)。
+ MyDebug.OutputDebugAndConsole("[テキストとの往復]");
+
+ DataSet textBack = DTTables.StringToDTTables(DTTables.DTTablesToString(dtts)).ToDataSet();
+ TestDto.OutputRoundTripResult(ds, textBack);
+ }
+
+ /// 型ごとの値を持つ DataTable を作る
+ /// DataTable
+ private static DataTable MakeTypedTable()
+ {
+ DataTable dt = new DataTable("TestTypes");
+ dt.Columns.Add("Id", typeof(int));
+ dt.Columns.Add("Name", typeof(string));
+ dt.Columns.Add("Price", typeof(decimal));
+ dt.Columns.Add("Rate", typeof(double));
+ dt.Columns.Add("Ordered", typeof(DateTime));
+ dt.Columns.Add("Flag", typeof(bool));
+ dt.Columns.Add("Blob", typeof(byte[]));
+
+ dt.Rows.Add(1, "あいう", 1234.56m, 0.1d + 0.2d,
+ new DateTime(2026, 8, 14, 12, 34, 56, 789), true, new byte[] { 0, 1, 255 });
+
+ // 空文字と空のバイト配列(null と見分けられること)
+ dt.Rows.Add(2, "", -0.01m, double.MaxValue,
+ new DateTime(1977, 4, 24), false, new byte[0]);
+
+ // 改行を含む文字列
+ dt.Rows.Add(3, "あ\r\nい", 0m, 0d,
+ new DateTime(2000, 1, 1), false, new byte[] { 1 });
+
+ // すべて null(Id 以外を設定しない)
+ DataRow nullRow = dt.NewRow();
+ nullRow["Id"] = 4;
+ dt.Rows.Add(nullRow);
+
+ return dt;
+ }
+
+ /// 列名を JSON のキーと重ねた DataTable を作る
+ /// DataTable
+ ///
+ /// 行ステータスをセルの外に出しているため、
+ /// "state" や "cels" という名前の列があっても衝突しない。
+ ///
+ private static DataTable MakeOddTable()
+ {
+ DataTable dt = new DataTable("TestOdd");
+ dt.Columns.Add("state", typeof(string));
+ dt.Columns.Add("cels", typeof(int));
+ dt.Rows.Add("わな", 7);
+
+ return dt;
+ }
+
+ /// 行ステータスの往復のテスト
+ ///
+ /// DataTable → DTTable → JSON → DTTable → DataTable を通して、
+ /// Added / Modified / Deleted / Unchanged が保たれることを確認する(#544)。
+ ///
+ /// <ここが崩れると何が起きるか>
+ /// 受け取った側で「どの行を INSERT / UPDATE / DELETE するか」を
+ /// 判別できなくなる。値は入っているので、**例外にならずに更新が漏れる**。
+ ///
+ /// <変更前の値は復元されない>
+ /// DTRow は現在値と行ステータスだけを持つ。
+ /// Modified の DataRowVersion.Original には現在値が入る。
+ ///
+ private static void TestRowState()
+ {
+ MyDebug.OutputDebugAndConsole("行ステータスの往復");
+
+ DataTable dt = new DataTable("TestRowState");
+ dt.Columns.Add("Id", typeof(int));
+ dt.Columns.Add("Note", typeof(string));
+
+ dt.Rows.Add(1, "そのまま");
+ dt.Rows.Add(2, "変更前");
+ dt.Rows.Add(3, "削除される");
+ dt.AcceptChanges();
+
+ dt.Rows[1]["Note"] = "変更後"; // Modified
+ dt.Rows[2].Delete(); // Deleted
+ dt.Rows.Add(4, "追加"); // Added
+
+ MyDebug.OutputDebugAndConsole("[元の DataTable]");
+ TestDto.OutputDataTableRowState(dt);
+
+ // DataTable → DTTable → JSON → DTTable
+ DTTables dtts = new DTTables();
+ dtts.Add(DTTable.FromDataTable(dt));
+
+ DTTables restored = DTTables.JsonToDTTables(DTTables.DTTablesToJson(dtts));
+
+ MyDebug.OutputDebugAndConsole("[JSON 往復後の DTTable]");
+ foreach (DTRow row in restored[0].Rows)
+ {
+ MyDebug.OutputDebugAndConsole(
+ "行 : " + row["Id"] + ", " + row["Note"] + ", RowState=" + row.RowState);
+ }
+
+ // DTTable → DataTable
+ MyDebug.OutputDebugAndConsole("[ToDataTable 後の DataTable]");
+ TestDto.OutputDataTableRowState(restored[0].ToDataTable());
+ }
+
#endregion
#region 出力のヘルパ
@@ -305,6 +494,225 @@ private static void OutputDTTable(DTTable dtt)
}
}
+ /// DTTable を全列出力する
+ /// DTTable
+ ///
+ /// **カルチャに依存しない書式で出す。**
+ /// 既定の ToString は小数点や日付の区切りがカルチャで変わり、
+ /// 期待値と一致しなくなる(CI は ja-JP に揃えているが、手元では違い得る)。
+ ///
+ /// 表ごとに列が違うため、列名は決め打ちにせず走査する。
+ ///
+ private static void OutputDTTableAll(DTTable dtt)
+ {
+ MyDebug.OutputDebugAndConsole("表名 : " + dtt.TableName);
+
+ string cols = "";
+ foreach (DTColumn col in dtt.Cols)
+ {
+ cols += (cols == "" ? "" : ", ") + col.ColName + "(" + col.ColType + ")";
+ }
+ MyDebug.OutputDebugAndConsole("列 : " + cols);
+
+ foreach (DTRow row in dtt.Rows)
+ {
+ string values = "";
+ foreach (DTColumn col in dtt.Cols)
+ {
+ values += (values == "" ? "" : ", ")
+ + col.ColName + "=" + TestDto.Fixed(row[col.ColName]);
+ }
+
+ MyDebug.OutputDebugAndConsole("行 : " + values + ", RowState=" + row.RowState);
+ }
+ }
+
+ /// DataSet を全表・全列出力する
+ /// DataSet
+ private static void OutputDataSet(DataSet ds)
+ {
+ MyDebug.OutputDebugAndConsole("表の数 : " + ds.Tables.Count);
+
+ foreach (DataTable dt in ds.Tables)
+ {
+ MyDebug.OutputDebugAndConsole("表名 : " + dt.TableName);
+
+ string cols = "";
+ foreach (DataColumn col in dt.Columns)
+ {
+ cols += (cols == "" ? "" : ", ") + col.ColumnName + "(" + col.DataType.Name + ")";
+ }
+ MyDebug.OutputDebugAndConsole("列 : " + cols);
+
+ foreach (DataRow row in dt.Rows)
+ {
+ // Deleted の行は現在値を読めないため、元の値を出す
+ DataRowVersion ver = row.RowState == System.Data.DataRowState.Deleted
+ ? DataRowVersion.Original : DataRowVersion.Current;
+
+ string values = "";
+ foreach (DataColumn col in dt.Columns)
+ {
+ values += (values == "" ? "" : ", ")
+ + col.ColumnName + "=" + TestDto.Fixed(row[col.ColumnName, ver]);
+ }
+
+ MyDebug.OutputDebugAndConsole("行 : " + values + ", RowState=" + row.RowState);
+ }
+ }
+ }
+
+ /// 往復の前後で値が一致したかを出力する
+ /// 元の DataSet
+ /// 往復後の DataSet
+ ///
+ /// **値そのものではなく、一致したかどうかを出す。**
+ /// 期待値の比較は CompareResult.ps1 が正規化してから行うが、
+ /// 16 文字以上の英数字の並びは Base64 と見なされて潰される。
+ /// ラウンドトリップ書式の Double は 17 桁になるため、
+ /// 値を出しただけでは**期待値の上で読めない**。
+ ///
+ private static void OutputRoundTripResult(DataSet src, DataSet dst)
+ {
+ if (src.Tables.Count != dst.Tables.Count)
+ {
+ MyDebug.OutputDebugAndConsole(
+ "往復 : 表の数が違う(" + src.Tables.Count + " → " + dst.Tables.Count + ")");
+ return;
+ }
+
+ for (int t = 0; t < src.Tables.Count; t++)
+ {
+ DataTable s = src.Tables[t];
+ DataTable d = dst.Tables[t];
+
+ if (s.TableName != d.TableName)
+ {
+ MyDebug.OutputDebugAndConsole(
+ "往復 : 表名が違う(" + s.TableName + " → " + d.TableName + ")");
+ continue;
+ }
+
+ if (s.Rows.Count != d.Rows.Count)
+ {
+ MyDebug.OutputDebugAndConsole(
+ "往復 : " + s.TableName + " の行数が違う("
+ + s.Rows.Count + " → " + d.Rows.Count + ")");
+ continue;
+ }
+
+ for (int i = 0; i < s.Rows.Count; i++)
+ {
+ string ng = "";
+
+ foreach (DataColumn col in s.Columns)
+ {
+ if (!d.Columns.Contains(col.ColumnName))
+ {
+ ng += (ng == "" ? "" : ", ") + col.ColumnName + "(列が無い)";
+ continue;
+ }
+
+ if (!TestDto.SameValue(s.Rows[i][col.ColumnName], d.Rows[i][col.ColumnName]))
+ {
+ ng += (ng == "" ? "" : ", ") + col.ColumnName;
+ }
+ }
+
+ MyDebug.OutputDebugAndConsole(
+ "往復 : " + s.TableName + " 行" + (i + 1) + " "
+ + (ng == "" ? "全列一致" : "不一致の列 = " + ng));
+ }
+ }
+ }
+ /// 2 つの値が同じかを判定する
+ /// 値
+ /// 値
+ /// 同じなら true
+ /// byte[] は参照ではなく中身で比べる。
+ private static bool SameValue(object a, object b)
+ {
+ if (a == null || a is DBNull) { return b == null || b is DBNull; }
+ if (b == null || b is DBNull) { return false; }
+
+ if (a is byte[] && b is byte[])
+ {
+ byte[] x = (byte[])a;
+ byte[] y = (byte[])b;
+
+ if (x.Length != y.Length) { return false; }
+
+ for (int i = 0; i < x.Length; i++)
+ {
+ if (x[i] != y[i]) { return false; }
+ }
+
+ return true;
+ }
+
+ return a.Equals(b);
+ }
+
+ /// 値をカルチャに依存しない形で文字列にする
+ /// 値
+ /// 文字列
+ private static string Fixed(object o)
+ {
+ if (o == null) { return "(null)"; }
+
+ if (o is byte[])
+ {
+ byte[] bytes = (byte[])o;
+ return "byte[" + bytes.Length + "]:" + BitConverter.ToString(bytes);
+ }
+
+ if (o is DateTime)
+ {
+ return ((DateTime)o).ToString("O", CultureInfo.InvariantCulture);
+ }
+
+ if (o is string)
+ {
+ // 空文字と null を見分けられるようにする。
+ // 改行は見えるようにエスケープする(欠けても気付けるように)。
+ return "\"" + ((string)o).Replace("\r", "\\r").Replace("\n", "\\n") + "\"";
+ }
+
+ // **浮動小数点はラウンドトリップ書式で出す。**
+ // 既定の書式は .NET Framework が有効桁 15 桁に丸めるため、
+ // net48 と net10.0 で期待値が割れる。
+ if (o is double) { return ((double)o).ToString("R", CultureInfo.InvariantCulture); }
+ if (o is float) { return ((float)o).ToString("R", CultureInfo.InvariantCulture); }
+
+ return Convert.ToString(o, CultureInfo.InvariantCulture);
+ }
+
+ /// DataTable を行ステータス付きで出力する
+ /// DataTable
+ /// Deleted の行は現在値を読めないため、元の値を出す。
+ private static void OutputDataTableRowState(DataTable dt)
+ {
+ MyDebug.OutputDebugAndConsole("行数 : " + dt.Rows.Count);
+
+ foreach (DataRow row in dt.Rows)
+ {
+ string values;
+
+ if (row.RowState == System.Data.DataRowState.Deleted)
+ {
+ values = "Id=" + TestDto.Fixed(row["Id", DataRowVersion.Original])
+ + ", Note=" + TestDto.Fixed(row["Note", DataRowVersion.Original]);
+ }
+ else
+ {
+ values = "Id=" + TestDto.Fixed(row["Id"])
+ + ", Note=" + TestDto.Fixed(row["Note"]);
+ }
+
+ MyDebug.OutputDebugAndConsole("行 : " + values + ", RowState=" + row.RowState);
+ }
+ }
+
/// Dictionary から値を取り出す
/// Dictionary
/// キー
diff --git a/root/programs/CS/Frameworks/Tests/TestTransmission/App.config b/root/programs/CS/Frameworks/Tests/TestTransmission/App.config
new file mode 100644
index 000000000..f8824a524
--- /dev/null
+++ b/root/programs/CS/Frameworks/Tests/TestTransmission/App.config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/root/programs/CS/Frameworks/Tests/TestTransmission/AssemblyInfo.cs b/root/programs/CS/Frameworks/Tests/TestTransmission/AssemblyInfo.cs
new file mode 100644
index 000000000..ad3236bcb
--- /dev/null
+++ b/root/programs/CS/Frameworks/Tests/TestTransmission/AssemblyInfo.cs
@@ -0,0 +1,18 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+[assembly: AssemblyTitle("TestTransmissionFx")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("TestTransmissionFx")]
+[assembly: AssemblyCopyright("Copyright © 2026")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+[assembly: ComVisible(false)]
+[assembly: Guid("3c7f9a10-546a-4f1e-9d2b-1a0e5c546001")]
+
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/root/programs/CS/Frameworks/Tests/TestTransmission/Program.cs b/root/programs/CS/Frameworks/Tests/TestTransmission/Program.cs
new file mode 100644
index 000000000..abdf2f80d
--- /dev/null
+++ b/root/programs/CS/Frameworks/Tests/TestTransmission/Program.cs
@@ -0,0 +1,778 @@
+#region Apache License
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+#endregion
+
+//**********************************************************************************
+//* クラス名 :Program
+//* クラス日本語名 :通信制御の接続オプションのテスト
+//*
+//* 作成者 :玄人 幸道
+//* 更新履歴 :
+//*
+//* 日時 更新者 内容
+//* ---------- ---------------- -------------------------------------------------
+//* 2026/08/14 玄人 幸道 新規作成(#546)
+//**********************************************************************************
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Compression;
+using System.Net;
+using System.Net.Security;
+using System.Net.Sockets;
+using System.Security.Authentication;
+using System.Security.Cryptography;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using System.Threading;
+
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+using Touryo.Infrastructure.Framework.Common;
+using Touryo.Infrastructure.Framework.Transmission;
+using Touryo.Infrastructure.Public.IO;
+using Touryo.Infrastructure.Public.Str;
+
+namespace TestTransmission
+{
+ #region テスト用の型
+
+ /// サーバが受け取った内容の記録
+ public class Recorded
+ {
+ /// 要求行
+ public string RequestLine = "";
+
+ /// ヘッダ
+ public Dictionary Headers = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ /// 要求の回数(認証の再送を数えるため)
+ public int Count = 0;
+
+ /// TLS の握手で受け取ったクライアント証明書のサブジェクト
+ public string ClientCertSubject = null;
+
+ /// ヘッダの取得(無ければ null)
+ /// ヘッダ名
+ /// 値
+ public string Header(string name)
+ {
+ return this.Headers.ContainsKey(name) ? this.Headers[name] : null;
+ }
+
+ /// 記録のクリア
+ public void Clear()
+ {
+ this.RequestLine = "";
+ this.Headers.Clear();
+ this.Count = 0;
+ this.ClientCertSubject = null;
+ }
+ }
+
+ /// テスト用のコンテキスト
+ [Serializable]
+ public class TestContext
+ {
+ /// 誰から
+ public string Who = "client";
+ }
+
+ /// テスト用の引数
+ [Serializable]
+ public class TestParam : BaseParameterValue
+ {
+ /// コンストラクタ
+ public TestParam() : base("testScreen", "testControl", "testAction") { }
+
+ /// 本文
+ public string Text = "";
+ }
+
+ /// テスト用の戻り値
+ [Serializable]
+ public class TestReturn : BaseReturnValue
+ {
+ /// 本文
+ public string Text = "";
+ }
+
+ #endregion
+
+ ///
+ /// 通信制御(CallController)の接続オプションを、外部環境なしで確認する。
+ ///
+ ///
+ /// <何を見るか>(#546)
+ /// TMProtocolDefinition.xml の Prop に書いた接続オプションが、
+ /// 実際の HTTP 要求に反映されているかを見る。
+ /// オプションはクライアント側の設定なので、**サーバが受け取った内容**を
+ /// 記録して突き合わせないと、効いたかどうかを判定できない。
+ ///
+ /// <1 プロセスに閉じている理由>
+ /// オリジンとプロキシを別プロセスにすると、起動順と後始末が要る。
+ /// また BinaryFormatter の型解決も、同一プロセスなら確実である。
+ ///
+ /// <HttpListener を使わない理由>
+ /// URL の予約(netsh http add urlacl)が要る場合がある。
+ /// TcpListener なら、その手当てなしに動く。
+ ///
+ /// <対象外>
+ /// ・Domain / PDomain … Windows 統合認証が要る(Basic 認証では無視される)
+ /// ・ConnGroupName … HttpClient へ移って設定する口が無くなった。
+ /// 目的(接続の仕切り)は、CallController が
+ /// サービス名ごとにハンドラをプールすることで満たされている
+ ///
+ class Program
+ {
+ #region 定数・変数
+
+ /// オリジンのポート
+ private const int OriginPort = 51090;
+
+ /// プロキシのポート
+ private const int ProxyPort = 51091;
+
+ /// オリジン(TLS)のポート
+ private const int TlsPort = 51092;
+
+ /// クライアント証明書のファイル名(定義 XML の CertFile と合わせる)
+ private const string ClientCertFile = "TestClient.pfx";
+
+ /// クライアント証明書のパスワード(定義 XML の CertPassword と合わせる)
+ private const string ClientCertPassword = "pfxpass";
+
+ /// クライアント証明書のサブジェクト
+ private const string ClientCertSubject = "CN=OpenTouryoTestClient";
+
+ /// オリジンが受け取った内容
+ private static readonly Recorded OriginRec = new Recorded();
+
+ /// プロキシが受け取った内容
+ private static readonly Recorded ProxyRec = new Recorded();
+
+ /// オリジンが認証を要求するか
+ private static bool RequireAuth = false;
+
+ /// プロキシが認証を要求するか
+ private static bool RequireProxyAuth = false;
+
+ /// NG の件数
+ private static int NG = 0;
+
+ #endregion
+
+ /// エントリ ポイント
+ static void Main()
+ {
+ Console.OutputEncoding = Encoding.UTF8;
+
+ #region TLS の準備(#546 段 4)
+
+ // **自己署名の証明書を、実行のたびに作る。**
+ // リポジトリに証明書を置くと、期限切れで或る日突然テストが落ちる。
+ // 作るのは一瞬なので、毎回作る方が確実である。
+ X509Certificate2 serverCert = Program.CreateSelfSigned("CN=OpenTouryoTestServer");
+ X509Certificate2 clientCert = Program.CreateSelfSigned(Program.ClientCertSubject);
+
+ // クライアント証明書は、定義 XML の CertFile が指すパスに書き出す。
+ // 実行時に作るので、パスは実行ファイルの隣(相対パス)で固定する。
+ File.WriteAllBytes(Program.ClientCertFile,
+ clientCert.Export(X509ContentType.Pfx, Program.ClientCertPassword));
+
+ // **自己署名なので、クライアント側の検証を通す。**
+ // CallController は ServerCertificateValidationCallback を公開していないため、
+ // ServicePointManager 側(プロセス全体)で受け入れる。テスト専用の措置である。
+ ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
+ ServicePointManager.ServerCertificateValidationCallback =
+ delegate { return true; };
+
+ #endregion
+
+ TcpListener origin = Program.StartOrigin();
+ TcpListener proxy = Program.StartProxy();
+ TcpListener tls = Program.StartListener(Program.TlsPort, Program.HandleOrigin, serverCert);
+
+ try
+ {
+ // 表題 サービス名 認証 Px認証 UserAgent gzip ユーザ プロキシ
+ Program.Case("素の呼び出し", "testPlain", false, false, null, false, null, false, null);
+ Program.Case("UserAgent", "testUA", false, false, "OpenTouryoTestAgent/1.0", false, null, false, null);
+ Program.Case("Compression", "testGzip", false, false, null, true, null, false, null);
+ Program.Case("認証", "testAuth", true, false, null, false, "fxuser:fxpass", false, null);
+ Program.Case("プロキシ経由", "testProxy", false, false, null, false, null, true, null);
+ Program.Case("プロキシ認証", "testProxyAuth", false, true, null, false, null, true, "pxuser:pxpass");
+ Program.Case("全オプション", "testAll", true, true, "OpenTouryoTestAgent/1.0", true, "fxuser:fxpass", true, "pxuser:pxpass");
+
+ // クライアント証明書(TLS。プロキシは経由しない)
+ Program.Case("クライアント証明書", "testCert", false, false, null, false, null, false, null,
+ Program.ClientCertSubject);
+ }
+ finally
+ {
+ origin.Stop();
+ proxy.Stop();
+ tls.Stop();
+ }
+
+ Console.WriteLine();
+ Console.WriteLine("NG : {0} 件", Program.NG);
+ }
+
+ #region 検証
+
+ /// 1 ケースを実行して判定する
+ /// 表題
+ /// サービス名(TMProtocolDefinition.xml の Transmission)
+ /// オリジンが認証を要求するか
+ /// プロキシが認証を要求するか
+ /// 期待する User-Agent(不要なら null)
+ /// Accept-Encoding に gzip を期待するか
+ /// 期待する Basic の中身(不要なら null)
+ /// プロキシを経由することを期待するか
+ /// 期待するプロキシ Basic の中身(不要なら null)
+ private static void Case(
+ string title, string serviceName, bool requireAuth, bool requireProxyAuth,
+ string expectUserAgent, bool expectGzip, string expectAuth,
+ bool expectProxy, string expectProxyAuth, string expectClientCert = null)
+ {
+ Program.RequireAuth = requireAuth;
+ Program.RequireProxyAuth = requireProxyAuth;
+
+ Console.WriteLine();
+ Console.WriteLine("=== {0}({1})===", title, serviceName);
+
+ lock (Program.OriginRec) { Program.OriginRec.Clear(); }
+ lock (Program.ProxyRec) { Program.ProxyRec.Clear(); }
+
+ #region 呼び出し
+
+ string returned = null;
+
+ try
+ {
+ TestParam param = new TestParam();
+ param.Text = "こんにちは";
+
+ CallController cc = new CallController(new TestContext());
+ TestReturn ret = (TestReturn)cc.Invoke(serviceName, param);
+
+ returned = (ret == null) ? null : ret.Text;
+ }
+ catch (Exception ex)
+ {
+ Exception e = ex;
+ while (e.InnerException != null) { e = e.InnerException; }
+ Console.WriteLine(" [NG] 例外 : {0} : {1}", e.GetType().Name, e.Message);
+ Program.NG++;
+ return;
+ }
+
+ #endregion
+
+ #region 判定
+
+ Program.Check("戻り値", "サーバからの戻り値", returned);
+
+ lock (Program.OriginRec)
+ {
+ Program.Check("オリジンへ到達", true, Program.OriginRec.Count > 0);
+
+ if (expectUserAgent != null)
+ {
+ Program.Check("User-Agent", expectUserAgent, Program.OriginRec.Header("User-Agent"));
+ }
+
+ if (expectGzip)
+ {
+ string accept = Program.OriginRec.Header("Accept-Encoding");
+ Program.Check("Accept-Encoding に gzip", true,
+ accept != null && accept.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) >= 0);
+ }
+
+ if (expectAuth != null)
+ {
+ // 認証は 401 を挟むため、要求は 2 回になる
+ Program.Check("認証の再送", 2, Program.OriginRec.Count);
+ Program.Check("Authorization", expectAuth, Program.Basic(Program.OriginRec.Header("Authorization")));
+ }
+
+ if (expectClientCert != null)
+ {
+ // TLS の握手でサーバが受け取ったクライアント証明書
+ Program.Check("クライアント証明書", expectClientCert, Program.OriginRec.ClientCertSubject);
+ }
+ }
+
+ lock (Program.ProxyRec)
+ {
+ Program.Check("プロキシ経由", expectProxy, Program.ProxyRec.Count > 0);
+
+ if (expectProxy)
+ {
+ // プロキシ宛は絶対 URI になる
+ Program.Check("要求行が絶対 URI", true,
+ Program.ProxyRec.RequestLine.IndexOf(" http://", StringComparison.Ordinal) > 0);
+ }
+
+ if (expectProxyAuth != null)
+ {
+ Program.Check("Proxy-Authorization", expectProxyAuth,
+ Program.Basic(Program.ProxyRec.Header("Proxy-Authorization")));
+ }
+ }
+
+ #endregion
+ }
+
+ /// 期待値と突き合わせて出力する
+ /// 項目名
+ /// 期待値
+ /// 実測値
+ private static void Check(string name, object expected, object actual)
+ {
+ bool ok = object.Equals(expected, actual);
+ if (!ok) { Program.NG++; }
+
+ Console.WriteLine(" [{0}] {1,-22} : {2}",
+ ok ? "OK" : "NG", name,
+ ok ? Program.Show(actual) : Program.Show(actual) + "(期待 : " + Program.Show(expected) + ")");
+ }
+
+ /// 値を出力用の文字列にする
+ /// 値
+ /// 文字列
+ private static string Show(object o)
+ {
+ return (o == null) ? "(なし)" : o.ToString();
+ }
+
+ /// Basic 認証の値を復号する
+ /// ヘッダの値
+ /// 「ユーザ:パスワード」(Basic でなければ元の値)
+ private static string Basic(string value)
+ {
+ if (value == null || !value.StartsWith("Basic ")) { return value; }
+
+ try
+ {
+ return Encoding.UTF8.GetString(Convert.FromBase64String(value.Substring(6)));
+ }
+ catch
+ {
+ return value;
+ }
+ }
+
+ #endregion
+
+ #region 証明書
+
+ /// 自己署名の証明書を作る
+ /// サブジェクト(例 : CN=Xxx)
+ /// 秘密鍵付きの証明書
+ ///
+ /// **証明書はリポジトリに置かず、実行のたびに作る。**(#546 段 4)
+ /// 置くと期限切れで或る日突然テストが落ちる。
+ ///
+ /// **Exportable で作り直している理由。**
+ /// CertificateRequest が返す証明書の秘密鍵は、そのままでは
+ /// SslStream のサーバ認証や PFX への書き出しに使えないことがある。
+ /// いったん PFX に通して読み直すと、確実に扱える形になる。
+ ///
+ private static X509Certificate2 CreateSelfSigned(string subject)
+ {
+ using (RSA rsa = RSA.Create(2048))
+ {
+ CertificateRequest request = new CertificateRequest(
+ new X500DistinguishedName(subject), rsa,
+ HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
+
+ // サーバ認証とクライアント認証の両方に使えるようにする
+ request.CertificateExtensions.Add(
+ new X509EnhancedKeyUsageExtension(
+ new OidCollection
+ {
+ new Oid("1.3.6.1.5.5.7.3.1"), // サーバ認証
+ new Oid("1.3.6.1.5.5.7.3.2") // クライアント認証
+ }, false));
+
+ X509Certificate2 cert = request.CreateSelfSigned(
+ DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(1));
+
+ return new X509Certificate2(
+ cert.Export(X509ContentType.Pfx, "temp"), "temp",
+ X509KeyStorageFlags.Exportable);
+ }
+ }
+
+ #endregion
+
+ #region オリジン
+
+ /// オリジンを起動する
+ /// TcpListener
+ private static TcpListener StartOrigin()
+ {
+ return Program.StartListener(Program.OriginPort, Program.HandleOrigin, null);
+ }
+
+ /// オリジンの応答
+ /// NetworkStream
+ private static void HandleOrigin(Stream ns)
+ {
+ string requestLine;
+ Dictionary headers;
+ byte[] body;
+
+ if (!Program.ReadRequest(ns, out requestLine, out headers, out body)) { return; }
+
+ lock (Program.OriginRec)
+ {
+ Program.OriginRec.Count++;
+ Program.OriginRec.RequestLine = requestLine;
+ foreach (KeyValuePair h in headers) { Program.OriginRec.Headers[h.Key] = h.Value; }
+ }
+
+ // 認証が要る場合、最初の要求には 401 を返して Basic を要求する。
+ // HttpClient は資格情報が設定されていれば、Authorization を付けて再送する。
+ if (Program.RequireAuth && !headers.ContainsKey("Authorization"))
+ {
+ Program.Write(ns,
+ "HTTP/1.1 401 Unauthorized\r\n"
+ + "WWW-Authenticate: Basic realm=\"fx\"\r\n"
+ + "Content-Length: 0\r\n"
+ + "Connection: close\r\n\r\n");
+ return;
+ }
+
+ byte[] payload = Encoding.UTF8.GetBytes(Program.BuildResponse(body));
+ string contentEncoding = "";
+
+ // 要求が gzip を受け付けるなら、圧縮して返す。
+ // クライアント側で復号できているかは、戻り値が取れるかで分かる。
+ string accept = headers.ContainsKey("Accept-Encoding") ? headers["Accept-Encoding"] : "";
+
+ if (accept.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ payload = Program.Gzip(payload);
+ contentEncoding = "Content-Encoding: gzip\r\n";
+ }
+
+ Program.Write(ns,
+ "HTTP/1.1 200 OK\r\n"
+ + "Content-Type: application/json; charset=utf-8\r\n"
+ + contentEncoding
+ + "Content-Length: " + payload.Length + "\r\n"
+ + "Connection: close\r\n\r\n");
+
+ ns.Write(payload, 0, payload.Length);
+ ns.Flush();
+ }
+
+ /// CallController が期待する形の応答を作る
+ /// 要求の本文
+ /// JSON
+ ///
+ /// 要求 : {"ServiceName":…,"ContextObject":base64,"ParameterValueObject":base64}
+ /// 応答 : {"Return":base64,"ContextObject":base64,"ReturnValueObject":base64}
+ /// Return が「空文字のバイト列」なら、例外なしと解釈される。
+ ///
+ private static string BuildResponse(byte[] requestBody)
+ {
+ JObject request = (JObject)JsonConvert.DeserializeObject(Encoding.UTF8.GetString(requestBody));
+
+ TestReturn ret = new TestReturn();
+ ret.Text = "サーバからの戻り値";
+
+ return JsonConvert.SerializeObject(new
+ {
+ Return = CustomEncode.ToBase64String(BinarySerialize.ObjectToBytes("")),
+
+ // 受け取ったコンテキストは、そのまま返す
+ ContextObject = (string)request["ContextObject"],
+
+ ReturnValueObject = CustomEncode.ToBase64String(BinarySerialize.ObjectToBytes(ret))
+ });
+ }
+
+ /// gzip で圧縮する
+ /// 元のバイト列
+ /// 圧縮後のバイト列
+ private static byte[] Gzip(byte[] raw)
+ {
+ using (MemoryStream ms = new MemoryStream())
+ {
+ using (GZipStream gz = new GZipStream(ms, CompressionMode.Compress, true))
+ {
+ gz.Write(raw, 0, raw.Length);
+ }
+
+ return ms.ToArray();
+ }
+ }
+
+ #endregion
+
+ #region プロキシ
+
+ /// プロキシを起動する
+ /// TcpListener
+ /// 平文 HTTP の順方向プロキシ。HTTPS を通すには CONNECT の実装が要る。
+ private static TcpListener StartProxy()
+ {
+ return Program.StartListener(Program.ProxyPort, Program.HandleProxy, null);
+ }
+
+ /// プロキシの中継
+ /// NetworkStream
+ private static void HandleProxy(Stream ns)
+ {
+ string requestLine;
+ Dictionary headers;
+ byte[] body;
+
+ if (!Program.ReadRequest(ns, out requestLine, out headers, out body)) { return; }
+
+ lock (Program.ProxyRec)
+ {
+ Program.ProxyRec.Count++;
+ Program.ProxyRec.RequestLine = requestLine;
+ foreach (KeyValuePair h in headers) { Program.ProxyRec.Headers[h.Key] = h.Value; }
+ }
+
+ if (Program.RequireProxyAuth && !headers.ContainsKey("Proxy-Authorization"))
+ {
+ Program.Write(ns,
+ "HTTP/1.1 407 Proxy Authentication Required\r\n"
+ + "Proxy-Authenticate: Basic realm=\"px\"\r\n"
+ + "Content-Length: 0\r\n"
+ + "Connection: close\r\n\r\n");
+ return;
+ }
+
+ // 要求行「POST http://host:port/path HTTP/1.1」から、パスを取り出す
+ string[] parts = requestLine.Split(' ');
+ Uri target = new Uri(parts[1]);
+
+ // **ホスト名は解決せず、必ずオリジンへ中継する。**
+ //
+ // 宛先に実在しない名前(fx-origin.test)を使っているのは、
+ // .NET Framework の WebProxy.IsBypassed が
+ // **ループバック宛を常に迂回する**ためである(.NET (Core) は迂回しない)。
+ // 127.0.0.1 や localhost を宛先にすると、プロキシを設定していても
+ // 直接オリジンへ行ってしまい、プロキシの検証にならない。
+ //
+ // 名前解決させると hosts ファイルの編集(管理者権限)が要るので、
+ // このテスト用プロキシが名前を無視して繋ぎ替える。
+ using (TcpClient upstream = new TcpClient("127.0.0.1", Program.OriginPort))
+ using (NetworkStream us = upstream.GetStream())
+ {
+ StringBuilder sb = new StringBuilder();
+ sb.Append(parts[0] + " " + target.PathAndQuery + " " + parts[2] + "\r\n");
+
+ foreach (KeyValuePair h in headers)
+ {
+ // プロキシ向けのヘッダは、そのまま送らない
+ if (h.Key.StartsWith("Proxy-", StringComparison.OrdinalIgnoreCase)) { continue; }
+ sb.Append(h.Key + ": " + h.Value + "\r\n");
+ }
+
+ sb.Append("Connection: close\r\n\r\n");
+
+ Program.Write(us, sb.ToString());
+ if (body.Length > 0) { us.Write(body, 0, body.Length); }
+ us.Flush();
+
+ // 応答は、そのまま返す
+ byte[] buffer = new byte[8192];
+ int length;
+
+ while ((length = us.Read(buffer, 0, buffer.Length)) > 0)
+ {
+ ns.Write(buffer, 0, length);
+ }
+
+ ns.Flush();
+ }
+ }
+
+ #endregion
+
+ #region HTTP の読み書き
+
+ /// 接続を受け付けて処理する
+ /// ポート
+ /// 処理
+ /// サーバ証明書(null なら平文)
+ /// TcpListener
+ ///
+ /// **サーバ証明書を渡すと TLS になる。**(#546 段 4)
+ /// SslStream を挟むだけなので、`netsh http add sslcert` によるポートへの
+ /// 証明書の紐付け(管理者権限)が要らない。Kestrel も要らない。
+ ///
+ private static TcpListener StartListener(int port, Action handler, X509Certificate2 serverCert)
+ {
+ TcpListener listener = new TcpListener(IPAddress.Loopback, port);
+ listener.Start();
+
+ ThreadPool.QueueUserWorkItem(delegate
+ {
+ while (true)
+ {
+ TcpClient client;
+
+ // Stop() すると例外で抜ける
+ try { client = listener.AcceptTcpClient(); }
+ catch { return; }
+
+ ThreadPool.QueueUserWorkItem(delegate
+ {
+ try
+ {
+ using (client)
+ using (NetworkStream ns = client.GetStream())
+ {
+ if (serverCert == null)
+ {
+ handler(ns);
+ }
+ else
+ {
+ // **クライアント証明書の検証を通す。**
+ // コールバックを渡さないと既定の検証になり、
+ // 自己署名のクライアント証明書は
+ // 「信頼されていない機関によって発行された」として
+ // 握手ごと失敗する。ここで見たいのは
+ // 「証明書が送られてきたか」なので、検証はしない。
+ using (SslStream ssl = new SslStream(ns, false,
+ delegate { return true; }))
+ {
+ // **クライアント証明書を要求する。**
+ // 要求しないと、クライアントが設定していても送られてこない。
+ ssl.AuthenticateAsServer(
+ serverCert, true, SslProtocols.Tls12, false);
+
+ lock (Program.OriginRec)
+ {
+ X509Certificate remote = ssl.RemoteCertificate;
+ Program.OriginRec.ClientCertSubject =
+ (remote == null) ? null : remote.Subject;
+ }
+
+ handler(ssl);
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(" [NG] サーバ側で例外 : {0} : {1}", ex.GetType().Name, ex.Message);
+ Program.NG++;
+ }
+ });
+ }
+ });
+
+ return listener;
+ }
+
+ /// 要求行・ヘッダ・本文を読む
+ /// NetworkStream
+ /// 要求行
+ /// ヘッダ
+ /// 本文
+ /// 読めたら true
+ private static bool ReadRequest(Stream ns, out string requestLine,
+ out Dictionary headers, out byte[] body)
+ {
+ requestLine = "";
+ headers = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ body = new byte[0];
+
+ #region ヘッダ
+
+ // **本文を読み過ぎないよう、CRLF CRLF まで 1 バイトずつ読む。**
+ MemoryStream head = new MemoryStream();
+ int b;
+ int match = 0;
+
+ while ((b = ns.ReadByte()) >= 0)
+ {
+ head.WriteByte((byte)b);
+
+ if ((match == 0 || match == 2) && b == '\r') { match++; }
+ else if ((match == 1 || match == 3) && b == '\n') { match++; }
+ else { match = (b == '\r') ? 1 : 0; }
+
+ if (match == 4) { break; }
+ }
+
+ if (head.Length == 0) { return false; }
+
+ string[] lines = Encoding.ASCII.GetString(head.ToArray())
+ .Split(new string[] { "\r\n" }, StringSplitOptions.None);
+
+ requestLine = lines[0];
+
+ for (int i = 1; i < lines.Length; i++)
+ {
+ if (lines[i] == "") { break; }
+
+ int colon = lines[i].IndexOf(':');
+ if (colon <= 0) { continue; }
+
+ headers[lines[i].Substring(0, colon).Trim()] = lines[i].Substring(colon + 1).Trim();
+ }
+
+ #endregion
+
+ #region 本文
+
+ if (headers.ContainsKey("Content-Length"))
+ {
+ int length = int.Parse(headers["Content-Length"]);
+ body = new byte[length];
+
+ int read = 0;
+
+ while (read < length)
+ {
+ int n = ns.Read(body, read, length - read);
+ if (n <= 0) { break; }
+ read += n;
+ }
+ }
+
+ #endregion
+
+ return true;
+ }
+
+ /// 文字列を送る
+ /// NetworkStream
+ /// 文字列
+ private static void Write(Stream ns, string text)
+ {
+ byte[] bytes = Encoding.ASCII.GetBytes(text);
+ ns.Write(bytes, 0, bytes.Length);
+ ns.Flush();
+ }
+
+ #endregion
+ }
+}
diff --git a/root/programs/CS/Frameworks/Tests/TestTransmission/TMProtocolDefinition.xml b/root/programs/CS/Frameworks/Tests/TestTransmission/TMProtocolDefinition.xml
new file mode 100644
index 000000000..e437a23c1
--- /dev/null
+++ b/root/programs/CS/Frameworks/Tests/TestTransmission/TMProtocolDefinition.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/root/programs/CS/Frameworks/Tests/TestTransmission/TestTransmissionFx48.sln b/root/programs/CS/Frameworks/Tests/TestTransmission/TestTransmissionFx48.sln
new file mode 100644
index 000000000..72917e7c1
--- /dev/null
+++ b/root/programs/CS/Frameworks/Tests/TestTransmission/TestTransmissionFx48.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 18
+VisualStudioVersion = 18.0.11205.157 d18.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestTransmissionFx", "net48\TestTransmissionFx.csproj", "{3C7F9A10-546A-4F1E-9D2B-1A0E5C546001}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {3C7F9A10-546A-4F1E-9D2B-1A0E5C546001}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3C7F9A10-546A-4F1E-9D2B-1A0E5C546001}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3C7F9A10-546A-4F1E-9D2B-1A0E5C546001}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3C7F9A10-546A-4F1E-9D2B-1A0E5C546001}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {3C7F9A10-546A-4F1E-9D2B-1A0E5C546002}
+ EndGlobalSection
+EndGlobal
diff --git a/root/programs/CS/Frameworks/Tests/TestTransmission/net48/TestTransmissionFx.csproj b/root/programs/CS/Frameworks/Tests/TestTransmission/net48/TestTransmissionFx.csproj
new file mode 100644
index 000000000..bc07907e5
--- /dev/null
+++ b/root/programs/CS/Frameworks/Tests/TestTransmission/net48/TestTransmissionFx.csproj
@@ -0,0 +1,75 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {3C7F9A10-546A-4F1E-9D2B-1A0E5C546001}
+ Exe
+ Properties
+ TestTransmission
+ TestTransmissionFx
+ v4.8
+ 512
+ true
+
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE;NET48
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE;NET48
+ prompt
+ 4
+
+
+ true
+
+
+
+ ..\..\..\Infrastructure\Build_net48\OpenTouryo.Public.dll
+
+
+ ..\..\..\Infrastructure\Build_net48\OpenTouryo.Framework.dll
+
+
+
+
+
+
+
+
+
+ App.config
+
+
+ TMProtocolDefinition.xml
+ Always
+
+
+
+
+ Properties\AssemblyInfo.cs
+
+
+ Program.cs
+
+
+
+
+ 13.0.3
+
+
+
+
diff --git a/root/programs/CS/Samples/WS_sample/WSClient_sample/WSClientWPF_sample/TMProtocolDefinition.xml b/root/programs/CS/Samples/WS_sample/WSClient_sample/WSClientWPF_sample/TMProtocolDefinition.xml
index 785a1d198..f5707fc1d 100644
--- a/root/programs/CS/Samples/WS_sample/WSClient_sample/WSClientWPF_sample/TMProtocolDefinition.xml
+++ b/root/programs/CS/Samples/WS_sample/WSClient_sample/WSClientWPF_sample/TMProtocolDefinition.xml
@@ -1,6 +1,6 @@
+
@@ -8,11 +8,12 @@
-
]>
-
+
+
@@ -37,8 +38,8 @@
-
-
+
+
\ No newline at end of file
diff --git a/root/programs/CS/Samples/WS_sample/WSClient_sample/WSClientWPF_sample/TMProtocolDefinition2.xml b/root/programs/CS/Samples/WS_sample/WSClient_sample/WSClientWPF_sample/TMProtocolDefinition2.xml
index b2315a221..34ebaed99 100644
--- a/root/programs/CS/Samples/WS_sample/WSClient_sample/WSClientWPF_sample/TMProtocolDefinition2.xml
+++ b/root/programs/CS/Samples/WS_sample/WSClient_sample/WSClientWPF_sample/TMProtocolDefinition2.xml
@@ -1,6 +1,6 @@
+
@@ -8,11 +8,12 @@
-
]>
-
+
+
@@ -37,8 +38,8 @@
-
-
+
+
\ No newline at end of file
diff --git a/root/programs/CS/Samples/WS_sample/WSClient_sample/WSClientWinCone_sample/TMProtocolDefinition.xml b/root/programs/CS/Samples/WS_sample/WSClient_sample/WSClientWinCone_sample/TMProtocolDefinition.xml
index 785a1d198..f5707fc1d 100644
--- a/root/programs/CS/Samples/WS_sample/WSClient_sample/WSClientWinCone_sample/TMProtocolDefinition.xml
+++ b/root/programs/CS/Samples/WS_sample/WSClient_sample/WSClientWinCone_sample/TMProtocolDefinition.xml
@@ -1,6 +1,6 @@
+
@@ -8,11 +8,12 @@
-
]>
-
+
+
@@ -37,8 +38,8 @@
-
-
+
+
\ No newline at end of file
diff --git a/root/programs/CS/Samples/WS_sample/WSClient_sample/WSClientWinCone_sample/TMProtocolDefinition2.xml b/root/programs/CS/Samples/WS_sample/WSClient_sample/WSClientWinCone_sample/TMProtocolDefinition2.xml
index b2315a221..34ebaed99 100644
--- a/root/programs/CS/Samples/WS_sample/WSClient_sample/WSClientWinCone_sample/TMProtocolDefinition2.xml
+++ b/root/programs/CS/Samples/WS_sample/WSClient_sample/WSClientWinCone_sample/TMProtocolDefinition2.xml
@@ -1,6 +1,6 @@
+
@@ -8,11 +8,12 @@
-
]>
-
+
+
@@ -37,8 +38,8 @@
-
-
+
+
\ No newline at end of file
diff --git a/root/programs/CS/Samples/WS_sample/WSClient_sample/WSClientWin_sample/TMProtocolDefinition.xml b/root/programs/CS/Samples/WS_sample/WSClient_sample/WSClientWin_sample/TMProtocolDefinition.xml
index 785a1d198..f5707fc1d 100644
--- a/root/programs/CS/Samples/WS_sample/WSClient_sample/WSClientWin_sample/TMProtocolDefinition.xml
+++ b/root/programs/CS/Samples/WS_sample/WSClient_sample/WSClientWin_sample/TMProtocolDefinition.xml
@@ -1,6 +1,6 @@
+
@@ -8,11 +8,12 @@
-
]>
-
+
+
@@ -37,8 +38,8 @@
-
-
+
+
\ No newline at end of file
diff --git a/root/programs/CS/Samples/WS_sample/WSClient_sample/WSClientWin_sample/TMProtocolDefinition2.xml b/root/programs/CS/Samples/WS_sample/WSClient_sample/WSClientWin_sample/TMProtocolDefinition2.xml
index b2315a221..34ebaed99 100644
--- a/root/programs/CS/Samples/WS_sample/WSClient_sample/WSClientWin_sample/TMProtocolDefinition2.xml
+++ b/root/programs/CS/Samples/WS_sample/WSClient_sample/WSClientWin_sample/TMProtocolDefinition2.xml
@@ -1,6 +1,6 @@
+
@@ -8,11 +8,12 @@
-
]>
-
+
+
@@ -37,8 +38,8 @@
-
-
+
+
\ No newline at end of file
diff --git a/root/programs/CS/Samples4NetCore/Legacy/WS_sample/WSClient_sample/WSClientWPF_sample/TMProtocolDefinition.xml b/root/programs/CS/Samples4NetCore/Legacy/WS_sample/WSClient_sample/WSClientWPF_sample/TMProtocolDefinition.xml
index 785a1d198..f5707fc1d 100644
--- a/root/programs/CS/Samples4NetCore/Legacy/WS_sample/WSClient_sample/WSClientWPF_sample/TMProtocolDefinition.xml
+++ b/root/programs/CS/Samples4NetCore/Legacy/WS_sample/WSClient_sample/WSClientWPF_sample/TMProtocolDefinition.xml
@@ -1,6 +1,6 @@
+
@@ -8,11 +8,12 @@
-
]>
-
+
+
@@ -37,8 +38,8 @@
-
-
+
+
\ No newline at end of file
diff --git a/root/programs/CS/Samples4NetCore/Legacy/WS_sample/WSClient_sample/WSClientWPF_sample/TMProtocolDefinition2.xml b/root/programs/CS/Samples4NetCore/Legacy/WS_sample/WSClient_sample/WSClientWPF_sample/TMProtocolDefinition2.xml
index b2315a221..34ebaed99 100644
--- a/root/programs/CS/Samples4NetCore/Legacy/WS_sample/WSClient_sample/WSClientWPF_sample/TMProtocolDefinition2.xml
+++ b/root/programs/CS/Samples4NetCore/Legacy/WS_sample/WSClient_sample/WSClientWPF_sample/TMProtocolDefinition2.xml
@@ -1,6 +1,6 @@
+
@@ -8,11 +8,12 @@
-
]>
-
+
+
@@ -37,8 +38,8 @@
-
-
+
+
\ No newline at end of file
diff --git a/root/programs/CS/Samples4NetCore/Legacy/WS_sample/WSClient_sample/WSClientWin_sample/TMProtocolDefinition.xml b/root/programs/CS/Samples4NetCore/Legacy/WS_sample/WSClient_sample/WSClientWin_sample/TMProtocolDefinition.xml
index 785a1d198..f5707fc1d 100644
--- a/root/programs/CS/Samples4NetCore/Legacy/WS_sample/WSClient_sample/WSClientWin_sample/TMProtocolDefinition.xml
+++ b/root/programs/CS/Samples4NetCore/Legacy/WS_sample/WSClient_sample/WSClientWin_sample/TMProtocolDefinition.xml
@@ -1,6 +1,6 @@
+
@@ -8,11 +8,12 @@
-
]>
-
+
+
@@ -37,8 +38,8 @@
-
-
+
+
\ No newline at end of file
diff --git a/root/programs/CS/Samples4NetCore/Legacy/WS_sample/WSClient_sample/WSClientWin_sample/TMProtocolDefinition2.xml b/root/programs/CS/Samples4NetCore/Legacy/WS_sample/WSClient_sample/WSClientWin_sample/TMProtocolDefinition2.xml
index b2315a221..34ebaed99 100644
--- a/root/programs/CS/Samples4NetCore/Legacy/WS_sample/WSClient_sample/WSClientWin_sample/TMProtocolDefinition2.xml
+++ b/root/programs/CS/Samples4NetCore/Legacy/WS_sample/WSClient_sample/WSClientWin_sample/TMProtocolDefinition2.xml
@@ -1,6 +1,6 @@
+
@@ -8,11 +8,12 @@
-
]>
-
+
+
@@ -37,8 +38,8 @@
-
-
+
+
\ No newline at end of file
diff --git a/root/programs/CS/y_Build_TestTransmission.bat b/root/programs/CS/y_Build_TestTransmission.bat
new file mode 100644
index 000000000..29fced846
--- /dev/null
+++ b/root/programs/CS/y_Build_TestTransmission.bat
@@ -0,0 +1,27 @@
+setlocal
+
+@rem --------------------------------------------------
+@rem Turn off the echo function.
+@rem --------------------------------------------------
+@echo off
+
+@rem --------------------------------------------------
+@rem Get the path to the executable file.
+@rem --------------------------------------------------
+set CURRENT_DIR="%~dp0"
+
+@rem --------------------------------------------------
+@rem Execution of the common processing.
+@rem --------------------------------------------------
+call %CURRENT_DIR%z_Common.bat
+
+@rem --------------------------------------------------
+@rem Batch build of TestTransmission.
+@rem --------------------------------------------------
+..\nuget.exe restore "Frameworks\Tests\TestTransmission\TestTransmissionFx48.sln" %NUGET_MSBUILD%
+%BUILDFILEPATH% %COMMANDLINE% "Frameworks\Tests\TestTransmission\TestTransmissionFx48.sln"
+
+pause
+
+rem -------------------------------------------------------
+endlocal
diff --git a/root/programs/RELEASE.md b/root/programs/RELEASE.md
index 0874fdf64..d29217a18 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**(22 件)
+- [ ] **`3_SmokeTest.ps1` が終了コード 0**(23 件)
> **`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`(22 件) | 2.3 分 |
+| `3_SmokeTest.ps1`(23 件) | 4.0 分 |
### VB 側は、この 3 本に含めない
diff --git a/root/programs/SMOKETEST.md b/root/programs/SMOKETEST.md
index ff8b4f947..ee4fc43da 100644
--- a/root/programs/SMOKETEST.md
+++ b/root/programs/SMOKETEST.md
@@ -50,7 +50,7 @@ cd root\programs
---
-## 3. 対象(22 件)
+## 3. 対象(23 件)
### バッチ(8 件)
@@ -224,6 +224,104 @@ Web アプリと違い**対象は EXE** なので、本文側の Web ホスト
`/ZIPGEN` を実装した直後、`.NET 10` 側を建て直さずに回して実際に踏んだ。
**片方のビルドだけを更新しない**こと。
+### 通信制御の接続オプション(1 件)
+
+`CallController` の接続オプション(`TMProtocolDefinition.xml` の `Prop`)が、
+実際の HTTP 要求に反映されているかを見る(#546)。
+
+| 対象 | 判定 |
+|---|---|
+| `TestTransmission` (net48) | 対象側が出す `NG : 0 件` |
+
+対象は **`Frameworks/Tests/TestTransmission`** の net48 コンソールで、
+**オリジンとプロキシを自前で立て、そこへ `CallController` から呼ぶ**。
+判定は対象側が行い、項目ごとに `OK` / `NG` を出して末尾に件数を出す。
+
+```
+=== プロキシ認証(testProxyAuth)===
+ [OK] 戻り値 : サーバからの戻り値
+ [OK] プロキシ経由 : True
+ [OK] 要求行が絶対 URI : True
+ [OK] Proxy-Authorization : pxuser:pxpass
+
+NG : 0 件
+```
+
+#### 見ているオプション(9 つ)
+
+| オプション | 見ていること |
+|---|---|
+| `ProxyUrl` | プロキシに要求が届き、**要求行が絶対 URI** になっている |
+| `PUserName` / `PPassword` | **407 → Basic で再送**され、値が届いている |
+| `UserName` / `Password` | **401 → Basic で再送**され、値が届いている |
+| `UserAgent` | オリジンに届いた `User-Agent` の値 |
+| `Compression` | `Accept-Encoding` に gzip が付き、**gzip の応答を復号できている** |
+| `CertFile` / `CertPassword` | **TLS の握手でサーバが受け取ったクライアント証明書**のサブジェクト |
+
+**オプションはクライアント側の設定なので、サーバが受け取った内容を記録して
+突き合わせないと、効いたかどうかを判定できない。** このため、値を返すだけの
+オリジンではなく、**受け取ったヘッダを記録するオリジン**を用意している。
+
+#### 外部環境が要らない
+
+オリジンもプロキシも `TcpListener` で立て、**1 プロセスに閉じている**。
+
+- **`HttpListener` を使わない。** URL の予約(`netsh http add urlacl`)が要る場合があり、
+ `TcpListener` ならその手当てなしに動く
+- **プロセスを分けない。** 起動順と後始末が要らず、`BinaryFormatter` の型解決も確実になる
+- 使うポートは **51090(オリジン)/ 51091(プロキシ)/ 51092(オリジン・TLS)**
+
+#### クライアント証明書は TLS で確認する
+
+`CertFile` だけは平文では確かめられないため、**`SslStream` を挟んだオリジン**(51092)を
+別に立てる。`AuthenticateAsServer(cert, clientCertificateRequired: true, …)` で
+**クライアント証明書を要求**し、握手後の `RemoteCertificate` を記録する。
+
+- **`netsh http add sslcert` が要らない。** ポートへの証明書の紐付け(管理者権限)は
+ `HttpListener` の話であって、`SslStream` には無関係
+- **Kestrel も要らない。** テストは net48 なので、別プロセスを立てずに済む
+- **証明書はリポジトリに置かず、実行のたびに作る**(`CertificateRequest`)。
+ 置くと期限切れで或る日突然テストが落ちる
+- 自己署名なので、**両側で検証を通す**。サーバ側は `SslStream` のコールバック、
+ クライアント側は `ServicePointManager.ServerCertificateValidationCallback`
+ (`CallController` は検証のコールバックを公開していないため)
+
+> **サーバ側にコールバックを渡さないと握手ごと失敗する。** 既定の検証が働き、
+> 自己署名のクライアント証明書は「信頼されていない機関によって発行された」として弾かれる。
+
+**このケースはプロキシを経由しない。** 平文 HTTP のプロキシで HTTPS を通すには
+`CONNECT` の実装が要るため、宛先は `127.0.0.1` のままでよい(下記の迂回の問題が起きない)。
+
+#### **宛先に実在しないホスト名を使っている理由**
+
+**.NET Framework の `WebProxy.IsBypassed` は、ループバック宛を常に迂回する。**
+
+```
+net48 : http://127.0.0.1:51090/ IsBypassed=True ← プロキシを使わない
+.NET 10 : 同上 IsBypassed=False ← 使う
+```
+
+`127.0.0.1` や `localhost` を宛先にすると、**プロキシを設定していても直接オリジンへ行き、
+プロキシの検証にならない**(実際、最初の実行は「プロキシ 0 回」だった)。
+
+このため、プロキシを使うケースの宛先を **`http://fx-origin.test:51090/`** とし、
+**テスト用プロキシが名前を解決せず、必ずオリジンへ繋ぎ替える**ようにしてある。
+`hosts` ファイルの編集(管理者権限)も、ファイアウォールへの露出も要らない。
+
+#### 対象外のオプション
+
+| | 理由 |
+|---|---|
+| `Domain` / `PDomain` | Windows 統合認証が前提。Basic 認証では無視される |
+| `ConnGroupName` | **効果が無い。** HttpWebRequest の接続プールを分割する名前だったが、HttpClient へ移って**設定する口が無くなった**。目的(接続の仕切り)は、CallController が**サービス名ごとにハンドラをプールする**ことで満たされている。定数は互換のため残し、定義例からは外した |
+
+#### net48 だけである理由
+
+ASP.NET WebAPI の経路(`protocol="5"`)は **.NET Framework 限定**である。
+引数と戻り値の受け渡しに `BinarySerialize` を使っており、
+.NET Core 版では `FxEnum.TmProtocol` ごと落とされている
+([`CS/Frameworks/ANALYSIS.md`](CS/Frameworks/ANALYSIS.md) と #543)。
+
### Web アプリ(3 件)
| 対象 | ホスト | 認証の実装 | 判定 |
@@ -281,6 +379,11 @@ WinForms / WPF 系は、リリース チェックリスト(段階 4)の**手
`ConnectionString_SQL` を読む。ここで別途ハードコードすると追随できなくなるため
- **IIS Express** がインストールされていること(net48 の Web アプリ)
- **ASP.NET 状態サービスが開始されている**こと(net48 の Web アプリ)
+- **ポート 51081 - 51086、51090 - 51092 が空いている**こと
+ - 51081 - 51083 … Web アプリ(net48 の 2 つと net10.0)
+ - 51084 … `DeployZipPackWithHTTP` の配信
+ - 51085 - 51086 … VB 側の Web アプリ(`-Lang VB`。10 節)
+ - **51090 - 51092 … 通信制御のオリジン・プロキシ・オリジン(TLS)(3 節)**
> **GitHub Actions でも実行している。** 前提の揃え方(SQL Server の導入と Northwind の
> ロード、`C:\root`、ロケール、`aspnet_state` の開始)は
@@ -452,6 +555,7 @@ DeployZip /MFTGEN (net48) OK マニュフェスト ファイルを生
DeployZip 配置 (net48) OK 履歴に新規追加しました。
DeployZip /MFTGEN (net10.0) OK マニュフェスト ファイルを生成しました。
DeployZip 配置 (net10.0) OK 履歴に新規追加しました。
+TestTransmission (net48) OK NG : 0 件
MVC_Sample (net48) OK ログイン後 /Crud1/Index = 200
WebForms_Sample (net48) OK ログイン後 menu.aspx = 200
MVC_Sample (net10.0) OK ログイン後 /Crud1/Index = 200
@@ -459,7 +563,7 @@ MVC_Sample (net10.0) OK ログイン後 /Crud1/Index = 200
全対象 OK
```
-全 22 件(ビルド 7 バッチ + 疎通 22 件)で **約 2.3 分**。
+全 23 件(ビルド 8 バッチ + 疎通 23 件)で **約 4 分**。
### リダイレクトの扱い
@@ -561,7 +665,7 @@ $client.Connect("localhost", $port)
```powershell
.\3_SmokeTest.ps1 -Lang VB # VB のみ(6 件)
-.\3_SmokeTest.ps1 -Lang Both # C# 22 件 + VB 6 件
+.\3_SmokeTest.ps1 -Lang Both # C# 23 件 + VB 6 件
```
**既定に VB を含めない。** リリース時の検証([`RELEASE.md`](RELEASE.md) 3 節)は
@@ -578,6 +682,7 @@ C# 側が対象で、VB を含めると毎回 VB のビルドが乗るためで
| `DeployZipPackWithHTTP` | 4 | 同上 |
| `MVC` / `WebForms` (net48) | 2 | **あり** |
| `MVC` (net10.0) | 1 | 無し |
+| 通信制御の接続オプション | 1 | 無し(net48 の C# 側だけ) |
### 判定は C# 版と共有している
diff --git a/root/programs/SummaryTable.ps1 b/root/programs/SummaryTable.ps1
new file mode 100644
index 000000000..03fbccec3
--- /dev/null
+++ b/root/programs/SummaryTable.ps1
@@ -0,0 +1,279 @@
+<#
+.SYNOPSIS
+ 集計結果を、全角文字を含んでいても桁が揃う表として出力する。
+
+.DESCRIPTION
+ <なぜ Format-Table を使わないのか>
+ Windows PowerShell 5.1 の Format-Table は、列の幅を**表示桁数ではなく文字数**で
+ 決める。日本語(全角)は 1 文字で 2 桁を占めるため、見出し・罫線・データが
+ それぞれ別々にずれる。PowerShell 7 は表示桁数で数えるので揃う。
+
+ 5.1 7
+ ステップ 結果 秒 ステップ 結果 秒
+ ---- -- - -------- ---- --
+ Clean (net48 基盤) OK 10.7 Clean (net48 基盤) OK 10.70
+
+ 利用者は 5.1(powershell.exe)で実行するため、こちらに合わせる必要がある
+ (CODING.md 5 節)。-AutoSize や -Wrap の指定では直らない。
+
+ <対処>
+ 表示桁数を自前で数え、PadRight ではなく空白の連結で桁を合わせる。
+ .NET の PadRight も文字数で数えるため、使ってはいけない。
+
+ <最終列>
+ 最終列だけは幅を揃えず、コンソール幅に収まるよう折り返す
+ (Format-Table -Wrap に相当)。内容が長い列を最後に置くこと。
+
+.PARAMETER Rows
+ 表にするオブジェクトの配列。最初の要素のプロパティを、その順で列にする。
+
+.PARAMETER Indent
+ 行頭に入れる空白の数。既定は 0。
+
+.EXAMPLE
+ . (Join-Path $PSScriptRoot "SummaryTable.ps1")
+ Write-SummaryTable $results
+
+.NOTES
+ 作成者 :玄人 幸道
+ 更新履歴 :
+ 日時 更新者 内容
+ ---------- ---------------- -------------------------------------------------
+ 2026/08/13 玄人 幸道 新規作成(5.1 で全角を含む表の桁がずれるため)
+#>
+
+# 表示桁数を数える。
+#
+# 全角(East Asian Wide / Fullwidth)は 2 桁として数える。
+# 範囲は Unicode の East Asian Width に基づく代表的なものに絞ってある。
+# 本リポジトリの表に出るのは日本語・記号だけなので、これで足りる。
+function Get-DisplayWidth
+{
+ param([string]$Text)
+
+ if ([string]::IsNullOrEmpty($Text)) { return 0 }
+
+ $w = 0
+ foreach ($c in $Text.ToCharArray())
+ {
+ $n = [int]$c
+
+ if (($n -ge 0x1100 -and $n -le 0x115F) -or # ハングル字母
+ ($n -ge 0x2E80 -and $n -le 0xA4CF) -or # CJK 部首・かな・漢字・記号
+ ($n -ge 0xAC00 -and $n -le 0xD7A3) -or # ハングル音節
+ ($n -ge 0xF900 -and $n -le 0xFAFF) -or # CJK 互換漢字
+ ($n -ge 0xFE30 -and $n -le 0xFE6F) -or # CJK 互換形・小字形
+ ($n -ge 0xFF00 -and $n -le 0xFF60) -or # 全角英数・記号
+ ($n -ge 0xFFE0 -and $n -le 0xFFE6)) # 全角通貨記号
+ {
+ $w += 2
+ }
+ else
+ {
+ $w += 1
+ }
+ }
+
+ return $w
+}
+
+# 指定した表示桁数まで、右側を空白で埋める。
+function Add-Padding
+{
+ param([string]$Text, [int]$Width)
+
+ $pad = $Width - (Get-DisplayWidth $Text)
+ if ($pad -le 0) { return $Text }
+ return $Text + (' ' * $pad)
+}
+
+# 指定した表示桁数まで、左側を空白で埋める(数値列用)。
+function Add-LeftPadding
+{
+ param([string]$Text, [int]$Width)
+
+ $pad = $Width - (Get-DisplayWidth $Text)
+ if ($pad -le 0) { return $Text }
+ return (' ' * $pad) + $Text
+}
+
+# 表示桁数で折り返す。語の区切りを優先し、1 語で収まらないときだけ途中で切る。
+function Split-ByWidth
+{
+ param([string]$Text, [int]$Width)
+
+ if ([string]::IsNullOrEmpty($Text)) { return @('') }
+ if ($Width -lt 1) { return @($Text) }
+
+ $lines = New-Object System.Collections.Generic.List[string]
+ $line = ''
+
+ foreach ($word in ($Text -split ' '))
+ {
+ $candidate = if ($line -eq '') { $word } else { $line + ' ' + $word }
+
+ if ((Get-DisplayWidth $candidate) -le $Width)
+ {
+ $line = $candidate
+ continue
+ }
+
+ if ($line -ne '') { $lines.Add($line); $line = '' }
+
+ # 1 語で収まらないものは、表示桁数で切る。
+ $rest = $word
+ while ((Get-DisplayWidth $rest) -gt $Width)
+ {
+ $take = ''
+ foreach ($c in $rest.ToCharArray())
+ {
+ if ((Get-DisplayWidth ($take + $c)) -gt $Width) { break }
+ $take += $c
+ }
+ $lines.Add($take)
+ $rest = $rest.Substring($take.Length)
+ }
+ $line = $rest
+ }
+
+ if ($line -ne '') { $lines.Add($line) }
+ if ($lines.Count -eq 0) { $lines.Add('') }
+
+ return $lines.ToArray()
+}
+
+function Write-SummaryTable
+{
+ param(
+ [object[]]$Rows,
+ [int]$Indent = 0
+ )
+
+ if ($null -eq $Rows -or $Rows.Count -eq 0) { return }
+
+ $names = @($Rows[0].PSObject.Properties | ForEach-Object { $_.Name })
+ if ($names.Count -eq 0) { return }
+
+ # 値を文字列にしておく($null は空文字)。
+ $cells = @()
+ foreach ($r in $Rows)
+ {
+ $row = @{}
+ foreach ($n in $names)
+ {
+ $v = $r.$n
+ if ($null -eq $v) { $row[$n] = '' } else { $row[$n] = [string]$v }
+ }
+ $cells += $row
+ }
+
+ # 数値だけの列は右寄せにする(Format-Table と同じ見え方にするため)。
+ $numeric = @{}
+ foreach ($n in $names)
+ {
+ $isNum = $true
+ foreach ($row in $cells)
+ {
+ $s = $row[$n]
+ if ($s -eq '') { continue }
+ $d = 0.0
+ if (-not [double]::TryParse($s, [ref]$d)) { $isNum = $false; break }
+ }
+ $numeric[$n] = $isNum
+ }
+
+ # 列幅(最終列を除く)
+ $widths = @{}
+ foreach ($n in $names)
+ {
+ $w = Get-DisplayWidth $n
+ foreach ($row in $cells)
+ {
+ $v = Get-DisplayWidth $row[$n]
+ if ($v -gt $w) { $w = $v }
+ }
+ $widths[$n] = $w
+ }
+
+ # コンソール幅。取得できない場合(リダイレクト時など)は 120 とする。
+ $console = 0
+ try { $console = $Host.UI.RawUI.WindowSize.Width } catch { }
+ if ($console -le 0) { $console = 120 }
+
+ $last = $names[$names.Count - 1]
+
+ # 最終列を折り返すのは、文字列の列のときだけ。
+ # 数値なら幅が知れているので、他の列と同じく揃えて右寄せする
+ # (1_BuildAll.ps1 の「秒」など。Format-Table もそう見せていた)。
+ $wrapLast = -not $numeric[$last]
+
+ $head = $Indent
+ foreach ($n in $names) { if ($n -ne $last) { $head += $widths[$n] + 1 } }
+
+ $lastWidth = $console - $head - 1
+ if ($lastWidth -lt 20) { $lastWidth = 20 }
+
+ $pad = ' ' * $Indent
+
+ # 1 セルを、列の幅まで詰めて返す。数値の列は右寄せ(見出しも合わせる)。
+ function Format-Cell
+ {
+ param([string]$Text, [int]$Width, [bool]$Right)
+
+ if ($Right) { return (Add-LeftPadding $Text $Width) }
+ return (Add-Padding $Text $Width)
+ }
+
+ # 見出し
+ $line = $pad
+ foreach ($n in $names)
+ {
+ if ($n -eq $last -and $wrapLast) { $line += $n; break }
+ $line += (Format-Cell $n $widths[$n] $numeric[$n])
+ if ($n -ne $last) { $line += ' ' }
+ }
+ Write-Host $line
+
+ # 罫線
+ #
+ # 折り返す最終列だけは、列幅ではなく**見出しの幅**に合わせる。
+ # 列幅に合わせると、内容の一番長い行と同じだけ罫線が伸びてしまう。
+ $line = $pad
+ foreach ($n in $names)
+ {
+ $w = if ($n -eq $last -and $wrapLast) { Get-DisplayWidth $n } else { $widths[$n] }
+ $line += ('-' * $w)
+ if ($n -ne $last) { $line += ' ' }
+ }
+ Write-Host $line
+
+ # 本体
+ foreach ($row in $cells)
+ {
+ $line = $pad
+ foreach ($n in $names)
+ {
+ if ($n -eq $last -and $wrapLast) { break }
+ $line += (Format-Cell $row[$n] $widths[$n] $numeric[$n])
+ if ($n -ne $last) { $line += ' ' }
+ }
+
+ if (-not $wrapLast)
+ {
+ Write-Host $line
+ continue
+ }
+
+ # 最終列は折り返す。2 行目以降は列の位置まで下げる。
+ #
+ # **@() で受けること。** PowerShell は要素 1 個の配列を返すとスカラーに
+ # 展開するため、そのまま [0] を取ると**文字列の 1 文字目**になる。
+ $parts = @(Split-ByWidth $row[$last] $lastWidth)
+ Write-Host ($line + $parts[0])
+
+ for ($i = 1; $i -lt $parts.Count; $i++)
+ {
+ Write-Host ((' ' * $head) + $parts[$i])
+ }
+ }
+}
diff --git a/root/programs/VB/Samples/WS_sample/WSClient_sample/WSClientWPF_sample/TMProtocolDefinition.xml b/root/programs/VB/Samples/WS_sample/WSClient_sample/WSClientWPF_sample/TMProtocolDefinition.xml
index 785a1d198..f5707fc1d 100644
--- a/root/programs/VB/Samples/WS_sample/WSClient_sample/WSClientWPF_sample/TMProtocolDefinition.xml
+++ b/root/programs/VB/Samples/WS_sample/WSClient_sample/WSClientWPF_sample/TMProtocolDefinition.xml
@@ -1,6 +1,6 @@
+
@@ -8,11 +8,12 @@
-
]>
-
+
+
@@ -37,8 +38,8 @@
-
-
+
+
\ No newline at end of file
diff --git a/root/programs/VB/Samples/WS_sample/WSClient_sample/WSClientWPF_sample/TMProtocolDefinition2.xml b/root/programs/VB/Samples/WS_sample/WSClient_sample/WSClientWPF_sample/TMProtocolDefinition2.xml
index b2315a221..34ebaed99 100644
--- a/root/programs/VB/Samples/WS_sample/WSClient_sample/WSClientWPF_sample/TMProtocolDefinition2.xml
+++ b/root/programs/VB/Samples/WS_sample/WSClient_sample/WSClientWPF_sample/TMProtocolDefinition2.xml
@@ -1,6 +1,6 @@
+
@@ -8,11 +8,12 @@
-
]>
-
+
+
@@ -37,8 +38,8 @@
-
-
+
+
\ No newline at end of file
diff --git a/root/programs/VB/Samples/WS_sample/WSClient_sample/WSClientWin_sample/TMProtocolDefinition.xml b/root/programs/VB/Samples/WS_sample/WSClient_sample/WSClientWin_sample/TMProtocolDefinition.xml
index 785a1d198..f5707fc1d 100644
--- a/root/programs/VB/Samples/WS_sample/WSClient_sample/WSClientWin_sample/TMProtocolDefinition.xml
+++ b/root/programs/VB/Samples/WS_sample/WSClient_sample/WSClientWin_sample/TMProtocolDefinition.xml
@@ -1,6 +1,6 @@
+
@@ -8,11 +8,12 @@
-
]>
-
+
+
@@ -37,8 +38,8 @@
-
-
+
+
\ No newline at end of file
diff --git a/root/programs/VB/Samples/WS_sample/WSClient_sample/WSClientWin_sample/TMProtocolDefinition2.xml b/root/programs/VB/Samples/WS_sample/WSClient_sample/WSClientWin_sample/TMProtocolDefinition2.xml
index b2315a221..34ebaed99 100644
--- a/root/programs/VB/Samples/WS_sample/WSClient_sample/WSClientWin_sample/TMProtocolDefinition2.xml
+++ b/root/programs/VB/Samples/WS_sample/WSClient_sample/WSClientWin_sample/TMProtocolDefinition2.xml
@@ -1,6 +1,6 @@
+
@@ -8,11 +8,12 @@
-
]>
-
+
+
@@ -37,8 +38,8 @@
-
-
+
+
\ No newline at end of file