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 _row = new List(); /// 行ステータス - private DataRowState _rowState; + private DTRowState _rowState; #endregion @@ -79,7 +80,7 @@ internal DTRow(DTColumns cols) } // 行ステータスを初期化する - this.RowState = DataRowState.Detached; + this.RowState = DTRowState.Detached; } #endregion @@ -253,14 +254,14 @@ public object this[int index] } // 行ステータス - if (this.RowState == DataRowState.Added || this.RowState == DataRowState.Deleted) + if (this.RowState == DTRowState.Added || this.RowState == DTRowState.Deleted) { // 行ステータスが "Added"(追加された行) または "Deleted"(削除された行) の場合は、行ステータスを変更しない } else { // 上記以外の場合は、行ステータスを "Modified"(変更された行) に変更 - this.RowState = DataRowState.Modified; + this.RowState = DTRowState.Modified; } } } @@ -295,7 +296,7 @@ public IEnumerator GetEnumerator() #endregion /// 行ステータス - public DataRowState RowState + public DTRowState RowState { internal set { diff --git a/root/programs/CS/Frameworks/Infrastructure/Public/Dto/DataRowState.cs b/root/programs/CS/Frameworks/Infrastructure/Public/Dto/DTRowState.cs similarity index 86% rename from root/programs/CS/Frameworks/Infrastructure/Public/Dto/DataRowState.cs rename to root/programs/CS/Frameworks/Infrastructure/Public/Dto/DTRowState.cs index 37632e7f6..166cfb2b0 100644 --- a/root/programs/CS/Frameworks/Infrastructure/Public/Dto/DataRowState.cs +++ b/root/programs/CS/Frameworks/Infrastructure/Public/Dto/DTRowState.cs @@ -19,7 +19,7 @@ #endregion //********************************************************************************** -//* クラス名 :DataRowState +//* クラス名 :DTRowState //* クラス日本語名 :マーシャリング機能付き汎用DTO(行ステータス列挙型) //* //* 作成者 :生技 前川 @@ -28,6 +28,9 @@ //* 日時 更新者 内容 //* ---------- ---------------- ------------------------------------------------- //* 2010/11/11 前川 祐介 新規作成 +//* 2026/08/14 玄人 幸道 DataRowStateから改名(#544)。 +//* System.Data.DataRowStateと名前が衝突し、 +//* 両方をusingすると素の名前がCS0104になっていた。 //********************************************************************************** using System; @@ -38,7 +41,7 @@ namespace Touryo.Infrastructure.Public.Dto /// 行ステータスを表す列挙型 /// [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