検索
リンク
タグ
ASP.NET
.NET
ASP.NET MVC
F#
Azure
Visual Studio
ASP.NET Core
ライトニングトーク
Plone
Selenium
AJAX
C#
jQuery
ADO.NET Entity Framework
JavaScript
SQL Server
EFCore
LINQ
WebMatrix
Fizz-Buzz
カテゴリ
最新の記事
最新のコメント
記事ランキング
最新のトラックバック
以前の記事
2024年 02月 2024年 01月 2023年 12月 2023年 11月 2023年 10月 2023年 09月 2023年 08月 2023年 07月 2023年 06月 2023年 05月 2023年 04月 2023年 03月 2023年 02月 2023年 01月 2022年 12月 2022年 11月 2022年 10月 2022年 09月 2022年 08月 2022年 07月 2022年 06月 2022年 05月 2022年 04月 2022年 03月 2022年 02月 2022年 01月 2021年 12月 2021年 11月 2021年 10月 2021年 09月 2021年 08月 2021年 07月 2021年 06月 2021年 05月 2021年 04月 2021年 03月 2021年 02月 2021年 01月 2020年 12月 2020年 11月 2020年 10月 2020年 09月 2020年 08月 2020年 07月 2020年 06月 2020年 05月 2020年 04月 2020年 03月 2020年 02月 2020年 01月 2019年 12月 2019年 11月 2019年 10月 2019年 09月 2019年 08月 2019年 07月 2019年 06月 2019年 05月 2019年 04月 2019年 03月 2019年 02月 2019年 01月 2018年 12月 2018年 11月 2018年 10月 2018年 09月 2018年 08月 2018年 07月 2018年 06月 2018年 05月 2018年 04月 2018年 03月 2018年 02月 2018年 01月 2017年 12月 2017年 11月 2017年 10月 2017年 09月 2017年 08月 2017年 07月 2017年 06月 2017年 05月 2017年 04月 2017年 02月 2017年 01月 2016年 12月 2016年 11月 2016年 10月 2016年 09月 2016年 08月 2016年 07月 2016年 06月 2016年 05月 2016年 04月 2016年 03月 2016年 02月 2016年 01月 2015年 12月 2015年 11月 2015年 10月 2015年 09月 2015年 08月 2015年 07月 2015年 05月 2015年 04月 2015年 03月 2015年 02月 2015年 01月 2014年 12月 2014年 11月 2014年 10月 2014年 09月 2014年 08月 2014年 06月 2014年 04月 2014年 03月 2014年 02月 2014年 01月 2013年 12月 2013年 10月 2013年 09月 2013年 08月 2013年 07月 2013年 06月 2013年 05月 2013年 04月 2013年 03月 2013年 02月 2013年 01月 2012年 12月 2012年 11月 2012年 10月 2012年 09月 2012年 08月 2012年 07月 2012年 06月 2012年 05月 2012年 04月 2012年 03月 2012年 02月 2012年 01月 2011年 12月 2011年 11月 2011年 10月 2011年 09月 2011年 08月 2011年 07月 2011年 06月 2011年 05月 2011年 04月 2011年 03月 2011年 02月 2011年 01月 2010年 12月 2010年 11月 2010年 10月 2010年 09月 2010年 08月 2010年 07月 2010年 06月 2010年 05月 2010年 04月 2010年 03月 2010年 02月 2010年 01月 2009年 12月 2009年 10月 2009年 09月 2009年 07月 2009年 06月 2009年 05月 2009年 04月 2009年 03月 2009年 02月 2009年 01月 2008年 12月 2008年 11月 2008年 10月 2008年 09月 2008年 08月 2008年 07月 2008年 06月 2008年 05月 2008年 04月 2008年 03月 2008年 02月 2008年 01月 2007年 12月 2007年 11月 2007年 04月 2007年 03月 2007年 02月 2007年 01月 2006年 11月 2006年 10月 2006年 09月 2006年 08月 2006年 07月 |
2024年 02月 17日
構文ハイライトライブラリ "Prism"Web ブラウザ上で、HTML や各種プログラミング言語のコードを記したコンテンツに、構文ハイライトを適用する JavaScript ライブラリとして、著名なもののひとつに Prism というライブラリがある。 この Prism を使う簡単な方法としては、公式が用意しているダウンロードページ上で、構文ハイライトを効かせたい言語やプラグインなどを指定の上、その指定のとおりカスタマイズされた "prsim.js" をダウンロードして使う。 そうしてダウンロードした prism.js を HTML ページ上で script タグで読み込んでおくと、読み込み時に自動で構文ハイライトすべき要素を探し出して適用するほか、"Prism" というオブジェクトがグローバル名前空間に設置されるので、この Prism オブジェクトの "highlightElement" などのメソッドを使って、個別に構文ハイライトを適用したりすることができるようになる。 ES モジュール形式で使いたいしかし今回は、この Prism を ES モジュール形式として他の JavaScript モジュールから import して使いたい需要が発生した。 ちなみに、公式からダウンロードしてきた prism.js を、他の JavaScript モジュールで import すれば (下記コード例)、HTML ページ上の script タグで読み込んだのと同様の振る舞い・効果となる。すなわち、読み込み時にその時点の HTLM コンテンツに対し構文ハイライトすべき要素を自動で探し出して適用し、かつ、グローバル名前空間に Prism オブジェクトが設置される。 // この JavaScript コードは <script type="module"> で読み込む await import('./prism.js'); しかし今回やりたいのはそういうことではなくて、グローバル名前空間は汚染させずに、import した prism.js モジュールの export しているメンバーを呼び出すように使いたい (下記コード例)。 // こうやって import して、 const { Prism } = await import('./prism.js'); // こんな感じで使いたい! Prism.highlightElement(element); だが当然のことながら、公式からダウンロードした prism.js は ES モジュール形式ではないので、上記コードのようなことはできない。 いっぽうで、Prism は npm パッケージとしても配付されている。 この npm パッケージ版と何かしらのバンドラーとで頑張れば、Prism の ES モジュール版をビルドできそうにも思えた。しかし構文ハイライト対象の言語をカスタマイズするためには、Babel やそのプラグインなども構成する必要があるようで、なかなかに面倒臭そうで気持が萎えてしまった。今どきは ChatGPT に聞きながらやればできちゃうのかもだが。 とまぁ、そういうわけで、今回は、雑に、直接に (公式のダウンロードサイトでカスタマイズしてダウンロードした) prism.js を直接書き換えて、なんちゃって ES モジュール化して使うことにした。 雑に prism.js を書き換えて ESM 化さてそのやり方だが、まず、prism.js がやっていることを突き詰めると、究極的には window オブジェクトに "Prism" というオブジェクトを生やしているだけである。 window.Prism = ...;// (※prism.js の実装を簡略化した擬似コード) そこで、この既存の prism.js のコードを、まるまる、"window" という名前の引数を持つアロー関数で囲う。そうすると、Prism オブジェクトがこのアロー関数のスコープ内に閉じ込められ、グローバル名前空間に露出しなくなる。 // "window" という名前の引数を持つアロー関数で囲む。 ((window) => { // すると本物の window オブジェクトではなく、この関数の引数として // 渡されたオブジェクト (以下のとおり空のオブジェクト) に Prism // オブジェクトを生やすことになり、グローバル名前空間に漏れ出なくなる。 window.Prism = ...; // (※prism.js の実装を簡略化した擬似コード) })({}); // 引数に空のオブジェクトを渡して即時実行。 そして、このアロー関数から明示的に Prism オブジェクトを戻り値で返すようにし、その戻り値 (= Prism オブジェクト) を ES モジュール形式でエクスポートする。 // このアロー関数の戻り値 (=Prism オブジェクト) をエクスポートする。 export const Prism = ((window) => { window.Prism = ...; // (※prism.js の実装を簡略化した擬似コード) return window.Prism})({}); // この関数内の window.Prism を返す。 これで以上のように書き換えた prism.js を、他の JavaScript モジュールからインポートして、グローバル名前空間を汚染することなく、以下のように利用することができるようになった。 // こうやって import して、 const { Prism } = await import('./prism.js'); // こんな感じで使える! 且つ、グローバル名前空間に Prism が漏れない! Prism.highlightElement(element); まとめ今回は、構文ハイライトライブラリ Prism について、公式のダウンロードサイトでカスタマイズしてダウンロードした prism.js ファイルを、雑に直接書き換えて、なんちゃって ES モジュール形式にしてみた。 理想的には、ちゃんとしかるべきビルドパイプラインを組んでカスタマイズされた prism.js を ES モジュール形式でビルドするのが良いのだろう。 いっぽうで、今回行なった Hack は、「 export const Prism = ((window) => { 」と「return window.Prism})({}); 」という、各1行のコードでオリジナルの JavaScript コードを挟むだけなので、雑ではあるけれども、極めて迅速に ES モジュール形式にすることができたので、妥協案のひとつとして、そんなに悪くない着地点ではないか、と思っている。 とはいえ、実はすごく間抜けなことをしているのかもしれないので、「いやいや、そんなことしなくても、ES モジュール形式にできる / ES モジュール形式で入手できるよ!」というツッコミがあれば、ぜひぜひ、SNS 上で言及してもらうか、コメント欄にてお知らせ頂きたい。 #
by developer-adjust
| 2024-02-17 16:44
| Web系一般
|
Comments(0)
2024年 01月 30日
C# プログラミングにおけるデータベースアクセスの話。 C# プログラミングにおいてデータベースの読み書きを行なうにはいろいろな方法があるが、ひとつのよく使われる方法として、"EntityFramework Core" という、いわゆる "O/R マッパー" に分類されるようなライブラリを利用する方法がある。 今回はふとした思いつきから、この EntityFramework Core を使い、データベースから百万件のレコードを C# のオブジェクトにマップしながら列挙、コンソールに各レコードをひたすら表示するだけのプログラムを作成・実行したときに、どれくらいメモリが消費されるのか、実際にそのようなプログラムを作って調べてみることにした。 データベースの選定と百万件のレコードの用意この際、データベースは PostgreSQL でも MySQL でも何でもよいのだが、とりあえず自分が慣れ親しんでいる Microsoft SQL Server を使うことにした。百万件の実レコードを含む実テーブルを作るのはだるかったので、下記の SQL を使って、On the fly で百万件のレコードを生成することにした。 SELECT TOP 1000000 Z.Id, CONVERT(nvarchar(36), NEWID()) AS Text1, CONVERT(nvarchar(36), NEWID()) AS Text2, CONVERT(nvarchar(36), NEWID()) AS Text3 FROM (SELECT ROW_NUMBER() OVER (ORDER BY X.object_id) AS Id FROM sys.all_objects X CROSS JOIN sys.all_objects Y) Z これを実行すると、64 bit 整数値の列 x 1 つと ("Id" 列)、36文字の文字列型の列 x 3 列 ("Text1", "Text2", "Text3") をもつレコードが百万件ほど返る。雑な計算で、正味のデータ量だけも 110 MB 以上になるデータ量だ。 C# プログラムの実装C# 側を実装しよう。まずはコンソールアプリケーションのプロジェクトをひとつ新規に作成する。 続けて、先の SQL 文の実行結果 (百万件のレコード) を、EntityFramework Core を使って C# のオブジェクトにマップしながら列挙したいので、そのマップ先の C# クラスを実装する。前述の SQL 文が返す結果に合せて、以下のように実装。 public class MyRecord { public Int64 Id { get; set; } public string? Text1 { get; set; } public string? Text2 { get; set; } public string? Text3 { get; set; } } あとは接続のためのデータベースコンテキストクラスを用意して (下記)、 using Microsoft.EntityFrameworkCore; public class MyDbContext : DbContext { public DbSet<MyRecord> MyRecords => Set<MyRecord>(); public MyDbContext(DbContextOptions<MyDbContext> options) : base(options) { } } これらを使用して Program.cs を以下のように実装した。 using Microsoft.EntityFrameworkCore; var option = new DbContextOptionsBuilder<MyDbContext>() .UseSqlServer("Server=(省略)") .Options; using var dbContext = new MyDbContext(option); var sql = @"(先に掲載の SQL)"; var records = dbContext.MyRecords.FromSqlRaw(sql).AsNoTracking(); foreach (var record in records) { Console.WriteLine($"{record.Id} - {record.Text1}, {record.Text2}, {record.Text3}"); } ここでちょっと注意点。 このプログラムにおいては、クエリの最後に「AsNoTracking()」メソッド呼び出しを追加しておく必要がある。そうしないと、列挙によって生成・マップされたオブジェクトが、その後に書き換えられたり削除用にマークされてたりしないかを "追跡 (Tracking)" できるようにするため、それらすべての生成されたオブジェクトへの参照をデータベースコンテキストが握ってしまうのだ。 今回のプログラムは、ひたすらクエリ結果のレコードを列挙しては、1レコードごとコンソールに表示しておしまい、である。なので、用が済んだレコードのオブジェクトは次々破棄したい。そのために、"NoTracking"、"このクエリの結果については追跡不要" とデータベースコンテキストに示すことで、データベースコンテキストが、マップ先として生成したオブジェクトを参照を掴んだままにしないように指示している。 実行ではいよいよ実行である。 自分は Windows 上で Visual Studio を使って実装していたので、このままデバッグ実行を開始。Visual Studio の診断ツールウィンドウでプロセスメモリの欄を眺めてみた。 そうして、プログラムを実行開始してから、百万件を列挙しきるまでの結果が以下のとおり。 グラフを見る限り、消費メモリはまったく増えていない。 プロセスメモリのグラフはほぼ平ら、というか、むしろ右肩下がりで減っている。実行時間が数十秒に及んだせいか、プログラムが暖まってきてガベージコレクション (GC) によって開始直後よりも消費メモリが減ったのかもしれない。上図でわかるように、百万件を列挙し終わる少し前の時点で 28 MB ほどであった。ちなみに開始直後は 32MB ほど。また、試しにデータベースに対してクエリを実行しなかった場合のプロセスメモリも見てみたが、その場合は 14 MB だった。 まとめO/R マッパーに分類されるであろう、C# プログラミングにおけるデータベースアクセス用ライブラリの定番 EntityFramework Core を使って、百万件のレコードをオブジェクトにマップしながら列挙しつつ、コンソール出力を行なってみた。 そのような C# コンソールプログラムの消費メモリの変化を観察してみたが、プログラム開始直後からメモリ消費量はほぼ変わらずのまま (というかむしろ減るくらい)、百万件のレコードを列挙しきることができた。 大量のレコードを EntityFramework Core を使ってオブジェクトにマップしながら読み取る場合でも、読み取るだけ、かつ、数レコードずつ列挙しながら処理するだけであれば、メモリを大量消費するようなことはないことが確認できた。 もちろん、前述のとおり、"AsNoTracking" を指定しなかった場合は、マップ先として生成したオブジェクトはガベージコレクタに回収されず (データベースコンテキストがその参照を保持しているため)、そうなるとすべてのレコードぶんのオブジェクトがプロセスメモリ内に存在したままとなり、相当な量のメモリが消費されることだろう。ToArray() や ToList() などを呼び出して、全レコードをメモリ上の配列や List<T> に収納しようとしても同様のことが起きる。あくまでも、foreach などを使って、IEnumerable<T> 越しにちょっとずつフェッチするぶんには、そのときに必要なメモリしか消費されない、という理解が必要である。
#
by developer-adjust
| 2024-01-30 23:46
| .NET
|
Comments(0)
2023年 12月 30日
C#/.NET プログラミングの話。 とある要件で、ASP.NET Core Web アプリケーションを起動し、HTTP リッスン状態になるまでを待機して、それからその ASP.NET Core Web アプリに HTTP 要求を送信して応答を得るという、そのようなコンソールアプリケーションを作成する需要が発生した。 もちろん、.NET の標準ライブラリにある System.Diagnostics.Process クラスを使えば実装できる。Process クラスに対し、起動したい実行可能ファイルパスやコマンドライン引数を指定するのに加えて、標準出力や標準エラー出力のリダイレクトを指定し、および Process クラスから発火される、標準出力や標準エラー出力への書き込み発生のイベントをハンドルすればよい。 ASP.NET Core Web アプリケーションであれば、HTTP リッスン状態になると「Now listening on: https://localhost:7241」といったテキストが標準出力への書き込まれるので (下図例)、これを検知すればよい。 具体的な実装例は以下のとおり。 using System.Diagnostics; // コマンドで起動する Process オブジェクトを準備。 // その際、標準出力のリダイレクトを指定。 var process = new Process { StartInfo = new ProcessStartInfo { FileName = "dotnet", Arguments = "run", WorkingDirectory = @"...", RedirectStandardOutput = true } }; // タスク完了ソースを用意 var tcs = new TaskCompletionSource(); // 標準出力発生のイベントをハンドルし、"Now listening on..." が出力 // されたら、タスクを完了させる process.OutputDataReceived += (_, e) => { var output = e.Data?.Trim() ?? string.Empty; if (output.StartsWith("Now listening on: http://localhost:")) { tcs.TrySetResult(); } }; // プロセスを開始 & 標準出力読み取りを開始 process.Start(); process.BeginOutputReadLine(); // "Now listening on..." の出力が検出されるまで非同期で待機 await tcs.Task; // ここまで来たら、起動した ASP.NET Core Web アプリは、 // HTTP リッスン開始している Console.WriteLine("DETECT START LISTENING!"); ちゃんと実装するとなると大変!ただ、上記実装例は、もう既にずいぶんと冗長な感じがすることに加え、標準エラー出力については実装していない。また、正常に動作しているぶんにはよいが、起動した ASP.NET Core Web アプリケーションプロセスが何らかのエラーを発生していた場合に、コンソールには標準出力や標準エラー出力の内容は表示されないため (出力をリダイレクトしてイベントで拾うようになってしまっているので)、何かあったときのトラブルシューティングが困難極める。タイムアウトの仕組みもないので、"Now listening on:..." の出力がされないと永遠に await し続けてしまい、エラーの発生に気づくことすらできなさそうだ。その他にも、このコンソールアプリケーション自体が例外を発生した時に、起動した ASP.NET Core Web アプリケーションプロセスをどう始末するのか、といったことも考慮が必要だろう。 それら考慮点をすべて網羅していくと、結構なコード量に膨れ上がることになる。 Process クラスの強化版的なライブラリ世の中には、こういった需要に応えるような、Process クラスの強化版的なライブラリが、無償利用可能な NuGet パッケージとして公開されていたりする。それらライブラリのうち、自分の中での推しのひとつは、ProcessX というライブラリだ (下記リンク先)。 非同期 Stream で標準出力を扱えたり、文字列に対する await 構文で外部コマンドを手軽に実行、完了を待機できたりと、なかなかに便利で興味深い機能が提供されている。 ただ、この ProcessX をもってしても、「指定の出力が現れるまで待機」や「トラブル発生時に、それまでの標準出力・標準エラー出力を見たい」といった今回の要件に伴う需要に応えるには、もう一息、実装を積み重ねる必要があった。 結局自作したそこで、今回要件のような、エッジケースに特化した、新たな "Process クラスの強化版" ライブラリを、もうひとつ新たに作成して OSS として世に送り出すことにした。 そうして作成したのが "XProcess" である (下記リンク先、MIT license)。 この XProcess は、前述のとおり今回の自分の要件に特化してあるため、先に挙げた「ASP.NET Core Web アプリケーションを起動し、HTTP リッスン状態になるまで待機」のコードは、以下にまで端的に書けるようになる。 using Toolbelt.Diagnostics; // 作業フォルダ指定して "dotnet run" で ASP.NET Core Web アプリ // プロジェクトを起動 using var process = XProcess.Start("dotnet", "run", workingDirectory: @"..."); // 指定した Predection が true を返す出力があるまで非同期で待機、 // 但し、最大 5 秒のタイムアウトを指定する。 var found = await process.WaitForOutputAsync(output => { output = output.Trim(); return output.StartsWith("Now listening on: http://localhost:"); }, millsecondsTimeout: 5000); // true が返されたら、目的の出力があったということ if (found) { Console.WriteLine("DETECT START LISTENING!"); } else { Console.WriteLine("SOME ERROR OCCURED!"); // Output プロパティを参照すれば、それまでの標準出力と標準エラー出力 // の内容を参照できる Console.WriteLine(process.Output); } まとめこのように、特定のユースケースに特化された XProcess は、ProcessX ほどの柔軟性と高度な機能は備えていないものの、その利用シナリオに適合した場合は、より簡潔にそのやりたいことを実現可能だ。 XProcess は、もっぱら自分の要件を満たすために作成した、特化されたライブラリであり、とはいえどうせ作ったのなら OSS & 無償利用可能なパッケージとして世の中に公開したら、同じ需要を持つ誰かの便利に役立つかもしれない、というノリでリリースしている。なので、"打倒、ProcessX!" みたいなつもりはないので悪しからず。 なので、XProcess に関しては、機能追加要望やバグ修正の Issue を上げられても、あまり積極的には対応しないかもしれない。その代わり、自分の新たな需要に対して機能不足やバグがあった場合は、速やかに改善・修正されるかもしれない、そんな性格のゆるい感じのライブラリだ。 #
by developer-adjust
| 2023-12-30 15:35
| .NET
|
Comments(0)
2023年 11月 30日
.NET プログラミングにおける、HTTP 要求送信に使われる、System.Net.Http 名前空間の HttpClient クラスの話。 HttpClient クラスには TimeSpan 型の "Timeout" というパブリックプロパティが設けられている。下記公式ドキュメントの説明に依れば「要求がタイムアウトするまで待機する期間を取得または設定」するプロパティであるとのこと。 しかし自分は、この HttpClient.Timeout プロパティによる "HTTP 要求のタイムアウト (時間切れ)" がどのように・何に対して機能するのかよくわかっていなかった。相手先サーバーとの TCP 接続が確立されるまでの時間なのだろうか? 接続が確立してから最初の応答バイト列が返ってくるまでの時間なのだろうか? ある程度応答が返ってきても、すべての応答が返却し終わる前に暫し応答が来なくなったらそれはタイムアウトになるのだろうか? 今までちゃんと理解しないで HttpClient を使ってきたので、今回、改めて調べてみることにした。なお、.NET ランタイムは GtHub 上でソースコードが公開されているので (下記リンク先)、ソースコードを確認すればこれら挙動について理解できることとも思う。 だが今回は実験方式で、 ASP.NET Core Minimal API による HTTP Web API サーバーと、その API サーバーに HttpClient を使って HTTP 要求を送信するコンソールアプリケーションとを C# でそれぞれ実装して、実際に実行して動作を観察してみた。 API サーバー側の C# ソースコードは以下。"/hello" の URL パスに HTTP GET 要求を送ると、"World!" の文字列が返却される。 var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapGet("/hello", () => "World!"); app.Run(); コンソールアプリケーション側の C# ソースコードは以下。前述の API サーバーの "/hello" へ HTTP GET 要求で文字列取得を試みるコードだ。以下では HttpClient の Timeout には 3 秒を設定してある。 using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(3), BaseAddress = new Uri("http://localhost:****") }; var message = await httpClient.GetStringAsync("hello"); Console.WriteLine($"message: [{message}]"); TCP 接続が確立できないとき接続先のホスト上でこの API サーバーが起動していない場合や、指定した IP アドレスではホストが TCP ネットワーク上に存在しない場合など、そもそも TCP 接続が確立しない場合を確認してみた。 この場合、HttpClient の Timeout プロパティに指定した時間が経過したのち、TaskCanceledException 例外が発生した。 Unhandled exception. System.Threading.Tasks.TaskCanceledException: The request was canceled due to the configured HttpClient.Timeout of 3 seconds elapsing. ---> System.TimeoutException: A task was canceled. ---> System.Threading.Tasks.TaskCanceledException: A task was canceled. なお、HttpClient からの HTTP 要求送信開始後 (GetStringAsync 呼び出し後)、Timeout プロパティに指定した時間が経過してしまう前に、API サーバー側プロセスを起動するなどして TCP 確立が成功する状態にすると、例外は発生せずに要求は成功した。 TCP 接続は成功するが、最初の応答がすぐに返らないときAPI サーバー側実装を以下のように変え、最初の応答を返すまで指定時間待機するようにしてみた。 app.MapGet("/hello", async () => { await Task.Delay(4000); // 4秒待つ return "World!"; }); 念のため curl コマンドで "curl -i http://localhost:***/hello" を実行し、応答ボディのみならず最初の応答ヘッダも含めて、Task.Delay() で指定した時間、何の応答も返らないことを確認する。 この状態でコンソールアプリ側を実行すると、HttpClient の Timeout プロパティに指定した時間が経過したのち、TaskCanceledException 例外が発生した。例外中のコールスタックが、そもそも TCP 接続が確立できなかった場合に比べて微妙に変わっている。 Unhandled exception. System.Threading.Tasks.TaskCanceledException: The request was canceled due to the configured HttpClient.Timeout of 3 seconds elapsing. ---> System.TimeoutException: The operation was canceled. ---> System.Threading.Tasks.TaskCanceledException: The operation was canceled. ---> System.IO.IOException: Unable to read data from the transport connection: The I/O operation has been aborted because of either a thread exit or an application request. TCP 接続は成功し、最初の応答もすぐ返るが、すべての応答に時間がかかるときさらに API サーバー側実装を以下のように変え、最初の応答はすぐに返るが、すべての応答を実施しきるまでに数百ミリ秒の待機を挟みながら、だらだら応答を続けるようにしてみる。 var utf8 = Encoding.UTF8; app.MapGet("/hello", async (HttpResponse res) => { foreach (var term in new[] { "lorem", "ipsum", "dolor", "sit", "amet" }) { await res.BodyWriter.WriteAsync(utf8.GetBytes(term + " ")); await res.BodyWriter.FlushAsync(); await Task.Delay(600); } }); こちらも念のため curl コマンドで動作を確認しておく。 この状態でコンソールアプリ側を実行すると、この場合も同じく、HttpClient の Timeout プロパティに指定した時間が経過したのち、TaskCanceledException 例外が発生した。 Unhandled exception. System.Threading.Tasks.TaskCanceledException: The request was canceled due to the configured HttpClient.Timeout of 3 seconds elapsing. ---> System.TimeoutException: The operation was canceled. ---> System.Threading.Tasks.TaskCanceledException: The operation was canceled. ---> System.IO.IOException: Unable to read data from the transport connection: The I/O operation has been aborted because of either a thread exit or an application request. まとめ以上の実験と観察結果を見ると、どうやら、HttpClient の Timeout プロパティに指定したタイムアウト時間というのは、「HTTP 要求を開始してから、すべての応答を受信し終わるまでの時間全体」を指しているらしい。 そもそも TCP 接続が確立できないままタイムアウト時間が経過した場合はもちろん、HTTP 応答が断片的に返って来続けている場合でも、Timeout プロパティに指定されたタイムアウト時間が経過してしまったら、TaskCanceledException 例外で中断されるようだ。また、タイムアウトが発生した場合は、発生理由の如何に依らず一律、TaskCanceledException 例外のスローという形で表現されることもわかった。 なお、今回は HttpClient の GetStringAsync メソッドに基づく実験だったが、GetStreamAsync で応答ストリームを入手してそのストリームから読み取りを繰り返すような場合は、もしかするともう少し違った挙動になるかもしれない。そうでないと、MJPEG をはじめ、HTTP 上でのストリーミング通信ができないのではないかと想像される。 今後要確認である。 #
by developer-adjust
| 2023-11-30 08:35
| .NET
|
Comments(2)
2023年 10月 28日
.NET Conf のセッションスケジュールここ数年、.NET は毎年1回、メジャーバージョンアップを行なっている。そして、そのバージョンアップのタイミングで、.NET Conf というオンラインイベントが開催されている。今年 2023 年は、11月14日から16日にかけて開催される予定なので、.NET 8 のリリースもおそらくこれと同時になるだろうと思われる。 さて今年 2023 年の .NET Conf、各セッションスケジュールは公式サイトの Agenda ページに公開されている (下記リンク先)。 これを見ると1セッションあたりの時間は 30 分程のものが多数を占め、その総セッション数は 90 を超え、2日目は 24 時間近くのセッションスケジュールとなっており、3日目に至っては同時間帯に2トラック並行で開催される。 オンラインカレンダーで扱いたいこういったセッションスケジュールを、自分のオンラインカレンダー (自分は主に Google カレンダーを使っている) にセッションスケジュールを登録しておけるといろいろと便利だ。Agenda ページの一覧で見るのに比べ、オンラインカレンダーのユーザーインターフェースでは、.NET Conf の全セッションを俯瞰でき、各セッションの時間帯もグラフィカルで直感的に把握しやすい。自分の興味のあるセッションだけを選び出すのも捗りそうだ。また、そうやってライブで視聴する予定のセッションスケジュールを自分のオンラインカレンダー上に構築できれば、「今日は何時からどれを視聴するのか」「次はどのセッションか」といった予定を、スマホやスマートウォッチでも手軽に把握できるようになる。 さらには、.NET Conf の Agenda ページでは、各セッションの開催時刻は PST (太平洋標準時間) での記載なのである。しかし自分は日本在住なので、Agenda ページを見るだけでは、日本の時刻 (JST) ではいったいこのセッションは何時に開催されるのかが、すぐにはわからないのである。PST から JST への時刻変換は、暗算が得意ならどうということはないのかもしれないが、あいにくと自分は暗算はかなり苦手なので、これは大変苦労する。その点、オンラインカレンダーの類いであれば、このようなタイムゾーンの換算はお手のものだ。 このように、セッションスケジュールを何かしらのオンラインカレンダーで扱えたらよいのであるが、しかし自分が把握できた限りでは、どうにも、この .NET Conf のセッションスケジュールを各種オンラインカレンダーで参照できる仕組みが用意されていないようなのである。 さりとて、これだけのセッション数を、自分のオンラインカレンダーに手作業でせっせと転記するのは、かなりの手間である。 さてどうしたものか。 スクレイピングして iCal で公開する Web API を自作...という背景事情があったので、今回、.NET Conf 公式サイトの Agenda ページの HTML をスクレイピングし、記載されているセッションスケジュールを iCal 形式に変換して返す、そのような Web API サイトをインターネット上に公開した (下記リンク先)。 すごく難しいことをしている訳ではないので、様々な言語・フレームワーク・処理系で同様のものを実装できると思うが、今回は自分が得意な C# と ASP.NET Core を用い、Minimal API 形式で実装した。実装した Web API アプリケーションは、Microsoft のパブリッククラウドサービスである Azure App Service に配置して公開した。気が早いかもしれないが、前述のとおり今日現在では公式リリース前の .NET 8 のリリース候補第2版を使って実装・稼働させている。 AngleSharp.NET Conf 公式サイトの Agenda ページを取得するのは普通に HttpClient クラスを使って実行している。 そうして取得した Agenda ページの HTML 文字列だが、これを走査して、各セッションスケジュールを読み取る処理は、その筋では有名な AngleSharp というライブラリ (MIT ライセンス) を使わせてもらった。 この AngleSharp を使って、ひとたび HTML 文字列を AngleSharp が提供するドキュメントオブジェクトへパースすると、Web ブラウザ上の JavaScript で querySelector とかを実行するのと同じように、C# コード内で、HTML の DOM 構造を CSS セレクタ構文など使いながらトラバースすることができるようになる。 iCal.NETこうして Agenda ページの HTML から各セッションスケジュールを表現した C# オブジェクトの集合に変換できたら、次はこれを iCal 形式のカレンダー情報文字列に変換する必要がある。さて、iCal 形式は究極的にはプレーンテキストであり、そこまで難解なフォーマットではないので、同形式の文字列を生成する処理を素にスクラッチで組むこともできなくはない。とはいえ、1行あたりの文字数に制約があるので折り返しを所定の仕様でマークアップする必要があるなど、細かいところで注意すべき点がいろいろでてくる。そこで今回は iCal.NET というライブラリ (MIT ライセンス) を使わせてもらうことにした。 iCal.NET を使えば、iCal 形式の諸々について気を払うこと無く、iCal.NET が提供するオブジェクトモデルでスケジュールを組み立てれば、それを最終的に iCal フォーマットの文字列にシリアル化することができる。 GitHub 上で公開以上を実装して、あとは ASP.NET Core の Minimal API の仕組みで URL エンドポイントで text/calendar の MIME-Type で公開して完了である。 この .NET Conf のセッションスケジュールを iCal 形式で公開するプログラム、ソースコードは GPL v3 ラインセンスで以下の GitHub リポジトリで公開してある。 実装でつまずいたところ開始する前に終わっているスケジュール実装にあたってはつまづいたところもあった。 まずひとつめは、Agenda ページでの時刻の記載で、次のような記載があった。 23:30 - 0:00 PST つまり、終了時刻が翌日になっているのである。しかし自分が最初に実装したときはこれを考慮しておらず、「開始 11/15 23:30」「終了 11/15 0:00」という、「開始する 23時間以上前に、終了してしまっている」というおかしなスケジュール情報で読み取ってしまっていた。とりあえず終了時刻が 0:00 なら日付を次の日付にする処理を足して乗り切った (ただし今考えるとこれはよくなくて、開始時刻よりも終了時刻が前の時刻を指していたら、終了時刻の日付を翌日にする、という処理にすべきであった)。 出力された iCal 上、終日イベントになっているもうひとつは、iCal.NET のオブジェクトモデルの扱い方である。iCal.NET が提供するクラスにスケジュール情報を詰め直す際、開始と終了時刻 (いずれも System.DateTime 型) について、最初は以下のように、iCal.NET の IDateTime 型である Start および End プロパティに、CalDateTime 型への明示的型変換で設定していた。 var icalEvent = new CalendarEvent { DtStart = (CalDateTime)session.StartTime, DtEnd = (CalDateTime)session.EndTime, ... }; これで大抵の場合は上手く動作した。ところが、開始時刻が 0:00 のイベントだけ、オンラインカレンダー上では終日イベントとして取り込まれてしまったのだ。これはどうやら iCal.NET の仕様に基づく動作のようで、時刻が 0:00 である日時情報は既定では「時刻部分がない」として扱われるようである。幸い、キャストによる明示的型変換ではく、日付だけではなく時刻付きであることを明示した時刻情報 CalDateTime オブジェクトを明示的にインスタンス化して設定することで、この問題は解決できた。 var icalEvent = new CalendarEvent { DtStart = new CalDateTime(session.StartTime) { HasTime = true }, DtEnd = new CalDateTime(session.EndTime) { HasTime = true }, ... }; タイムゾーンの略称から TimeZoneInfo を取得する.NET Conf の Agenda ページ上、セッションの開催時刻の記載には、以下のとおり、太平洋標準時間であることを示す "PST" の文字が付加されている。 8:00 - 9:00 PST この時刻表現を C# 上では最終的に UTC 日時に変換する必要があったので、そのために、"PST" というタイムゾーンの「略称」から、System.TimeZoneInfo を取得する必要がある。しかし TimeZoneInfo.FindSystemTimeZoneById("PST") を実行したところ、該当するタイムゾーンは存在しないという例外になってしまった。興味深いことに、ASP.NET Core Web アプリケーションプロジェクトではなく、素の .NET コンソールアプリケーションでこれを実行すると、ちゃんと期待どおりの TimeZoneInfo が返ってくる。プロジェクト形式の違いでどうしてこのような違いが生まれるのか理解できていないが、とりあえず今回は深追いするのはやめておいた。その代わりに、TimeZoneNames というライブラリを使わせてもらうことにした。 このライブラリは、TimeZoneInfo.GetSystemTimeZones() が返すタイムゾーンの ID から、そのタイムゾーンの略称を取得することができる。タイムゾーンの略称から ID を直接表引きできれば良かったのだが、どうもそのような機能提供はなかったようなので、自前のコードで自分でそのような変換辞書を構築することにした。 この辞書で "PST" をキーに値を取得すると "Pacific Standard Time" という正式なタイムゾーン ID を取得できる、という寸法だ。この変換辞書を介することで、"PST" というタイムゾーンの略称から TimeZoneInfo を取得し、最終的に UTC 日時への換算を実装することができた。 まぁ、ここまでこだわらずに、PST であることを前提にして固定の TimeZonInfo で換算するように実装してもよかったかもしれないが、ちょっとだけ汎用化してみた次第。 まとめ以上、これにて .NET Conf のセッションスケジュールを、好みのオンラインカレンダーで参照することができるようになった。 HTML のスクレイピングで実装したので、.NET Conf 公式サイトの Agenda ページの構造が変わってしまうとすぐに破綻してしまう仕組みなので、できれば公式のほうで iCal 形式での公開をサポートしてくれるに越したことはない。しかしそうはいっても、今時点でないものはないので、当面はこの Web API サイトが役に立ってくれるはずだ。
#
by developer-adjust
| 2023-10-28 15:18
| .NET
|
Comments(0)
|
ファン申請 |
||