<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Emiliano Montesdeoca</title><link>https://emimontesdeoca.github.io/ja/</link><description>Microsoft MVP in Developer Technologies. Cloud Solutions Team Lead. Speaker, blogger, and community advocate.</description><language>ja</language><managingEditor>emimontesdeoca@outlook.es (Emiliano Montesdeoca)</managingEditor><webMaster>emimontesdeoca@outlook.es (Emiliano Montesdeoca)</webMaster><lastBuildDate>Mon, 01 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://emimontesdeoca.github.io/ja/index.xml" rel="self" type="application/rss+xml"/><item><title>Blazor をゼロから: 第3章 — スケールするコンポーネント設計</title><link>https://emimontesdeoca.github.io/ja/posts/blazor-from-scratch-chapter-3/</link><pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/blazor-from-scratch-chapter-3/</guid><description>Blazor をゼロからシリーズ第3章。パラメーター、コンポジション、RenderFragment、そしてアプリが成長してもUIを整理しやすいフォルダー構成を解説します。</description><content:encoded>&lt;p>&lt;strong>Blazor をゼロから&lt;/strong> 第3章へようこそ。まだ &lt;a href="../blazor-from-scratch-chapter-2">第2章&lt;/a> を読んでいない場合は、先にそちらを読むとスムーズです。&lt;/p>
&lt;p>第2章ではアプリを動かしました。第3章ではそれを &lt;strong>保守しやすい構造&lt;/strong> にします。&lt;/p>
&lt;p>Blazor は、各ページが巨大な &lt;code>.razor&lt;/code> ファイルになると一気に複雑化します。そこで重要なのがコンポーネントです。再利用性、一貫性、責務の分離を実現できます。&lt;/p>
&lt;hr>
&lt;h2 id="blazor-コンポーネントとは何か">Blazor コンポーネントとは何か&lt;/h2>
&lt;p>コンポーネントは、次のことができる &lt;code>.razor&lt;/code> ファイルです。&lt;/p>
&lt;ul>
&lt;li>マークアップを描画する&lt;/li>
&lt;li>ローカル状態を持つ&lt;/li>
&lt;li>パラメーターで入力を受け取る&lt;/li>
&lt;li>親コンポーネントへイベントを通知する&lt;/li>
&lt;li>子コンテンツを描画する&lt;/li>
&lt;/ul>
&lt;p>実行時、Blazor は各コンポーネントを小さな状態マシンとして扱います。状態が変わると再描画し、DOM の差分だけを更新します。&lt;/p>
&lt;p>つまり、明確な入力と予測可能な動作を設計することが大切です。&lt;/p>
&lt;hr>
&lt;h2 id="ステップ1-役割が明確な小さなコンポーネントから始める">ステップ1: 役割が明確な小さなコンポーネントから始める&lt;/h2>
&lt;p>&lt;code>Components/Common/SectionHeader.razor&lt;/code> を作成します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>&amp;lt;header class=&amp;#34;section-header&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;h2&amp;gt;@Title&amp;lt;/h2&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @if (!string.IsNullOrWhiteSpace(Subtitle))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;@Subtitle&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/header&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter] public string Title { get; set; } = string.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter] public string? Subtitle { get; set; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>Components/Pages/Home.razor&lt;/code> で使用します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@page &amp;#34;/&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;PageTitle&amp;gt;Blazor をゼロから&amp;lt;/PageTitle&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;SectionHeader
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Title=&amp;#34;Blazor をゼロから&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Subtitle=&amp;#34;第3章はコンポーネント設計です。&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>小さな例ですが重要です。コンポーネントは、パラメーターを見るだけで意図が分かるべきです。&lt;/p>
&lt;hr>
&lt;h2 id="ステップ2-パラメーターで振る舞いを明示する">ステップ2: パラメーターで振る舞いを明示する&lt;/h2>
&lt;p>再利用可能なボタンを作ります。&lt;/p>
&lt;p>&lt;code>Components/Common/AppButton.razor&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>button &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;app-button @VariantCssClass&amp;#34;&lt;/span> &lt;span style="color:#f85149">@&lt;/span>onclick&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;OnClick&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>Text
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>button&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter] public string Text { get; set; } &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;Button&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter] public string Variant { get; set; } &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;primary&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter] public EventCallback OnClick { get; set; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private string VariantCssClass &lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span> Variant&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToLowerInvariant() &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;secondary&amp;#34;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;app-button--secondary&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;danger&amp;#34;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;app-button--danger&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _ &lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;app-button--primary&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>使用例:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private int _savedCount;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private void Save()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _savedCount++;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;AppButton Text=&amp;#34;Save&amp;#34; Variant=&amp;#34;primary&amp;#34; OnClick=&amp;#34;Save&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;p&amp;gt;保存回数: @_savedCount&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>大事なのは契約設計です。&lt;/p>
&lt;ul>
&lt;li>パラメーターは少なく明確に&lt;/li>
&lt;li>暗黙的な挙動より明示的な名前&lt;/li>
&lt;li>安全なデフォルト値&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="ステップ3-renderfragment-でレイアウトを合成する">ステップ3: &lt;code>RenderFragment&lt;/code> でレイアウトを合成する&lt;/h2>
&lt;p>&lt;code>RenderFragment&lt;/code> を使うと、親から子へ UI ブロックを渡せます。&lt;/p>
&lt;p>&lt;code>Components/Common/Card.razor&lt;/code> を作成:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>&amp;lt;article class=&amp;#34;card&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;header class=&amp;#34;card__header&amp;#34;&amp;gt;@Title&amp;lt;/header&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;section class=&amp;#34;card__body&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @ChildContent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/section&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/article&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter] public string Title { get; set; } = string.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter] public RenderFragment? ChildContent { get; set; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>使用例:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>&amp;lt;Card Title=&amp;#34;Roadmap&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;ul&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;li&amp;gt;コンポーネント&amp;lt;/li&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;li&amp;gt;Data binding&amp;lt;/li&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;li&amp;gt;Routing&amp;lt;/li&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/ul&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/Card&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>このパターンにより、ラッパーのマークアップを何度も書かずに一貫した構造を作れます。&lt;/p>
&lt;hr>
&lt;h2 id="ステップ4-巨大ページよりコンポジションを選ぶ">ステップ4: 巨大ページよりコンポジションを選ぶ&lt;/h2>
&lt;p>ページが大きくなったら責務で分割します。&lt;/p>
&lt;ul>
&lt;li>&lt;code>ProfileSummary&lt;/code>（プロフィール概要）&lt;/li>
&lt;li>&lt;code>ProfileStats&lt;/code>（メトリクス）&lt;/li>
&lt;li>&lt;code>ProfileActivityList&lt;/code>（最近のアクティビティ）&lt;/li>
&lt;/ul>
&lt;p>ページは実装の塊ではなく、オーケストレーション層になります。&lt;/p>
&lt;hr>
&lt;h2 id="ステップ5-マークアップとロジックのバランスを取る">ステップ5: マークアップとロジックのバランスを取る&lt;/h2>
&lt;p>小さいコンポーネントなら &lt;code>@code&lt;/code> のインラインで十分です。&lt;/p>
&lt;p>大きい場合は code-behind に分けます。&lt;/p>
&lt;ul>
&lt;li>&lt;code>UserCard.razor&lt;/code>&lt;/li>
&lt;li>&lt;code>UserCard.razor.cs&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>UI と C# ロジックの可読性がどちらも上がります。&lt;/p>
&lt;hr>
&lt;h2 id="ステップ6-実用的なフォルダー構成">ステップ6: 実用的なフォルダー構成&lt;/h2>
&lt;p>スケールしやすい構成の例:&lt;/p>
&lt;ul>
&lt;li>&lt;code>Components/Pages/&lt;/code> -&amp;gt; ルーティング対象ページ&lt;/li>
&lt;li>&lt;code>Components/Layout/&lt;/code> -&amp;gt; アプリ共通レイアウトとナビ&lt;/li>
&lt;li>&lt;code>Components/Common/&lt;/code> -&amp;gt; 汎用の共有コンポーネント&lt;/li>
&lt;li>&lt;code>Components/Features/&amp;lt;FeatureName&amp;gt;/&lt;/code> -&amp;gt; 機能別コンポーネント&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="初期の-blazor-プロジェクトでよくあるミス">初期の Blazor プロジェクトでよくあるミス&lt;/h2>
&lt;ul>
&lt;li>専用モデルを作らず、パラメーターを増やしすぎる&lt;/li>
&lt;li>ページコンポーネントに業務ロジックを入れすぎる&lt;/li>
&lt;li>数百行の &amp;ldquo;God Component&amp;rdquo; を作る&lt;/li>
&lt;li>マークアップを繰り返し、共通化しない&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="次回予告">次回予告&lt;/h2>
&lt;p>第4章では &lt;strong>データバインディングとイベント&lt;/strong> を扱います。&lt;code>@bind&lt;/code>、イベント処理、双方向バインディングのトレードオフ、予測可能な状態管理パターンを見ていきます。&lt;/p></content:encoded><category>Blazor</category><category>.NET</category><category>Web Development</category><category>Series</category></item><item><title>Blazor をゼロから: 第2章 — 最初の Blazor アプリ</title><link>https://emimontesdeoca.github.io/ja/posts/blazor-from-scratch-chapter-2/</link><pubDate>Thu, 14 May 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/blazor-from-scratch-chapter-2/</guid><description>Blazor をゼロからシリーズ第2章。最初のアプリを作成し、ローカルで実行し、重要なファイルを確認してプロジェクト構成を最初に理解します。</description><content:encoded>&lt;p>&lt;strong>Blazor をゼロから&lt;/strong>第2章へようこそ。&lt;a href="../blazor-from-scratch-chapter-1">第1章&lt;/a>をまだ読んでいない場合は、先に読むことをおすすめします。&lt;/p>
&lt;p>この章では、新しい Blazor アプリを作成し、ローカルで実行し、主要ファイルの役割を理解します。&lt;/p>
&lt;hr>
&lt;h2 id="ステップ1-プロジェクトを作成する">ステップ1: プロジェクトを作成する&lt;/h2>
&lt;p>ターミナルで実行:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet new blazor -o BlazorFromScratch
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cd BlazorFromScratch
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これで .NET 9 の Blazor Web App が既定の構成で作成されます。&lt;/p>
&lt;p>ツールが対話モデルを尋ねる場合は、まず &lt;strong>Interactive Server&lt;/strong> を選びましょう。&lt;/p>
&lt;hr>
&lt;h2 id="ステップ2-ローカルで実行する">ステップ2: ローカルで実行する&lt;/h2>
&lt;p>アプリを起動:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet watch
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ターミナルに表示された URL（通常は &lt;code>https://localhost:5xxx&lt;/code>）を開きます。Home、Counter、Weather ページが表示されれば成功です。&lt;/p>
&lt;p>ここまで動けば、開発環境は準備完了です。&lt;/p>
&lt;hr>
&lt;h2 id="ステップ3-プロジェクト構成を理解する">ステップ3: プロジェクト構成を理解する&lt;/h2>
&lt;p>最初に覚えるべき主なファイル/フォルダーは次のとおりです。&lt;/p>
&lt;ul>
&lt;li>&lt;code>Program.cs&lt;/code> — サービス登録と HTTP パイプライン構成。&lt;/li>
&lt;li>&lt;code>Components/App.razor&lt;/code> — アプリのルートコンポーネント。&lt;/li>
&lt;li>&lt;code>Components/Routes.razor&lt;/code> — ページのルーティング定義。&lt;/li>
&lt;li>&lt;code>Components/Pages/&lt;/code> — Home、Counter などのルート付きページ。&lt;/li>
&lt;li>&lt;code>Components/Layout/&lt;/code> — 共通レイアウト（&lt;code>MainLayout.razor&lt;/code>、メニュー）。&lt;/li>
&lt;li>&lt;code>wwwroot/&lt;/code> — 静的アセット（CSS、画像、favicon）。&lt;/li>
&lt;li>&lt;code>appsettings.json&lt;/code> — 設定値。&lt;/li>
&lt;/ul>
&lt;p>この段階では全ファイルを深く理解する必要はありません。UI の場所と起動設定の場所を把握できれば十分です。&lt;/p>
&lt;hr>
&lt;h2 id="ステップ4-最初の変更を加える">ステップ4: 最初の変更を加える&lt;/h2>
&lt;p>&lt;code>Components/Pages/Home.razor&lt;/code> を開き、内容を次に置き換えてください。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@page &amp;#34;/&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;PageTitle&amp;gt;Blazor をゼロから&amp;lt;/PageTitle&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h1&amp;gt;Blazor をゼロから&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;p&amp;gt;第2章のアプリがローカルで動作しています。&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>保存してブラウザーを更新します。&lt;code>dotnet watch&lt;/code> を使っていればすぐ反映されます。&lt;/p>
&lt;p>この小さな変更で、編集 -&amp;gt; ビルド -&amp;gt; 表示 の流れを確認できます。&lt;/p>
&lt;hr>
&lt;h2 id="ステップ5-programcs-を一度読む">ステップ5: Program.cs を一度読む&lt;/h2>
&lt;p>暗記は不要ですが、次の行は認識しておきましょう。&lt;/p>
&lt;ul>
&lt;li>&lt;code>AddRazorComponents()&lt;/code> Razor コンポーネントを有効化。&lt;/li>
&lt;li>&lt;code>AddInteractiveServerComponents()&lt;/code> サーバー対話型コンポーネントを有効化。&lt;/li>
&lt;li>&lt;code>MapRazorComponents&amp;lt;App&amp;gt;()&lt;/code> ルートコンポーネントをエンドポイントにマップ。&lt;/li>
&lt;/ul>
&lt;p>これらの行で、アプリの起動方法と有効なレンダリング機能が分かります。&lt;/p>
&lt;hr>
&lt;h2 id="よくある問題">よくある問題&lt;/h2>
&lt;p>うまくいかない場合、主な原因は次のとおりです。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>SDK が古い&lt;/strong>: &lt;code>dotnet --version&lt;/code> で .NET 9 を確認。&lt;/li>
&lt;li>&lt;strong>HTTPS 証明書の警告&lt;/strong>: &lt;code>dotnet dev-certs https --trust&lt;/code> を実行。&lt;/li>
&lt;li>&lt;strong>ポート競合&lt;/strong>: 古い &lt;code>dotnet&lt;/code> プロセスを停止して再実行。&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="次回">次回&lt;/h2>
&lt;p>第3章では &lt;strong>コンポーネント&lt;/strong> を深掘りします。パラメーター、コンポジション、再利用しやすい UI 構成を扱います。&lt;/p></content:encoded><category>Blazor</category><category>.NET</category><category>Web Development</category><category>Series</category></item><item><title>ゼロから始めるBlazor：第1章 — Blazorとは何か？</title><link>https://emimontesdeoca.github.io/ja/posts/blazor-from-scratch-chapter-1/</link><pubDate>Fri, 08 May 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/blazor-from-scratch-chapter-1/</guid><description>ゼロから始めるBlazorシリーズの第1章。Blazorが実際に何であるか、どこから来たのか、今日利用可能なさまざまなレンダリングモデル、そしてJavaScriptフレームワークとの比較を解説します。</description><content:encoded>&lt;p>&lt;strong>ゼロから始めるBlazor&lt;/strong>の第1章へようこそ。このシリーズの目的と対象読者を説明した&lt;a href="../blazor-from-scratch-intro">イントロ記事&lt;/a>を見逃した方は、まずそちらをお読みください — 短いです。&lt;/p>
&lt;p>この章では根本的な疑問に答えます：&lt;em>Blazorとは何か？&lt;/em> シンプルに聞こえますが、特に「Blazor」が年月を経てわずかに異なる意味を持つようになってきたため、答えにはいくつかの層があります。この記事の終わりには、Blazorが何であるか、.NETエコシステムにどのように適合するか、さまざまなレンダリングモデルが何であるか、そしてなぜJavaScriptフレームワークの代わりにそれを選ぶかもしれないかがわかるでしょう。&lt;/p>
&lt;hr>
&lt;h2 id="簡単な歴史">簡単な歴史&lt;/h2>
&lt;p>Blazorは2017年頃、MicrosoftのSteve Sandersonによる実験的プロジェクトとして始まりました。アイデアは挑発的でした：WebAssemblyを使ってブラウザでC#を実行し、JavaScriptの必要性を完全に排除するというものでした。それはプルーフ・オブ・コンセプトであり、名前は&lt;strong>Bla&lt;/strong>zerとRa&lt;strong>zor&lt;/strong> — Blazorが最終的にその上に構築されることになるテンプレートエンジン — の意図的な組み合わせでした。&lt;/p>
&lt;p>この実験はMicrosoftが真剣に受け止めるほどの熱狂を生み出しました。Blazorは2019年9月のASP.NET Core 3.0の一部として出荷され、最初は&lt;strong>Blazor Server&lt;/strong> — サーバーでC#コードを実行し、リアルタイムのSignalR接続を使用してブラウザにUI更新を送信するモデルとして登場しました。&lt;strong>Blazor WebAssembly&lt;/strong>はASP.NET Core 3.1の一部として2020年5月に続き、フレームワークに真のクライアントサイド実行をもたらしました。&lt;/p>
&lt;p>.NET 6と7は開発者体験を洗練させました。そして2023年11月にリリースされた.NET 8は、Microsoftが&lt;em>フルスタックウェブUI&lt;/em>と呼ぶものでレンダリングモデルを根本的に再考しました。静的サーバーサイドレンダリング、ストリーミングレンダリング、インタラクティブサーバー、インタラクティブWebAssembly、そして新しいAutoモードが、単一プロジェクトの同じ屋根の下に共存するようになりました。.NET 9はこの基盤の上に構築し、粗い部分を整えました。&lt;/p>
&lt;p>これが今日の現状です。&lt;/p>
&lt;hr>
&lt;h2 id="blazorが実際に何であるか">Blazorが実際に何であるか&lt;/h2>
&lt;p>本質的に、Blazorは**.NET向けのコンポーネントベースのUIフレームワーク**です。コンポーネントからUIを構築します — 状態を保持し、イベントに反応し、より大きな構造に合成できるC#とHTMLマークアップの自己完結型の部品です。ReactやVueで作業したことがある場合、そのメンタルモデルは馴染み深く感じるでしょう。重要な違いは、JavaScriptの代わりにC#を書くということです。&lt;/p>
&lt;p>Blazorのコンポーネントは&lt;code>.razor&lt;/code>ファイルに記述されます。&lt;code>.razor&lt;/code>ファイルは、ASP.NET MVCビューですでに知っているかもしれないRazor構文を使用してHTMLマークアップとC#コードを混在させます。シンプルなコンポーネントは次のようになります：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@page &amp;#34;/counter&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h1&amp;gt;カウンター&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;p&amp;gt;現在のカウント: @currentCount&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;button @onclick=&amp;#34;IncrementCount&amp;#34;&amp;gt;クリックしてください&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private int currentCount = 0;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private void IncrementCount()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentCount++;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>JavaScriptは見当たりません。&lt;code>@onclick&lt;/code>ディレクティブはボタンクリックをC#メソッドに結びつけます。&lt;code>@currentCount&lt;/code>式は現在の値をレンダリングします。状態が変更されると、BlazorはDOMで何を更新するかを把握します。&lt;/p>
&lt;p>これがコアです。それ以外のすべて — ルーティング、依存性注入、フォーム、HTTP呼び出し — このコンポーネントモデルの上に構築されています。&lt;/p>
&lt;hr>
&lt;h2 id="レンダリングモデル">レンダリングモデル&lt;/h2>
&lt;p>ここには多くの混乱があるので、正確に見ていきましょう。今日の「Blazor」は単一のデプロイメントモデルではなく、レンダリングモードのファミリーを指します。違いを理解することが重要なのは、パフォーマンス、インフラ要件、そしてコンポーネントができることとできないことに影響するためです。&lt;/p>
&lt;h3 id="静的ssr">静的SSR&lt;/h3>
&lt;p>最もシンプルなモードです。RazorコンポーネントがサーバーでHTMLにレンダリングされ、そのHTMLがブラウザに送信されます。永続的な接続もなく、WebAssemblyもなく、デフォルトではクライアントサイドのインタラクティビティもありません。これは本質的にRazor Pagesが行うことですが、コンポーネントモデルを使用しています。&lt;/p>
&lt;p>コンテンツの多いページ、ランディングページ、ライブインタラクティビティが不要なものに使用してください。&lt;/p>
&lt;h3 id="インタラクティブサーバー">インタラクティブサーバー&lt;/h3>
&lt;p>コンポーネントコードがサーバーで実行されます。ブラウザとサーバー間にSignalR WebSocket接続が確立されます。ボタンをクリックしたり入力フィールドに入力したりすると、イベントがWebSocketを通じてサーバーに送られ、C#が実行され、差分が計算され、DOMの更新がブラウザに送り返されます。&lt;/p>
&lt;p>&lt;strong>利点：&lt;/strong> サーバーリソース（データベース、ファイルシステム、シークレット）への完全なアクセス。高速な初期ロード。小さなダウンロードサイズ。WebAssemblyサポートなしのブラウザでも動作します。&lt;/p>
&lt;p>&lt;strong>欠点：&lt;/strong> すべてのユーザーインタラクションがサーバーへの往復を必要とし、レイテンシが増加します。サーバーリソースはアクティブな接続数に応じてスケールします。接続が切断されるとアプリが劣化します。&lt;/p>
&lt;p>基幹業務アプリ、内部ツール、オフライン機能よりもサーバーサイドデータアクセスが重要なアプリに使用してください。&lt;/p>
&lt;h3 id="インタラクティブwebassembly">インタラクティブWebAssembly&lt;/h3>
&lt;p>.NETランタイムがブラウザにダウンロードされ、アプリが完全にクライアントで実行されます。初期ダウンロード後、アプリは完全にオフラインで動作し、すべてのインタラクションが即座です — UIロジックのためのサーバー往復がありません。&lt;/p>
&lt;p>&lt;strong>利点：&lt;/strong> 真のクライアントサイド実行。オフライン動作。アプリロード後のサーバー負荷軽減。&lt;/p>
&lt;p>&lt;strong>欠点：&lt;/strong> より大きな初期ダウンロード（.NETランタイム + アプリ）。より遅い最初のロード。サーバーサイドデータにアクセスするにはAPIが必要です。&lt;/p>
&lt;p>オフライン機能を持つアプリ、即時の応答性が必要なツール、UIにサーバー処理が不要なシナリオに使用してください。&lt;/p>
&lt;h3 id="autoモード">Autoモード&lt;/h3>
&lt;p>.NET 8で導入されました。アプリはインタラクティブサーバーモードで開始します（高速な初期ロード、WebAssemblyのダウンロードを待たない）。WebAssemblyファイルがバックグラウンドでダウンロードされると、以降の訪問は自動的にWebAssemblyモードに切り替わります。&lt;/p>
&lt;p>これにより、サーバーモードの高速な最初のロードと、最終的にはWebAssemblyの完全なクライアントサイド実行の両方が得られます。理解するのが最も複雑なモデルですが、多くのアプリにとって実用的なデフォルトです。&lt;/p>
&lt;h3 id="同じアプリでモードを混在させる">同じアプリでモードを混在させる&lt;/h3>
&lt;p>.NET 8以降では、アプリ全体に対して単一のレンダリングモードに縛られません。マーケティングランディングページは静的SSRにできます；認証されたダッシュボードはインタラクティブサーバーにできます；データ視覚化ウィジェットはインタラクティブWebAssemblyにできます。フレームワークが遷移を処理します。&lt;/p>
&lt;p>このシリーズでは、インタラクティブサーバーから始めます。なぜなら、最も簡単に動作させることができ、最も直感的なメンタルモデルを持っているからです。他のモードは進めながら探求していきます。&lt;/p>
&lt;hr>
&lt;h2 id="blazorとjavascriptフレームワークの比較">BlazorとJavaScriptフレームワークの比較&lt;/h2>
&lt;p>React、Angular、またはVueで開発したことがある場合、おそらくこれが気になる比較でしょう。&lt;/p>
&lt;p>&lt;strong>類似点は本物です。&lt;/strong> BlazorのコンポーネントモデルはReactのそれと意図的に似ています。props（Blazorではパラメータ）、ローカル状態（&lt;code>@code&lt;/code>ブロックのフィールド）、ライフサイクルフック、そして同じ一方向データフロー / イベントアップの通信パターンがあります。Reactを知っていれば、数時間以内に慣れるでしょう。&lt;/p>
&lt;p>&lt;strong>重要な違いは言語です。&lt;/strong> BlazorではC#を書きます。ビジネスロジック、バリデーションルール、データモデルをBlazorフロントエンドとASP.NET Coreバックエンドの間で共有できます。すでにC#にある&lt;code>User&lt;/code>クラスをTypeScriptで重複して作成する必要はありません。&lt;/p>
&lt;p>&lt;strong>エコシステムのギャップは現実ですが縮まっています。&lt;/strong> npmエコシステムは巨大です。UIコンポーネントのNuGetエコシステムは小さいですが、大幅に成長しています。特定のチャートライブラリやドラッグ＆ドロップウィジェットが必要な場合、JavaScriptにはまだより多くのオプションがあります。しかし、ほとんどの基幹業務アプリケーションにとって、.NETの世界で利用可能なものは十分以上です。&lt;/p>
&lt;p>&lt;strong>JavaScriptの相互運用性は必要な時のために存在します。&lt;/strong> BlazorではC#からJavaScriptを、JavaScriptからC#を呼び出すことができます。.NETラッパーがないブラウザAPIや既存のJSライブラリを使用したい場合、相互運用が利用できます。レイヤーを追加しますが、苦痛ではありません。&lt;/p>
&lt;p>&lt;strong>正直な答え：&lt;/strong> チーム全体がJavaScript開発者であり、SEO、パフォーマンス、npmエコシステムが重要な公開製品を構築している場合はJavaScriptにとどまってください。チームが.NET開発者であり、内部または基幹業務アプリケーションを構築しているか、フロントエンドとバックエンド間のコード共有が重要な場合は、Blazorは説得力のある選択です。&lt;/p>
&lt;hr>
&lt;h2 id="一緒に進めるために必要なもの">一緒に進めるために必要なもの&lt;/h2>
&lt;p>このシリーズの残りのために必要なもの：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>.NET 9 SDK&lt;/strong> — &lt;a href="https://dotnet.microsoft.com/download">dot.net&lt;/a>からダウンロード&lt;/li>
&lt;li>&lt;strong>IDE&lt;/strong> — Visual Studio 2022（Communityエディションは無料）、C# Dev Kit拡張機能付きのVS Code、またはJetBrains Rider&lt;/li>
&lt;li>C#の基本的な知識 — クラス、プロパティ、インターフェース、async/await&lt;/li>
&lt;/ul>
&lt;p>以上です。Node、npm、webpackは不要です。&lt;/p>
&lt;hr>
&lt;h2 id="次回">次回&lt;/h2>
&lt;p>第2章では、最初のBlazorアプリをスキャフォールドし、プロジェクト構造を確認し、ローカルで実行できることを確認します。最後には動作するアプリと各ファイルが何をしているかの明確な全体像を持てるようになります。&lt;/p>
&lt;p>そこでお会いしましょう。&lt;/p></content:encoded><category>Blazor</category><category>.NET</category><category>Web Development</category><category>Series</category></item><item><title>Blazorをゼロから始める: 新シリーズ</title><link>https://emimontesdeoca.github.io/ja/posts/blazor-from-scratch-intro/</link><pubDate>Mon, 04 May 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/blazor-from-scratch-intro/</guid><description>Blazorの基礎から丁寧に積み上げる長期シリーズのスタート — 近道なし、ごまかしなし、明確な解説と実際のコードだけ。</description><content:encoded>&lt;p>しばらくの間、Blazorについて書いてきました — コンポーネントのライフサイクル、スコープ付きCSS、インタラクティビティモデル、認証。これらの記事はそれぞれ有用でしたが、いつも「土台」が欠けていると感じていました。Blazorとは何か、なぜ存在するのか、.NETエコシステムのどこに位置するのかをすでに知っていることを前提にしていたのです。そうでない人も多く、それはまったく構いません。&lt;/p>
&lt;p>そこで新しいことを始めます：&lt;strong>Blazorをゼロから&lt;/strong>。しっかりとしたシリーズで、基礎から構築し、自分が作っているものを本当に理解したい開発者を対象としています — 動くまでコピー＆ペーストするだけでなく。&lt;/p>
&lt;h2 id="このシリーズの対象者">このシリーズの対象者&lt;/h2>
&lt;p>このシリーズは、以下に当てはまる方のためのものです：&lt;/p>
&lt;ul>
&lt;li>Blazorのことは聞いたことがあるが、深く掘り下げる時間や適切な出発点がなかった.NET開発者&lt;/li>
&lt;li>Blazorを試して動かしたことがあるが、なぜ動くのかを推測している感覚がある方&lt;/li>
&lt;li>JavaScript/React/Angular の世界から来て、Microsoftのモダンフロントエンドへの回答を理解したい方&lt;/li>
&lt;li>散らばったドキュメントやブログ記事ではなく、一つのまとまったリソースが欲しい方&lt;/li>
&lt;/ul>
&lt;p>シニア開発者である必要はありません。ただし、C#の基礎 — クラス、インターフェース、async/await — に慣れている必要があります。ASP.NET CoreでシンプルなCRUD APIが書ければ、準備完了です。&lt;/p>
&lt;h2 id="カバーする内容">カバーする内容&lt;/h2>
&lt;p>以下に計画している大まかなロードマップを示します。必要に応じていくつかのトピックは複数の記事に分かれることがあります：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Blazorとは？&lt;/strong> — ホスティングモデル、歴史、従来のWeb開発との比較&lt;/li>
&lt;li>&lt;strong>はじめてのBlazorアプリ&lt;/strong> — スキャフォールディング、プロジェクト構造、ローカル実行&lt;/li>
&lt;li>&lt;strong>コンポーネント&lt;/strong> — すべてのBlazor UIの基本ブロック&lt;/li>
&lt;li>&lt;strong>データバインディングとイベント&lt;/strong> — UIをリアクティブにする&lt;/li>
&lt;li>&lt;strong>コンポーネント間の通信&lt;/strong> — パラメーター、EventCallbacks、カスケード値&lt;/li>
&lt;li>&lt;strong>ルーティングとナビゲーション&lt;/strong> — BlazorがURLとページ遷移を処理する方法&lt;/li>
&lt;li>&lt;strong>依存性の注入&lt;/strong> — サービス、スコープ、BlazorのDIコンテナ&lt;/li>
&lt;li>&lt;strong>フォームとバリデーション&lt;/strong> — EditForm、DataAnnotations、カスタムバリデーター&lt;/li>
&lt;li>&lt;strong>HTTPと外部データ&lt;/strong> — BlazorアプリからAPIを呼び出す&lt;/li>
&lt;li>&lt;strong>認証と認可&lt;/strong> — アプリを正しく保護する&lt;/li>
&lt;li>&lt;strong>JavaScriptインターオップ&lt;/strong> — ブラウザに直接アクセスが必要な場合&lt;/li>
&lt;li>&lt;strong>パフォーマンスと最適化&lt;/strong> — 仮想化、遅延読み込み、レンダリング戦略&lt;/li>
&lt;li>&lt;strong>Blazorコンポーネントのテスト&lt;/strong> — bUnitとしっかりしたテストの形&lt;/li>
&lt;li>&lt;strong>デプロイ&lt;/strong> — Azure、IIS、静的ホストへの公開&lt;/li>
&lt;/ol>
&lt;p>このリストは進化します。いくつかのトピックは複数の記事に分割され、他のものはまとめられるかもしれません。シリーズの進捗に合わせてこの記事を更新し、公開時に各エントリへのリンクを追加します。&lt;/p>
&lt;h2 id="なぜシリーズをなぜ今">なぜシリーズを、なぜ今&lt;/h2>
&lt;p>Blazorはかなり成熟しました。.NET 8と9で、レンダリングモデルが大幅に刷新されました — 静的SSR、ストリーミングレンダリング、インタラクティブサーバー、インタラクティブWebAssembly、オートモードが同じ屋根の下に共存しています。本当に興味深く有能なフレームワークですが、複雑さの増大により、入門体験が戸惑いやすくなっています。&lt;/p>
&lt;p>あなたがいる場所に合わせて出発し、全体を体系的に案内するリソースを作りたいと思っています。公式ドキュメントの代替ではありません — あれは良いものですし、読むべきです — が、「何」の背後にある「なぜ」を説明するコンパニオンとなります。&lt;/p>
&lt;h2 id="シリーズのフォロー方法">シリーズのフォロー方法&lt;/h2>
&lt;p>シリーズの各記事は単独で読めるほど自己完結していますが、互いに積み上げ式にもなっています。最初から始める場合は順番通りに読むことをおすすめします。特定のギャップを埋めるために参加する場合も大丈夫です — 重要な場合は前提となる記事へのリンクを貼ります。&lt;/p>
&lt;p>各記事のコードはGitHubで公開します。進捗に合わせてリンクを共有します。&lt;/p>
&lt;p>次の記事でお会いしましょう。&lt;/p></content:encoded><category>Blazor</category><category>.NET</category><category>Web Development</category><category>Series</category></item><item><title>C# ユニオン型: 差別的なユニオンがついに登場</title><link>https://emimontesdeoca.github.io/ja/posts/csharp-union-types/</link><pubDate>Wed, 01 Apr 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/csharp-union-types/</guid><description>今後の C# 差別ユニオン型について詳しく説明します。その種類、仕組み、ドメインのモデル化方法が変わる理由について説明します。</description><content:encoded>&lt;p>かなりの期間 C# を書いてきた場合、「いくつかあるもののうちの 1 つ」となるものをモデル化しようとしたときに壁にぶつかった可能性が高くなります。おそらく、成功値またはエラーのいずれかを返すメソッドが必要でした。おそらく、クレジット カード、銀行振込、デジタル ウォレットを処理する支払いシステムを構築していたのでしょう。それぞれにまったく異なるデータが含まれていました。あるいは、F# や Rust を見て、「なぜ C# でそれができないの?」と思ったかもしれません。&lt;/p>
&lt;p>待ち時間はもう終わりに近づいています。 &lt;strong>差別的な共用体が C# に登場します。&lt;/strong>&lt;/p>
&lt;p>これは長年にわたり最も要望の多かった言語機能の 1 つであり、コミュニティでの議論は 2017 年以前にまで遡ります。 C# 言語設計チームは、網羅的なパターン マッチングを使用して閉じた型の階層を定義するための &lt;code>union&lt;/code> キーワードを導入する提案に取り組んでいます。この投稿では、識別共用体とは何か、それがそれほど重要である理由、これまでどのように偽装してきたのか、そして提案された構文が実際にどのようなものであるのかを、それぞれの実際のコード例とともに説明したいと思います。&lt;/p>
&lt;p>本題に入る前に簡単な注意事項: この記事の執筆時点では、ユニオン型機能はまだ提案とプレビューの段階にあります。ここで説明する構文と動作は、公開されている最新の設計ドキュメントと C# 言語設計チームからのディスカッションに基づいています。最終リリースまでに状況が変わる可能性があります。何が確認されているのか、何がまだ議論中であるのかを明確にさせていただきます。&lt;/p>
&lt;h2 id="差別的な組合とは何ですか">差別的な組合とは何ですか?&lt;/h2>
&lt;p>本質的に、識別共用体 (「タグ付き共用体」または「合計型」と呼ばれることもあります) は、可能な値の固定セットの 1 つを保持できる型であり、各バリアントは異なるデータを保持できます。 「識別された」部分は、ランタイムがそれがどのバリアントであるかを常に知っていることを意味します。それを識別するタグがあります。&lt;/p>
&lt;p>これは &lt;code>enum&lt;/code> のようなものですが、各メンバーが独自のデータ ペイロードを運ぶことができます。&lt;/p>
&lt;p>他の言語を使用したことがある場合は、おそらくこの概念を以前に見たことがあるでしょう。&lt;/p>
&lt;p>&lt;strong>F#&lt;/strong> は初日から差別的な組合を持っていました:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fsharp" data-lang="fsharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">type&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Shape&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">|&lt;/span> Circle &lt;span style="color:#ff7b72">of&lt;/span> radius&lt;span style="color:#ff7b72;font-weight:bold">:&lt;/span> &lt;span style="color:#ff7b72">float&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">|&lt;/span> Rectangle &lt;span style="color:#ff7b72">of&lt;/span> width&lt;span style="color:#ff7b72;font-weight:bold">:&lt;/span> &lt;span style="color:#ff7b72">float&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">*&lt;/span> height&lt;span style="color:#ff7b72;font-weight:bold">:&lt;/span> &lt;span style="color:#ff7b72">float&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">|&lt;/span> Triangle &lt;span style="color:#ff7b72">of&lt;/span> &lt;span style="color:#ff7b72">base&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">:&lt;/span> &lt;span style="color:#ff7b72">float&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">*&lt;/span> height&lt;span style="color:#ff7b72;font-weight:bold">:&lt;/span> &lt;span style="color:#ff7b72">float&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Rust&lt;/strong> は同じアイデアに &lt;code>enum&lt;/code> を使用します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-rust" data-lang="rust">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">enum&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Shape&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>{&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>Circle&lt;span style="color:#6e7681"> &lt;/span>{&lt;span style="color:#6e7681"> &lt;/span>radius: &lt;span style="color:#ff7b72">f64&lt;/span> },&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>Rectangle&lt;span style="color:#6e7681"> &lt;/span>{&lt;span style="color:#6e7681"> &lt;/span>width: &lt;span style="color:#ff7b72">f64&lt;/span>,&lt;span style="color:#6e7681"> &lt;/span>height: &lt;span style="color:#ff7b72">f64&lt;/span> },&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>Triangle&lt;span style="color:#6e7681"> &lt;/span>{&lt;span style="color:#6e7681"> &lt;/span>base: &lt;span style="color:#ff7b72">f64&lt;/span>,&lt;span style="color:#6e7681"> &lt;/span>height: &lt;span style="color:#ff7b72">f64&lt;/span> },&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>}&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>TypeScript&lt;/strong> は、タグ付きユニオンを使用して同様のことを実現します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-typescript" data-lang="typescript">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">type&lt;/span> Shape &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">|&lt;/span> { kind&lt;span style="color:#ff7b72;font-weight:bold">:&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;circle&amp;#34;&lt;/span>; radius: &lt;span style="color:#ff7b72">number&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">|&lt;/span> { kind&lt;span style="color:#ff7b72;font-weight:bold">:&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;rectangle&amp;#34;&lt;/span>; width: &lt;span style="color:#ff7b72">number&lt;/span>; height: &lt;span style="color:#ff7b72">number&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">|&lt;/span> { kind&lt;span style="color:#ff7b72;font-weight:bold">:&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;triangle&amp;#34;&lt;/span>; base: &lt;span style="color:#ff7b72">number&lt;/span>; height: &lt;span style="color:#ff7b72">number&lt;/span> };
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これらすべてのケースにおいて、コンパイラは考えられるすべてのバリアントを認識しており、それらをすべて処理するように強制できます。それが超能力です。&lt;strong>徹底的なチェック&lt;/strong>です。新しいバリアントを追加すると、コンパイラは、それを処理するのを忘れた場所をすべて通知します。&lt;/p>
&lt;p>C# にはこれを表現する最上級の方法がありませんでした。今まで。&lt;/p>
&lt;h2 id="今日の組合のシミュレーション方法">今日の組合のシミュレーション方法&lt;/h2>
&lt;p>長年にわたり、C# コミュニティはいくつかの回避策を考案してきましたが、それぞれに独自のトレードオフがあります。最も一般的なアプローチについて説明します。&lt;/p>
&lt;h3 id="継承のある抽象レコード">継承のある抽象レコード&lt;/h3>
&lt;p>最新の C# における最も慣用的な回避策は、シールされた派生型を持つ抽象レコードを使用することです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">abstract&lt;/span> &lt;span style="color:#ff7b72">record&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Shape&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">sealed&lt;/span> &lt;span style="color:#ff7b72">record&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Circle&lt;/span>(&lt;span style="color:#ff7b72">double&lt;/span> Radius) : Shape;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">sealed&lt;/span> &lt;span style="color:#ff7b72">record&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Rectangle&lt;/span>(&lt;span style="color:#ff7b72">double&lt;/span> Width, &lt;span style="color:#ff7b72">double&lt;/span> Height) : Shape;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">sealed&lt;/span> &lt;span style="color:#ff7b72">record&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Triangle&lt;/span>(&lt;span style="color:#ff7b72">double&lt;/span> Base, &lt;span style="color:#ff7b72">double&lt;/span> Height) : Shape;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> Shape() { } &lt;span style="color:#8b949e;font-style:italic">// Prevent external inheritance&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これはかなりうまく機能します。不変性と値の同等性が得られ、パターン マッチングを使用できます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">double&lt;/span> Area(Shape shape) =&amp;gt; shape &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Shape.Circle c =&amp;gt; Math.PI * c.Radius * c.Radius,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Shape.Rectangle r =&amp;gt; r.Width * r.Height,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Shape.Triangle t =&amp;gt; &lt;span style="color:#a5d6ff">0.5&lt;/span> * t.Base * t.Height,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _ =&amp;gt; &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> InvalidOperationException(&lt;span style="color:#a5d6ff">&amp;#34;Unknown shape&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```しかし、重大な欠点もあります。コンパイラーは階層が閉じられていることを認識しないため、常に&lt;/span> &lt;span style="color:#f85149">`&lt;/span>_&lt;span style="color:#f85149">`&lt;/span> &lt;span style="color:#f85149">破棄アームが必要です。そうしないと警告が表示されます。新しいバリアントを追加した場合、コンパイラはそれを処理するのを忘れたすべての場所を通知しません。破棄はそれを黙って飲み込みます。それは完全に目的を裏切ります。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">###&lt;/span> OneOf &lt;span style="color:#f85149">ライブラリ&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">もう&lt;/span> &lt;span style="color:#a5d6ff">1&lt;/span> &lt;span style="color:#f85149">つの一般的なアプローチは、&lt;/span>[OneOf](https:&lt;span style="color:#8b949e;font-style:italic">//github.com/mcintyre321/OneOf) NuGet パッケージです。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```&lt;/span>csharp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> OneOf&amp;lt;Success&amp;lt;Order&amp;gt;, NotFound, ValidationError&amp;gt; ProcessOrder(OrderRequest request)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// ...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Usage&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> result = ProcessOrder(request);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>result.Switch(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> success =&amp;gt; Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;Order {success.Value.Id} processed&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> notFound =&amp;gt; Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;Order not found&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> error =&amp;gt; Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;Validation failed: {error.Message}&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>OneOf は、ジェネリック型パラメーターを通じてコンパイル時に徹底的なチェックを提供します。これは優れています。しかし、位置一致 (最初のタイプ、2 番目のタイプなど) に依存しており、汎用署名はすぐに扱いにくくなり、言語のパターン マッチングと統合されていません。これは賢いハックですが、それでもハックです。&lt;/p>
&lt;h3 id="手動列挙--データ-パターン">手動列挙 + データ パターン&lt;/h3>
&lt;p>一部の開発者は、enum タグとデータ コンテナーを使用する古典的なルートを選択します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">enum&lt;/span> PaymentType { CreditCard, BankTransfer, DigitalWallet }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Payment&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> PaymentType Type { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">init&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> CreditCardInfo? CreditCard { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">init&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> BankTransferInfo? BankTransfer { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">init&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> DigitalWalletInfo? DigitalWallet { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">init&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これは壊れやすいです。 &lt;code>Type&lt;/code> を &lt;code>CreditCard&lt;/code> に設定すること以外は、&lt;code>BankTransfer&lt;/code> プロパティを設定することを妨げるものはありません。コンパイラは役に立たず、実行時エラーやあらゆる場所で null チェックが発生することになります。これは型モデリングに対する「文字列型指定」アプローチであり、拡張性がありません。&lt;/p>
&lt;p>これらのアプローチはすべて、基本的な問題を共有しています。&lt;strong>言語を操作するのではなく、言語と戦っている&lt;/strong>。コンパイラーは、可能性の閉じたセットについて推論することができないため、弁別共用体の最も貴重な特性である徹底的なチェックが失われます。&lt;/p>
&lt;h2 id="c-提案-union-キーワード">C# 提案: &lt;code>union&lt;/code> キーワード&lt;/h2>
&lt;p>C# 言語チームの提案では、専用の &lt;code>union&lt;/code> キーワードを導入しています。これは、名前付きメンバーの閉じたセットを定義し、それぞれがオプションでデータを保持します。提案されている基本的な構文は次のとおりです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>union Shape
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Circle(&lt;span style="color:#ff7b72">double&lt;/span> Radius),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Rectangle(&lt;span style="color:#ff7b72">double&lt;/span> Width, &lt;span style="color:#ff7b72">double&lt;/span> Height),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Triangle(&lt;span style="color:#ff7b72">double&lt;/span> Base, &lt;span style="color:#ff7b72">double&lt;/span> Height)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>それだけです。すっきりしていて簡潔で、すぐに読めます。共用体の内部の各メンバーは、独自のデータを持つ個別のバリアントを定義します。コンパイラは、&lt;code>Shape&lt;/code> がこれら 3 つのうちの 1 つにしかなり得ないことを知っています。&lt;/p>
&lt;p>内部では、コンパイラーは密閉型階層を生成します。これは、抽象レコードを使用して手動で記述するものと似ていますが、コンパイラーは型の閉じた性質を完全に認識します。これは、コンパイラがパターン マッチングの網羅性を強制できることを意味し、これが主な利点です。&lt;/p>
&lt;h3 id="価値のみのメンバー">価値のみのメンバー&lt;/h3>
&lt;p>組合員はデータを持ち歩く必要がありません。データ保持メンバーと単純な値メンバーを混在させることができます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>union Option&amp;lt;T&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Some(T Value),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> None
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これは、関数型プログラマが C# で長年求めてきた古典的な &lt;code>Option&lt;/code>/&lt;code>Maybe&lt;/code> 型です。 &lt;code>None&lt;/code> にはデータは含まれません。これは単なるタグです。&lt;/p>
&lt;h3 id="ジェネリックユニオン">ジェネリックユニオン&lt;/h3>
&lt;p>上記の &lt;code>Option&amp;lt;T&amp;gt;&lt;/code> の例からわかるように、共用体はジェネリックスをサポートしています。より複雑な例を次に示します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>union Result&amp;lt;T, E&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Ok(T Value),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Error(E Err)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これにより、予期される失敗ケースの例外に依存しない、エラー処理のスタイル全体が開かれます。これは、Rust および関数型言語で長年にわたって標準的に行われてきたことです。&lt;/p>
&lt;h3 id="メソッドとの結合">メソッドとの結合&lt;/h3>
&lt;p>この提案では、他の型と同様に、共用体がメソッド、計算されたプロパティを持つこと、インターフェイスを実装することも許可されています。```csharp
union Shape
{
Circle(double Radius),
Rectangle(double Width, double Height),
Triangle(double Base, double Height);&lt;/p>
&lt;pre>&lt;code>public double Area =&amp;gt; this switch
{
Circle(var r) =&amp;gt; Math.PI * r * r,
Rectangle(var w, var h) =&amp;gt; w * h,
Triangle(var b, var h) =&amp;gt; 0.5 * b * h
};
public double Perimeter =&amp;gt; this switch
{
Circle(var r) =&amp;gt; 2 * Math.PI * r,
Rectangle(var w, var h) =&amp;gt; 2 * (w + h),
Triangle(var b, var h) =&amp;gt; b + h + Math.Sqrt(b * b + h * h)
};
&lt;/code>&lt;/pre>
&lt;p>}&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-mysql" data-lang="mysql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>Area&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">および&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>Perimeter&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">内の&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>switch&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">式がデフォルトのアームを必要としないことに注目してください。コンパイラーは、共用体が完全であることを認識しています。バリアントは&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">3&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">つだけあり、&lt;/span>&lt;span style="color:#a5d6ff">3&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">つすべてが処理されます。後で&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">4&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">番目のバリアントを追加すると、コンパイラはそれを処理しないすべての&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>switch&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">にフラグを立てます。&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#8b949e;font-style:italic">## パターン マッチングの統合
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>C&lt;span style="color:#8b949e;font-style:italic"># ではパターン マッチングがバージョン 7.0 以降進化しており、共用体型はそのシステムの第一級市民となるように設計されています。
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#8b949e;font-style:italic">### 網羅的なスイッチ式
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#f85149">最も影響力のある機能は、徹底的なスイッチ&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">チェックです。共用体を使用すると、コンパイラは考えられるすべてのケースを&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span>&lt;span style="color:#f85149">認識&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span>&lt;span style="color:#f85149">します。&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">```&lt;/span>csharp&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>string&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72">Describe&lt;/span>(Shape&lt;span style="color:#6e7681"> &lt;/span>shape)&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>shape&lt;span style="color:#6e7681"> &lt;/span>switch&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#f85149">{&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#d2a8ff;font-weight:bold">Circle&lt;/span>(var&lt;span style="color:#6e7681"> &lt;/span>r)&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">$&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;A circle with radius {r}&amp;#34;&lt;/span>,&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#d2a8ff;font-weight:bold">Rectangle&lt;/span>(var&lt;span style="color:#6e7681"> &lt;/span>w,&lt;span style="color:#6e7681"> &lt;/span>var&lt;span style="color:#6e7681"> &lt;/span>h)&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">$&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;A {w}x{h} rectangle&amp;#34;&lt;/span>,&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#d2a8ff;font-weight:bold">Triangle&lt;/span>(var&lt;span style="color:#6e7681"> &lt;/span>b,&lt;span style="color:#6e7681"> &lt;/span>var&lt;span style="color:#6e7681"> &lt;/span>h)&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">$&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;A triangle with base {b} and height {h}&amp;#34;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#f85149">}&lt;/span>;&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>廃棄アームはありません。 &lt;code>_ =&amp;gt; throw new NotImplementedException()&lt;/code> いいえ。大文字と小文字を忘れた場合、コンパイラは警告ではなくエラーを生成します。これは安全性の根本的な改善です。&lt;/p>
&lt;h3 id="ネストされたパターンマッチング">ネストされたパターンマッチング&lt;/h3>
&lt;p>ユニオンは、ネストされたパターンで自然に構成されます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>union Expr
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Literal(&lt;span style="color:#ff7b72">double&lt;/span> Value),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Add(Expr Left, Expr Right),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Multiply(Expr Left, Expr Right),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Negate(Expr Inner)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">double&lt;/span> Evaluate(Expr expr) =&amp;gt; expr &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Literal(&lt;span style="color:#ff7b72">var&lt;/span> v) =&amp;gt; v,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Add(&lt;span style="color:#ff7b72">var&lt;/span> left, &lt;span style="color:#ff7b72">var&lt;/span> right) =&amp;gt; Evaluate(left) + Evaluate(right),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Multiply(&lt;span style="color:#ff7b72">var&lt;/span> left, &lt;span style="color:#ff7b72">var&lt;/span> right) =&amp;gt; Evaluate(left) * Evaluate(right),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Negate(&lt;span style="color:#ff7b72">var&lt;/span> inner) =&amp;gt; -Evaluate(inner)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>この種の再帰的なデータ構造は、コンパイラ、インタプリタ、ルール エンジン、および数学的モデリングで非常に一般的です。現在の C# では、深いクラス階層と訪問者パターンが必要になります。共用体を使用すると、コードが大幅に単純になります。&lt;/p>
&lt;h3 id="ガード条項">ガード条項&lt;/h3>
&lt;p>共用体を使用したパターン マッチングは、ご想像どおり &lt;code>when&lt;/code> ガードをサポートしています。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">string&lt;/span> Classify(Shape shape) =&amp;gt; shape &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Circle(&lt;span style="color:#ff7b72">var&lt;/span> r) when r &amp;gt; &lt;span style="color:#a5d6ff">100&lt;/span> =&amp;gt; &lt;span style="color:#a5d6ff">&amp;#34;Large circle&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Circle(&lt;span style="color:#ff7b72">var&lt;/span> r) when r &amp;gt; &lt;span style="color:#a5d6ff">10&lt;/span> =&amp;gt; &lt;span style="color:#a5d6ff">&amp;#34;Medium circle&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Circle(_) =&amp;gt; &lt;span style="color:#a5d6ff">&amp;#34;Small circle&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Rectangle(&lt;span style="color:#ff7b72">var&lt;/span> w, &lt;span style="color:#ff7b72">var&lt;/span> h) when w == h =&amp;gt; &lt;span style="color:#a5d6ff">&amp;#34;Square&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Rectangle _ =&amp;gt; &lt;span style="color:#a5d6ff">&amp;#34;Rectangle&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Triangle _ =&amp;gt; &lt;span style="color:#a5d6ff">&amp;#34;Triangle&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="実用的な例">実用的な例&lt;/h2>
&lt;p>共用体型によってコードが劇的に改善される実際のシナリオをいくつか見てみましょう。&lt;/p>
&lt;h3 id="結果のパターン-予期されるエラーの例外を置き換える">結果のパターン: 予期されるエラーの例外を置き換える&lt;/h3>
&lt;p>最新のアプリケーション開発で最も一般的なパターンの 1 つは、制御フローの例外を使用せずに、成功または失敗する可能性のある操作を表現することです。例外は、ネットワーク障害やメモリ不足状態など、例外的なものである必要があります。検証エラーまたは「見つからない」という結果は予期された結果であり、例外ではありません。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>union Result&amp;lt;T, E&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Ok(T Value),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Error(E Err)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>union OrderError
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NotFound(Guid OrderId),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> InsufficientStock(&lt;span style="color:#ff7b72">string&lt;/span> ProductId, &lt;span style="color:#ff7b72">int&lt;/span> Requested, &lt;span style="color:#ff7b72">int&lt;/span> Available),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PaymentDeclined(&lt;span style="color:#ff7b72">string&lt;/span> Reason),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ValidationFailed(IReadOnlyList&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; Errors)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Result&amp;lt;Order, OrderError&amp;gt; ProcessOrder(OrderRequest request)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!Validate(request, &lt;span style="color:#ff7b72">out&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> errors))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> ValidationFailed(errors);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> product = catalog.Find(request.ProductId);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (product &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> NotFound(request.OrderId);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (product.Stock &amp;lt; request.Quantity)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> InsufficientStock(request.ProductId, request.Quantity, product.Stock);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> paymentResult = paymentGateway.Charge(request.Payment);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!paymentResult.Success)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> PaymentDeclined(paymentResult.Message);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> order = CreateOrder(request, product);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> Ok(order);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>その後、呼び出し元は考えられるすべての結果を処理する必要があります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> result = ProcessOrder(request);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> response = result &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Ok(&lt;span style="color:#ff7b72">var&lt;/span> order) =&amp;gt; Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/orders/{order.Id}&amp;#34;&lt;/span>, order),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Error(NotFound(&lt;span style="color:#ff7b72">var&lt;/span> id)) =&amp;gt; Results.NotFound(&lt;span style="color:#a5d6ff">$&amp;#34;Order {id} not found&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Error(InsufficientStock(&lt;span style="color:#ff7b72">var&lt;/span> pid, &lt;span style="color:#ff7b72">var&lt;/span> req, &lt;span style="color:#ff7b72">var&lt;/span> avail)) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Results.Conflict(&lt;span style="color:#a5d6ff">$&amp;#34;Product {pid}: requested {req}, only {avail} available&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Error(PaymentDeclined(&lt;span style="color:#ff7b72">var&lt;/span> reason)) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Results.UnprocessableEntity(&lt;span style="color:#a5d6ff">$&amp;#34;Payment declined: {reason}&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Error(ValidationFailed(&lt;span style="color:#ff7b72">var&lt;/span> errors)) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Results.BadRequest(&lt;span style="color:#ff7b72">new&lt;/span> { Errors = errors })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>try-catch ブロック、忘れられた例外タイプ、実行時のサプライズはありません。すべての障害モードは型シグネチャに表示され、コンパイラによって強制されます。これは API の信頼性にとって大幅な改善です。&lt;/p>
&lt;h3 id="ドメイン-モデリング-支払いタイプ">ドメイン モデリング: 支払いタイプ&lt;/h3>
&lt;p>これは、電子商取引システムでさまざまな支払い方法を処理する実際のドメイン モデリングの例です。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>union PaymentMethod
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CreditCard(&lt;span style="color:#ff7b72">string&lt;/span> CardNumber, &lt;span style="color:#ff7b72">string&lt;/span> Expiry, &lt;span style="color:#ff7b72">string&lt;/span> Cvv),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> BankTransfer(&lt;span style="color:#ff7b72">string&lt;/span> Iban, &lt;span style="color:#ff7b72">string&lt;/span> Bic),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> DigitalWallet(&lt;span style="color:#ff7b72">string&lt;/span> Provider, &lt;span style="color:#ff7b72">string&lt;/span> Token),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CashOnDelivery
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">decimal&lt;/span> CalculateProcessingFee(PaymentMethod method, &lt;span style="color:#ff7b72">decimal&lt;/span> amount) =&amp;gt; method &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CreditCard _ =&amp;gt; amount * &lt;span style="color:#a5d6ff">0.029&lt;/span>m + &lt;span style="color:#a5d6ff">0.30&lt;/span>m,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> BankTransfer _ =&amp;gt; &lt;span style="color:#a5d6ff">1.50&lt;/span>m,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> DigitalWallet(provider: &lt;span style="color:#a5d6ff">&amp;#34;PayPal&amp;#34;&lt;/span>, _) =&amp;gt; amount * &lt;span style="color:#a5d6ff">0.034&lt;/span>m + &lt;span style="color:#a5d6ff">0.35&lt;/span>m,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> DigitalWallet _ =&amp;gt; amount * &lt;span style="color:#a5d6ff">0.025&lt;/span>m,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CashOnDelivery =&amp;gt; &lt;span style="color:#a5d6ff">4.99&lt;/span>m
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">string&lt;/span> FormatForReceipt(PaymentMethod method) =&amp;gt; method &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CreditCard(&lt;span style="color:#ff7b72">var&lt;/span> num, _, _) =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;Credit Card ending in {num[^4..]}&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> BankTransfer(&lt;span style="color:#ff7b72">var&lt;/span> iban, _) =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;Bank Transfer ({iban[..4]}****)&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> DigitalWallet(&lt;span style="color:#ff7b72">var&lt;/span> provider, _) =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;Digital Wallet ({provider})&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CashOnDelivery =&amp;gt; &lt;span style="color:#a5d6ff">&amp;#34;Cash on Delivery&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これを、5 つのファイルにまたがる 5 つの異なる実装を持つインターフェイスまたは抽象クラスがあり、その上に訪問者パターンが含まれる現在のアプローチと比較してください。ユニオンのアプローチでは、データ定義と操作をまとめて読み取り可能に保ち、徹底的にチェックします。&lt;/p>
&lt;h3 id="ステートマシン">ステートマシン&lt;/h3>
&lt;p>ステート マシンは、注文処理、ワークフロー エンジン、接続管理、UI 状態など、ソフトウェアのあらゆる場所に存在します。 Union はそれらを明示的かつ安全なものにします。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>union ConnectionState
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Disconnected,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Connecting(&lt;span style="color:#ff7b72">string&lt;/span> Host, &lt;span style="color:#ff7b72">int&lt;/span> Port, DateTime StartedAt),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Connected(&lt;span style="color:#ff7b72">string&lt;/span> Host, &lt;span style="color:#ff7b72">int&lt;/span> Port, TcpClient Client),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Reconnecting(&lt;span style="color:#ff7b72">string&lt;/span> Host, &lt;span style="color:#ff7b72">int&lt;/span> Port, &lt;span style="color:#ff7b72">int&lt;/span> Attempt, TimeSpan Delay),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Failed(&lt;span style="color:#ff7b72">string&lt;/span> Host, Exception Error)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ConnectionState HandleEvent(ConnectionState state, ConnectionEvent evt) =&amp;gt; (state, evt) &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (Disconnected, Connect(&lt;span style="color:#ff7b72">var&lt;/span> host, &lt;span style="color:#ff7b72">var&lt;/span> port)) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Connecting(host, port, DateTime.UtcNow),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (Connecting(&lt;span style="color:#ff7b72">var&lt;/span> h, &lt;span style="color:#ff7b72">var&lt;/span> p, _), ConnectionSucceeded(&lt;span style="color:#ff7b72">var&lt;/span> client)) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Connected(h, p, client),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (Connecting(&lt;span style="color:#ff7b72">var&lt;/span> h, &lt;span style="color:#ff7b72">var&lt;/span> p, _), ConnectionFailed(&lt;span style="color:#ff7b72">var&lt;/span> ex)) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Reconnecting(h, p, Attempt: &lt;span style="color:#a5d6ff">1&lt;/span>, Delay: TimeSpan.FromSeconds(&lt;span style="color:#a5d6ff">1&lt;/span>)),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (Reconnecting(&lt;span style="color:#ff7b72">var&lt;/span> h, &lt;span style="color:#ff7b72">var&lt;/span> p, &lt;span style="color:#ff7b72">var&lt;/span> attempt, _), ConnectionSucceeded(&lt;span style="color:#ff7b72">var&lt;/span> client)) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Connected(h, p, client),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (Reconnecting(&lt;span style="color:#ff7b72">var&lt;/span> h, &lt;span style="color:#ff7b72">var&lt;/span> p, &lt;span style="color:#ff7b72">var&lt;/span> attempt, _), ConnectionFailed(&lt;span style="color:#ff7b72">var&lt;/span> ex)) when attempt &amp;gt;= &lt;span style="color:#a5d6ff">5&lt;/span> =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Failed(h, ex),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (Reconnecting(&lt;span style="color:#ff7b72">var&lt;/span> h, &lt;span style="color:#ff7b72">var&lt;/span> p, &lt;span style="color:#ff7b72">var&lt;/span> attempt, _), ConnectionFailed(_)) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Reconnecting(h, p, attempt + &lt;span style="color:#a5d6ff">1&lt;/span>, TimeSpan.FromSeconds(Math.Pow(&lt;span style="color:#a5d6ff">2&lt;/span>, attempt))),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (Connected(_, _, &lt;span style="color:#ff7b72">var&lt;/span> client), Disconnect) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Disconnected,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _ =&amp;gt; state &lt;span style="color:#8b949e;font-style:italic">// Ignore events that don&amp;#39;t apply to current state&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>各状態は、その状態に関連するデータを正確に保持します。 &lt;code>Connecting&lt;/code> 状態では &lt;code>Connecting&lt;/code> が存在しないため、誤って [[TOK_39]]] にアクセスすることはできません。型システムはステート マシンの不変条件を強制します。&lt;/p>
&lt;h2 id="シリアル化と相互運用に関する考慮事項共用体型についてすぐに浮かぶ実際的な疑問の-1-つはどうやってそれらをシリアル化するのかというものです-api-を構築している場合またはデータを保存している場合正しく機能するには-json-シリアル化が必要です">シリアル化と相互運用に関する考慮事項共用体型についてすぐに浮かぶ実際的な疑問の 1 つは、「どうやってそれらをシリアル化するのか?」というものです。 API を構築している場合、またはデータを保存している場合、正しく機能するには JSON シリアル化が必要です。&lt;/h2>
&lt;p>設計チームは、共用体型の組み込みの &lt;code>System.Text.Json&lt;/code> サポートについて議論してきました。期待されるアプローチには、識別子プロパティを使用したシリアル化が含まれます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;$type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Circle&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;radius&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">5.0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;$type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Rectangle&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;width&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">10.0&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;height&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">20.0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これは、.NET 7 で &lt;code>JsonDerivedType&lt;/code> 属性を使用して導入された既存のポリモーフィック シリアル化サポートと一致しています。デフォルトで型識別子としてバリアント名を使用し、共用体が &lt;code>System.Text.Json&lt;/code> でそのまま動作することが期待されています。&lt;/p>
&lt;p>Entity Framework Core の場合、おそらく階層ごとのテーブル (TPH) 継承マッピングが既に機能している方法と同様に、識別子列を使用して共用体値を格納するアプローチが考えられます。正確な EF Core 統合はまだ設計中ですが、閉じた型の階層を処理するためのインフラストラクチャはすでに存在しています。&lt;/p>
&lt;p>ユニオンは内部で標準の IL クラス階層にコンパイルされるため、他の .NET 言語との相互運用がスムーズになることは注目に値します。 C# ユニオンを使用する F# コードは、それを標準の型階層として認識し、その逆も同様です。&lt;/p>
&lt;h2 id="試してみる方法">試してみる方法&lt;/h2>
&lt;p>この記事の執筆時点では、ユニオン型は最新の .NET SDK プレビューのプレビュー機能として利用できます。提案された構文を試してみるには、次のことを行う必要があります。&lt;/p>
&lt;ol>
&lt;li>最新の .NET プレビュー SDK をインストールします&lt;/li>
&lt;li>プロジェクト ファイルでプレビュー言語バージョンを有効にします。&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;Project&lt;/span> Sdk=&lt;span style="color:#a5d6ff">&amp;#34;Microsoft.NET.Sdk&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;PropertyGroup&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;TargetFramework&amp;gt;&lt;/span>net10.0&lt;span style="color:#7ee787">&amp;lt;/TargetFramework&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;LangVersion&amp;gt;&lt;/span>preview&lt;span style="color:#7ee787">&amp;lt;/LangVersion&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/PropertyGroup&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;/Project&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>プレビュー機能は変更される可能性があることに注意してください。構文、動作、およびコンパイラ診断は、最終リリース前に大幅に進化する可能性があります。プレビュー言語機能に依存して製品コードを出荷しないでください。ただし、必ずそれらを試してフィードバックを提供してください。 C# チームは、&lt;a href="https://github.com/dotnet/csharplang">csharplang&lt;/a> リポジトリのディスカッションを積極的に監視しています。&lt;/p>
&lt;p>提案の進捗状況を追跡したい場合は、注目すべき重要な場所は次のとおりです。&lt;/p>
&lt;ul>
&lt;li>言語設計ディスカッションのための &lt;a href="https://github.com/dotnet/csharplang">dotnet/csharplang&lt;/a> リポジトリ&lt;/li>
&lt;li>コンパイラ実装の進捗状況のための &lt;a href="https://github.com/dotnet/roslyn">dotnet/roslyn&lt;/a> リポジトリ&lt;/li>
&lt;li>公式発表に関する .NET ブログ&lt;/li>
&lt;/ul>
&lt;h2 id="既存のアプローチとの比較">既存のアプローチとの比較&lt;/h2>
&lt;p>さまざまなアプローチがどのように積み重なるかを確認できるように、簡単な比較をまとめてみましょう。&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>特集&lt;/th>
&lt;th>抽象的な記録&lt;/th>
&lt;th>OneOf&amp;lt;T1,T2&amp;gt;&lt;/th>
&lt;th>列挙型 + データ&lt;/th>
&lt;th>ユニオンの種類&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>徹底チェック&lt;/td>
&lt;td>❌ いいえ&lt;/td>
&lt;td>✅ はい&lt;/td>
&lt;td>❌ いいえ&lt;/td>
&lt;td>✅ はい&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>パターンマッチング&lt;/td>
&lt;td>✅ はい&lt;/td>
&lt;td>❌限定&lt;/td>
&lt;td>❌ マニュアル&lt;/td>
&lt;td>✅ ネイティブ&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>コンパイラによる強制的なクロージャ&lt;/td>
&lt;td>❌ いいえ&lt;/td>
&lt;td>✅ はい&lt;/td>
&lt;td>❌ いいえ&lt;/td>
&lt;td>✅ はい&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>バリアントごとのデータ&lt;/td>
&lt;td>✅ はい&lt;/td>
&lt;td>✅ はい&lt;/td>
&lt;td>⚠️壊れやすい&lt;/td>
&lt;td>✅ はい&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>可読性&lt;/td>
&lt;td>⚠️ 詳細&lt;/td>
&lt;td>⚠️ 位置&lt;/td>
&lt;td>❌ 悪い&lt;/td>
&lt;td>✅ 素晴らしい&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>連載&lt;/td>
&lt;td>✅ マニュアル&lt;/td>
&lt;td>⚠️コンプレックス&lt;/td>
&lt;td>✅ マニュアル&lt;/td>
&lt;td>✅ 内蔵&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>定型文&lt;/td>
&lt;td>⚠️中程度&lt;/td>
&lt;td>✅ 低い&lt;/td>
&lt;td>⚠️高い&lt;/td>
&lt;td>✅ ミニマル&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>外部依存性なし&lt;/td>
&lt;td>✅ はい&lt;/td>
&lt;td>❌NuGet&lt;/td>
&lt;td>✅ はい&lt;/td>
&lt;td>✅ はい&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="これが-net-エコシステムにとって何を意味するか">これが .NET エコシステムにとって何を意味するか&lt;/h2>
&lt;p>差別的結合の導入は、.NET エコシステム全体に波及します。私が期待しているものは次のとおりです。&lt;/p>
&lt;p>&lt;strong>ライブラリの設計が改善されます。&lt;/strong> 現在、「見つからない」ことを示すために &lt;code>null&lt;/code> を返したり、検証の失敗に対して例外をスローしたりする API は、代わりに &lt;code>Result&amp;lt;T, E&amp;gt;&lt;/code> タイプを返すことができるようになります。これにより、型シグネチャで障害モードが明確になります。ドキュメントやソース コードを読むのではなく、メソッド シグネチャを見ることで、何が問題になるのかがわかります。&lt;/p>
&lt;p>&lt;strong>ドメイン モデリングがより表現力豊かになります。&lt;/strong> 問題のドメインとコード表現の間のギャップが劇的に縮小します。ドメインの専門家が「支払いにはクレジット カード、銀行振込、または代金引換が可能です」と言う場合、それを継承階層に変換するのではなく、結合として直接モデル化できます。&lt;/p>
&lt;p>&lt;strong>C# 開発者が F# のアイデアにアクセスできるようになります。&lt;/strong> 多くの C# 開発者は、遠くから F# の型システムに憧れてきましたが、組織に F# を採用することができませんでした。ユニオン型は、F# の最も強力な機能の 1 つを C# にもたらします。これは、.NET エコシステム全体にとって有益です。&lt;/p>
&lt;p>&lt;strong>実行時エラーが減少します。&lt;/strong> 徹底的なチェックだけでも、カテゴリ全体のバグを防ぐことができます。新しいバリアントを共用体に追加するたびに、コンパイラはコードベース内の更新が必要なすべての場所を案内します。 &lt;code>switch&lt;/code> の忘れ去られるケースや、運用環境でのみ表示されるデフォルトのブランチの &lt;code>NotImplementedException&lt;/code> はもう必要ありません。&lt;/p>
&lt;h2 id="結論">結論&lt;/h2>
&lt;p>差別的ユニオンは 10 年近くにわたって C# コミュニティの要望リストのトップにありましたが、それには十分な理由があります。これらは、型システムの根本的なギャップ、つまりコンパイラによって強制される安全性を備えた「いくつかのもののうちの 1 つ」となるデータをモデル化する機能を解決します。&lt;/p>
&lt;p>提案された &lt;code>union&lt;/code> キーワードは、C# のパターン マッチングと深く統合し、ジェネリックスと連携し、メソッドとインターフェイスをサポートし、実行時ではなくコンパイル時にバグを検出する徹底的なチェックを可能にする、クリーンで簡潔な構文をもたらします。&lt;/p>
&lt;p>ドメイン モデルを構築している場合でも、明示的なエラー タイプを使用した API を設計している場合でも、ステート マシンを実装している場合でも、単に厄介な継承階層をより自然なものに置き換えようとしている場合でも、ユニオン型は C# の記述方法を変えることになります。&lt;/p>
&lt;p>私たちはこれを長い間待っていました。構文は洗練されており、既存の言語機能との統合は思慮深く行われており、実際的な影響は .NET エコシステム全体にわたって感じられます。&lt;/p>
&lt;p>プレビュー リリースに注目し、機能を試して、言語設計チームにフィードバックを提供してください。これは、一度使用すると、これなしでどうして生きていたのかと不思議に思うような機能の 1 つです。&lt;/p></content:encoded><category>.NET</category><category>C#</category></item><item><title>セマンティック カーネルを使用した C# での RAG システムの構築</title><link>https://emimontesdeoca.github.io/ja/posts/rag-csharp-semantic-kernel/</link><pubDate>Wed, 18 Mar 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/rag-csharp-semantic-kernel/</guid><description>セマンティック カーネル、埋め込み、ベクトル検索を使用して、C# で検索拡張生成を実装します。</description><content:encoded>&lt;h2 id="はじめに">はじめに&lt;/h2>
&lt;p>LLM を使用して自分のデータ (会社のドキュメント、製品仕様、社内ナレッジベース) に関する質問に答えようとしたことがあるなら、おそらく、LLM が幻覚を示すか、単に「それに関する情報がありません」と表示されることに気づいたでしょう。それは、モデルは何に基づいてトレーニングされたかしか知らないからです。&lt;/p>
&lt;p>RAG (検索拡張生成) はこれを修正します。データに基づいてモデルを微調整する代わりに、クエリ時にドキュメントの関連するチャンクを取得し、それらをコンテキストとして LLM に渡します。その後、モデルは実際のデータに基づいた回答を生成します。&lt;/p>
&lt;p>この投稿では、セマンティック カーネルを使用して C# で完全な RAG パイプラインを構築する手順を説明します。&lt;/p>
&lt;h2 id="rag-の仕組み">RAG の仕組み&lt;/h2>
&lt;p>流れは簡単です。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>取り込み&lt;/strong>: ドキュメントをチャンクに分割し、各チャンクのエンベディングを生成し、ベクトル データベースに保存します。&lt;/li>
&lt;li>&lt;strong>クエリ&lt;/strong>: ユーザーが質問すると、クエリの埋め込みを生成し、ベクター データベースで類似のチャンクを検索します。&lt;/li>
&lt;li>&lt;strong>生成&lt;/strong>: 取得したチャンクを、ユーザーの質問とともにコンテキストとして LLM に渡します。&lt;/li>
&lt;/ol>
&lt;p>それだけです。魔法は埋め込みにあります。埋め込みはテキストの意味論的な意味をベクトルとして取得するため、単語が正確に一致しない場合でも、関連するコンテンツを見つけることができます。&lt;/p>
&lt;h2 id="前提条件">前提条件&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel.Connectors.AzureOpenAI
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.Extensions.VectorData.Abstractions
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel.Connectors.InMemory
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>運用環境では、メモリ内ストアを Azure AI Search、Qdrant、Pinecone、またはその他のサポートされているベクター データベースに交換します。ただし、インメモリは学習やプロトタイピングには最適です。&lt;/p>
&lt;h2 id="カーネルのセットアップ">カーネルのセットアップ&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Connectors.AzureOpenAI&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.Extensions.VectorData&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Connectors.InMemory&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Embeddings&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = Kernel.CreateBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: config[&lt;span style="color:#a5d6ff">&amp;#34;AzureOpenAI:Endpoint&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: config[&lt;span style="color:#a5d6ff">&amp;#34;AzureOpenAI:ApiKey&amp;#34;&lt;/span>]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureOpenAITextEmbeddingGeneration(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;text-embedding-3-small&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: config[&lt;span style="color:#a5d6ff">&amp;#34;AzureOpenAI:Endpoint&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: config[&lt;span style="color:#a5d6ff">&amp;#34;AzureOpenAI:ApiKey&amp;#34;&lt;/span>]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> kernel = builder.Build();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>2 つのモデルが必要です。1 つはチャット完了 (質問に答える) 用、もう 1 つは埋め込みを生成する (テキストをベクトルに変換する) 用です。&lt;/p>
&lt;h2 id="データモデルの定義">データモデルの定義&lt;/h2>
&lt;p>ベクター ストア内のドキュメント チャンクを表すクラスが必要です。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.Extensions.VectorData&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">DocumentChunk&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [VectorStoreRecordKey]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Id { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = Guid.NewGuid().ToString();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [VectorStoreRecordData]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Content { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">string&lt;/span>.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [VectorStoreRecordData]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Source { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">string&lt;/span>.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [VectorStoreRecordData]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> ChunkIndex { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [VectorStoreRecordVector(1536)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ReadOnlyMemory&amp;lt;&lt;span style="color:#ff7b72">float&lt;/span>&amp;gt; Embedding { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>VectorStoreRecordVector(1536)&lt;/code> 属性は、ベクトル ストアに埋め込みの次元を伝えます。 &lt;code>text-embedding-3-small&lt;/code> モデルは 1536 次元のベクトルを生成します。&lt;/p>
&lt;h2 id="ドキュメントのチャンク化">ドキュメントのチャンク化&lt;/h2>
&lt;p>埋め込みを作成する前に、ドキュメントを管理可能なチャンクに分割する必要があります。単純なテキスト分割ツールを次に示します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">TextChunker&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; SplitText(&lt;span style="color:#ff7b72">string&lt;/span> text, &lt;span style="color:#ff7b72">int&lt;/span> maxChunkSize = &lt;span style="color:#a5d6ff">500&lt;/span>, &lt;span style="color:#ff7b72">int&lt;/span> overlap = &lt;span style="color:#a5d6ff">50&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> chunks = &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> paragraphs = text.Split(&lt;span style="color:#a5d6ff">&amp;#34;\n\n&amp;#34;&lt;/span>, StringSplitOptions.RemoveEmptyEntries);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> currentChunk = &lt;span style="color:#ff7b72">new&lt;/span> System.Text.StringBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> paragraph &lt;span style="color:#ff7b72">in&lt;/span> paragraphs)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (currentChunk.Length + paragraph.Length &amp;gt; maxChunkSize &amp;amp;&amp;amp; currentChunk.Length &amp;gt; &lt;span style="color:#a5d6ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> chunks.Add(currentChunk.ToString().Trim());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Keep overlap from the end of the previous chunk&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> overlapText = currentChunk.ToString();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentChunk.Clear();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (overlapText.Length &amp;gt; overlap)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentChunk.Append(overlapText[^overlap..]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentChunk.Append(&lt;span style="color:#a5d6ff">&amp;#39; &amp;#39;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentChunk.Append(paragraph);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentChunk.Append(&lt;span style="color:#a5d6ff">&amp;#34;\n\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (currentChunk.Length &amp;gt; &lt;span style="color:#a5d6ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> chunks.Add(currentChunk.ToString().Trim());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> chunks;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>オーバーラップは重要です。これにより、チャンク間の境界のコンテキストが失われないことが保証されます。関連する文が 2 つのチャンクに分割されている場合、重複することは、その文が少なくとも 1 つのチャンクに完全に表示されることを意味します。&lt;/p>
&lt;h2 id="ドキュメントの取り込み">ドキュメントの取り込み&lt;/h2>
&lt;p>次に、ドキュメントをベクター ストアに取り込むためにすべてをまとめてみましょう。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> vectorStore = &lt;span style="color:#ff7b72">new&lt;/span> InMemoryVectorStore();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> collection = vectorStore.GetCollection&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, DocumentChunk&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;documents&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> collection.CreateCollectionIfNotExistsAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> embeddingService = kernel.GetRequiredService&amp;lt;ITextEmbeddingGenerationService&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">async&lt;/span> Task IngestDocument(&lt;span style="color:#ff7b72">string&lt;/span> content, &lt;span style="color:#ff7b72">string&lt;/span> source)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> chunks = TextChunker.SplitText(content);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">for&lt;/span> (&lt;span style="color:#ff7b72">int&lt;/span> i = &lt;span style="color:#a5d6ff">0&lt;/span>; i &amp;lt; chunks.Count; i++)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> embedding = &lt;span style="color:#ff7b72">await&lt;/span> embeddingService.GenerateEmbeddingAsync(chunks[i]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> chunk = &lt;span style="color:#ff7b72">new&lt;/span> DocumentChunk
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Content = chunks[i],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Source = source,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ChunkIndex = i,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Embedding = embedding
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> collection.UpsertAsync(chunk);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;✅ Ingested {chunks.Count} chunks from {source}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Ingest some documents&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> doc1 = &lt;span style="color:#ff7b72">await&lt;/span> File.ReadAllTextAsync(&lt;span style="color:#a5d6ff">&amp;#34;docs/product-guide.md&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> doc2 = &lt;span style="color:#ff7b72">await&lt;/span> File.ReadAllTextAsync(&lt;span style="color:#a5d6ff">&amp;#34;docs/faq.md&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> doc3 = &lt;span style="color:#ff7b72">await&lt;/span> File.ReadAllTextAsync(&lt;span style="color:#a5d6ff">&amp;#34;docs/troubleshooting.md&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> IngestDocument(doc1, &lt;span style="color:#a5d6ff">&amp;#34;product-guide.md&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> IngestDocument(doc2, &lt;span style="color:#a5d6ff">&amp;#34;faq.md&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> IngestDocument(doc3, &lt;span style="color:#a5d6ff">&amp;#34;troubleshooting.md&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="関連するチャンクの検索">関連するチャンクの検索&lt;/h2>
&lt;p>ユーザーが質問すると、クエリの埋め込みが生成され、類似したチャンクが検索されます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;List&amp;lt;DocumentChunk&amp;gt;&amp;gt; SearchAsync(&lt;span style="color:#ff7b72">string&lt;/span> query, &lt;span style="color:#ff7b72">int&lt;/span> topK = &lt;span style="color:#a5d6ff">3&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> queryEmbedding = &lt;span style="color:#ff7b72">await&lt;/span> embeddingService.GenerateEmbeddingAsync(query);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> searchResults = &lt;span style="color:#ff7b72">await&lt;/span> collection.VectorizedSearchAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> queryEmbedding,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> VectorSearchOptions { Top = topK });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> results = &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;DocumentChunk&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> result &lt;span style="color:#ff7b72">in&lt;/span> searchResults.Results)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> results.Add(result.Record);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> results;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="コンテキストを含む回答の生成">コンテキストを含む回答の生成&lt;/h2>
&lt;p>次に、RAG 部分です。取得したチャンクを取得し、プロンプトにコンテキストとして含めます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.ChatCompletion&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> chatService = kernel.GetRequiredService&amp;lt;IChatCompletionService&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; AskAsync(&lt;span style="color:#ff7b72">string&lt;/span> question)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Step 1: Retrieve relevant chunks&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> relevantChunks = &lt;span style="color:#ff7b72">await&lt;/span> SearchAsync(question);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Step 2: Build context from chunks&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> context = &lt;span style="color:#ff7b72">string&lt;/span>.Join(&lt;span style="color:#a5d6ff">&amp;#34;\n\n---\n\n&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> relevantChunks.Select(c =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;[Source: {c.Source}]\n{c.Content}&amp;#34;&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Step 3: Generate answer with context&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> history = &lt;span style="color:#ff7b72">new&lt;/span> ChatHistory();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.AddSystemMessage(&lt;span style="color:#f85149">$&lt;/span>&lt;span style="color:#a5d6ff">$&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> You are a helpful assistant that answers questions based on the provided context.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Use ONLY the information from the context to answer. If the context doesn&amp;#39;t contain
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> enough information to answer the question, say &amp;#34;&lt;/span>I don&lt;span style="color:#f85149">&amp;#39;&lt;/span>t have enough information
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> to answer that question.&lt;span style="color:#a5d6ff">&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Do not make up information. Always cite the source document when possible.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Context:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {{context}}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;);
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.AddUserMessage(question);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> chatService.GetChatMessageContentAsync(history);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> response.Content ?? &lt;span style="color:#a5d6ff">&amp;#34;No response generated.&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="それを使用する">それを使用する&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Ask questions about your documents&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> answer1 = &lt;span style="color:#ff7b72">await&lt;/span> AskAsync(&lt;span style="color:#a5d6ff">&amp;#34;How do I reset my password?&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;Q: How do I reset my password?\nA: {answer1}\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> answer2 = &lt;span style="color:#ff7b72">await&lt;/span> AskAsync(&lt;span style="color:#a5d6ff">&amp;#34;What are the system requirements?&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;Q: What are the system requirements?\nA: {answer2}\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> answer3 = &lt;span style="color:#ff7b72">await&lt;/span> AskAsync(&lt;span style="color:#a5d6ff">&amp;#34;What&amp;#39;s the capital of France?&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;Q: What&amp;#39;s the capital of France?\nA: {answer3}\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Should respond with &amp;#34;I don&amp;#39;t have enough information&amp;#34; since it&amp;#39;s not in the docs&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="本番環境への移行">本番環境への移行&lt;/h2>
&lt;p>インメモリ ベクター ストアはプロトタイピングには最適ですが、運用環境では永続的なベクター データベースが必要になります。セマンティック カーネルには、いくつかのオプション用のコネクタがあります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic"># Azure AI Search&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel.Connectors.AzureAISearch
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic"># Qdrant&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel.Connectors.Qdrant
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic"># Redis&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel.Connectors.Redis
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これらはすべて同じ &lt;code>IVectorStore&lt;/code> インターフェイスを実装しているため、交換は簡単です。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Instead of InMemoryVectorStore, use:&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Connectors.AzureAISearch&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> vectorStore = &lt;span style="color:#ff7b72">new&lt;/span> AzureAISearchVectorStore(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Azure.Search.Documents.Indexes.SearchIndexClient(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Uri(config[&lt;span style="color:#a5d6ff">&amp;#34;AzureAISearch:Endpoint&amp;#34;&lt;/span>]),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> AzureKeyCredential(config[&lt;span style="color:#a5d6ff">&amp;#34;AzureAISearch:ApiKey&amp;#34;&lt;/span>])));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```他はすべて同じままです。それが抽象化の美しさです。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">##&lt;/span> RAG &lt;span style="color:#f85149">システム構築のヒント&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">私が苦労して学んだことがいくつかあります。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">チャンク&lt;/span> &lt;span style="color:#f85149">サイズは非常に重要です。&lt;/span>** &lt;span style="color:#f85149">小さすぎるとコンテキストが失われます。大きすぎると、無関係なコンテンツにトークンを無駄に消費することになります。&lt;/span> &lt;span style="color:#a5d6ff">500&lt;/span> &lt;span style="color:#f85149">～&lt;/span> &lt;span style="color:#a5d6ff">800&lt;/span> &lt;span style="color:#f85149">トークンから始めて、データに基づいて調整します。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">オーバーラップにより境界の問題が防止されます。&lt;/span>** &lt;span style="color:#f85149">通常、チャンク間のトークンのオーバーラップは&lt;/span> &lt;span style="color:#a5d6ff">50&lt;/span> &lt;span style="color:#f85149">～&lt;/span> &lt;span style="color:#a5d6ff">100&lt;/span> &lt;span style="color:#f85149">で十分です。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">思っている以上に多くの情報を取得します。&lt;/span>** &lt;span style="color:#f85149">`&lt;/span>topK = &lt;span style="color:#a5d6ff">5&lt;/span>&lt;span style="color:#f85149">`&lt;/span> &lt;span style="color:#f85149">から始めて、ノイズが多すぎる場合は減らしてください。関連するチャンクを見逃してしまうよりは、追加のコンテキストがあったほうが良いでしょう。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">システム&lt;/span> &lt;span style="color:#f85149">プロンプトは非常に重要です。&lt;/span>** &lt;span style="color:#f85149">提供されたコンテキストのみを使用することを明確にしてください。その指示がなければ、モデルは「トレーニング&lt;/span> &lt;span style="color:#f85149">データに基づいて」喜んで幻覚を見せるでしょう。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">ソースを追跡します。&lt;/span>** &lt;span style="color:#f85149">常にチャンクとともにメタデータを保存して、答えの出所を引用できるようにします。ソースを確認できる場合、ユーザーは回答をより信頼します。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">必要に応じて再ランク付けします。&lt;/span>** &lt;span style="color:#f85149">ベクトルの類似性は完璧ではありません。クリティカルなアプリケーションの場合は、クロスエンコーダー&lt;/span> &lt;span style="color:#f85149">モデルを使用して再ランキング&lt;/span> &lt;span style="color:#f85149">ステップを追加し、精度を向上させます。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">##&lt;/span> &lt;span style="color:#f85149">結論&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>RAG &lt;span style="color:#f85149">は、現在&lt;/span> AI &lt;span style="color:#f85149">で最も実用的なパターンの&lt;/span> &lt;span style="color:#a5d6ff">1&lt;/span> &lt;span style="color:#f85149">つです。これにより、微調整することなく独自のデータに&lt;/span> AI &lt;span style="color:#f85149">を活用した&lt;/span> Q&amp;amp;A &lt;span style="color:#f85149">システムを構築でき、セマンティック&lt;/span> &lt;span style="color:#f85149">カーネルにより&lt;/span> C&lt;span style="color:#f85149">#&lt;/span> &lt;span style="color:#f85149">で驚くほどクリーンになります。インメモリ&lt;/span> &lt;span style="color:#f85149">ストアから始めて、チャンクとプロンプトを正しく取得し、本番環境の準備ができたら実際のベクトル&lt;/span> &lt;span style="color:#f85149">データベースに交換します。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">コーディングを楽しんでください&lt;/span>!
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">##&lt;/span> &lt;span style="color:#f85149">リソース&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- [&lt;span style="color:#f85149">セマンティック&lt;/span> &lt;span style="color:#f85149">カーネル&lt;/span> &lt;span style="color:#f85149">ベクター&lt;/span> &lt;span style="color:#f85149">ストアのドキュメント&lt;/span>](https:&lt;span style="color:#8b949e;font-style:italic">//learn.microsoft.com/en-us/semantic-kernel/concepts/vector-store-connectors/)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- [Azure AI Search &lt;span style="color:#f85149">を使用した&lt;/span> RAG &lt;span style="color:#f85149">パターン&lt;/span>](https:&lt;span style="color:#8b949e;font-style:italic">//learn.microsoft.com/en-us/azure/search/retrieval-augmented-generation-overview)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- [&lt;span style="color:#f85149">テキスト埋め込みモデル&lt;/span>](https:&lt;span style="color:#8b949e;font-style:italic">//learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models#embeddings)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></content:encoded><category>.NET</category><category>AI</category><category>Semantic Kernel</category><category>C#</category><category>Azure</category></item><item><title>Microsoft Agent Framework を使用したエージェント ワークフローの構築</title><link>https://emimontesdeoca.github.io/ja/posts/agent-framework-workflows/</link><pubDate>Tue, 10 Feb 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/agent-framework-workflows/</guid><description>.NET の Microsoft Agent Framework を使用して、順次および並列のエージェント ワークフローを設計および調整します。</description><content:encoded>&lt;h2 id="はじめに">はじめに&lt;/h2>
&lt;p>Microsoft のエージェント フレームワークに関する私の以前の投稿を読んだことがあれば、個々のエージェント、さらには複数エージェントのグループ チャットを作成する方法をご存知でしょう。しかし、現実のシナリオでは、多くの場合、より構造化されたものが必要になります。これは、エージェントが特定の順序で実行し、結果を相互に渡し、結果に基づいて分岐ロジックを処理するワークフローです。&lt;/p>
&lt;p>これはまさに、Agent Framework のワークフロー機能が提供するものです。エージェントをグループ チャットに放り込んで、彼らがそれを理解してくれることを期待するのではなく、明示的なステップ、依存関係、およびエージェント間のデータ フローを定義します。&lt;/p>
&lt;h2 id="エージェントのワークフローとは何ですか">エージェントのワークフローとは何ですか?&lt;/h2>
&lt;p>エージェントのワークフローはパイプラインのようなものだと考えてください。各ステップは専門のエージェントによって処理され、あるステップの出力は次のステップにフィードされます。ステップは、結果に基づいて、順次、並列、または条件付きで実行できます。&lt;/p>
&lt;p>いくつかの例:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>コンテンツ パイプライン&lt;/strong>: 調査 → 草案 → レビュー → 公開&lt;/li>
&lt;li>&lt;strong>データ処理&lt;/strong>: 抽出 → 変換 → 検証 → ロード&lt;/li>
&lt;li>&lt;strong>カスタマー サポート&lt;/strong>: 分類 → ルート → 対応 → フォローアップ&lt;/li>
&lt;/ul>
&lt;h2 id="前提条件">前提条件&lt;/h2>
&lt;p>最新のエージェント フレームワーク パッケージがあることを確認してください。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel.Agents.Core
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Azure OpenAI または OpenAI エンドポイントが構成されています。&lt;/p>
&lt;h2 id="順次ワークフローの構築">順次ワークフローの構築&lt;/h2>
&lt;p>研究者、ライター、編集者の 3 人のエージェントによるコンテンツ作成ワークフローを構築してみましょう。&lt;/p>
&lt;h3 id="エージェントの定義">エージェントの定義&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Agents&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> kernel = Kernel.CreateBuilder()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddAzureOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: config[&lt;span style="color:#a5d6ff">&amp;#34;AzureOpenAI:Endpoint&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: config[&lt;span style="color:#a5d6ff">&amp;#34;AzureOpenAI:ApiKey&amp;#34;&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> researcher = &lt;span style="color:#ff7b72">new&lt;/span> ChatCompletionAgent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Researcher&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a research specialist. Given a topic, find key facts, statistics,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> and talking points. Return a structured research brief with bullet points.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Be thorough but concise.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> writer = &lt;span style="color:#ff7b72">new&lt;/span> ChatCompletionAgent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Writer&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a technical blog writer. Given a research brief, write a clear
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> and engaging blog post. Use a conversational tone, include code examples
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">where&lt;/span> relevant, and structure the post with clear headings.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> editor = &lt;span style="color:#ff7b72">new&lt;/span> ChatCompletionAgent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Editor&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a senior editor. Review the blog post &lt;span style="color:#ff7b72">for&lt;/span> clarity, accuracy,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> grammar, and flow. Return the corrected version with a summary of
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> changes made at the end.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="順次実行">順次実行&lt;/h3>
&lt;p>最も単純なワークフローは逐次的です。各エージェントは前のエージェントの出力を処理します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Agents.Chat&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.ChatCompletion&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; RunSequentialWorkflow(&lt;span style="color:#ff7b72">string&lt;/span> topic)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> history = &lt;span style="color:#ff7b72">new&lt;/span> ChatHistory();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> currentInput = &lt;span style="color:#a5d6ff">$&amp;#34;Research the following topic: {topic}&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> agents = &lt;span style="color:#ff7b72">new&lt;/span>[] { researcher, writer, editor };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> agent &lt;span style="color:#ff7b72">in&lt;/span> agents)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.AddUserMessage(currentInput);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">new&lt;/span> System.Text.StringBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> message &lt;span style="color:#ff7b72">in&lt;/span> agent.InvokeAsync(history))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> response.Append(message.Content);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentInput = response.ToString();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;✅ {agent.Name} completed&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> currentInput;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">await&lt;/span> RunSequentialWorkflow(&lt;span style="color:#a5d6ff">&amp;#34;Blazor render modes in .NET 9&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(result);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>各エージェントは蓄積されたコンテキストを取得して処理し、結果は次のステップに進みます。&lt;/p>
&lt;h2 id="並列実行">並列実行&lt;/h2>
&lt;p>エージェントが独立して作業できる場合もあります。たとえば、複数のサブトピックを同時に調査したい場合があります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt;&amp;gt; RunParallelResearch(&lt;span style="color:#ff7b72">string&lt;/span>[] topics)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> tasks = topics.Select(&lt;span style="color:#ff7b72">async&lt;/span> topic =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> history = &lt;span style="color:#ff7b72">new&lt;/span> ChatHistory();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.AddUserMessage(&lt;span style="color:#a5d6ff">$&amp;#34;Research: {topic}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">new&lt;/span> System.Text.StringBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> message &lt;span style="color:#ff7b72">in&lt;/span> researcher.InvokeAsync(history))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> response.Append(message.Content);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;✅ Research completed: {topic}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> response.ToString();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> results = &lt;span style="color:#ff7b72">await&lt;/span> Task.WhenAll(tasks);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> results.ToList();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> topics = &lt;span style="color:#ff7b72">new&lt;/span>[]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Blazor SSR streaming&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Enhanced navigation in .NET 9&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Render mode boundaries&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> briefs = &lt;span style="color:#ff7b72">await&lt;/span> RunParallelResearch(topics);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>その後、すべての研究概要を 1 つのライター エージェントにフィードして、1 つのまとまりのある投稿を作成できます。&lt;/p>
&lt;h2 id="条件分岐">条件分岐&lt;/h2>
&lt;p>実際のワークフローには意思決定が必要です。投稿が十分でない場合にライターに戻す品質チェックのステップが必要な場合があります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> qualityChecker = &lt;span style="color:#ff7b72">new&lt;/span> ChatCompletionAgent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;QualityChecker&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a quality assurance reviewer. Evaluate the blog post &lt;span style="color:#ff7b72">on&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">1.&lt;/span> Technical accuracy
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">2.&lt;/span> Clarity and readability
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">3.&lt;/span> Completeness
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Respond with either:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#a5d6ff">&amp;#34;APPROVED&amp;#34;&lt;/span> &lt;span style="color:#ff7b72">if&lt;/span> the post meets all criteria
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#a5d6ff">&amp;#34;REVISION NEEDED: [specific feedback]&amp;#34;&lt;/span> &lt;span style="color:#ff7b72">if&lt;/span> changes are required
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Be strict. Only approve posts that are truly ready to publish.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; RunWithQualityLoop(&lt;span style="color:#ff7b72">string&lt;/span> topic, &lt;span style="color:#ff7b72">int&lt;/span> maxRevisions = &lt;span style="color:#a5d6ff">3&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> history = &lt;span style="color:#ff7b72">new&lt;/span> ChatHistory();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Step 1: Research&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.AddUserMessage(&lt;span style="color:#a5d6ff">$&amp;#34;Research: {topic}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> research = &lt;span style="color:#ff7b72">await&lt;/span> InvokeAgent(researcher, history);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Step 2: Write&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.AddUserMessage(research);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> draft = &lt;span style="color:#ff7b72">await&lt;/span> InvokeAgent(writer, history);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Step 3: Quality loop&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">for&lt;/span> (&lt;span style="color:#ff7b72">int&lt;/span> i = &lt;span style="color:#a5d6ff">0&lt;/span>; i &amp;lt; maxRevisions; i++)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> qaHistory = &lt;span style="color:#ff7b72">new&lt;/span> ChatHistory();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> qaHistory.AddUserMessage(&lt;span style="color:#a5d6ff">$&amp;#34;Review this post:\n\n{draft}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> qaResult = &lt;span style="color:#ff7b72">await&lt;/span> InvokeAgent(qualityChecker, qaHistory);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (qaResult.Contains(&lt;span style="color:#a5d6ff">&amp;#34;APPROVED&amp;#34;&lt;/span>, StringComparison.OrdinalIgnoreCase))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;✅ Approved after {i + 1} review(s)&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> draft;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;🔄 Revision {i + 1}: {qaResult[..Math.Min(100, qaResult.Length)]}...&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Send back to writer with feedback&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.AddUserMessage(&lt;span style="color:#a5d6ff">$&amp;#34;Please revise based on this feedback:\n{qaResult}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> draft = &lt;span style="color:#ff7b72">await&lt;/span> InvokeAgent(writer, history);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;⚠️ Max revisions reached, returning latest draft&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> draft;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; InvokeAgent(ChatCompletionAgent agent, ChatHistory history)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">new&lt;/span> System.Text.StringBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> message &lt;span style="color:#ff7b72">in&lt;/span> agent.InvokeAsync(history))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> response.Append(message.Content);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> response.ToString();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>このパターンは超便利です。品質チェッカーはゲートとして機能し、出力が十分に良好になるか、最大リビジョン制限に達するまでワークフローがループします。&lt;/p>
&lt;h2 id="ワークフロー-エージェントへのプラグインの追加">ワークフロー エージェントへのプラグインの追加&lt;/h2>
&lt;p>ワークフロー内のエージェントは、スタンドアロン エージェントと同様にプラグインを使用できます。ここからが非常に強力になります。エージェントは、ワークフロー ステップの一部として API を呼び出したり、データベースにクエリを実行したり、ファイル操作を実行したりできます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> researcherWithTools = &lt;span style="color:#ff7b72">new&lt;/span> ChatCompletionAgent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Researcher&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;Research the topic using available tools. Summarize findings.&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Add a web search plugin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>kernel.Plugins.AddFromType&amp;lt;WebSearchPlugin&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Add a database plugin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>kernel.Plugins.AddFromObject(&lt;span style="color:#ff7b72">new&lt;/span> DatabasePlugin(connectionString), &lt;span style="color:#a5d6ff">&amp;#34;Database&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">WebSearchPlugin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [KernelFunction, Description(&amp;#34;Search the web for information&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; SearchAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;The search query&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> query)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Your search implementation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> httpClient = &lt;span style="color:#ff7b72">new&lt;/span> HttpClient();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> httpClient.GetStringAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">$&amp;#34;https://api.search.example.com?q={Uri.EscapeDataString(query)}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> response;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="ワークフローでのエラー処理">ワークフローでのエラー処理&lt;/h2>
&lt;p>複数のエージェントをチェーンしている場合、エラー処理が重要になります。 1 つのステップが失敗しただけで、ワークフロー全体が静かにクラッシュすることは望ましくありません。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;WorkflowResult&amp;gt; RunResilientWorkflow(&lt;span style="color:#ff7b72">string&lt;/span> input)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">new&lt;/span> WorkflowResult();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> result.Research = &lt;span style="color:#ff7b72">await&lt;/span> InvokeAgent(researcher, &lt;span style="color:#ff7b72">new&lt;/span> ChatHistory(input));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception ex)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> result.Errors.Add(&lt;span style="color:#a5d6ff">$&amp;#34;Research failed: {ex.Message}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> result;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> writeHistory = &lt;span style="color:#ff7b72">new&lt;/span> ChatHistory(result.Research);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> result.Draft = &lt;span style="color:#ff7b72">await&lt;/span> InvokeAgent(writer, writeHistory);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception ex)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> result.Errors.Add(&lt;span style="color:#a5d6ff">$&amp;#34;Writing failed: {ex.Message}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> result.Draft = result.Research; &lt;span style="color:#8b949e;font-style:italic">// Fallback to research output&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> result.Success = result.Errors.Count == &lt;span style="color:#a5d6ff">0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> result;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">WorkflowResult&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Research { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Draft { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; Errors { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">bool&lt;/span> Success { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="ベストプラクティス">ベストプラクティス&lt;/h2>
&lt;p>いくつかのエージェント ワークフローを構築した後、私が学んだことは次のとおりです。- &lt;strong>エージェントの指示に重点を置く&lt;/strong> — 各エージェントは 1 つのことをうまく実行する必要があります。スイスアーミーナイフのエージェントになろうとしないでください。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>コンテキストを制限する&lt;/strong> — 会話履歴全体をすべてのエージェントに渡さないでください。各ステップに必要なものだけを与えます。&lt;/li>
&lt;li>&lt;strong>リビジョン制限を設定&lt;/strong> — 品質ループは優れていますが、注意しないと永久に実行される可能性があります。常に最大反復回数の上限を設定します。&lt;/li>
&lt;li>&lt;strong>すべてをログに記録&lt;/strong> — エージェントの出力は予測できない場合があります。デバッグのためにすべてのステップの入力と出力をログに記録します。&lt;/li>
&lt;li>&lt;strong>並列実行を賢く使用してください&lt;/strong> - 速度は向上しますが、API レート制限とトークン コストに注意してください。&lt;/li>
&lt;li>&lt;strong>最初に小規模なモデルでテストします&lt;/strong> — 本番環境で GPT-4o に切り替える前に、GPT-3.5 でワークフロー ロジックを開発およびテストします。&lt;/li>
&lt;/ul>
&lt;h2 id="結論">結論&lt;/h2>
&lt;p>エージェント ワークフローを使用すると、各エージェントが専門家である複雑な複数ステップの AI パイプラインを構築できます。順次ワークフローから開始し、ステップが独立した並列実行を追加し、品質ゲートのために条件付き分岐を使用します。パターンは構成可能です。一度コツを掴めば、かなり洗練された自動化を構築できます。&lt;/p>
&lt;p>コーディングを楽しんでください!&lt;/p>
&lt;h2 id="リソース">リソース&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/semantic-kernel/frameworks/agent">Microsoft Agent Framework ドキュメント&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://github.com/microsoft/semantic-kernel/tree/main/dotnet/samples/GettingStartedWithAgents">セマンティック カーネル エージェントのサンプル&lt;/a>&lt;/li>
&lt;/ul></content:encoded><category>.NET</category><category>AI</category><category>Agent Framework</category><category>Semantic Kernel</category></item><item><title>Blazor コンポーネントのライフサイクル: 完全なガイド</title><link>https://emimontesdeoca.github.io/ja/posts/blazor-component-lifecycle/</link><pubDate>Thu, 15 Jan 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/blazor-component-lifecycle/</guid><description>初期化から破棄までのすべての Blazor コンポーネントのライフサイクル メソッドと、それぞれをいつ使用するかを理解します。</description><content:encoded>&lt;p>私はしばらく Blazor を使用してきましたが、正直に言って、最初はライフサイクルの方法に戸惑いました。 &lt;code>OnInitialized&lt;/code> 対 &lt;code>OnInitializedAsync&lt;/code>? &lt;code>OnParametersSet&lt;/code> vs &lt;code>OnAfterRender&lt;/code>? &lt;code>StateHasChanged&lt;/code> が実際に再レンダリングをトリガーするのはいつですか?多くの試行錯誤を経て、最終的にすべてについての確固たるメンタルモデルができました。&lt;/p>
&lt;h1 id="ライフサイクルの概要">ライフサイクルの概要&lt;/h1>
&lt;p>Blazor コンポーネントがレンダリングされるとき、次のメソッドが順番に実行されます。&lt;/p>
&lt;ol>
&lt;li>&lt;code>SetParametersAsync&lt;/code>&lt;/li>
&lt;li>&lt;code>OnInitialized&lt;/code> / &lt;code>OnInitializedAsync&lt;/code>&lt;/li>
&lt;li>&lt;code>OnParametersSet&lt;/code> / &lt;code>OnParametersSetAsync&lt;/code>&lt;/li>
&lt;li>&lt;code>OnAfterRender&lt;/code> / &lt;code>OnAfterRenderAsync&lt;/code>&lt;/li>
&lt;li>&lt;code>Dispose&lt;/code> / &lt;code>DisposeAsync&lt;/code>&lt;/li>
&lt;/ol>
&lt;p>それぞれについて見ていきましょう。&lt;/p>
&lt;h1 id="setparametersasync">SetParametersAsync&lt;/h1>
&lt;p>これは最初に呼び出されるメソッドです。親コンポーネントから生の &lt;code>ParameterView&lt;/code> を受け取ります。ほとんどの場合、これをオーバーライドすることはありません。Blazor はマッピング パラメーターを自動的に処理します。ただし、カスタム パラメーターの処理または割り当て前に検証が必要な場合は、次のようにします。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task SetParametersAsync(ParameterView parameters)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Custom logic before parameters are set&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (parameters.TryGetValue&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;Title&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">out&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> title))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;Title is being set to: {title}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">base&lt;/span>.SetParametersAsync(parameters);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>実際のプロジェクトでこれを一度だけ使用しました。ほとんどの場合はスキップします。&lt;/p>
&lt;h1 id="oninitialized--oninitializedasync">OnInitialized / OnInitializedAsync&lt;/h1>
&lt;p>ここで、データのロード、サービスの初期化、デフォルト値の設定などのセットアップ作業を行います。これは、コンポーネントが最初に作成されたときに &lt;strong>1 回&lt;/strong>実行されます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> List&amp;lt;Product&amp;gt; products = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> products = &lt;span style="color:#ff7b72">await&lt;/span> Http.GetFromJsonAsync&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;api/products&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>私をつまずかせた点が 1 つあります。Blazor サーバーでは、プリレンダリング中に &lt;code>OnInitializedAsync&lt;/code> が &lt;strong>2 回&lt;/strong>呼び出されます。 1 回目はサーバー側のプリレンダリング中、2 回目は SignalR 接続の確立時に行われます。 API 呼び出しが高価な場合は、次のように処理することをお勧めします。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">bool&lt;/span> isPrerendering = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> products = &lt;span style="color:#ff7b72">await&lt;/span> Http.GetFromJsonAsync&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;api/products&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> isPrerendering = &lt;span style="color:#79c0ff">false&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>あるいは、&lt;code>PersistentComponentState&lt;/code> を使用して二重呼び出しを完全に回避することもできます。&lt;/p>
&lt;h1 id="onparametersset--onparameterssetasync">OnParametersSet / OnParametersSetAsync&lt;/h1>
&lt;p>これは、親コンポーネントが再レンダリングされ、新しいパラメータ値が渡されるたびに起動されます。 &lt;code>OnInitialized&lt;/code> の後にも起動されます。ここはパラメータの変更に反応するのに適切な場所です。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[Parameter]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> CategoryId { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> previousCategoryId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> List&amp;lt;Product&amp;gt; products = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnParametersSetAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (CategoryId != previousCategoryId)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> previousCategoryId = CategoryId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> products = &lt;span style="color:#ff7b72">await&lt;/span> Http.GetFromJsonAsync&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt;(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">$&amp;#34;api/products?category={CategoryId}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>CategoryId != previousCategoryId&lt;/code> のチェックは重要です。これがないと、カテゴリが変更されなかったとしても、親が再レンダリングされるたびにデータをリロードすることになります。&lt;/p>
&lt;h1 id="onafterrender--onafterrenderasync">OnAfterRender / OnAfterRenderAsync&lt;/h1>
&lt;p>これは、コンポーネントが DOM にレンダリングされた後に起動されます。 &lt;code>firstRender&lt;/code> パラメータは、それが最初のレンダリングであるかどうかを示します。この時点で DOM 要素が存在するため、これが JS Interop 呼び出しの場所です。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[Inject]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> IJSRuntime JS { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnAfterRenderAsync(&lt;span style="color:#ff7b72">bool&lt;/span> firstRender)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (firstRender)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> JS.InvokeVoidAsync(&lt;span style="color:#a5d6ff">&amp;#34;initializeChart&amp;#34;&lt;/span>, chartElement);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>再レンダリングのたびに JavaScript ライブラリが再初期化されるのを避けるために、私は &lt;code>firstRender&lt;/code> をよく使用します。イベント リスナー、チャート ライブラリ、または DOM に直接関わるものを設定している場合は、ここで設定します。&lt;/p>
&lt;h1 id="shouldrender">ShouldRender&lt;/h1>
&lt;p>これはあまり知られていませんが、非常に便利です。 &lt;code>StateHasChanged&lt;/code> が呼び出されたときにコンポーネントを再レンダリングするかどうかを制御します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">bool&lt;/span> shouldRender = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">bool&lt;/span> ShouldRender() =&amp;gt; shouldRender;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> HeavyOperation()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> shouldRender = &lt;span style="color:#79c0ff">false&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Do a bunch of state changes without triggering renders&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">for&lt;/span> (&lt;span style="color:#ff7b72">int&lt;/span> i = &lt;span style="color:#a5d6ff">0&lt;/span>; i &amp;lt; &lt;span style="color:#a5d6ff">1000&lt;/span>; i++)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> items[i].Process();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> shouldRender = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> StateHasChanged(); &lt;span style="color:#8b949e;font-style:italic">// Now render once with all changes&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>私はこれを使用して、大きなリストを処理するコンポーネントを最適化しました。項目が変更されるたびに再レンダリングするのではなく、更新をバッチ処理し、最後に 1 回レンダリングします。&lt;/p>
&lt;h1 id="破棄--disposeasync">破棄 / DisposeAsync&lt;/h1>
&lt;p>コンポーネントが UI から削除された場合は、リソースをクリーンアップする必要があります。 &lt;code>IDisposable&lt;/code> または &lt;code>IAsyncDisposable&lt;/code> を実装します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@implements IAsyncDisposable
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> Timer? timer;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> IJSObjectReference? jsModule;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> OnInitialized()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> timer = &lt;span style="color:#ff7b72">new&lt;/span> Timer(OnTick, &lt;span style="color:#79c0ff">null&lt;/span>, &lt;span style="color:#a5d6ff">0&lt;/span>, &lt;span style="color:#a5d6ff">1000&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnAfterRenderAsync(&lt;span style="color:#ff7b72">bool&lt;/span> firstRender)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (firstRender)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> jsModule = &lt;span style="color:#ff7b72">await&lt;/span> JS.InvokeAsync&amp;lt;IJSObjectReference&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;import&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;./timer.js&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> ValueTask DisposeAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> timer?.Dispose();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (jsModule &lt;span style="color:#ff7b72">is&lt;/span> not &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> jsModule.DisposeAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>破棄する一般的なもの: タイマー、イベント サブスクリプション、JS モジュール参照、&lt;code>CancellationTokenSource&lt;/code>、および作成した &lt;code>IDisposable&lt;/code> サービス。&lt;/p>
&lt;h1 id="状態が変化しましたこれはライフサイクル手法ではありませんが密接に関連していますこれは-blazor-に状態が変わったので再レンダリングしてくださいと伝えます-blazor-はイベント-ハンドラーの後に自動的にこれを呼び出しますが通常は通常の-blazor-イベント-フローの外側から状態が変化する場合に手動で呼び出す必要がある場合があります">状態が変化しましたこれはライフサイクル手法ではありませんが、密接に関連しています。これは Blazor に「状態が変わったので、再レンダリングしてください」と伝えます。 Blazor はイベント ハンドラーの後に自動的にこれを呼び出しますが、通常は通常の Blazor イベント フローの外側から状態が変化する場合に、手動で呼び出す必要がある場合があります。&lt;/h1>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task StartPolling()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">while&lt;/span> (!cts.Token.IsCancellationRequested)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> data = &lt;span style="color:#ff7b72">await&lt;/span> Http.GetFromJsonAsync&amp;lt;Data&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;api/data&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> StateHasChanged(); &lt;span style="color:#8b949e;font-style:italic">// Manual call needed since this isn&amp;#39;t a Blazor event&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> Task.Delay(&lt;span style="color:#a5d6ff">5000&lt;/span>, cts.Token);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>重要な注意事項: Blazor Server のバックグラウンド スレッドから更新している場合は、 &lt;code>InvokeAsync&lt;/code> を使用してください。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> InvokeAsync(() =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> data = newData;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> StateHasChanged();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="全体像">全体像&lt;/h1>
&lt;p>すべてが起こる順序は次のとおりです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>Component created
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ SetParametersAsync
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ OnInitialized / OnInitializedAsync
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ OnParametersSet / OnParametersSetAsync
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ Render
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ OnAfterRender(firstRender: true)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Parameter change from parent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ SetParametersAsync
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ OnParametersSet / OnParametersSetAsync
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ ShouldRender?
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ Render
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ OnAfterRender(firstRender: false)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Component removed
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> └─ Dispose / DisposeAsync
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>このフローを頭の中に入れておくと、ライフサイクルの問題のデバッグがはるかに簡単になります。&lt;/p>
&lt;p>投稿が気に入っていただければ幸いです!ソーシャルメディアで**@emimontesdeoca**までお気軽にご連絡ください。&lt;/p>
&lt;h1 id="リソース">リソース&lt;/h1>
&lt;ul>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/components/lifecycle">ASP.NET Core Blazor コンポーネントのライフサイクル&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/components/lifecycle#component-disposal-with-idisposable-and-iasyncdisposable">IDisposable および IAsyncDisposable によるコンポーネントの破棄&lt;/a>&lt;/li>
&lt;/ul></content:encoded><category>.NET</category><category>Blazor</category><category>C#</category></item><item><title>クリスマスを救う Microsoft のエージェント フレームワーク</title><link>https://emimontesdeoca.github.io/ja/posts/agent-framework-christmas-presents/</link><pubDate>Tue, 16 Dec 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/agent-framework-christmas-presents/</guid><description>Microsoft の Agent Framework と .NET を使用して、マルチエージェントのクリスマス ギフト ショッピング システムを構築します。</description><content:encoded>&lt;h2 id="はじめに">はじめに&lt;/h2>
&lt;p>完璧なクリスマスプレゼントを見つけるのはストレスになるかもしれません。ギフトのアイデアをブレインストーミングしたり、店舗間の価格を比較したり、すべてが予定通りに届くようにしたりする間に、ホリデー ショッピングはすぐに大変になってしまいます。これらのタスクを、連携して動作する専門の AI エージェントに委任できたらどうなるでしょうか?この投稿では、Microsoft のエージェント フレームワークを使用して、ギフトのアイデアの生成から価格の比較まで、ワークフローを通じてすべてが調整される特定のタスクを各エージェントが専門とするマルチエージェント システムを構築する方法を説明します。&lt;/p>
&lt;h2 id="フェスティブ-テック-カレンダー-2025">フェスティブ テック カレンダー 2025&lt;/h2>
&lt;p align="center">
&lt;img src="https://sessionize.com/image/49aa-1140o400o3-sdJUGhdR3FCmm1KuPRM3D3.png"/>
&lt;/p>
&lt;p>このプロジェクトは、ホリデー シーズン中のテクノロジーを祝う素晴らしいコミュニティ イベントである &lt;strong>Festive Tech Calendar 2025&lt;/strong> での私のセッションの一部です。イベントの詳細については、&lt;a href="https://sessionize.com/festive-tech-calendar-2025/">Sessionize&lt;/a> をご覧ください。&lt;/p>
&lt;h2 id="microsoft-のエージェント-フレームワークとは何ですか">Microsoft のエージェント フレームワークとは何ですか?&lt;/h2>
&lt;p>Agent Framework は、AI エージェントおよびマルチエージェント システムを構築、オーケストレーション、展開するための Microsoft のソリューションです。これは、順次、同時、またはハンドオフ パターンを通じて動作できるエージェントを作成するための柔軟な基盤を提供します。このフレームワークは OpenAI、Azure OpenAI、Microsoft Foundry モデルをサポートしており、非常に多用途です。&lt;/p>
&lt;p>主な機能は次のとおりです。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>マルチエージェント オーケストレーション&lt;/strong>: グループ チャット、順次、同時、ハンドオフ パターン&lt;/li>
&lt;li>&lt;strong>プラグイン エコシステム&lt;/strong>: ネイティブ関数、OpenAPI、およびモデル コンテキスト プロトコル (MCP) で拡張します。&lt;/li>
&lt;li>&lt;strong>ワークフロー サポート&lt;/strong>: エグゼキュータとエッジを使用して複雑なエージェント パイプラインを構築&lt;/li>
&lt;/ul>
&lt;h2 id="前提条件">前提条件&lt;/h2>
&lt;p>コードに入る前に、以下が揃っていることを確認してください。&lt;/p>
&lt;ul>
&lt;li>.NET9&lt;/li>
&lt;li>Azure OpenAI アクセス (または OpenAI API キー)&lt;/li>
&lt;li>Visual Studio または Visual Studio Code&lt;/li>
&lt;/ul>
&lt;p>必要なパッケージをインストールします (プレビュー中は &lt;code>--prerelease&lt;/code> フラグが必要であることに注意してください)。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.Agents.AI.OpenAI --prerelease
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.Agents.AI.Workflows --prerelease
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Azure.AI.OpenAI
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Azure.Identity
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Azure.AI.Agents.Persistent --prerelease
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="ギフト-ショッピング-エージェント">ギフト ショッピング エージェント&lt;/h2>
&lt;p>当社のクリスマス プレゼント ファインダーは、連携する 3 人の専門エージェントで構成されます。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>ギフト アイデア エージェント&lt;/strong> - 受取人のプロフィールに基づいてクリエイティブなギフトの提案を生成します&lt;/li>
&lt;li>&lt;strong>価格比較エージェント&lt;/strong> - さまざまな店舗間で最高の価格を見つけます&lt;/li>
&lt;li>&lt;strong>サマリー エージェント&lt;/strong> - 最終的な推奨事項をまとめます&lt;/li>
&lt;/ol>
&lt;h2 id="モデル">モデル&lt;/h2>
&lt;p>まず、エージェント パイプラインを流れるデータ モデルを定義します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">GiftRecipient&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Name { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">string&lt;/span>.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> Age { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; Interests { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = [];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">decimal&lt;/span> Budget { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">GiftIdea&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Name { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">string&lt;/span>.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Description { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">string&lt;/span>.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Category { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">string&lt;/span>.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">decimal&lt;/span> EstimatedPrice { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">PriceResult&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GiftName { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">string&lt;/span>.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Store { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">string&lt;/span>.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">decimal&lt;/span> Price { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Url { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">string&lt;/span>.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">GiftRecommendation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> GiftIdea Gift { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;PriceResult&amp;gt; Prices { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = [];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> PriceResult? BestDeal { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="エージェントの構築">エージェントの構築&lt;/h2>
&lt;p>エージェント フレームワークの利点は、特化したエージェントを簡単に作成できることです。各エージェントは、その専門知識を定義する特定のシステム プロンプトを備えた単純な &lt;code>ChatClientAgent&lt;/code> です。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.AI.OpenAI&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.Identity&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.Agents.AI&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.Agents.AI.Workflows&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.Extensions.AI&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ChristmasAgentFactory&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> AIAgent CreateGiftIdeaAgent(IChatClient chatClient)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> ChatClientAgent(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> chatClient,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">@&amp;#34;You are a creative Christmas gift advisor. When given information about a person
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> (age, interests, budget), you suggest thoughtful and personalized gift ideas.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> For each suggestion, provide:
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Gift name
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Brief description of why it&amp;#39;s a good fit
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Category (Electronics, Books, Fashion, Home, Experience, etc.)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Estimated price range
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Always suggest 3-5 gift options within the specified budget.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Format your response as a structured list.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> AIAgent CreatePriceComparisonAgent(IChatClient chatClient)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> ChatClientAgent(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> chatClient,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">@&amp;#34;You are a price comparison specialist. Given a list of gift ideas,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> you research and compare prices from different online retailers.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> For each gift, provide:
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Store name (Amazon, Best Buy, Target, Walmart, etc.)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Current price
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Any available discounts or deals
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Always highlight the best deal for each item.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Consider shipping costs and delivery times for Christmas.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> AIAgent CreateSummaryAgent(IChatClient chatClient)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> ChatClientAgent(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> chatClient,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">@&amp;#34;You are a gift recommendation summarizer. Take the gift ideas and price
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> comparisons and create a final recommendation report.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Your summary should:
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Rank gifts by value (quality vs price)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Highlight the top pick with reasoning
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Include total cost estimate
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Add any tips for holiday shopping
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Make the summary cheerful and festive!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="ワークフローの作成">ワークフローの作成&lt;/h2>
&lt;p>ここからが楽しい部分で、エージェントを一連のワークフローに接続します。エージェント フレームワークは、エージェントをさまざまなパターンに構成するための &lt;code>WorkflowBuilder&lt;/code> および &lt;code>AgentWorkflowBuilder&lt;/code> を提供します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ChristmasGiftWorkflow&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task RunAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Set up the Azure OpenAI client&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> endpoint = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;AZURE_OPENAI_ENDPOINT&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ?? &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> InvalidOperationException(&lt;span style="color:#a5d6ff">&amp;#34;AZURE_OPENAI_ENDPOINT is not set.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> deploymentName = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;AZURE_OPENAI_DEPLOYMENT_NAME&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ?? &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o-mini&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> chatClient = &lt;span style="color:#ff7b72">new&lt;/span> AzureOpenAIClient(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Uri(endpoint),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> DefaultAzureCredential())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .GetChatClient(deploymentName)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AsIChatClient();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Create our specialized agents&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> AIAgent giftIdeaAgent = ChristmasAgentFactory.CreateGiftIdeaAgent(chatClient);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> AIAgent priceAgent = ChristmasAgentFactory.CreatePriceComparisonAgent(chatClient);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> AIAgent summaryAgent = ChristmasAgentFactory.CreateSummaryAgent(chatClient);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Build a sequential workflow: Ideas -&amp;gt; Prices -&amp;gt; Summary&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> workflow = AgentWorkflowBuilder.BuildSequential(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;ChristmasGiftFinder&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [giftIdeaAgent, priceAgent, summaryAgent]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Define our gift recipient&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> recipient = &lt;span style="color:#ff7b72">new&lt;/span> GiftRecipient
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Mom&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Age = &lt;span style="color:#a5d6ff">55&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Interests = [&lt;span style="color:#a5d6ff">&amp;#34;gardening&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;cooking&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;reading&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;yoga&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Budget = &lt;span style="color:#a5d6ff">100&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> prompt = &lt;span style="color:#a5d6ff">$@&amp;#34;Find Christmas gifts for {recipient.Name},
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> age {recipient.Age}, who enjoys {string.Join(&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;, recipient.Interests)}.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Budget: &lt;span style="color:#f85149">$&lt;/span>{recipient.Budget}&lt;span style="color:#a5d6ff">&amp;#34;;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Execute the workflow with streaming&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">StreamingRun&lt;/span> run = &lt;span style="color:#ff7b72">await&lt;/span> InProcessExecution.StreamAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> workflow,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> ChatMessage(ChatRole.User, prompt));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Send the turn token to start processing&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> run.TrySendMessageAsync(&lt;span style="color:#ff7b72">new&lt;/span> TurnToken(emitEvents: &lt;span style="color:#79c0ff">true&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Watch for workflow events&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (WorkflowEvent evt &lt;span style="color:#ff7b72">in&lt;/span> run.WatchStreamAsync())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (evt &lt;span style="color:#ff7b72">is&lt;/span> AgentRunUpdateEvent agentUpdate)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.Write(agentUpdate.Data);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span> &lt;span style="color:#ff7b72">if&lt;/span> (evt &lt;span style="color:#ff7b72">is&lt;/span> WorkflowOutputEvent outputEvent)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;\n🎄 Final Recommendations:&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(outputEvent.Data);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="エージェントの同時実行">エージェントの同時実行&lt;/h2>
&lt;p>複数の人へのギフトを一度に検索したい場合はどうすればよいですか?エージェント フレームワークは同時実行をサポートしているため、次のシナリオに最適です。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task FindGiftsForEveryoneAsync(IChatClient chatClient, List&amp;lt;GiftRecipient&amp;gt; recipients)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Create an agent for each recipient&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> agents = recipients.Select(r =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span> ChatClientAgent(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> chatClient,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">$@&amp;#34;Find the perfect Christmas gift for {r.Name} (age {r.Age}),
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> who loves {string.Join(&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;, r.Interests)}. Budget: ${r.Budget}.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Provide one well-researched recommendation with price.&lt;span style="color:#a5d6ff">&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> )).ToList();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Build a concurrent workflow - all agents run in parallel&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> workflow = AgentWorkflowBuilder.BuildConcurrent(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;FamilyGiftFinder&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> agents);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">StreamingRun&lt;/span> run = &lt;span style="color:#ff7b72">await&lt;/span> InProcessExecution.StreamAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> workflow,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> ChatMessage(ChatRole.User, &lt;span style="color:#a5d6ff">&amp;#34;Find gifts now!&amp;#34;&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> run.TrySendMessageAsync(&lt;span style="color:#ff7b72">new&lt;/span> TurnToken(emitEvents: &lt;span style="color:#79c0ff">true&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (WorkflowEvent evt &lt;span style="color:#ff7b72">in&lt;/span> run.WatchStreamAsync())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (evt &lt;span style="color:#ff7b72">is&lt;/span> WorkflowOutputEvent output)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;🎁 All gift recommendations ready!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(output.Data);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="より詳細な制御のためのカスタム-エグゼキュータより複雑なシナリオの場合はワークフローをきめ細かく制御できるカスタム-エグゼキューターを作成できます">より詳細な制御のためのカスタム エグゼキュータより複雑なシナリオの場合は、ワークフローをきめ細かく制御できるカスタム エグゼキューターを作成できます。&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">sealed&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">GiftValidatorExecutor&lt;/span> : Executor&amp;lt;List&amp;lt;ChatMessage&amp;gt;, List&amp;lt;ChatMessage&amp;gt;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> GiftValidatorExecutor() : &lt;span style="color:#ff7b72">base&lt;/span>(&lt;span style="color:#a5d6ff">&amp;#34;GiftValidator&amp;#34;&lt;/span>) { }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> ValueTask&amp;lt;List&amp;lt;ChatMessage&amp;gt;&amp;gt; HandleAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> List&amp;lt;ChatMessage&amp;gt; messages,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IWorkflowContext context,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CancellationToken cancellationToken = &lt;span style="color:#ff7b72">default&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> lastMessage = messages.LastOrDefault()?.Text ?? &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Validate that suggestions are within budget&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (lastMessage.Contains(&lt;span style="color:#a5d6ff">&amp;#34;over budget&amp;#34;&lt;/span>, StringComparison.OrdinalIgnoreCase))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;⚠️ Some suggestions exceeded budget, filtering...&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Add validation logic here&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;✅ Gift suggestions validated!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> messages;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>次に、このエグゼキューターをワークフローに挿入できます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> validator = &lt;span style="color:#ff7b72">new&lt;/span> GiftValidatorExecutor();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> workflow = &lt;span style="color:#ff7b72">new&lt;/span> WorkflowBuilder(giftIdeaAgent)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddEdge(giftIdeaAgent, validator)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddEdge(validator, priceAgent)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddEdge(priceAgent, summaryAgent)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithOutputFrom(summaryAgent)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Build();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="bing-grounding-を使用して実際の-web-検索を追加する">Bing Grounding を使用して実際の Web 検索を追加する&lt;/h2>
&lt;p>これまでのところ、エージェントは AI モデルの知識に基づいて応答を生成しています。しかし、Web で実際の製品の価格と在庫状況を検索したい場合はどうすればよいでしょうか?ここで、&lt;strong>Bing Search によるグラウンディング&lt;/strong> が登場します。これは、Microsoft Foundry (旧称 Azure AI Foundry) で利用可能なツールで、エージェントが応答を生成するときにリアルタイムの公開 Web データを組み込むことができます。&lt;/p>
&lt;p>まず、&lt;a href="https://portal.azure.com/#create/Microsoft.BingGroundingSearch">Azure Portal&lt;/a> で &lt;strong>Grounding with Bing Search&lt;/strong> リソースを作成する必要があります。必ず AI プロジェクトと同じリソース グループに作成してください。&lt;/p>
&lt;h3 id="bing-search-を使用したグラウンディングの設定">Bing Search を使用したグラウンディングの設定&lt;/h3>
&lt;p>Grounding with Bing Search の利点は、Azure AI エージェントと直接統合できることです。エージェントは、ユーザーのクエリに基づいて検索ツールをいつ使用するかを決定し、Web を検索し、その結果を使用して根拠のある応答を生成します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.AI.Agents.Persistent&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.Identity&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">BingGroundingSetup&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;PersistentAgent&amp;gt; CreateAgentWithBingGroundingAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> projectEndpoint,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> modelDeploymentName,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> bingConnectionId)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Create the Persistent Agents client&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> agentClient = &lt;span style="color:#ff7b72">new&lt;/span> PersistentAgentsClient(projectEndpoint, &lt;span style="color:#ff7b72">new&lt;/span> DefaultAzureCredential());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Configure the Bing Grounding tool&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> bingGroundingTool = &lt;span style="color:#ff7b72">new&lt;/span> BingGroundingToolDefinition(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> BingGroundingSearchToolParameters(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [new BingGroundingSearchConfiguration(bingConnectionId)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Create the agent with Bing Grounding enabled&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> agent = &lt;span style="color:#ff7b72">await&lt;/span> agentClient.Administration.CreateAgentAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> model: modelDeploymentName,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> name: &lt;span style="color:#a5d6ff">&amp;#34;ChristmasPriceHunter&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> instructions: &lt;span style="color:#a5d6ff">@&amp;#34;You are a Christmas gift price comparison specialist.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> When given gift ideas, use Bing to search for:
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Current prices at major retailers (Amazon, Best Buy, Target, Walmart)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Available discounts and holiday deals
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> - Shipping times to ensure delivery before Christmas
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Always provide URLs to the products you find.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Highlight the best deals and recommend where to buy.&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> tools: [bingGroundingTool]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> agent;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="real-search-を使用した価格比較エージェントの作成">Real Search を使用した価格比較エージェントの作成&lt;/h3>
&lt;p>次に、Web で実際の製品情報を検索する完全な価格比較エージェントを作成しましょう。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.AI.Agents.Persistent&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.Identity&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ChristmasPriceAgent&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> PersistentAgentsClient _client;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> _modelDeployment;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> _bingConnectionId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ChristmasPriceAgent(&lt;span style="color:#ff7b72">string&lt;/span> projectEndpoint, &lt;span style="color:#ff7b72">string&lt;/span> modelDeployment, &lt;span style="color:#ff7b72">string&lt;/span> bingConnectionId)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _client = &lt;span style="color:#ff7b72">new&lt;/span> PersistentAgentsClient(projectEndpoint, &lt;span style="color:#ff7b72">new&lt;/span> DefaultAzureCredential());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _modelDeployment = modelDeployment;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _bingConnectionId = bingConnectionId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; FindGiftPricesAsync(&lt;span style="color:#ff7b72">string&lt;/span> giftIdeas)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Create agent with Bing Grounding&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> bingTool = &lt;span style="color:#ff7b72">new&lt;/span> BingGroundingToolDefinition(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> BingGroundingSearchToolParameters(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [new BingGroundingSearchConfiguration(_bingConnectionId)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> agent = &lt;span style="color:#ff7b72">await&lt;/span> _client.Administration.CreateAgentAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> model: _modelDeployment,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> name: &lt;span style="color:#a5d6ff">&amp;#34;PriceHunter&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> instructions: &lt;span style="color:#a5d6ff">@&amp;#34;Search the web for current prices on the given gift ideas.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> For each gift, find prices from at least 2-3 different stores.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Include direct links to the products.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Note any Christmas sales or discounts available.&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> tools: [bingTool]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Create a thread for the conversation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> thread = &lt;span style="color:#ff7b72">await&lt;/span> _client.Threads.CreateThreadAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Add the gift ideas as a message&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> _client.Messages.CreateMessageAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> thread.Id,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MessageRole.User,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">$&amp;#34;Find current prices for these gift ideas: {giftIdeas}&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Run the agent&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> run = &lt;span style="color:#ff7b72">await&lt;/span> _client.Runs.CreateRunAsync(thread.Id, agent.Id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Wait for completion&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> Task.Delay(&lt;span style="color:#a5d6ff">500&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> run = &lt;span style="color:#ff7b72">await&lt;/span> _client.Runs.GetRunAsync(thread.Id, run.Id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">while&lt;/span> (run.Status == RunStatus.Queued || run.Status == RunStatus.InProgress);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Get the response&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> messages = _client.Messages.GetMessages(thread.Id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = messages
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(m =&amp;gt; m.Role == MessageRole.Agent)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .SelectMany(m =&amp;gt; m.ContentItems)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OfType&amp;lt;MessageTextContent&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .FirstOrDefault();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Clean up&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> _client.Threads.DeleteThreadAsync(thread.Id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> response?.Text ?? &lt;span style="color:#a5d6ff">&amp;#34;No results found.&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">finally&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Clean up the agent&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> _client.Administration.DeleteAgentAsync(agent.Id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="bing-grounding-をワークフローに統合する">Bing Grounding をワークフローに統合する&lt;/h3>
&lt;p>完全なギフト検索ワークフローで Bing を利用した価格エージェントを使用する方法は次のとおりです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task RunWithBingGroundingAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> projectEndpoint = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;PROJECT_ENDPOINT&amp;#34;&lt;/span>)!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> modelDeployment = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;MODEL_DEPLOYMENT_NAME&amp;#34;&lt;/span>) ?? &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> bingConnectionId = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;BING_CONNECTION_ID&amp;#34;&lt;/span>)!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Connection ID format: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.CognitiveServices/accounts/{account}/projects/{project}/connections/{connection}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;🔍 Searching the web for real prices with Bing Grounding...\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> priceAgent = &lt;span style="color:#ff7b72">new&lt;/span> ChristmasPriceAgent(projectEndpoint, modelDeployment, bingConnectionId);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// First, generate gift ideas (could come from another agent)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> giftIdeas = &lt;span style="color:#a5d6ff">&amp;#34;1. Milwaukee cordless drill set, 2. Weber portable grill, 3. Carhartt beanie&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Search for real prices&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> priceResults = &lt;span style="color:#ff7b72">await&lt;/span> priceAgent.FindGiftPricesAsync(giftIdeas);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;📊 Price Comparison Results:&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(priceResults);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;\n🎁 Happy Shopping! 🎁&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="bing-結果からの引用を処理する">Bing 結果からの引用を処理する&lt;/h3>
&lt;p>Bing Search を使用したグラウンディングの重要な側面の 1 つは、応答にソース Web サイトへのリンクを含む引用が含まれていることです。それらを抽出して表示する方法は次のとおりです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ProcessBingGroundingResponse(IEnumerable&amp;lt;PersistentThreadMessage&amp;gt; messages)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> message &lt;span style="color:#ff7b72">in&lt;/span> messages.Where(m =&amp;gt; m.Role == MessageRole.Agent))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> content &lt;span style="color:#ff7b72">in&lt;/span> message.ContentItems)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (content &lt;span style="color:#ff7b72">is&lt;/span> MessageTextContent textContent)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = textContent.Text;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Process URL citations&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (textContent.Annotations != &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> annotation &lt;span style="color:#ff7b72">in&lt;/span> textContent.Annotations)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (annotation &lt;span style="color:#ff7b72">is&lt;/span> MessageTextUriCitationAnnotation uriAnnotation)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Replace citation placeholder with markdown link&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> response = response.Replace(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> uriAnnotation.Text,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">$&amp;#34; [{uriAnnotation.UriCitation.Title}]({uriAnnotation.UriCitation.Uri})&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(response);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>価格比較エージェントが実行されると、&lt;strong>Bing Search によるグラウンディング&lt;/strong> を使用して、実際の製品リスト、現在の価格、利用可能なセールをライブ Web から検索します。エージェントはクエリに基づいていつ検索するかを自動的に決定し、適切な引用を含む根拠のある応答を返します。&lt;/p>
&lt;h2 id="完全なプログラム">完全なプログラム&lt;/h2>
&lt;p>&lt;code>Program.cs&lt;/code> ではすべてがどのようにまとめられているかを次に示します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.AI.OpenAI&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.Identity&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.Agents.AI&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.Agents.AI.Workflows&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.Extensions.AI&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;🎄 Christmas Gift Finder - Powered by AI Agents 🎄\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> endpoint = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;AZURE_OPENAI_ENDPOINT&amp;#34;&lt;/span>)!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> deployment = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;AZURE_OPENAI_DEPLOYMENT_NAME&amp;#34;&lt;/span>) ?? &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o-mini&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> chatClient = &lt;span style="color:#ff7b72">new&lt;/span> AzureOpenAIClient(&lt;span style="color:#ff7b72">new&lt;/span> Uri(endpoint), &lt;span style="color:#ff7b72">new&lt;/span> DefaultAzureCredential())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .GetChatClient(deployment)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AsIChatClient();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Create the agent team&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> ideaAgent = ChristmasAgentFactory.CreateGiftIdeaAgent(chatClient);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> priceAgent = ChristmasAgentFactory.CreatePriceComparisonAgent(chatClient);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> summaryAgent = ChristmasAgentFactory.CreateSummaryAgent(chatClient);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Build the workflow&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> workflow = AgentWorkflowBuilder.BuildSequential(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;ChristmasGiftWorkflow&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [ideaAgent, priceAgent, summaryAgent]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.Write(&lt;span style="color:#a5d6ff">&amp;#34;Who are you shopping for? &amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> recipientName = Console.ReadLine() ?? &lt;span style="color:#a5d6ff">&amp;#34;Someone special&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.Write(&lt;span style="color:#a5d6ff">&amp;#34;What are their interests? &amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> interests = Console.ReadLine() ?? &lt;span style="color:#a5d6ff">&amp;#34;general&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.Write(&lt;span style="color:#a5d6ff">&amp;#34;What&amp;#39;s your budget? $&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> budget = Console.ReadLine() ?? &lt;span style="color:#a5d6ff">&amp;#34;50&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> prompt = &lt;span style="color:#a5d6ff">$&amp;#34;Find Christmas gifts for {recipientName} who enjoys {interests}. Budget: ${budget}&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;\n🔍 Searching for the perfect gifts...\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> run = &lt;span style="color:#ff7b72">await&lt;/span> InProcessExecution.StreamAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> workflow,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> ChatMessage(ChatRole.User, prompt));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> run.TrySendMessageAsync(&lt;span style="color:#ff7b72">new&lt;/span> TurnToken(emitEvents: &lt;span style="color:#79c0ff">true&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> evt &lt;span style="color:#ff7b72">in&lt;/span> run.WatchStreamAsync())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">switch&lt;/span> (evt)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> AgentRunUpdateEvent update:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.Write(update.Update.Text);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> WorkflowOutputEvent output:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;\n\n🎁 Happy Shopping! 🎁&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>Microsoft のエージェント フレームワークを使用すると、各エージェントが特定のタスクに特化したマルチエージェント システムを驚くほど簡単に構築できます。ワークフローを通じてこれらのエージェントを調整することで、クリスマスの買い物などの複雑な問題に体系的かつ効率的な方法で取り組むことができます。&lt;/p>
&lt;p>これをさらに強力にするのは、ツールを通じて現実世界の機能を追加できることです。 Microsoft Foundry の &lt;strong>Grounding と Bing Search&lt;/strong> を統合することにより、当社の価格比較エージェントは実際に Web で現在の価格、取引、在庫状況を検索できるようになり、単純な AI チャットボットが適切な引用を備えた真に役立つショッピング アシスタントに変わります。&lt;/p>
&lt;p>フレームワークは逐次パターン、同時パターン、およびハンドオフ パターンをサポートしているため、ニーズに正確に一致するエージェント システムを設計できます。ギフトを探す場合でも、旅行を計画する場合でも、その他の複数ステップのタスクでも、エージェント フレームワークはそれを実現するための構成要素を提供します。このホリデー シーズンは、AI エージェントにリサーチを任せて、あなたはプレゼントのラッピングや家族との時間を楽しむことに集中しましょう。&lt;/p>
&lt;h2 id="ソースコード">ソースコード&lt;/h2>
&lt;p>この投稿で示されている概念は、公式のエージェント フレームワーク サンプルに基づいています。 &lt;a href="https://github.com/microsoft/agent-framework">Microsoft Agent Framework GitHub リポジトリ&lt;/a> でさらに多くの例を調べることができます。&lt;/p>
&lt;p>楽しい休暇とコーディングを楽しんでください! 🎄&lt;/p></content:encoded><category>.NET</category><category>Azure</category><category>AI</category><category>Agent Framework</category></item><item><title>AI を活用した RSS フィード アグリゲーターの構築</title><link>https://emimontesdeoca.github.io/ja/posts/ai-agent-socials/</link><pubDate>Fri, 12 Dec 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/ai-agent-socials/</guid><description>セマンティック カーネルと Azure OpenAI を使用して、RSS フィードの監視とソーシャル メディアの投稿生成を自動化します。</description><content:encoded>&lt;p>Microsoft MVP でありテクノロジー愛好家である私は、Microsoft の DevBlog で公開されている素晴らしいコンテンツの海に常に溺れていることに気づきます。 .NET の発表から Visual Studio の更新、Azure のイノベーションからセマンティック カーネルの詳細まで、Microsoft エコシステムでは常に新しくてエキサイティングなことが起こっています。&lt;/p>
&lt;p>問題？ &lt;strong>すべてを維持し続けることはほぼ不可能です。&lt;/strong>&lt;/p>
&lt;p>最新の発表を常に把握してネットワークと共有したいと考えていましたが、7 つの異なる RSS フィードを手動でチェックし、記事を読み、魅力的なソーシャル メディア投稿を作成し、すでに共有した内容を追跡すること自体がフルタイムの仕事になりつつありました。毎朝、ブラウザの複数のタブを開いて何十もの記事に目を通し、どの記事をすでに共有したかを思い出し、注意を引いた記事についての投稿を書くのに貴重な時間を費やしていました。&lt;/p>
&lt;p>そこで私は、開発者であれば誰もが行うことと同じことを行いました。&lt;strong>それを自動化しました。&lt;/strong>&lt;/p>
&lt;p>この包括的なガイドでは、新しいコンテンツの複数の Microsoft DevBlog RSS フィードを監視し、Azure OpenAI とセマンティック カーネルを使用して記事を分析して魅力的な投稿を生成し、分析された記事ごとに詳細なマークダウン ドキュメントを作成し、コンテンツを確認して共有できるように Telegram 経由で通知を送信し、投稿の重複を避けるためにすべてを追跡し、GitHub Actions 経由で自動的に実行する、AI を活用した RSS フィード アグリゲーターをどのように構築したかについて説明します。&lt;/p>
&lt;p>このソリューションのあらゆる側面を詳しく見てみましょう。&lt;/p>
&lt;h2 id="このプロジェクトの裏話">このプロジェクトの裏話&lt;/h2>
&lt;h3 id="情報過多と共存する">情報過多と共存する&lt;/h3>
&lt;p>このツールを作成する前の私の典型的な朝の様子を描いてみましょう。私は朝起きてコーヒーを飲み、ラップトップを開いて Microsoft 開発者エコシステムの最新情報をチェックしました。まず、メインの DevBlogs サイトに移動して、重大な発表があるかどうかを確認します。次に、.NET ブログを特にチェックします。それが私の主要なテクノロジ スタックだからです。その後、AI の重要性がますます高まっているため、セマンティック カーネルのブログに移動します。 IDE の更新は日常のワークフローに大きな影響を与える可能性があるため、Visual Studio ブログがリストの次に挙げられました。次に、CI/CD および GitHub 関連のニュースに関する DevOps ブログ、クラウド インフラストラクチャの更新に関する All Things Azure ブログ、最後にデータベースのイノベーションに関する Azure SQL ブログが続きました。&lt;/p>
&lt;p>チェックすべきフィードは 7 つあります。これらの各ブログは、週に複数の記事を公開します。.NET Conf や Build などの主要な発表期間には、1 日に複数の記事を公開することもあります。追跡し、読み、共有する必要がある記事は数十件になる可能性があります。ここで重要なのは、コミュニティと知識を共有することを重視する人間として、これらの記事をただ読むことだけを望んでいたわけではありません。最も価値のあるものを LinkedIn のネットワークと共有し、他の開発者も最新情報を入手できるようにしたいと考えました。しかし、LinkedIn で優れた投稿を作成するには時間がかかります。記事を徹底的に読み、重要なポイントを理解し、それが読者にとってなぜ重要なのかを考え、魅力的なフックを作成し、すべてを適切にフォーマットする必要があります。これに週に数本の記事を掛けると、作業時間になります。&lt;/p>
&lt;h3 id="私が本当に欲しかったもの">私が本当に欲しかったもの&lt;/h3>
&lt;p>何か月もこの問題に対処した後、私は腰を据えて、理想的な解決策とはどのようなものかを考えました。何よりもまず、重要な発表を二度と見逃したくありませんでした。新しい記事が公開されるとすぐに、システムが自動的にそれをキャッチする必要があります。また、AI に魅力的な投稿の作成を支援してもらい、コンテンツ作成の時間を節約したいと考えていました。これは、私の声を完全に置き換えるのではなく、カスタマイズできる確かな出発点を提供するためでした。&lt;/p>
&lt;p>一貫性も大きな要素でした。毎日手動で行うことを忘れずに、コンテンツを定期的に共有したいと考えていました。追跡の側面も重要でした。重複投稿やフォロワーの迷惑を避けるために、自分がすでに共有した内容を知る方法が必要でした。最後に、処理したすべての内容を永久に記録して整理整頓し、どのようなトピックを扱ったかを振り返って確認できるようにしたいと思いました。&lt;/p>
&lt;h3 id="ソリューションが形になる">ソリューションが形になる&lt;/h3>
&lt;p>私が思い描いていたソリューションは、GitHub Actions を使用して完全にハンズフリーでスケジュールに従って実行されるものです。ブラウザのタブを 1 つも開かなくても、7 つのフィードすべてが自動的に取得されます。 AI コンポーネントは実際にコンテンツを読んで理解し、視聴者にとって役立つ方法で要約します。投稿を一から書く必要がなくなり、すぐに共有できるソーシャル メディア コンテンツが作成され、必要に応じて調整できるようになります。すべてはレビューのために私の Telegram に送信されるので、携帯電話をざっと見て、何を共有するかを決めることができました。そしてもちろん、将来の参照のためにすべてを永久に記録します。&lt;/p>
&lt;h2 id="構築を始める前に">構築を始める前に&lt;/h2>
&lt;h3 id="マシンに必要なもの">マシンに必要なもの&lt;/h3>
&lt;p>このチュートリアルを進めるには、開発マシンにいくつかのものがインストールされている必要があります。最も重要なものは、.NET SDK バージョン 9.0 以降です。これは私たちのランタイムであり、必要なすべてのビルド ツールを提供します。インストールしていない場合は、dot.net にアクセスして最新バージョンをダウンロードしてください。 Windows、macOS、または Linux ではインストールは簡単です。&lt;/p>
&lt;p>バージョン管理のために Git もインストールする必要があります。コードを GitHub にプッシュし、自動化に GitHub Actions を使用するため、Git をローカルにセットアップすることが不可欠です。最新のバージョンであれば問題なく動作します。&lt;/p>
&lt;p>開発環境としては、Visual Studio または VS Code をお勧めします。個人的には、最近ほとんどの仕事に VS Code を使用しています。VS Code は軽量であり、C# Dev Kit 拡張機能による優れた C# サポートを備えているからです。ただし、完全な Visual Studio の方が使い慣れている場合は、それも完璧に機能します。&lt;/p>
&lt;h3 id="必要なサービスとアカウントローカル-ツール以外にもいくつかのサービスのアカウントが必要になります最も重要なものはai-分析を強化する-azure-openai-ですこれは従量課金制のサービスですがこの使用例ではコストは最小限で分析される記事あたり数セントです-azure-アカウントをお持ちでない場合はクレジットを含む無料試用版にサインアップして開始できます">必要なサービスとアカウントローカル ツール以外にも、いくつかのサービスのアカウントが必要になります。最も重要なものは、AI 分析を強化する Azure OpenAI です。これは従量課金制のサービスですが、この使用例ではコストは最小限で、分析される記事あたり数セントです。 Azure アカウントをお持ちでない場合は、クレジットを含む無料試用版にサインアップして開始できます。&lt;/h3>
&lt;p>通知には Telegram Bot を使用します。 Telegram の素晴らしい点は、ボット API が完全に無料で使用できることです。必要なだけボットを作成し、無制限にメッセージを送信できます。セットアッププロセスについては、このガイドの後半で説明します。&lt;/p>
&lt;p>最後に、コードをホストし、GitHub Actions を実行するために GitHub アカウントが必要になります。このプロジェクトには無料利用枠で十分です。 GitHub では、プライベート リポジトリでは月あたり 2,000 分のアクション ランタイムが提供され、パブリック リポジトリでは無制限のアクション ランタイムが提供されます。&lt;/p>
&lt;h3 id="これを可能にするライブラリ">これを可能にするライブラリ&lt;/h3>
&lt;p>私たちのプロジェクトは 3 つの主要な NuGet パッケージに依存しており、それぞれが特定の目的を果たします。&lt;/p>
&lt;p>1 つ目は HtmlAgilityPack で、.NET での HTML 解析のゴールド スタンダードです。ブログから記事を取得すると、ナビゲーション メニュー、フッター、広告、その他私たちが気にしないあらゆる種類の要素を含む、ページの完全な HTML が返されます。 HtmlAgilityPack を使用すると、その HTML を解析して、必要な記事コンテンツだけを抽出できます。&lt;/p>
&lt;p>2 番目のパッケージは Microsoft.SemanticKernel で、AI モデルをアプリケーションに統合するための Microsoft の SDK です。これは、.NET コードと GPT-4 などの大規模な言語モデルの間の橋渡しであると考えてください。 API 呼び出し、トークン管理、応答解析の複雑さをすべて処理するため、AI に実際に実行してもらいたいことに集中できます。&lt;/p>
&lt;p>3 番目のパッケージは System.ServiceModel.Syndication で、RSS フィードと Atom フィードを解析するための組み込みサポートを提供します。 RSS は古いテクノロジーのように思えるかもしれませんが、ブログやニュース サイトから構造化された更新情報を取得するための最良の方法であることに変わりはありません。このパッケージは、生の XML フィードを、扱いやすい厳密に型指定された C# オブジェクトに変換します。&lt;/p>
&lt;h2 id="アーキテクチャを理解する">アーキテクチャを理解する&lt;/h2>
&lt;h3 id="ピースがどのように組み合わされるか">ピースがどのように組み合わされるか&lt;/h3>
&lt;p>コードの説明に入る前に、すべてのコンポーネントがどのように連携して動作するかを説明しましょう。全体像を理解すると、実装の詳細がより明確になります。&lt;/p>
&lt;p>最上位レベルには、オーケストレーターとして機能するメインの Program.cs ファイルがあります。これはアプリケーションのエントリ ポイントであり、他のすべてのコンポーネントを調整します。アプリケーションが実行されると、まず環境変数 (API キーや Telegram 認証情報など) から設定が読み込まれます。次に、7 つの Microsoft DevBlog ソースすべてから RSS フィードを取得します。これらのフィードを処理するときに、同じ記事が複数のフィードに表示される場合に対処するために記事の重複が排除されます。各記事を追跡ファイルと照合して、すでに処理されているかどうかを確認します。新しい記事の場合は、処理のために AI アナライザーに渡します。ArticleAnalyzer クラスは、AI の魔法が起こる場所です。このコンポーネントは記事を受け取り、それに対していくつかの処理を実行します。まず、記事の URL から完全な HTML コンテンツを取得します。次に、その HTML からクリーンなテキストを抽出し、不要なナビゲーション要素、スクリプト、スタイルをすべて削除します。クリーン テキストを取得すると、慎重に作成されたプロンプトとともにセマンティック カーネルを介してそれを Azure OpenAI に送信します。 AI は記事を分析し、概要、重要なトピック、関連性の説明、そして最も重要なことに、すぐに使用できる LinkedIn 投稿を含む構造化された応答を返します。アナライザーはこの応答を解析し、このすべての情報を含む ArticleAnalysis オブジェクトを返します。&lt;/p>
&lt;p>MarkdownGenerator クラスは、ArticleAnalysis オブジェクトを取得し、その永続的なレコードを作成します。すべての記事のメタデータ、AI の分析、生成された投稿を含む、適切にフォーマットされたマークダウン ファイルが生成されます。これらのファイルは generated-posts ディレクトリに保存され、処理したすべてのものを検索可能なアーカイブとして保存できます。&lt;/p>
&lt;p>最後に、Telegram 統合により、生成された投稿コンテンツが携帯電話に送信されます。これは、人間として AI の作業をレビューし、それを共有するかどうかを決定するポイントです。ボットは投稿コンテンツを含むメッセージを送信します。それを LinkedIn に直接コピーするか、最初に変更することができます。&lt;/p>
&lt;h3 id="データの流れ">データの流れ&lt;/h3>
&lt;p>新しい記事が .NET ブログに公開されると何が起こるかを説明します。ワークフローは、GitHub Actions がスケジュールに従って (たとえば 6 時間ごとに) アプリケーションをトリガーすると開始されます。アプリケーションが起動し、7 つすべての RSS フィードの取得を開始します。各フィードは、そのブログの最新の記事を含む XML ドキュメントを返します。&lt;/p>
&lt;p>各フィードを解析すると、個々の記事が抽出され、リストに保存されます。ただし、ここには注意が必要です。DevBlog のメイン フィードには、個々のカテゴリ フィードにも表示される記事が含まれることがよくあります。したがって、「.NET 10」に関する記事は、メイン フィードと .NET 固有のフィードの両方に表示される可能性があります。これは、HashSet 内の URL を追跡することで処理され、重複が自動的に防止されます。&lt;/p>
&lt;p>重複を除去した記事のリストを取得したら、フィルタリングして最近の記事のみに絞ります。通常は、ここ 1 日ほどに公開された記事です。以前の実行ですでに処理した古い記事は処理したくありません。次に、最近の記事を追跡ファイルと照合してチェックします。記事についてすでに処理して投稿している場合は、その記事をスキップします。&lt;/p>
&lt;p>新しい記事ごとに、AI 分析パイプラインが開始されます。アナライザーは記事全体の HTML を取得し、クリーンアップして、プロンプトとともに GPT-4 に送信します。 AI は記事を読み取り、LinkedIn の投稿とともに包括的な分析を生成します。この分析を記録のためにマークダウン ファイルに保存します。分析が完了したら、メッセージをフォーマットし、テレグラム経由で送信します。メッセージには、URL とハッシュタグが追加された生成された投稿コンテンツが含まれます。携帯電話で通知を受け取り、投稿を確認し、気に入ったものがあれば、数回タップするだけでコピーして LinkedIn で共有できます。&lt;/p>
&lt;p>最後に、追跡ファイルを更新してこの記事を処理済みとしてマークします。これにより、今後の実行では再び処理されなくなります。ファイルが作成または変更された場合、GitHub Actions はこれらの変更をリポジトリにコミットして、すべての同期を維持します。&lt;/p>
&lt;h2 id="プロジェクトを最初からセットアップする">プロジェクトを最初からセットアップする&lt;/h2>
&lt;h3 id="ソリューション構造の作成">ソリューション構造の作成&lt;/h3>
&lt;p>構築を始めましょう。ターミナルを開き、プロジェクトを作成する場所に移動します。私はプロジェクトを Development フォルダーに整理しておくのが好きですが、自分にとって都合の良い場所にどこにでも置くことができます。&lt;/p>
&lt;p>まず、新しいソリューション ファイルを作成します。 .NET では、ソリューションは複数のプロジェクトを保持できるコンテナです。今のところプロジェクトは 1 つだけですが、ソリューションから始めると、後で必要に応じてプロジェクトを追加するのが簡単になります。コマンド &lt;code>dotnet new sln -n vs-feed-linkedin&lt;/code> を実行して、vs-feed-linkedin という名前のソリューションを作成します。&lt;/p>
&lt;p>次に、コンソール アプリケーション プロジェクトを作成する必要があります。整理しておくために、これを src サブディレクトリに置きます。 &lt;code>dotnet new console -n VsFeedLinkedin -o src&lt;/code> を実行して、src フォルダーに VsFeedLinkedin という名前のコンソール プロジェクトを作成します。次に、&lt;code>dotnet sln add src/VsFeedLinkedin.csproj&lt;/code> を使用してこのプロジェクトをソリューションに追加します。&lt;/p>
&lt;p>&lt;code>cd src&lt;/code> を使用して src ディレクトリに移動します。ここで NuGet パッケージを追加し、開発のほとんどを行います。&lt;/p>
&lt;h3 id="必要なパッケージの追加">必要なパッケージの追加&lt;/h3>
&lt;p>プロジェクトを作成したら、前に述べた 3 つの NuGet パッケージを追加する必要があります。これらの各コマンドを順番に実行します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet add package System.ServiceModel.Syndication --version 9.0.9
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel --version 1.30.0
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package HtmlAgilityPack --version 1.11.72
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これらのコマンドを実行すると、プロジェクト ファイルは次のようになります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;Project&lt;/span> Sdk=&lt;span style="color:#a5d6ff">&amp;#34;Microsoft.NET.Sdk&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;PropertyGroup&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;OutputType&amp;gt;&lt;/span>Exe&lt;span style="color:#7ee787">&amp;lt;/OutputType&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;TargetFramework&amp;gt;&lt;/span>net9.0&lt;span style="color:#7ee787">&amp;lt;/TargetFramework&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;ImplicitUsings&amp;gt;&lt;/span>enable&lt;span style="color:#7ee787">&amp;lt;/ImplicitUsings&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;Nullable&amp;gt;&lt;/span>enable&lt;span style="color:#7ee787">&amp;lt;/Nullable&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/PropertyGroup&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;ItemGroup&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;PackageReference&lt;/span> Include=&lt;span style="color:#a5d6ff">&amp;#34;HtmlAgilityPack&amp;#34;&lt;/span> Version=&lt;span style="color:#a5d6ff">&amp;#34;1.11.72&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;PackageReference&lt;/span> Include=&lt;span style="color:#a5d6ff">&amp;#34;Microsoft.SemanticKernel&amp;#34;&lt;/span> Version=&lt;span style="color:#a5d6ff">&amp;#34;1.30.0&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;PackageReference&lt;/span> Include=&lt;span style="color:#a5d6ff">&amp;#34;System.ServiceModel.Syndication&amp;#34;&lt;/span> Version=&lt;span style="color:#a5d6ff">&amp;#34;9.0.9&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/ItemGroup&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;/Project&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>プロジェクト ファイルは、.NET 9.0 をターゲットとして、暗黙的な using や null 許容参照型などの最新の C# 機能を使用して、実行可能ファイル (OutputType は Exe) を構築していることを .NET に伝えます。 ItemGroup セクションには、3 つのパッケージの依存関係とその正確なバージョンがリストされています。&lt;/p>
&lt;h2 id="rss-フィードを詳しく見る">RSS フィードを詳しく見る&lt;/h2>
&lt;h3 id="rss-とは何ですか">RSS とは何ですか?&lt;/h3>
&lt;p>フィードを取得するコードを書き始める前に、何を扱うのかを必ず理解してください。 RSS は Really Simple Syndication の略で、コンテンツ更新を配布するための標準化された XML 形式です。アイデアはシンプルです。ユーザーに新しいコンテンツがあるかどうかを確認するために Web サイトにアクセスするよう要求する代わりに、最近のコンテンツをリストした機械可読ファイルを公開します。アプリケーションはこのファイルを定期的にポーリングして、新しい記事を検出できます。&lt;/p>
&lt;p>RSS は 1990 年代後半から 2000 年代初頭にかけて存在しました。時代遅れのテクノロジーだと思うかもしれませんが、実際には今でも、特にブログ、ニュース サイト、ポッドキャストなどで広く使用されています。 RSS の利点はそのシンプルさです。これは定義された構造を持つ単なる XML であり、どのアプリケーションでも解析できます。&lt;/p>
&lt;h3 id="devblogs-フィードの構造microsoft-devblog-から-rss-フィードをフェッチすると特定の構造に従った-xml-ドキュメントが返されます最上位には単一のチャネル要素を含む-rss-要素がありますチャネルはブログ自体を表しブログのタイトルurl説明などのメタデータが含まれます">DevBlogs フィードの構造Microsoft DevBlog から RSS フィードをフェッチすると、特定の構造に従った XML ドキュメントが返されます。最上位には、単一のチャネル要素を含む rss 要素があります。チャネルはブログ自体を表し、ブログのタイトル、URL、説明などのメタデータが含まれます。&lt;/h3>
&lt;p>チャネル内には複数の item 要素があり、それぞれが個々のブログ投稿を表します。各項目には、タイトル (記事の見出し)、リンク (記事全文を読める URL)、pubDate (記事の公開日)、dc:creator 要素 (著者の名前)、1 つ以上の category 要素 (記事のタグ)、説明 (通常は記事の概要または抜粋) が含まれます。&lt;/p>
&lt;p>これがどのようなものかを単純化した例を次に示します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;UTF-8&amp;#34;?&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;rss&lt;/span> version=&lt;span style="color:#a5d6ff">&amp;#34;2.0&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;channel&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;title&amp;gt;&lt;/span>.NET Blog&lt;span style="color:#7ee787">&amp;lt;/title&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;link&amp;gt;&lt;/span>https://devblogs.microsoft.com/dotnet&lt;span style="color:#7ee787">&amp;lt;/link&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;description&amp;gt;&lt;/span>The latest news about .NET&lt;span style="color:#7ee787">&amp;lt;/description&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;item&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;title&amp;gt;&lt;/span>Announcing .NET 10&lt;span style="color:#7ee787">&amp;lt;/title&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;link&amp;gt;&lt;/span>https://devblogs.microsoft.com/dotnet/announcing-dotnet-10&lt;span style="color:#7ee787">&amp;lt;/link&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;pubDate&amp;gt;&lt;/span>Mon, 10 Dec 2025 12:00:00 GMT&lt;span style="color:#7ee787">&amp;lt;/pubDate&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;dc:creator&amp;gt;&lt;/span>Microsoft&lt;span style="color:#7ee787">&amp;lt;/dc:creator&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;category&amp;gt;&lt;/span>Announcements&lt;span style="color:#7ee787">&amp;lt;/category&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;category&amp;gt;&lt;/span>.NET&lt;span style="color:#7ee787">&amp;lt;/category&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;description&amp;gt;&lt;/span>Article summary...&lt;span style="color:#7ee787">&amp;lt;/description&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/item&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/channel&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;/rss&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>.NET の System.ServiceModel.Syndication パッケージの優れた点は、これらすべてを解析してくれることです。 XML ノードを手動で移動したり、RSS のバージョンの違いを心配したりする必要はありません。フィードをロードして、厳密に型指定されたオブジェクトを取得するだけです。&lt;/p>
&lt;h3 id="私たちが監視する-7-つのフィード">私たちが監視する 7 つのフィード&lt;/h3>
&lt;p>私の実装では、7 つの異なる Microsoft DevBlogs フィードを監視しています。 devblogs.microsoft.com/feed にあるメインの DevBlogs フィードでは、Microsoft がすべての開発者ブログで公開しているすべての情報を幅広く見ることができます。 devblogs.microsoft.com/dotnet/feed にある .NET 固有のフィードは、特に .NET のリリース、機能、ベスト プラクティスに焦点を当てています。 devblogs.microsoft.com/semantic-kernel/feed のセマンティック カーネル フィードでは、AI のオーケストレーションと統合について説明しています。これは、AI が現代の開発の中心となるにつれてますます重要になっています。&lt;/p>
&lt;p>Visual Studio フィード (devblogs.microsoft.com/visualstudio/feed) では、IDE の改善点と生産性機能に関する最新情報を入手できます。 devblogs.microsoft.com/devops/feed の DevOps フィードでは、Azure DevOps、GitHub、CI/CD のトピックがカバーされています。 devblogs.microsoft.com/all-things-azure/feed にある All Things Azure フィードは、クラウド サービスとアーキテクチャ パターンに焦点を当てています。最後に、devblogs.microsoft.com/azure-sql/feed にある Azure SQL フィードでは、データベースの革新と機能について説明しています。&lt;/p>
&lt;p>なぜメイン フィードと個々のカテゴリ フィードの両方をチェックするのか疑問に思われるかもしれません。メイン フィードは幅を与えてくれます。私が知らないものも含め、Microsoft 開発者ブログの記事が表示されます。カテゴリ フィードは私に深みを与えてくれます。たとえ新しいコンテンツによって記事がメイン フィードから追い出されてしまったとしても、カテゴリ フィードにより、私の関心の中核となる分野で重要なものを見逃すことがなくなります。&lt;/p>
&lt;h2 id="rss-フェッチ-ロジックの構築">RSS フェッチ ロジックの構築&lt;/h2>
&lt;h3 id="コアフェッチ関数">コアフェッチ関数&lt;/h3>
&lt;p>それでは、コードを書いてみましょう。私たちのアプリケーションの基礎は、RSS フィードを取得して解析する機能です。これを処理する関数は次のとおりです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;SyndicationFeed?&amp;gt; FetchRssFeedAsync(&lt;span style="color:#ff7b72">string&lt;/span> url)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> httpClient = &lt;span style="color:#ff7b72">new&lt;/span> HttpClient();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> httpClient.DefaultRequestHeaders.Add(&lt;span style="color:#a5d6ff">&amp;#34;User-Agent&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;VsFeedLinkedin/1.0&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> httpClient.GetStringAsync(url);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> stringReader = &lt;span style="color:#ff7b72">new&lt;/span> StringReader(response);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> settings = &lt;span style="color:#ff7b72">new&lt;/span> XmlReaderSettings
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> DtdProcessing = DtdProcessing.Parse,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MaxCharactersFromEntities = &lt;span style="color:#a5d6ff">1024&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> xmlReader = XmlReader.Create(stringReader, settings);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> SyndicationFeed.Load(xmlReader);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>このコードが何をするのかを見ていきましょう。まず、HTTP リクエストを行うための .NET の組み込みクラスである HttpClient を作成します。一部のサーバーは自身を識別しないリクエストをブロックするため、User-Agent ヘッダーを設定します。サーバーが必要としない場合でも、これを設定することをお勧めします。次に、フィード URL に対して GET リクエストを実行し、応答を文字列として受け取ります。この文字列には、RSS フィードの生の XML が含まれています。&lt;/p>
&lt;p>この XML を解析するには、応答文字列をラップする StringReader を作成し、いくつかの XmlReaderSettings を構成します。 DtdProcessing 設定は重要です。RSS フィードには、処理が必要な DTD (Document Type Definition) 宣言が含まれる場合があります。 MaxCharactersFromEntities 設定は、発生するエンティティ拡張の量を制限することで XML 爆弾攻撃を防ぐセキュリティ対策です。&lt;/p>
&lt;p>最後に、これらの設定で XmlReader を作成し、SyndicationFeed.Load を使用して XML を解析して、厳密に型指定された SyndicationFeed オブジェクトにします。これにより、生の XML ナビゲーションではなく、優れた C# プロパティを通じてフィードのメタデータとそのすべてのアイテムにアクセスできるようになります。&lt;/p>
&lt;h3 id="エラー処理を使用した複数のフィードの取得">エラー処理を使用した複数のフィードの取得&lt;/h3>
&lt;p>現実の世界では、ネットワーク リクエストは失敗します。サーバーがダウンし、接続がタイムアウトになり、XML の形式が不正になる可能性があります。私たちはこうしたケースを丁寧に処理する必要があります。障害に強く、すべてのフィードを取得する方法は次のとおりです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> allArticles = &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;(SyndicationItem item, &lt;span style="color:#ff7b72">string&lt;/span> feedUrl)&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> seenUrls = &lt;span style="color:#ff7b72">new&lt;/span> HashSet&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> feedUrl &lt;span style="color:#ff7b72">in&lt;/span> feedUrls)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34; 📡 Fetching {feedUrl}...&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> feed = &lt;span style="color:#ff7b72">await&lt;/span> FetchRssFeedAsync(feedUrl);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (feed?.Items != &lt;span style="color:#79c0ff">null&lt;/span> &amp;amp;&amp;amp; feed.Items.Any())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> item &lt;span style="color:#ff7b72">in&lt;/span> feed.Items)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> itemUrl = item.Links.FirstOrDefault()?.Uri.ToString() ?? &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrEmpty(itemUrl) &amp;amp;&amp;amp; seenUrls.Add(itemUrl))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> allArticles.Add((item, feedUrl));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception ex)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34; ⚠️ Failed to fetch {feedUrl}: {ex.Message}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ここでは 2 つのコレクションを管理しています。 allArticles リストには、見つかったすべての記事が、どのフィードから来たのかとともに保持されます。 seenUrls HashSet は、すでに見た記事の URL を追跡し、重複を避けるのに役立ちます。&lt;/p>
&lt;p>各フィード URL をループし、取得操作を try-catch ブロックでラップします。特定のフィードの取得が失敗した場合 (おそらくサーバーが一時的にダウンしている可能性があります)、警告をログに記録し、次のフィードを続行します。このようにすると、1 つのフィードに問題があっても、他のフィードの処理が妨げられることはありません。&lt;/p>
&lt;p>正常にフェッチされたフィードごとに、その項目を反復処理します。アイテムのリンク コレクションから記事の URL を抽出します。 URL がすでにセット内にある場合、HashSet.Add メソッドは false を返します。これは重複排除ロジックに最適です。記事が新しい場合にのみリストに追加します。&lt;/p>
&lt;p>各記事の横にフィード URL を保存します。これは、この情報が後で役立つ可能性があるためです。たとえば、デバッグやログ記録の目的で、記事がどの特定のフィードから来たのかを知りたい場合があります。&lt;/p>
&lt;h2 id="重複の処理と状態の追跡">重複の処理と状態の追跡&lt;/h2>
&lt;h3 id="重複排除の課題">重複排除の課題&lt;/h3>
&lt;p>前に述べたように、Microsoft DevBlogs には階層的なフィード構造があり、これが興味深い課題を生み出しています。 .NET チームのメンバーが、たとえば .NET 10 のパフォーマンス向上に関する記事を公開すると、その記事はメインの DevBlogs フィードと .NET 固有のフィードの両方に表示される可能性があります。場合によっては、IDE 機能に関連する場合、Visual Studio フィードに表示されることもあります。&lt;/p>
&lt;p>すべてのフィードのすべての記事を単純に処理すると、同じ記事を何度も分析して投稿することになります。これにより、Azure OpenAI への API 呼び出しが無駄になり、Telegram に重複した通知がスパム送信され、重複した通知を投稿するとフォロワーが迷惑する可能性があります。解決策は URL ベースの重複排除です。各記事には固有の URL があるため、それを識別子として使用できます。 HashSet データ構造は、O(1) の検索時間を提供し、重複を自動的に防止するため、これに最適です。すでにセット内にある URL を追加しようとすると、Add メソッドは単に false を返し、その記事をスキップする必要があることを知らせます。&lt;/p>
&lt;h3 id="マークダウンを使用した永続的な状態">マークダウンを使用した永続的な状態&lt;/h3>
&lt;p>重複排除は 1 回の実行内で重複を処理しますが、複数の実行ではどうなるでしょうか?アプリケーションが 6 時間ごとに実行される場合、どの記事がすでに処理されたかを記憶し、再度処理しないようにする必要があります。&lt;/p>
&lt;p>この状態を posted-articles.md というマークダウン ファイルに保存することにしました。なぜマークダウンなのか？理由はいくつかあります。まず、人間が判読できるという点です。ファイルを開いて、どの記事を共有したかをすぐに確認できます。 2 番目に、バージョン管理されています。このファイルは Git リポジトリに存在するため、記事が処理されたときの完全な履歴がわかります。第三に、ドキュメントとしての役割を果たします。リポジトリを見れば誰でもアプリケーションが何をしたかを確認できます。&lt;/p>
&lt;p>このファイルの形式は単純です。これには、ヘッダー、アプリケーションが最後に実行された時間を示すタイムスタンプ、そしてマークダウン リンク形式の記事のリストが含まれています。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-markdown" data-lang="markdown">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#79c0ff;font-weight:bold"># Posted Articles
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#79c0ff;font-weight:bold">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="font-style:italic">*Last run: 2025-12-10 15:30:00*&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>List of articles posted to LinkedIn:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">-&lt;/span> [&lt;span style="color:#7ee787">Announcing .NET 10&lt;/span>](https://devblogs.microsoft.com/dotnet/announcing-dotnet-10?wt.mc_id=DT-MVP-5004972) - Posted on 2025-12-10 15:30:00 (Published: 2025-12-10)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">-&lt;/span> [&lt;span style="color:#7ee787">Visual Studio 2026 Preview&lt;/span>](https://devblogs.microsoft.com/visualstudio/vs-2026-preview?wt.mc_id=DT-MVP-5004972) - Posted on 2025-12-09 10:15:00 (Published: 2025-12-09)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="トラッキング-ファイルのロードと解析">トラッキング ファイルのロードと解析&lt;/h3>
&lt;p>記事がすでに処理されているかどうかを確認するには、このファイルをロードして URL を抽出する必要があります。これを行う関数は次のとおりです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">static&lt;/span> HashSet&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; LoadPostedArticles(&lt;span style="color:#ff7b72">string&lt;/span> filePath)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> postedUrls = &lt;span style="color:#ff7b72">new&lt;/span> HashSet&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!File.Exists(filePath))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> postedUrls;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> lines = File.ReadAllLines(filePath);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> line &lt;span style="color:#ff7b72">in&lt;/span> lines)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> match = System.Text.RegularExpressions.Regex.Match(line, &lt;span style="color:#a5d6ff">@&amp;#34;\(([^)]+)\)&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (match.Success)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> url = match.Groups[&lt;span style="color:#a5d6ff">1&lt;/span>].Value;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (url.Contains(&lt;span style="color:#a5d6ff">&amp;#34;?wt.mc_id=&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> url = url.Substring(&lt;span style="color:#a5d6ff">0&lt;/span>, url.IndexOf(&lt;span style="color:#a5d6ff">&amp;#34;?wt.mc_id=&amp;#34;&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span> &lt;span style="color:#ff7b72">if&lt;/span> (url.Contains(&lt;span style="color:#a5d6ff">&amp;#34;?&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> url = url.Substring(&lt;span style="color:#a5d6ff">0&lt;/span>, url.IndexOf(&lt;span style="color:#a5d6ff">&amp;#34;?&amp;#34;&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> url = url.TrimEnd(&lt;span style="color:#a5d6ff">&amp;#39;/&amp;#39;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> postedUrls.Add(url);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> postedUrls;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>この関数は、すでに処理したすべての URL を含む HashSet を返します。まずファイルが存在するかどうかを確認します。最初の実行では存在しないため、空のセットを返します。&lt;/p>
&lt;p>ファイル内の各行について、正規表現を使用してマークダウン リンク形式から URL を抽出します。正規表現 &lt;code>\(([^)]+)\)&lt;/code> は、マークダウン リンクが URL を保存する括弧内のすべてのものと一致します。&lt;/p>
&lt;p>次に、URL の正規化という重要なステップが始まります。同じ記事の URL の形式が異なる場合があります。 RSS フィードには &lt;code>https://devblogs.microsoft.com/dotnet/article&lt;/code> が表示される場合がありますが、保存されたバージョンには追跡パラメーターが追加されています: &lt;code>https://devblogs.microsoft.com/dotnet/article?wt.mc_id=DT-MVP-5004972&lt;/code>。 URL には末尾にスラッシュが付いているものと、付いていないものがあります。&lt;/p>
&lt;p>これを処理するには、クエリ パラメータ (&lt;code>?&lt;/code> 以降のすべて) を削除し、末尾のスラッシュを削除します。この正規化により、URL が表面的に異なる場合でも記事が重複していると認識されるようになります。&lt;/p>
&lt;h3 id="新しい記事を保存する">新しい記事を保存する&lt;/h3>
&lt;p>記事が正常に処理されたら、それを追跡ファイルに追加する必要があります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> SavePostedArticle(&lt;span style="color:#ff7b72">string&lt;/span> filePath, &lt;span style="color:#ff7b72">string&lt;/span> url, &lt;span style="color:#ff7b72">string&lt;/span> title, DateTimeOffset publishDate)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> markdownEntry = &lt;span style="color:#a5d6ff">$&amp;#34;- [{title}]({url}) - Posted on {DateTime.Now:yyyy-MM-dd HH:mm:ss} (Published: {publishDate:yyyy-MM-dd})\n&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!File.Exists(filePath))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> File.WriteAllText(filePath, &lt;span style="color:#a5d6ff">&amp;#34;# Posted Articles\n\n*Last run: {DateTime.Now:yyyy-MM-dd HH:mm:ss}*\n\nList of articles posted:\n\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> File.AppendAllText(filePath, markdownEntry);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>この関数は、記事のタイトルをリンクとして付けたマークダウン形式のエントリを作成し、その後に記事を投稿したときと最初に公開したときを示すタイムスタンプが続きます。ファイルがまだ存在しない場合は、最初にヘッダーを付けてファイルを作成します。&lt;/p>
&lt;h2 id="ai-分析エンジン">AI 分析エンジン&lt;/h2>
&lt;h3 id="セマンティック-カーネルを理解するここでアプリケーションの最もエキサイティングな部分である-ai-分析に進みます-semantic-kernel-は大規模な言語モデルをアプリケーションに統合するための-microsoft-のオープンソース-sdk-ですこれは単なる-api-呼び出しのラッパーではありませんプラグインプランナーメモリなどの機能を備えた高度な-ai-アプリケーションを構築するためのフレームワークを提供します">セマンティック カーネルを理解するここで、アプリケーションの最もエキサイティングな部分である AI 分析に進みます。 Semantic Kernel は、大規模な言語モデルをアプリケーションに統合するための Microsoft のオープンソース SDK です。これは単なる API 呼び出しのラッパーではありません。プラグイン、プランナー、メモリなどの機能を備えた高度な AI アプリケーションを構築するためのフレームワークを提供します。&lt;/h3>
&lt;p>このユースケースでは、セマンティック カーネルのチャット完了機能を使用しています。 Azure OpenAI にプロンプ​​トを送信すると、モデルが記事を分析して応答を生成します。セマンティック カーネルは、複雑な API 認証、リクエストのフォーマット、レスポンスの解析をすべて処理します。&lt;/p>
&lt;h3 id="記事アナライザーのセットアップ">記事アナライザーのセットアップ&lt;/h3>
&lt;p>アナライザー クラスを設定する方法を見てみましょう。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.ChatCompletion&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">HtmlAgilityPack&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">VsFeedLinkedin.Services&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ArticleAnalyzer&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> Kernel _kernel;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> IChatCompletionService _chatService;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ArticleAnalyzer(&lt;span style="color:#ff7b72">string&lt;/span> endpoint, &lt;span style="color:#ff7b72">string&lt;/span> apiKey, &lt;span style="color:#ff7b72">string&lt;/span> deploymentName)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> builder = Kernel.CreateBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.AddAzureOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: deploymentName,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: endpoint,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: apiKey
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _kernel = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _chatService = _kernel.GetRequiredService&amp;lt;IChatCompletionService&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>セマンティック カーネルは、構成にビルダー パターンを使用します。 KernelBuilder を作成し、必要な資格情報を備えた Azure OpenAI チャット完了サービスを追加して、カーネルを構築します。構築されたカーネルから、プロンプトの送信と応答の受信に使用する IChatCompletionService インターフェイスを取得します。&lt;/p>
&lt;p>コンストラクターは、Azure OpenAI エンドポイント (&lt;code>https://your-resource.openai.azure.com/&lt;/code> など)、API キー、デプロイメント名 (&lt;code>gpt-4o&lt;/code> など) の 3 つのパラメーターを取ります。これらは環境変数から渡され、認証情報を安全に保ちます。&lt;/p>
&lt;h3 id="完璧なプロンプトを作成する">完璧なプロンプトを作成する&lt;/h3>
&lt;p>AI に送信するプロンプトは非常に重要です。適切に作成されたプロンプトにより、一貫した高品質の出力が生成されます。プロンプトが曖昧だったり、構造が不十分だったりすると、一貫性のない平凡な結果が生じます。満足のいく出力を得るために、このプロンプトを繰り返すのにかなりの時間を費やしました。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> prompt = &lt;span style="color:#a5d6ff">$&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> You are a professional tech content analyst and LinkedIn content creator.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Analyze the following Microsoft DevBlogs article and create an engaging LinkedIn post.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Article Title: {title}
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Author: {author}
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> URL: {url}
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> Tags: {string.Join(&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;, tags)}
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Article Content:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {cleanContent}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Please provide:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">1.&lt;/span> A brief summary (&lt;span style="color:#a5d6ff">2&lt;/span>-&lt;span style="color:#a5d6ff">3&lt;/span> sentences) of the key points
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">2.&lt;/span> The main technologies or topics covered
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">3.&lt;/span> Why &lt;span style="color:#ff7b72">this&lt;/span> &lt;span style="color:#ff7b72">is&lt;/span> relevant &lt;span style="color:#ff7b72">for&lt;/span> developers/tech professionals
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">4.&lt;/span> An engaging LinkedIn post (max &lt;span style="color:#a5d6ff">1300&lt;/span> characters) that:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Starts with a hook or attention-grabbing statement
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Highlights the key &lt;span style="color:#ff7b72">value&lt;/span> &lt;span style="color:#ff7b72">for&lt;/span> readers
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Includes a call to action
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Uses appropriate emojis (but not too many)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Maintains a professional yet approachable tone
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - DO NOT include hashtags &lt;span style="color:#ff7b72">in&lt;/span> the post (they will be added separately)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - DO NOT include the URL &lt;span style="color:#ff7b72">in&lt;/span> the post (it will be added separately)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Format your response &lt;span style="color:#ff7b72">as&lt;/span> follows:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">##&lt;/span> Summary
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Your summary here]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">##&lt;/span> Key Topics
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [List of main topics/technologies]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">##&lt;/span> Relevance
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Why this matters]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">##&lt;/span> LinkedIn Post
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Your engaging LinkedIn post here]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;;
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ここで設計上の決定について説明しましょう。まずは AI に「あなたはプロのテクノロジー コンテンツ アナリストであり、LinkedIn コンテンツ クリエイターです」という明確な役割を与えることから始めます。これにより、モデルは適切なスタイルと音声で応答できるようになります。&lt;/p>
&lt;p>AI が必要とするすべてのコンテキスト (記事のタイトル、著者、URL、RSS フィードのタグ、記事の全コンテンツ) を提供します。提供するコンテキストが多ければ多いほど、分析はより適切になります。&lt;/p>
&lt;p>次に、何を返したいかを正確に指定します。要約、重要なトピック、関連性の説明、LinkedIn への投稿の 4 つをお願いします。特に LinkedIn の投稿については、良い投稿の条件について詳しく説明します。フックがあり、価値が強調され、行動喚起が含まれ、絵文字が適切に使用され、プロフェッショナルな口調が維持される必要があります。&lt;/p>
&lt;p>否定的な指示も同様に重要です。投稿にハッシュタグや URL を含めないよう AI に明示的に指示します。なぜ？これらを個別に追加するため、AI がそれらを含めると重複が発生するためです。この種の明示的な指示により、よくある間違いを防ぐことができます。&lt;/p>
&lt;p>最後に、正確な出力形式を指定します。 ## ヘッダーでマークされたセクションを要求することで、プログラムによる応答の解析が容易になります。 AI はフォーマット指示に従うのが非常に得意で、この一貫性により解析コードがよりシンプルかつ信頼性の高いものになります。&lt;/p>
&lt;h3 id="分析の実行">分析の実行&lt;/h3>
&lt;p>すべてをまとめると次のようになります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;ArticleAnalysis&amp;gt; AnalyzeArticleAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> title,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> url,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> htmlContent,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> author,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; tags)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> cleanContent = ExtractTextFromHtml(htmlContent);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (cleanContent.Length &amp;gt; &lt;span style="color:#a5d6ff">8000&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> cleanContent = cleanContent.Substring(&lt;span style="color:#a5d6ff">0&lt;/span>, &lt;span style="color:#a5d6ff">8000&lt;/span>) + &lt;span style="color:#a5d6ff">&amp;#34;...&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> chatHistory = &lt;span style="color:#ff7b72">new&lt;/span> ChatHistory();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> chatHistory.AddUserMessage(prompt);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> _chatService.GetChatMessageContentAsync(chatHistory);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> responseText = response.Content ?? &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> ParseAnalysisResponse(responseText, title, url, author, tags);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```まず、&lt;/span>HTML &lt;span style="color:#f85149">コンテンツからクリーン&lt;/span> &lt;span style="color:#f85149">テキストを抽出します&lt;/span> (&lt;span style="color:#f85149">これについては次のセクションで説明します&lt;/span>)&lt;span style="color:#f85149">。次に、コンテンツが長すぎる場合は切り捨てます。大規模な言語モデルにはトークン制限があり、非常に長い記事はトークン制限を超える可能性があります。文字数を&lt;/span> &lt;span style="color:#a5d6ff">8&lt;/span>,&lt;span style="color:#a5d6ff">000&lt;/span> &lt;span style="color:#f85149">文字に制限することで、実質的なコンテキストを提供しながら制限内に収まることを保証します。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ChatHistory &lt;span style="color:#f85149">オブジェクトを作成し、プロンプトをユーザー&lt;/span> &lt;span style="color:#f85149">メッセージとして追加します。これは、チャットベースの対話のためのセマンティック&lt;/span> &lt;span style="color:#f85149">カーネルの抽象化です。これをチャット完了サービスに送信し、応答を返します。最後に、応答を解析して個々のセクションを抽出します。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">###&lt;/span> AI &lt;span style="color:#f85149">応答の解析&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>AI &lt;span style="color:#f85149">は、要求された構造でフォーマットされたテキストとして応答を返します。これを個々のフィールドに解析する必要があります。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```&lt;/span>csharp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> ArticleAnalysis ParseAnalysisResponse(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> response,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> title,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> url,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> author,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; tags)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> analysis = &lt;span style="color:#ff7b72">new&lt;/span> ArticleAnalysis
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Title = title,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Url = url,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Author = author,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Tags = tags,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RawAnalysis = response
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> sections = response.Split(&lt;span style="color:#a5d6ff">&amp;#34;##&amp;#34;&lt;/span>, StringSplitOptions.RemoveEmptyEntries);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> section &lt;span style="color:#ff7b72">in&lt;/span> sections)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> lines = section.Trim().Split(&lt;span style="color:#a5d6ff">&amp;#39;\n&amp;#39;&lt;/span>, &lt;span style="color:#a5d6ff">2&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (lines.Length &amp;lt; &lt;span style="color:#a5d6ff">2&lt;/span>) &lt;span style="color:#ff7b72">continue&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> sectionTitle = lines[&lt;span style="color:#a5d6ff">0&lt;/span>].Trim().ToLower();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> sectionContent = lines[&lt;span style="color:#a5d6ff">1&lt;/span>].Trim();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">switch&lt;/span> (sectionTitle)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;summary&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> analysis.Summary = sectionContent;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;key topics&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> analysis.KeyTopics = sectionContent;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;relevance&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> analysis.Relevance = sectionContent;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;linkedin post&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> analysis.LinkedInPost = sectionContent;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> analysis;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>応答を &lt;code>##&lt;/code> マーカーで分割し、各セクションを取得します。各セクションは改行で分割され、ヘッダーとコンテンツが分離されます。次に、switch ステートメントを使用して、各セクションのコンテンツを適切なプロパティに割り当てます。&lt;/p>
&lt;p>解析されていない生の応答も保存します。これはデバッグに役立ちます。解析で何か問題が発生した場合、AI が実際に何を返したかを確認できます。&lt;/p>
&lt;h2 id="html-からのコンテンツの抽出">HTML からのコンテンツの抽出&lt;/h2>
&lt;h3 id="html-をクリーンアップする必要がある理由">HTML をクリーンアップする必要がある理由&lt;/h3>
&lt;p>ブログから記事を取得すると、ページの完全な HTML が取得されます。これには記事のコンテンツだけではなく、ナビゲーション メニュー、ヘッダー、フッター、サイドバー、関連記事ウィジェット、コメント セクション、分析と追跡用のスクリプト、スタイルシート、その他あらゆる種類の要素が含まれます。&lt;/p>
&lt;p>これらすべてを AI に送信すると、いくつかの悪いことが起こるでしょう。 AI は大量の無関係なテキストを処理する必要があり、トークンが無駄になり、分析が混乱する可能性があります。ナビゲーションとフッターのテキストが概要に含まれる場合があります。スクリプトと CSS はコンテンツとして扱われるため、分析がさらに汚染されてしまいます。&lt;/p>
&lt;p>記事のコンテンツ、つまり人間の読者が実際に読む部分だけを抽出する必要があります。&lt;/p>
&lt;h3 id="htmlagilitypack-の使用">HtmlAgilityPack の使用&lt;/h3>
&lt;p>HtmlAgilityPack は、.NET 用の堅牢な HTML 解析ライブラリです。 XML とは異なり、HTML は不正な形式であることがよくあります。タグが適切に閉じられていなかったり、属性が正しく引用されていなかったりする可能性があります。 HtmlAgilityPack はこれらすべてを適切に処理し、クエリと操作が可能な DOM のような構造を提供します。&lt;/p>
&lt;p>抽出関数は次のとおりです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> ExtractTextFromHtml(&lt;span style="color:#ff7b72">string&lt;/span> html)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrWhiteSpace(html))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> doc = &lt;span style="color:#ff7b72">new&lt;/span> HtmlDocument();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> doc.LoadHtml(html);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> nodesToRemove = doc.DocumentNode.SelectNodes(&lt;span style="color:#a5d6ff">&amp;#34;//script|//style|//nav|//footer|//header&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (nodesToRemove != &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> node &lt;span style="color:#ff7b72">in&lt;/span> nodesToRemove)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> node.Remove();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> text = doc.DocumentNode.InnerText;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> text = System.Text.RegularExpressions.Regex.Replace(text, &lt;span style="color:#a5d6ff">@&amp;#34;\s+&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34; &amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> text.Trim();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>HTML を HtmlDocument にロードし、HTML をツリー構造に解析します。次に、XPath を使用して、削除するすべてのノードを選択します。 XPath 式 &lt;code>//script|//style|//nav|//footer|//header&lt;/code> は、すべてのスクリプト要素 (不要な JavaScript コード)、スタイル要素 (不要な CSS)、nav 要素 (ナビゲーション メニュー)、フッター要素、およびヘッダー要素を選択します。&lt;/p>
&lt;p>これらのノードを削除すると、HTML タグを削除しながらすべてのテキスト コンテンツを抽出する InnerText プロパティを取得します。これにより、記事のプレーンテキストが得られます。最後に、空白を削除します。 HTML には、複数のスペース、タブ、改行など、書式設定のための余分な空白が多く含まれていることがよくあります。正規表現を使用して一連の空白文字を単一のスペースに置き換え、結果をトリミングします。&lt;/p>
&lt;h3 id="記事全文の取得">記事全文の取得&lt;/h3>
&lt;p>RSS フィードでは概要のみが提供され、記事の全内容は提供されません。完全なテキストを取得するには、記事の Web ページを取得する必要があります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; FetchArticleContentAsync(&lt;span style="color:#ff7b72">string&lt;/span> url)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> httpClient = &lt;span style="color:#ff7b72">new&lt;/span> HttpClient();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> httpClient.DefaultRequestHeaders.Add(&lt;span style="color:#a5d6ff">&amp;#34;User-Agent&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;VsFeedLinkedin/1.0&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">await&lt;/span> httpClient.GetStringAsync(url);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception ex)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;⚠️ Failed to fetch article content: {ex.Message}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>.Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これは簡単です。記事の URL に対して HTTP GET リクエストを作成し、HTML レスポンスを返します。ネットワークリクエストは失敗する可能性があり、アプリケーション全体をクラッシュさせるよりは空の文字列を返したいため、これを try-catch でラップします。&lt;/p>
&lt;h2 id="永続的なドキュメントの作成">永続的なドキュメントの作成&lt;/h2>
&lt;h3 id="マークダウン-ファイルを生成する理由">マークダウン ファイルを生成する理由&lt;/h3>
&lt;p>記事を分析するたびに、その分析を文書化した詳細なマークダウン ファイルが生成されます。これにはいくつかの目的があります。&lt;/p>
&lt;p>まず、検索可能なアーカイブを作成します。時間が経つにつれて、分析された記事のコレクションが構築されます。これらのファイルを検索して、特定のトピックに関する過去のコンテンツを見つけることができます。&lt;/p>
&lt;p>第二に、透明性が得られます。完全な分析や LinkedIn の投稿など、AI が各記事に対して何を生成したかを正確に確認できます。&lt;/p>
&lt;p>第三に、デバッグに役立ちます。投稿で何か問題が発生した場合は、マークダウン ファイルを調べて何が起こったのかを理解できます。&lt;/p>
&lt;h3 id="マークダウン-ジェネレーター-クラス">マークダウン ジェネレーター クラス&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">MarkdownGenerator&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> _outputDirectory;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> MarkdownGenerator(&lt;span style="color:#ff7b72">string&lt;/span> outputDirectory)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _outputDirectory = outputDirectory;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!Directory.Exists(_outputDirectory))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Directory.CreateDirectory(_outputDirectory);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GenerateMarkdownFile(ArticleAnalysis analysis)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> sb = &lt;span style="color:#ff7b72">new&lt;/span> StringBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> safeTitle = GenerateSafeFileName(analysis.Title);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> fileName = &lt;span style="color:#a5d6ff">$&amp;#34;{analysis.AnalyzedAt:yyyy-MM-dd}_{safeTitle}.md&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> filePath = Path.Combine(_outputDirectory, fileName);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">$&amp;#34;# {analysis.Title}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;## Article Information&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">$&amp;#34;- **Author:** {analysis.Author}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">$&amp;#34;- **URL:** [{analysis.Url}]({analysis.Url})&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">$&amp;#34;- **Published:** {analysis.PublishDate:yyyy-MM-dd}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">$&amp;#34;- **Analyzed:** {analysis.AnalyzedAt:yyyy-MM-dd HH:mm:ss}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">$&amp;#34;- **Tags:** {string.Join(&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;, analysis.Tags)}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;## AI Analysis&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;### Summary&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(analysis.Summary);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;### Key Topics&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(analysis.KeyTopics);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;### Relevance for Developers&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(analysis.Relevance);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;## Generated LinkedIn Post&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;```&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#f85149">分析&lt;/span>.LinkedInPost);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;```&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sb.AppendLine(&lt;span style="color:#a5d6ff">&amp;#34;*This analysis was generated using AI (Semantic Kernel with Azure OpenAI)*&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> File.WriteAllText(filePath, sb.ToString());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> filePath;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>コンストラクターは出力ディレクトリのパスを取得し、存在しない場合はそれを作成します。 GenerateMarkdownFile メソッドは ArticleAnalysis オブジェクトを受け取り、適切にフォーマットされたマークダウン ドキュメントを生成します。&lt;/p>
&lt;p>ファイル名には、日付とタイトルのサニタイズされたバージョンが含まれます。これにより、ファイルを時系列に簡単に並べ替えて、一目で識別できるようになります。&lt;/p>
&lt;h3 id="安全でないファイル名の処理">安全でないファイル名の処理&lt;/h3>
&lt;p>記事のタイトルには、ファイル名に使用できない文字 (コロン、スラッシュ、疑問符、引用符など) を含めることができます。これらをサニタイズする必要があります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GenerateSafeFileName(&lt;span style="color:#ff7b72">string&lt;/span> title)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> invalidChars = Path.GetInvalidFileNameChars();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> safeTitle = &lt;span style="color:#ff7b72">new&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>(title
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(c =&amp;gt; !invalidChars.Contains(c))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToArray());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> safeTitle = safeTitle.Replace(&lt;span style="color:#a5d6ff">&amp;#34; &amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-&amp;#34;&lt;/span>).Replace(&lt;span style="color:#a5d6ff">&amp;#34;--&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (safeTitle.Length &amp;gt; &lt;span style="color:#a5d6ff">50&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> safeTitle = safeTitle.Substring(&lt;span style="color:#a5d6ff">0&lt;/span>, &lt;span style="color:#a5d6ff">50&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> safeTitle.TrimEnd(&lt;span style="color:#a5d6ff">&amp;#39;-&amp;#39;&lt;/span>).ToLowerInvariant();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Path.GetInvalidFileNameChars() を使用して、現在のオペレーティング システムでファイル名に使用できない文字のリストを取得します。これらをフィルターで除外し、読みやすくするためにスペースをハイフンに置き換え、長さを 50 文字に制限し、一貫性を保つために小文字に変換します。&lt;/p>
&lt;h2 id="テレグラム通知の設定">テレグラム通知の設定&lt;/h2>
&lt;h3 id="私がテレグラムを選んだ理由">私がテレグラムを選んだ理由&lt;/h3>
&lt;p>通知コンポーネントについては、電子メール、SMS、Slack、Discord、Telegram などのいくつかのオプションを検討しました。私が最終的に Telegram を選んだ理由はいくつかあります。&lt;/p>
&lt;p>API は完全に無料で、適切な使用であればレート制限はありません。多くの通知サービスでは無料で送信できるメッセージの数に制限がありますが、Telegram はボット メッセージを個々のユーザーに制限しません。&lt;/p>
&lt;p>ボット API は非常にシンプルです。これは JSON ペイロードを含む単なる HTTP リクエストです。複雑な認証フローや、基本的な機能に必要な Webhook はありません。Telegram は、携帯電話、デスクトップ、Web ブラウザなど、どこでも機能します。どこにいても通知を受信し、すぐに応答できます。&lt;/p>
&lt;p>メッセージはリッチフォーマットをサポートします。太字テキスト、斜体、さらにはコード ブロックを使用して、通知を読みやすくすることができます。&lt;/p>
&lt;h3 id="telegram-ボットの作成">Telegram ボットの作成&lt;/h3>
&lt;p>Telegram ボットのセットアップは驚くほど簡単です。 Telegram を開いて @BotFather を検索します。これは、ボットを作成および管理するための Telegram の公式ボットです。 BotFather との会話を開始し、コマンド /newbot を送信します。 BotFather は、ボットの名前 (これは表示名です) とユーザー名 (これは一意であり、「bot」で終わる必要があります) を尋ねます。これらを指定すると、BotFather がボットを作成し、API トークンを提供します。このトークンはパスワードのようなものです。秘密にして、パブリック リポジトリにコミットしないでください。&lt;/p>
&lt;p>ボットがメッセージの送信先を認識できるようにチャット ID を見つけるには、チャット ID を検索して [開始] を押して、新しいボットとの会話を開始します。次に、ブラウザまたはカールを使用して URL &lt;code>https://api.telegram.org/bot&amp;lt;YOUR_TOKEN&amp;gt;/getUpdates&lt;/code> にアクセスします。応答で &lt;code>chat&lt;/code> オブジェクトを探します。&lt;code>id&lt;/code> フィールドはチャット ID です。&lt;/p>
&lt;h3 id="api-経由でメッセージを送信する">API 経由でメッセージを送信する&lt;/h3>
&lt;p>Telegram メッセージを送信するための関数は次のとおりです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task SendToTelegramAsync(&lt;span style="color:#ff7b72">string&lt;/span> botToken, &lt;span style="color:#ff7b72">string&lt;/span> chatId, &lt;span style="color:#ff7b72">string&lt;/span> message)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> httpClient = &lt;span style="color:#ff7b72">new&lt;/span> HttpClient();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> telegramApiUrl = &lt;span style="color:#a5d6ff">$&amp;#34;https://api.telegram.org/bot{botToken}/sendMessage&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> payload = &lt;span style="color:#ff7b72">new&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> chat_id = chatId,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> text = message,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> parse_mode = &lt;span style="color:#a5d6ff">&amp;#34;HTML&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> jsonContent = JsonSerializer.Serialize(payload);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> content = &lt;span style="color:#ff7b72">new&lt;/span> StringContent(jsonContent, Encoding.UTF8, &lt;span style="color:#a5d6ff">&amp;#34;application/json&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> httpClient.PostAsync(telegramApiUrl, content);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!response.IsSuccessStatusCode)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> errorContent = &lt;span style="color:#ff7b72">await&lt;/span> response.Content.ReadAsStringAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> Exception(&lt;span style="color:#a5d6ff">$&amp;#34;Telegram API error: {response.StatusCode} - {errorContent}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Telegram Bot API は REST ベースです。チャット ID (送信先)、メッセージ テキスト (送信内容)、およびオプションで解析モード (フォーマット用) を含む JSON 本文を使用して、sendMessage エンドポイントに対して POST リクエストを作成します。&lt;/p>
&lt;p>parse_mode を「HTML」に設定すると、&lt;code>&amp;lt;b&amp;gt;bold&amp;lt;/b&amp;gt;&lt;/code> や &lt;code>&amp;lt;i&amp;gt;italic&amp;lt;/i&amp;gt;&lt;/code> などの基本的な HTML タグをメッセージ内で使用できるようになります。これにより通知が読みやすくなりますが、現在の使用例ではプレーン テキストを送信します。&lt;/p>
&lt;p>リクエストが失敗した場合は、何が問題だったかの詳細を示す例外がスローされます。これは、何かが動作しない場合のデバッグに役立ちます。&lt;/p>
&lt;h2 id="アプリケーションの構成">アプリケーションの構成&lt;/h2>
&lt;h3 id="環境変数">環境変数&lt;/h3>
&lt;p>私たちのアプリケーションには、API キー、ボット トークン、エンドポイント URL など、いくつかの機密情報が必要です。これらをハードコーディングしたり、バージョン管理にコミットしたりしないでください。代わりに、アプリケーションが実行される各環境で安全に設定できる環境変数を使用します。&lt;/p>
&lt;p>Telegram の場合、TELEGRAM_BOT_TOKEN (BotFather から与えられたトークン) と TELEGRAM_CHAT_ID (メッセージが送信されるチャット ID) が必要です。&lt;/p>
&lt;p>Azure OpenAI の場合、AZURE_OPENAI_ENDPOINT (リソースの URL)、AZURE_OPENAI_API_KEY (API キー)、および AZURE_OPENAI_DEPLOYMENT (「gpt-4o」など、デプロイされたモデルの名前) が必要です。&lt;/p>
&lt;h3 id="コードで構成をロードする">コードで構成をロードする&lt;/h3>
&lt;p>アプリケーションの起動時にこれらの値をロードする方法は次のとおりです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> telegramBotToken = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;TELEGRAM_BOT_TOKEN&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> telegramChatId = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;TELEGRAM_CHAT_ID&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> azureOpenAiEndpoint = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;AZURE_OPENAI_ENDPOINT&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> azureOpenAiKey = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;AZURE_OPENAI_API_KEY&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> azureOpenAiDeployment = Environment.GetEnvironmentVariable(&lt;span style="color:#a5d6ff">&amp;#34;AZURE_OPENAI_DEPLOYMENT&amp;#34;&lt;/span>) ?? &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> aiAnalysisEnabled = !&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrWhiteSpace(azureOpenAiEndpoint) &amp;amp;&amp;amp;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> !&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrWhiteSpace(azureOpenAiKey);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>各値を読み取るには、Environment.GetEnvironmentVariable を使用します。デプロイメント名には、値が設定されていない場合、デフォルトの「gpt-4o」が提供されます。&lt;/p>
&lt;p>次に、エンドポイントと API キーの両方があることを確認して、AI 分析を有効にするかどうかを確認します。これにより、Azure OpenAI が構成されていない場合でも、アプリケーションは縮退モードで実行できます。AI 分析を行わずに、フィードを取得して記事を追跡します。### グレースフル デグラデーション&lt;/p>
&lt;p>このグレースフル デグラデーションの概念は重要です。 1 つのオプション機能が構成されていないという理由だけでアプリケーションがクラッシュすることは望ましくありません。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>ArticleAnalyzer? articleAnalyzer = &lt;span style="color:#79c0ff">null&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>MarkdownGenerator? markdownGenerator = &lt;span style="color:#79c0ff">null&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">if&lt;/span> (aiAnalysisEnabled)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;🤖 AI Analysis enabled - Using Azure OpenAI with Semantic Kernel&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> articleAnalyzer = &lt;span style="color:#ff7b72">new&lt;/span> ArticleAnalyzer(azureOpenAiEndpoint!, azureOpenAiKey!, azureOpenAiDeployment);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> markdownGenerator = &lt;span style="color:#ff7b72">new&lt;/span> MarkdownGenerator(articlesOutputDir);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;ℹ️ AI Analysis disabled - Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_API_KEY to enable&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>AI が有効な場合は、アナライザーとマークダウン ジェネレーターを作成します。そうでない場合は、それらを null のままにし、処理中の AI 関連のステップをスキップします。 AI の機能強化がなくても、アプリケーションはフィードを取得し、基本的な通知を送信することで価値を提供します。&lt;/p>
&lt;h2 id="github-アクションによる自動化">GitHub アクションによる自動化&lt;/h2>
&lt;h3 id="github-アクションを使用する理由">GitHub アクションを使用する理由&lt;/h3>
&lt;p>このソリューションの真の力は自動化によってもたらされます。アプリケーションを数時間ごとに手動で実行するのではなく、バックグラウンドで自動的に実行したいのです。&lt;/p>
&lt;p>GitHub Actions はこれに最適です。 GitHub に組み込まれているため、追加のサービスを設定する必要はありません。パブリック リポジトリは無料で、プライベート リポジトリには十分な無料時間が含まれています。スケジュールに基づいて実行でき、定期的にアプリケーションをトリガーします。 API キーを安全に保存するためのシークレット管理が組み込まれています。また、変更をリポジトリにコミットして、追跡ファイルを最新の状態に保つことができます。&lt;/p>
&lt;h3 id="ワークフロー-ファイル">ワークフロー ファイル&lt;/h3>
&lt;p>次の内容のファイルを .github/workflows/fetch-and-notify.yml に作成します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Fetch DevBlogs and Notify&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">on&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">schedule&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">cron&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">&amp;#39;0 */6 * * *&amp;#39;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">workflow_dispatch&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">jobs&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">fetch-and-notify&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">runs-on&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">ubuntu-latest&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">steps&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Checkout repository&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">uses&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">actions/checkout@v4&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Setup .NET&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">uses&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">actions/setup-dotnet@v4&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">with&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">dotnet-version&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">&amp;#39;9.0.x&amp;#39;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Restore dependencies&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">run&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">dotnet restore src/VsFeedLinkedin.csproj&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Build&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">run&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">dotnet build src/VsFeedLinkedin.csproj --no-restore&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Run application&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">env&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">TELEGRAM_BOT_TOKEN&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">${{ secrets.TELEGRAM_BOT_TOKEN }}&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">TELEGRAM_CHAT_ID&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">${{ secrets.TELEGRAM_CHAT_ID }}&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">AZURE_OPENAI_ENDPOINT&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">${{ secrets.AZURE_OPENAI_ENDPOINT }}&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">AZURE_OPENAI_API_KEY&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">${{ secrets.AZURE_OPENAI_API_KEY }}&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">AZURE_OPENAI_DEPLOYMENT&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">${{ secrets.AZURE_OPENAI_DEPLOYMENT }}&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">run&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">dotnet run --project src/VsFeedLinkedin.csproj&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Commit and push changes&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">run&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>|&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> git config user.name &amp;#34;GitHub Actions Bot&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> git config user.email &amp;#34;actions@github.com&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> if [[ -n $(git status --porcelain posted-articles.md generated-posts/) ]]; then
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> TIMESTAMP=$(date +%Y%m%d_%H%M%S)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> git add posted-articles.md generated-posts/
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> git commit -m &amp;#34;chore($TIMESTAMP): processed new articles&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> git push
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> else
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> echo &amp;#34;No changes to commit&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> fi&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>それぞれの部分について説明していきます。 on セクションでは、ワークフローをいつ実行するかを定義します。スケジュール トリガーは cron 構文を使用します。&lt;code>0 */6 * * *&lt;/code> は「6 時間ごとの 0 分に」を意味します。したがって、ワークフローは UTC の午前 0 時、午前 6 時、正午、午後 6 時に実行されます。 workflow_dispatch トリガーを使用すると、GitHub UI から手動で実行できるため、テストに役立ちます。&lt;/p>
&lt;p>ジョブは、Linux 仮想マシンである ubuntu-latest 上で実行されます。リポジトリをチェックアウトし、.NET 9 をセットアップし、NuGet パッケージを復元し、プロジェクトをビルドします。&lt;/p>
&lt;p>アプリケーションの実行ステップでは魔法が起こります。 ${{ Secrets.SECRET_NAME }} 構文を使用して、シークレットを環境変数として渡します。これらのシークレットは GitHub に安全に保存され、ログに公開されることはありません。&lt;/p>
&lt;p>最後に、変更をリポジトリにコミットします。ボット ID を使用して Git を構成し、追跡ファイルまたは生成された投稿ディレクトリに変更があるかどうかを確認し、変更がある場合はコミットを作成してプッシュします。&lt;/p>
&lt;h3 id="シークレットの設定">シークレットの設定&lt;/h3>
&lt;p>GitHub リポジトリにシークレットを追加するには、リポジトリの [設定]、[シークレットと変数]、[アクション] の順に移動します。 [新しいリポジトリ シークレット] をクリックし、各環境変数を追加します。名前は、ワークフロー ファイルで参照するものと正確に一致する必要があります。&lt;/p>
&lt;h2 id="まとめ">まとめ&lt;/h2>
&lt;h3 id="私たちが築いてきたもの">私たちが築いてきたもの&lt;/h3>
&lt;p>これまで取り上げてきたすべてを振り返ると、これまで面倒な手動プロセスであったものを自動化する、AI を活用した包括的な RSS フィード アグリゲーターを構築しました。このアプリケーションは 7 つの Microsoft DevBlog フィードを自動的に監視し、新しい記事が公開されるとすぐにそれをキャッチします。重複排除の複雑さを処理し、同じ記事が複数のフィードに表示されることを認識します。Semantic Kernel と Azure OpenAI を活用した AI 分析は、記事の内容を読んで理解し、要約の生成、重要なトピックの特定、関連性の説明をすべて自動的に行います。最も重要なことは、最小限の編集で共有できる魅力的な LinkedIn 投稿を作成できることです。&lt;/p>
&lt;p>Telegram の統合により、レビューする新しいコンテンツがあるたびに携帯電話に通知が届きます。メッセージをざっと見て、共有するかどうかを決定し、すぐに行動することができます。&lt;/p>
&lt;p>また、スケジュールに基づいて GitHub Actions 上で実行されるため、何もすることを覚えておく必要はありません。システムはバックグラウンドで動作しており、私は共有する価値のあるものがある場合にのみ関与します。&lt;/p>
&lt;h3 id="それを可能にしたテクノロジー">それを可能にしたテクノロジー&lt;/h3>
&lt;p>このプロジェクトでは、それぞれが重要な役割を果たすいくつかのテクノロジーを統合しました。 .NET 9 は、最新の言語機能と優れたパフォーマンスによる強固な基盤を提供しました。セマンティック カーネルは AI の統合を簡単にし、複雑な API 呼び出しと応答管理をすべて処理します。 Azure OpenAI は、技術的な内容を実際に理解して分析する機能であるインテリジェンスを提供しました。 HtmlAgilityPack は、Web ページからクリーン テキストを抽出するという厄介な問題を解決しました。 System.ServiceModel.Syndication により、RSS の解析が簡単になりました。 Telegram Bot API は無料で信頼性の高い通知を提供してくれました。そして、GitHub Actions は、自動化されたスケジュールされた実行によってすべてを結び付けました。&lt;/p>
&lt;h3 id="コストについて考える">コストについて考える&lt;/h3>
&lt;p>あなたが抱く疑問の 1 つは、「これを実行するのにどれくらいのコストがかかるのか?」ということです。答えは「あまり多くはない」です。&lt;/p>
&lt;p>Telegram は完全に無料です。ボットを介してメッセージを送信するのに料金はかかりません。&lt;/p>
&lt;p>GitHub Actions はパブリック リポジトリでは無料です。プライベート リポジトリの場合、無料枠で毎月 2,000 分を利用できますが、これはこのユースケースには十分以上です。&lt;/p>
&lt;p>Azure OpenAI は唯一の有料コンポーネントであり、コストは最小限です。 GPT-4o を使用すると、一般的なブログ記事の分析にかかる費用は 1 ～ 3 セントになります。たとえ月に数十の記事を処理しているとしても、AI コストは 1 ドル未満です。&lt;/p>
&lt;h3 id="次はどこに行けばよいでしょうか">次はどこに行けばよいでしょうか&lt;/h3>
&lt;p>このソリューションは私のニーズにはうまく機能しますが、拡張する方法はたくさんあります。複数のソーシャル プラットフォームのサポートを追加できます。LinkedIn に加えて Twitter/X、Mastodon、または Bluesky に投稿することもできます。センチメント分析を実装すると、長期にわたる記事の論調を追跡し、傾向を特定できます。フィードごとに異なるプロンプト テンプレートを許可し、トピックごとに異なるスタイルの投稿を生成できます。 Telegram を使用する代わりに、投稿を確認および管理するための Web ダッシュボードを構築することもできます。投稿されたコンテンツのエンゲージメント指標を追跡して、どのトピックが視聴者に最も共感を呼んだかを確認できます。&lt;/p>
&lt;h3 id="最終的な考えこのプロジェクトで私が最も気に入っている点は私が強く信じている哲学を体現していることですつまり退屈な部分は自動化で処理し創造的で意思決定の部分は人間に任せるべきですシステムは取得解析分析生成などの面倒な作業をすべて実行しますが共有する前にすべてを確認します-ai-によって生成された投稿はカスタマイズおよびパーソナライズできる出発点となります">最終的な考えこのプロジェクトで私が最も気に入っている点は、私が強く信じている哲学を体現していることです。つまり、退屈な部分は自動化で処理し、創造的で意思決定の部分は人間に任せるべきです。システムは、取得、解析、分析、生成などの面倒な作業をすべて実行しますが、共有する前にすべてを確認します。 AI によって生成された投稿は、カスタマイズおよびパーソナライズできる出発点となります。&lt;/h3>
&lt;p>.NET、セマンティック カーネル、Azure OpenAI の力を組み合わせることで、品質と一貫性を維持しながら毎週何時間もの手作業を節約できるツールを作成しました。これは、日常生活に大きな変化をもたらす実用的な自動化です。&lt;/p>
&lt;p>同様のものを作成したり、改善のアイデアがある場合は、ぜひお知らせください。 LinkedIn でお気軽にご連絡ください。&lt;/p>
&lt;p>コーディングを楽しんでください、そしてメリークリスマス! 🎄&lt;/p></content:encoded><category>.NET</category><category>Azure</category><category>NuGet</category><category>CI/CD</category><category>Docker</category><category>AI</category></item><item><title>Microsoft のエージェント フレームワークを使用したマルチエージェント AI システムの構築</title><link>https://emimontesdeoca.github.io/ja/posts/microsoft-agent-framework-multi-agent/</link><pubDate>Mon, 01 Dec 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/microsoft-agent-framework-multi-agent/</guid><description>.NET の Microsoft エージェント フレームワークを使用して、マルチエージェント AI システムを構築、オーケストレーション、展開するための実践的なガイドです。</description><content:encoded>&lt;h2 id="はじめに">はじめに&lt;/h2>
&lt;p>マルチエージェント AI システムの時代が到来しました。業界は、単一のモノリシック AI がすべてを処理するのではなく、よく組織された専門家チームのように、協力して複雑な問題を解決する専門エージェントに移行しています。 1 人のエージェントが調査を行い、もう 1 人が分析し、3 人目が書き込みを行い、コーディネーターが全員の進捗状況を把握します。&lt;/p>
&lt;p>大規模な言語モデルを扱ったことがある場合は、1 つのプロンプトで実行できる処理の上限に達している可能性があります。コンテキスト ウィンドウがいっぱいになり、命令が複雑になり、品質が低下します。マルチエージェント アーキテクチャは、各エージェントが 1 つの分野の専門家となり、複雑なタスクを焦点を絞った責任に分解することでこの問題を解決します。&lt;/p>
&lt;p>より広範なセマンティック カーネル エコシステムの一部である Microsoft のエージェント フレームワークは、.NET 開発者にまさにこの種のシステムを構築するための最上級のツールキットを提供します。この投稿では、ゼロからマルチエージェント パイプラインを完全に機能させるまでに取り組み、開始するために必要な中心的な概念、オーケストレーション パターン、実践的なコードを取り上げます。&lt;/p>
&lt;h2 id="microsoft-のエージェント-フレームワークとは何ですか">Microsoft のエージェント フレームワークとは何ですか?&lt;/h2>
&lt;p>Agent Framework は、.NET で AI エージェントとマルチエージェント システムを構築、オーケストレーション、展開するための Microsoft の答えです。これは、2023 年以来 Microsoft の AI オーケストレーション用のオープンソース SDK であるセマンティック カーネルと並行し、深く統合されています。&lt;/p>
&lt;p>次のように考えてください。&lt;strong>セマンティック カーネル&lt;/strong> はプリミティブ (カーネル、プラグイン、メモリ、プランナー) を提供し、&lt;strong>エージェント フレームワーク&lt;/strong> は、エージェント間の通信と調整のために特別に設計された高レベルの抽象化を提供します。&lt;/p>
&lt;p>このフレームワークは、Azure OpenAI、OpenAI、Azure AI Foundry でホストされるモデルなど、複数のモデル プロバイダーをサポートします。モデルに依存しない設計ですが、Azure エコシステムと深く統合されているため、エンタープライズ シナリオにとって特に魅力的です。&lt;/p>
&lt;p>主な機能は次のとおりです。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>複数のエージェント タイプ&lt;/strong>: さまざまなバックエンドの &lt;code>ChatCompletionAgent&lt;/code>、&lt;code>OpenAIAssistantAgent&lt;/code>、および &lt;code>AzureAIAgent&lt;/code>&lt;/li>
&lt;li>&lt;strong>オーケストレーション パターン&lt;/strong>: 順次、同時、ハンドオフ、グループ チャットのワークフロー&lt;/li>
&lt;li>&lt;strong>プラグイン エコシステム&lt;/strong>: ネイティブ C# 機能、OpenAPI 仕様、モデル コンテキスト プロトコル (MCP) ツールを使用してエージェントを拡張します。&lt;/li>
&lt;li>&lt;strong>会話管理&lt;/strong>: 組み込みのスレッド化、履歴管理、および終了戦略&lt;/li>
&lt;li>&lt;strong>可観測性&lt;/strong>: エージェントの対話を追跡するための OpenTelemetry との統合&lt;/li>
&lt;/ul>
&lt;h2 id="主要な概念">主要な概念&lt;/h2>
&lt;p>コードを書く前に、語彙を確立しましょう。エージェント フレームワークは、いくつかのコア抽象化を中心に展開します。&lt;/p>
&lt;h3 id="エージェント">エージェント&lt;/h3>
&lt;p>エージェントは AI モデルによってサポートされるエンティティであり、特定の命令 (システム プロンプト)、名前、およびオプションでプラグインまたはツールのセットで構成されます。各エージェントはスペシャリストです。エージェントが何を知っているか、何ができるか、どのように動作するかを定義します。&lt;/p>
&lt;h3 id="chatcompletionagent最も単純なエージェント-タイプチャット完了エンドポイント-azure-openaiopenai-など-をラップし会話を維持します呼び出し間はステートレスです履歴を提供すると応答しますこれにより軽量になり推論が容易になります">ChatCompletionAgent最も単純なエージェント タイプ。チャット完了エンドポイント (Azure OpenAI、OpenAI など) をラップし、会話を維持します。呼び出し間はステートレスです。履歴を提供すると、応答します。これにより、軽量になり、推論が容易になります。&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>ChatCompletionAgent agent = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Reviewer&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;You are a senior code reviewer. Analyze the provided code for bugs, security issues, and style violations. Be concise and actionable.&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="openaiassistantagent">OpenAIAssistantAgent&lt;/h3>
&lt;p>このエージェント タイプは、サーバー側の会話状態、ファイル処理、コード解釈を提供する OpenAI Assistants API を利用します。これはより重いですが、永続的なスレッドと、コードインタープリタやファイル検索などの組み込みツールを提供します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>OpenAIAssistantAgent agent = &lt;span style="color:#ff7b72">await&lt;/span> OpenAIAssistantAgent.CreateAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> clientProvider: clientProvider,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> definition: &lt;span style="color:#ff7b72">new&lt;/span> OpenAIAssistantDefinition(&lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;DataAnalyst&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;You analyze datasets and produce statistical summaries.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="エージェントグループチャット">エージェントグループチャット&lt;/h3>
&lt;p>これがオーケストレーターです。 &lt;code>AgentGroupChat&lt;/code> は、複数のエージェント間のマルチターン会話を管理し、次に話す人、会話がいつ終了するか、履歴がどのように共有されるかを制御します。ここで、マルチエージェントのコラボレーションの魔法が起こります。&lt;/p>
&lt;h2 id="オーケストレーション-パターン">オーケストレーション パターン&lt;/h2>
&lt;p>このフレームワークは 4 つの主要なオーケストレーション パターンをサポートしており、それぞれが異なる問題に適しています。&lt;/p>
&lt;h3 id="シーケンシャル">シーケンシャル&lt;/h3>
&lt;p>エージェントは、定義された順序で次々に実行されます。エージェント A の出力はエージェント B にフィードされ、エージェント B の出力はエージェント C にフィードされます。これは、パイプライン (ドラフト → レビュー → 編集 → 公開) に最適です。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Conceptual flow&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> draft = &lt;span style="color:#ff7b72">await&lt;/span> writerAgent.InvokeAsync(&lt;span style="color:#a5d6ff">&amp;#34;Write a blog post about .NET 9&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> reviewed = &lt;span style="color:#ff7b72">await&lt;/span> reviewerAgent.InvokeAsync(&lt;span style="color:#a5d6ff">$&amp;#34;Review this: {draft}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> edited = &lt;span style="color:#ff7b72">await&lt;/span> editorAgent.InvokeAsync(&lt;span style="color:#a5d6ff">$&amp;#34;Edit based on feedback: {reviewed}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="同時実行">同時実行&lt;/h3>
&lt;p>複数のエージェントが同じ入力に対して同時に作業します。作業を扇状に広げてから、結果を集計します。 3 人のレビュー担当者が同じプル リクエストを検討するなど、多様な視点を得るのに最適です。&lt;/p>
&lt;h3 id="ハンドオフ">ハンドオフ&lt;/h3>
&lt;p>エージェントは、会話コンテキストに基づいて制御を別のエージェントに移すことを決定します。これはカスタマー サービス チームの仕組みを模倣しています。最前線のエージェントが基本的な問い合わせを処理し、必要に応じて専門家にエスカレーションします。&lt;/p>
&lt;h3 id="グループチャット">グループチャット&lt;/h3>
&lt;p>複数のエージェントがオープンな会話に参加し、選択戦略に基づいて交代します。 &lt;code>AgentGroupChat&lt;/code> クラスは、構成可能な交代および終了ロジックを使用してこのパターンを実装します。&lt;/p>
&lt;h2 id="最初のエージェントを構築する">最初のエージェントを構築する&lt;/h2>
&lt;p>実践してみましょう。最初のエージェントを段階的に構築する方法を次に示します。&lt;/p>
&lt;h3 id="前提条件">前提条件&lt;/h3>
&lt;p>必要なものは次のとおりです。&lt;/p>
&lt;ul>
&lt;li>.NET 9 SDK&lt;/li>
&lt;li>デプロイされたモデルを含む Azure OpenAI リソース (例: &lt;code>gpt-4o&lt;/code>)&lt;/li>
&lt;li>Visual Studio または VS Code&lt;/li>
&lt;/ul>
&lt;h3 id="プロジェクトのセットアップ">プロジェクトのセットアップ&lt;/h3>
&lt;p>新しいコンソール アプリケーションを作成し、必要なパッケージをインストールします。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet new console -n AgentDemo
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cd AgentDemo
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel --prerelease
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel.Agents.Core --prerelease
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Azure.AI.OpenAI
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Azure.Identity
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="単純なエージェントの作成">単純なエージェントの作成&lt;/h3>
&lt;p>まず、Azure OpenAI 構成を使用してカーネルをセットアップします。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.Identity&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Agents&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.ChatCompletion&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = Kernel.CreateBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: &lt;span style="color:#a5d6ff">&amp;#34;https://your-resource.openai.azure.com/&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> credentials: &lt;span style="color:#ff7b72">new&lt;/span> DefaultAzureCredential()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Kernel kernel = builder.Build();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>次に、エージェントを作成して呼び出します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>ChatCompletionAgent agent = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;TechWriter&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a technical writer specializing &lt;span style="color:#ff7b72">in&lt;/span> software documentation.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Write clear, concise content aimed at experienced developers.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Use code examples when appropriate.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Always structure your output with headings and bullet points.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ChatHistory history = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>history.AddUserMessage(&lt;span style="color:#a5d6ff">&amp;#34;Explain dependency injection in .NET in 200 words.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (ChatMessageContent response &lt;span style="color:#ff7b72">in&lt;/span> agent.InvokeAsync(history))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(response.Content);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>それだけです。働いているエージェントがいます。しかし、本当の力はエージェントが協力するときに発揮されます。&lt;/p>
&lt;h2 id="agentgroupchat-を使用したマルチエージェント-オーケストレーション">AgentGroupChat を使用したマルチエージェント オーケストレーション&lt;/h2>
&lt;p>もっと興味深いもの、つまり複数のエージェントが協力するグループ チャットを構築してみましょう。 &lt;code>AgentGroupChat&lt;/code> クラスは、次に誰が話すか、いつ停止するかなどの会話フローを管理します。&lt;/p>
&lt;h3 id="エージェントの定義">エージェントの定義&lt;/h3>
&lt;p>ライター、レビューアー、編集者の 3 人のエージェントを作成します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>ChatCompletionAgent writer = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Writer&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a content writer. When given a topic, produce a well-structured
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> draft. Focus &lt;span style="color:#ff7b72">on&lt;/span> clarity and technical accuracy.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> When you receive feedback &lt;span style="color:#ff7b72">from&lt;/span> the Reviewer, incorporate it &lt;span style="color:#ff7b72">into&lt;/span> a
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> revised draft.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ChatCompletionAgent reviewer = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Reviewer&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a content reviewer. Analyze drafts &lt;span style="color:#ff7b72">for&lt;/span> technical accuracy,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> clarity, and completeness. Provide specific, actionable feedback.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Do NOT rewrite the content &lt;span style="color:#f85149">—&lt;/span> only provide feedback.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> When the content meets your standards, respond with: APPROVED
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ChatCompletionAgent editor = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Editor&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are an editor. Once content &lt;span style="color:#ff7b72">is&lt;/span> approved &lt;span style="color:#ff7b72">by&lt;/span> the Reviewer,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> polish it &lt;span style="color:#ff7b72">for&lt;/span> grammar, tone, and formatting.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Output only the final polished version.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> When you have produced the final version, respond with: COMPLETE
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="グループチャットの設定">グループチャットの設定&lt;/h3>
&lt;p>&lt;code>AgentGroupChat&lt;/code> には 2 つの重要な設定が必要です。&lt;strong>選択戦略&lt;/strong> (次に誰が話すか) と &lt;strong>終了戦略&lt;/strong> (いつ停止するか) です。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>AgentGroupChat chat = &lt;span style="color:#ff7b72">new&lt;/span>(writer, reviewer, editor)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ExecutionSettings = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> SelectionStrategy = &lt;span style="color:#ff7b72">new&lt;/span> SequentialSelectionStrategy(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> TerminationStrategy = &lt;span style="color:#ff7b72">new&lt;/span> ApprovalTerminationStrategy()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Agents = [editor],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MaximumIterations = &lt;span style="color:#a5d6ff">12&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="カスタム終了戦略終了戦略は会話がいつ終了するかを定義しますこれはcompleteキーワードを検索するカスタムのものです">カスタム終了戦略終了戦略は、会話がいつ終了するかを定義します。これは、「COMPLETE」キーワードを検索するカスタムのものです。&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ApprovalTerminationStrategy&lt;/span> : TerminationStrategy
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">bool&lt;/span>&amp;gt; ShouldAgentTerminateAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Agent agent,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IReadOnlyList&amp;lt;ChatMessageContent&amp;gt; history,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CancellationToken cancellationToken = &lt;span style="color:#ff7b72">default&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">bool&lt;/span> isComplete = history
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Last()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Content?
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Contains(&lt;span style="color:#a5d6ff">&amp;#34;COMPLETE&amp;#34;&lt;/span>, StringComparison.OrdinalIgnoreCase) ?? &lt;span style="color:#79c0ff">false&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Task.FromResult(isComplete);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="会話を進める">会話を進める&lt;/h3>
&lt;p>ユーザー メッセージで会話を開始し、エージェントが協力できるようにします。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>chat.AddChatMessage(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> ChatMessageContent(AuthorRole.User,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Write a 300-word technical overview of gRPC vs REST for microservices.&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (ChatMessageContent message &lt;span style="color:#ff7b72">in&lt;/span> chat.InvokeAsync())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;[{message.AuthorName}]: {message.Content}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;---&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>フローは次のようになります。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>ライター&lt;/strong>が草稿を作成します&lt;/li>
&lt;li>&lt;strong>レビュー担当者&lt;/strong>がフィードバックを提供します&lt;/li>
&lt;li>&lt;strong>ライター&lt;/strong>はフィードバックに基づいて修正します&lt;/li>
&lt;li>&lt;strong>査読者&lt;/strong>は「承認済み」と言う&lt;/li>
&lt;li>&lt;strong>編集者&lt;/strong>が磨きをかけて「完了」と言います&lt;/li>
&lt;li>会話が終了します&lt;/li>
&lt;/ol>
&lt;p>このやり取りは、終了条件が満たされるか &lt;code>MaximumIterations&lt;/code> に達するまで自動的に継続されます。&lt;/p>
&lt;h2 id="プラグインとツール">プラグインとツール&lt;/h2>
&lt;p>エージェントは、外部システムと対話できるようになると真に強力になります。 Agent Framework は 3 つの主要な拡張メカニズムをサポートしています。&lt;/p>
&lt;h3 id="ネイティブ関数-カーネルプラグイン">ネイティブ関数 (カーネルプラグイン)&lt;/h3>
&lt;p>エージェントにツールとして C# メソッドへのアクセスを許可できます。エージェントは、必要であると判断した場合に、これらの関数を呼び出します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ContentToolsPlugin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [KernelFunction(&amp;#34;word_count&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Counts the number of words in the provided text.&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> WordCount([Description(&lt;span style="color:#a5d6ff">&amp;#34;The text to count words in&amp;#34;&lt;/span>)] &lt;span style="color:#ff7b72">string&lt;/span> text)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> text.Split(&lt;span style="color:#a5d6ff">&amp;#39; &amp;#39;&lt;/span>, StringSplitOptions.RemoveEmptyEntries).Length;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [KernelFunction(&amp;#34;check_readability&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Calculates a readability score for the given text.&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> CheckReadability([Description(&lt;span style="color:#a5d6ff">&amp;#34;The text to analyze&amp;#34;&lt;/span>)] &lt;span style="color:#ff7b72">string&lt;/span> text)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> words = text.Split(&lt;span style="color:#a5d6ff">&amp;#39; &amp;#39;&lt;/span>, StringSplitOptions.RemoveEmptyEntries).Length;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> sentences = text.Split([&lt;span style="color:#a5d6ff">&amp;#39;.&amp;#39;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#39;!&amp;#39;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#39;?&amp;#39;&lt;/span>], StringSplitOptions.RemoveEmptyEntries).Length;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (sentences == &lt;span style="color:#a5d6ff">0&lt;/span>) &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;Unable to calculate — no sentences found.&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">double&lt;/span> avgWordsPerSentence = (&lt;span style="color:#ff7b72">double&lt;/span>)words / sentences;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> avgWordsPerSentence &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt; &lt;span style="color:#a5d6ff">15&lt;/span> =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;Easy to read (avg {avgWordsPerSentence:F1} words/sentence)&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt; &lt;span style="color:#a5d6ff">25&lt;/span> =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;Moderate difficulty (avg {avgWordsPerSentence:F1} words/sentence)&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _ =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;Difficult to read (avg {avgWordsPerSentence:F1} words/sentence). Consider shorter sentences.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>エージェントを作成する前に、カーネルにプラグインを登録します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>kernel.Plugins.AddFromType&amp;lt;ContentToolsPlugin&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ChatCompletionAgent analyst = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;ContentAnalyst&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;Analyze content using available tools. Report word count and readability.&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Kernel = kernel,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Arguments = &lt;span style="color:#ff7b72">new&lt;/span> KernelArguments(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> PromptExecutionSettings
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="モデル-コンテキスト-プロトコル-mcp">モデル コンテキスト プロトコル (MCP)&lt;/h3>
&lt;p>MCP は、AI モデルを外部ツールやデータ ソースに接続するためのオープン スタンダードです。エージェント フレームワークは MCP をサポートしています。つまり、エージェントは MCP 準拠のサーバーによって公開されているツールを使用できます。これにより、ファイル システム、データベース、API などへの扉がすべて標準化されたインターフェイスを通じて開かれます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Example: Adding an MCP server for file operations&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>kernel.Plugins.AddFromMcpServer(&lt;span style="color:#a5d6ff">&amp;#34;filesystem&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Uri(&lt;span style="color:#a5d6ff">&amp;#34;http://localhost:3000/mcp&amp;#34;&lt;/span>));
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これは、エージェントがあなたが構築したものに限定されず、他の人が開発して共有している MCP ツールのエコシステムを利用できることを意味するため、特に興味深いものです。&lt;/p>
&lt;h2 id="実際の例-コンテンツ-レビュー-パイプライン">実際の例: コンテンツ レビュー パイプライン&lt;/h2>
&lt;p>実践的なシナリオですべてをまとめてみましょう。ドキュメント チームのコンテンツ レビューを自動化する内部ツールを構築していると想像してください。パイプラインには 4 つのステージがあります。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>研究者&lt;/strong> — 関連する技術情報を収集します&lt;/li>
&lt;li>&lt;strong>ライター&lt;/strong> — 調査に基づいて草稿を作成します&lt;/li>
&lt;li>&lt;strong>レビュー担当者&lt;/strong> — 正確さと完全性をチェックします&lt;/li>
&lt;li>&lt;strong>発行者&lt;/strong> — 最終出力をフォーマットして準備します&lt;/li>
&lt;/ol>
&lt;p>要約された実装は次のとおりです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Azure.Identity&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Agents&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.ChatCompletion&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Build the kernel&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = Kernel.CreateBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: &lt;span style="color:#a5d6ff">&amp;#34;https://your-resource.openai.azure.com/&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> credentials: &lt;span style="color:#ff7b72">new&lt;/span> DefaultAzureCredential()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Kernel kernel = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Define specialized agents&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ChatCompletionAgent researcher = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Researcher&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a technical researcher. Given a topic, identify the key
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> concepts, recent developments, and important details that should
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> be covered. Output a structured research brief.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ChatCompletionAgent writer = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Writer&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a technical writer. Using the research brief provided,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> write a comprehensive, well-structured article. Include code
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> examples &lt;span style="color:#ff7b72">where&lt;/span> relevant. Target audience: experienced developers.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ChatCompletionAgent reviewer = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Reviewer&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a senior technical reviewer. Check the article &lt;span style="color:#ff7b72">for&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Technical accuracy
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Completeness relative to the research brief
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Code correctness
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - Clarity and structure
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> If everything looks good, respond with APPROVED.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Otherwise, provide specific feedback &lt;span style="color:#ff7b72">for&lt;/span> revision.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>ChatCompletionAgent publisher = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;Publisher&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a content publisher. Once the article &lt;span style="color:#ff7b72">is&lt;/span> approved,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> format it with proper markdown, &lt;span style="color:#ff7b72">add&lt;/span> a summary at the top,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> and ensure consistent formatting throughout.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> End your response with COMPLETE.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Configure the group chat&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>AgentGroupChat chat = &lt;span style="color:#ff7b72">new&lt;/span>(researcher, writer, reviewer, publisher)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ExecutionSettings = &lt;span style="color:#ff7b72">new&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> SelectionStrategy = &lt;span style="color:#ff7b72">new&lt;/span> SequentialSelectionStrategy(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> TerminationStrategy = &lt;span style="color:#ff7b72">new&lt;/span> ApprovalTerminationStrategy()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Agents = [publisher],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MaximumIterations = &lt;span style="color:#a5d6ff">15&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Start the pipeline&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>chat.AddChatMessage(&lt;span style="color:#ff7b72">new&lt;/span> ChatMessageContent(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> AuthorRole.User,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Create a technical article about implementing health checks in ASP.NET Core microservices.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (ChatMessageContent message &lt;span style="color:#ff7b72">in&lt;/span> chat.InvokeAsync())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.ForegroundColor = message.AuthorName &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Researcher&amp;#34;&lt;/span> =&amp;gt; ConsoleColor.Cyan,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Writer&amp;#34;&lt;/span> =&amp;gt; ConsoleColor.Green,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Reviewer&amp;#34;&lt;/span> =&amp;gt; ConsoleColor.Yellow,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Publisher&amp;#34;&lt;/span> =&amp;gt; ConsoleColor.Magenta,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _ =&amp;gt; ConsoleColor.White
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;\n{&amp;#39;=&amp;#39;new string(&amp;#39;=&amp;#39;, 60)}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;[{message.AuthorName}]&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#ff7b72">new&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>(&lt;span style="color:#a5d6ff">&amp;#39;=&amp;#39;&lt;/span>, &lt;span style="color:#a5d6ff">60&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(message.Content);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.ResetColor();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>このパイプラインは、エージェントの協力を通じて、完全に調査、執筆、レビュー、フォーマットされた記事を作成します。各エージェントは自分が最も得意とすることに集中し、&lt;code>AgentGroupChat&lt;/code> がワークフローを調整します。&lt;/p>
&lt;h2 id="ベストプラクティス">ベストプラクティス&lt;/h2>
&lt;p>いくつかのマルチエージェント システムを構築した後、私が最も価値があると感じたパターンと実践方法を次に示します。&lt;/p>
&lt;h3 id="エラー処理">エラー処理&lt;/h3>
&lt;p>終了戦略には常に &lt;code>MaximumIterations&lt;/code> を設定してください。これがないと、エージェントは無限ループに陥る可能性があります。特に、レビュー担当者が問題を発見し続け、ライターが改善せずに改訂を続ける場合に発生します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>TerminationStrategy = &lt;span style="color:#ff7b72">new&lt;/span> ApprovalTerminationStrategy()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MaximumIterations = &lt;span style="color:#a5d6ff">12&lt;/span> &lt;span style="color:#8b949e;font-style:italic">// Safety net&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>エージェントの呼び出しを try-catch ブロックでラップします。 API レート制限、ネットワークの問題、モデル エラーはすべて実稼働システムの現実です。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> message &lt;span style="color:#ff7b72">in&lt;/span> chat.InvokeAsync(cancellationToken))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Process messages&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">catch&lt;/span> (HttpOperationException ex) when (ex.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Implement retry with exponential backoff&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> Task.Delay(TimeSpan.FromSeconds(&lt;span style="color:#a5d6ff">30&lt;/span>), cancellationToken);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="可観測性agent-framework-は-opentelemetry-と統合されているためエージェントのあらゆる対話ツール呼び出しトークンの使用状況を追跡できますこれはどのエージェントが問題を引き起こしたかが必ずしも明らかではないマルチエージェント-ワークフローをデバッグする場合に不可欠です">可観測性Agent Framework は OpenTelemetry と統合されているため、エージェントのあらゆる対話、ツール呼び出し、トークンの使用状況を追跡できます。これは、どのエージェントが問題を引き起こしたかが必ずしも明らかではないマルチエージェント ワークフローをデバッグする場合に不可欠です。&lt;/h3>
&lt;p>セマンティック カーネル テレメトリ パッケージを追加し、優先エクスポーター (Application Insights、Jaeger など) を構成することにより、基本的なテレメトリをセットアップします。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddOpenTelemetry()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithTracing(tracing =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> tracing.AddSource(&lt;span style="color:#a5d6ff">&amp;#34;Microsoft.SemanticKernel*&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> tracing.AddOtlpExporter();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="コスト管理">コスト管理&lt;/h3>
&lt;p>マルチエージェント システムでは API コストが倍増します。すべてのエージェント ターンは API 呼び出しであり、グループ チャットでは多くのターンが発生する可能性があります。コストを管理するためのいくつかの戦略:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>より単純なエージェントには安価なモデルを使用&lt;/strong>: すべてのエージェントが GPT-4o を必要とするわけではありません。レビュー担当者は小規模なモデルで問題なく機能する可能性がありますが、強力なモデルを必要とするのはライターだけです。&lt;/li>
&lt;li>&lt;strong>履歴を制限&lt;/strong>: 実行設定で &lt;code>ReducedHistoryCount&lt;/code> を使用して、各エージェントが受信する会話コンテキストの量を制限します。&lt;/li>
&lt;li>&lt;strong>厳格な反復制限を設定&lt;/strong>: 適切な &lt;code>MaximumIterations&lt;/code> 値を使用して暴走した会話を防ぎます。&lt;/li>
&lt;li>&lt;strong>可能な場合はキャッシュ&lt;/strong>: エージェントが同じ検索を繰り返し実行する場合は、結果をプラグインにキャッシュします。&lt;/li>
&lt;/ul>
&lt;h3 id="エージェントの設計">エージェントの設計&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>指示に重点を置く&lt;/strong>: 各エージェントは単一の明確な責任を負う必要があります。広範囲にわたる指示は、すべてのタスクにわたって平凡なパフォーマンスにつながります。&lt;/li>
&lt;li>&lt;strong>出力形式について明確にする&lt;/strong>: エージェントに応答の構成方法を正確に伝えます。これにより、ダウンストリーム解析の信頼性が高まります。&lt;/li>
&lt;li>&lt;strong>終了キーワードを使用する&lt;/strong>: エージェントが終了を示すために使用する明確なシグナル (「承認」や「完了」など) を定義します。これにより、終了戦略がシンプルかつ予測可能になります。&lt;/li>
&lt;/ul>
&lt;h2 id="結論">結論&lt;/h2>
&lt;p>マルチエージェント AI システムは、インテリジェント アプリケーションの構築方法における根本的な変化を表しています。単一のプロンプトですべてを処理するのではなく、問題を専門的な役割に分解し、エージェントが協力できるようにすることができます。&lt;/p>
&lt;p>Microsoft の Agent Framework を使用すると、.NET 開発者にとってこれが実用的になります。エージェント、グループ チャット、選択と終了の戦略などの抽象化はクリーンであり、自然に構成されています。セマンティック カーネルのプラグイン エコシステムと Azure のモデル ホスティングを組み合わせることで、運用グレードのマルチエージェント システムを構築するためのフルスタックが得られます。&lt;/p>
&lt;p>フレームワークはまだ進化中ですが (多くのパッケージはプレビュー段階です)、核となるパターンはしっかりしており、方向性は明確です。 .NET で AI を活用したアプリケーションを構築している場合は、今がマルチエージェント アーキテクチャの実験を開始する時期です。&lt;/p>
&lt;p>小規模から始めて、単純なタスクで共同作業する 2 つのエージェントを構築します。単一エージェントのアプローチと比較して品質の向上が確認できたら、すべてをエージェント チームに分解したくなるでしょう。&lt;/p>
&lt;p>コーディングを楽しんでください!&lt;/p></content:encoded><category>AI</category><category>.NET</category><category>Agent Framework</category><category>Semantic Kernel</category></item><item><title>MarkupString を使用して Blazor で生の HTML をレンダリングする</title><link>https://emimontesdeoca.github.io/ja/posts/blazor-markup-string-raw-html/</link><pubDate>Sat, 22 Nov 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/blazor-markup-string-raw-html/</guid><description>エスケープされたテキストの代わりに MarkupString を使用して、Blazor コンポーネントで生の HTML コンテンツを安全にレンダリングします。</description><content:encoded>&lt;p>先日、私は CMS からの HTML をレンダリングする必要があるコンポーネントを構築していました。変数に HTML 文字列を入れて、それを &lt;code>@myHtml&lt;/code> のようにテンプレートにドロップしました。そしてもちろん、Blazor はすべてをエスケープし、実際のタグをページ上のテキストとしてレンダリングしました。私が望んでいたものではありませんでした。&lt;/p>
&lt;h1 id="問題">問題&lt;/h1>
&lt;p>既定では、Blazor はテンプレートでレンダリングする文字列をエンコードします。したがって、次のものがある場合:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> content = &lt;span style="color:#a5d6ff">&amp;#34;&amp;lt;strong&amp;gt;Hello&amp;lt;/strong&amp;gt; &amp;lt;em&amp;gt;world&amp;lt;/em&amp;gt;&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>そして、次のようにします。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#7ee787">div&lt;/span>&amp;gt;@content&amp;lt;/&lt;span style="color:#7ee787">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ページには &lt;strong>Hello&lt;/strong> &lt;em>world&lt;/em> の代わりにリテラル テキスト &lt;code>&amp;lt;strong&amp;gt;Hello&amp;lt;/strong&amp;gt; &amp;lt;em&amp;gt;world&amp;lt;/em&amp;gt;&lt;/code> が表示されます。 Blazor は XSS 攻撃を防ぐために意図的にこれを行いますが、これは正しい既定の動作です。&lt;/p>
&lt;h1 id="解決策-markupstring">解決策: MarkupString&lt;/h1>
&lt;p>実際に生の HTML をレンダリングする必要がある場合は、文字列を &lt;code>MarkupString&lt;/code> でラップします。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#7ee787">div&lt;/span>&amp;gt;@((MarkupString)content)&amp;lt;/&lt;span style="color:#7ee787">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>それで終わりです！これで、Blazor は HTML を実際のマークアップとしてレンダリングします。変数に代入することもできます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> rawHtml = &lt;span style="color:#a5d6ff">&amp;#34;&amp;lt;strong&amp;gt;Hello&amp;lt;/strong&amp;gt; &amp;lt;em&amp;gt;world&amp;lt;/em&amp;gt;&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> MarkupString HtmlContent =&amp;gt; (MarkupString)rawHtml;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#7ee787">div&lt;/span>&amp;gt;@HtmlContent&amp;lt;/&lt;span style="color:#7ee787">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="実際の例">実際の例&lt;/h1>
&lt;p>API からブログ コンテンツを取得しており、それをプレビュー コンポーネントでレンダリングする必要がありました。コンテンツには、見出し、コード ブロック、リンク、画像など、あらゆる種類の HTML が含まれていました。おおよそ次のような感じです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@inject HttpClient Http
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@if (article &lt;span style="color:#ff7b72">is&lt;/span> not &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;article&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;h1&amp;gt;@article.Title&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;content&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>((MarkupString)article.HtmlBody)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/article&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> ArticleId { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> ArticleDto? article;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> article = &lt;span style="color:#ff7b72">await&lt;/span> Http.GetFromJsonAsync&amp;lt;ArticleDto&amp;gt;(&lt;span style="color:#a5d6ff">$&amp;#34;api/articles/{ArticleId}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>完璧に動作します。 API からの HTML は実際のマークアップとしてレンダリングされます。&lt;/p>
&lt;h1 id="信頼できないコンテンツには注意してください">信頼できないコンテンツには注意してください&lt;/h1>
&lt;p>これは重要です。&lt;code>MarkupString&lt;/code> は HTML をサニタイズしません**。 &lt;code>&amp;lt;script&amp;gt;&lt;/code> タグを含め、指定されたものはすべてレンダリングされます。したがって、コンテンツがユーザー入力または信頼できないソースからのものである場合は、最初にそれをサニタイズする必要があります。&lt;/p>
&lt;p>Blazor には組み込みの HTML サニタイザーはありませんが、&lt;a href="https://github.com/mganss/HtmlSanitizer">HtmlSanitizer&lt;/a> のようなライブラリを使用できます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Ganss.Xss&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> HtmlSanitizer sanitizer = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> MarkupString SafeHtml(&lt;span style="color:#ff7b72">string&lt;/span> html)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> clean = sanitizer.Sanitize(html);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> (MarkupString)clean;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#7ee787">div&lt;/span>&amp;gt;@SafeHtml(untrustedContent)&amp;lt;/&lt;span style="color:#7ee787">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これにより、&lt;code>&amp;lt;script&amp;gt;&lt;/code>、&lt;code>onclick&lt;/code> ハンドラーなどの危険な要素や、ユーザー提供のコンテンツからレンダリングしたくないその他の要素が削除されます。&lt;/p>
&lt;p>#いつ使用するか&lt;/p>
&lt;p>私は &lt;code>MarkupString&lt;/code> を次の目的で使用します。&lt;/p>
&lt;ul>
&lt;li>サーバー側で HTML に変換された CMS コンテンツまたはマークダウン&lt;/li>
&lt;li>リッチテキストエディタの出力&lt;/li>
&lt;li>電子メールテンプレートのプレビュー&lt;/li>
&lt;li>信頼できるソースからの事前に構築された HTML&lt;/li>
&lt;/ul>
&lt;p>ユーザー入力から得られるものはすべて、常に最初にサニタイズしてください。後悔するよりは安全な方が良いです。&lt;/p>
&lt;p>投稿が気に入っていただければ幸いです!ソーシャルメディアで**@emimontesdeoca**までお気軽にご連絡ください。&lt;/p>
&lt;h1 id="リソース">リソース&lt;/h1>
&lt;ul>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.markupstring">MarkupString 構造体&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/security/content-security-policy">Blazor XSS 防止&lt;/a>&lt;/li>
&lt;/ul></content:encoded><category>.NET</category><category>Blazor</category><category>C#</category></item><item><title>EF Core 9 の新機能: 知っておくべき機能</title><link>https://emimontesdeoca.github.io/ja/posts/whats-new-ef-core-9/</link><pubDate>Tue, 18 Nov 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/whats-new-ef-core-9/</guid><description>LINQ の改善や一括操作から JSON 列や AOT コンパイルのサポートまで、Entity Framework Core 9 の最も影響力のある機能を包括的に紹介します。</description><content:encoded>&lt;p>Entity Framework Core 9 は、2024 年 11 月に .NET 9 とともに出荷されました。いくつかのプロジェクトにわたって Entity Framework Core 9 の作業に十分な時間を費やした後、これはここしばらくで最も意味のあるリリースの 1 つであると言えます。車輪の再発明ではなく、クエリ変換、パフォーマンス、最新のデータ パターンの操作など、これまで EF Core が最も摩擦を引き起こしてきた領域を磨き上げるためです。&lt;/p>
&lt;p>この投稿では、私の日常の仕事に最も大きな影響を与えた機能について説明します。まだ EF Core 8 (または 7) を使用している場合、これにより、アップグレードの向こう側で何が待っているかが明確にわかるはずです。&lt;/p>
&lt;h2 id="net-9-エコシステムの-ef-core-9">.NET 9 エコシステムの EF Core 9&lt;/h2>
&lt;p>EF Core 9 は .NET 8 および .NET 9 をターゲットとしています。つまり、これらの機能のほとんどを利用するためにアプリケーション全体を .NET 9 にアップグレードする必要は必ずしもありません。とはいえ、AOT とパフォーマンスの向上の一部は .NET 9 ランタイムの変更と密接に関係しているため、最後まで実行することで最大限の効果を得ることができます。&lt;/p>
&lt;p>このリリースは、Microsoft が確立した奇数/偶数のペースに従います。奇数番号のリリース (.NET 9 など) は 18 か月のサポートが付いた標準期間サポート (STS) であり、偶数のリリース (.NET 8 など) は長期サポート (LTS) です。アップグレードのスケジュールを計画するときは、この点に留意してください。&lt;/p>
&lt;h2 id="linq-変換の改善">LINQ 変換の改善&lt;/h2>
&lt;p>これは、ほとんどの開発者がすぐに違いを感じる点です。 EF Core 9 は、LINQ 式を実際に意味のある SQL に変換する点で大幅な進歩を遂げました。&lt;/p>
&lt;h3 id="groupby-翻訳の改善">GroupBy 翻訳の改善&lt;/h3>
&lt;p>EF Core で &lt;code>GroupBy&lt;/code> クエリを作成し、最終的にクライアント側の評価警告や奇妙な SQL が発生したことがある方なら、その苦しみをご存知でしょう。 EF Core 9 は、より広範な &lt;code>GroupBy&lt;/code> シナリオのセットを SQL で直接処理します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> salesByCategory = &lt;span style="color:#ff7b72">await&lt;/span> context.Products
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .GroupBy(p =&amp;gt; p.Category.Name)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Select(g =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Category = g.Key,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> TotalRevenue = g.Sum(p =&amp;gt; p.Price * p.UnitsSold),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> AveragePrice = g.Average(p =&amp;gt; p.Price),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ProductCount = g.Count()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OrderByDescending(x =&amp;gt; x.TotalRevenue)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>以前のバージョンでは、&lt;code>GroupBy&lt;/code> 内のナビゲーション プロパティに対する集計を含むクエリは、クライアント評価にフォールバックすることがありました。 EF Core 9 は、これを &lt;code>GROUP BY&lt;/code>、&lt;code>SUM&lt;/code>、&lt;code>AVG&lt;/code>、および &lt;code>COUNT&lt;/code> を使用した単一の SQL クエリにきれいに変換します。&lt;/p>
&lt;h3 id="複雑なプロジェクションとサブクエリ">複雑なプロジェクションとサブクエリ&lt;/h3>
&lt;p>ネストされたサブクエリと複雑なプロジェクションも大幅にアップグレードされました。次のようなことを考えてみましょう。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> orderSummaries = &lt;span style="color:#ff7b72">await&lt;/span> context.Customers
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Select(c =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span> CustomerSummaryDto
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = c.FullName,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> TotalOrders = c.Orders.Count(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MostRecentOrder = c.Orders
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OrderByDescending(o =&amp;gt; o.OrderDate)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Select(o =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span> OrderBriefDto
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Id = o.Id,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Date = o.OrderDate,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Total = o.LineItems.Sum(li =&amp;gt; li.Quantity * li.UnitPrice)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .FirstOrDefault(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> TopCategory = c.Orders
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .SelectMany(o =&amp;gt; o.LineItems)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .GroupBy(li =&amp;gt; li.Product.Category.Name)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OrderByDescending(g =&amp;gt; g.Count())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Select(g =&amp;gt; g.Key)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .FirstOrDefault()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>EF Core 9 では、クライアント側の評価をトリガーせずに、この式全体を SQL に変換できるようになりました。生成されたクエリでは、必要に応じて相関サブクエリとラテラル結合が使用され、SQL プランは以前のバージョンで生成されたものよりも大幅に効率的です。&lt;/p>
&lt;h3 id="パラメータ化されたプリミティブ-コレクション">パラメータ化されたプリミティブ コレクション&lt;/h3>
&lt;p>LINQ の顕著な改善点の 1 つは、プリミティブ値のコレクションをクエリに直接渡す機能です。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> statusFilter = &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; { &lt;span style="color:#a5d6ff">&amp;#34;Active&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Pending&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Review&amp;#34;&lt;/span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> filteredOrders = &lt;span style="color:#ff7b72">await&lt;/span> context.Orders
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(o =&amp;gt; statusFilter.Contains(o.Status))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>EF Core 8 では、これはインライン化された値を持つ &lt;code>IN&lt;/code> 句を使用して変換されました。つまり、リストが変更されたときにクエリ プラン キャッシュを再利用できませんでした。 EF Core 9 は、これらのコレクションを適切にパラメーター化し、構造化パラメーターとして送信します。これは、SQL Server および PostgreSQL のクエリ プラン キャッシュにとって大きな問題です。## 一括操作 - ExecuteUpdate および ExecuteDelete&lt;/p>
&lt;p>&lt;code>ExecuteUpdate&lt;/code> と &lt;code>ExecuteDelete&lt;/code> は EF Core 7 で導入されましたが、EF Core 9 では、それらを使用してできることが有意義な方法で拡張されています。&lt;/p>
&lt;h3 id="より複雑な更新式">より複雑な更新式&lt;/h3>
&lt;p>&lt;code>ExecuteUpdate&lt;/code> で、ナビゲーション プロパティを介した他のテーブルへの参照など、より複雑な式を使用できるようになりました。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> context.Products
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(p =&amp;gt; p.Category.IsDiscontinued)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ExecuteUpdateAsync(setters =&amp;gt; setters
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .SetProperty(p =&amp;gt; p.IsAvailable, &lt;span style="color:#79c0ff">false&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .SetProperty(p =&amp;gt; p.DiscontinuedDate, DateTimeOffset.UtcNow)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .SetProperty(p =&amp;gt; p.Price, p =&amp;gt; p.Price * &lt;span style="color:#a5d6ff">0.5&lt;/span>m));
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これにより、&lt;code>JOIN&lt;/code> を含む単一の &lt;code>UPDATE&lt;/code> ステートメントがカテゴリ テーブルに生成されます。エンティティをメモリにロードする必要はなく、変更追跡のオーバーヘッドもありません。&lt;/p>
&lt;h3 id="サブクエリを使用した条件付き一括削除">サブクエリを使用した条件付き一括削除&lt;/h3>
&lt;p>サブクエリ フィルターを使用した一括削除が完全にサポートされるようになりました。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> context.AuditLogs
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(log =&amp;gt; log.CreatedAt &amp;lt; DateTime.UtcNow.AddYears(-&lt;span style="color:#a5d6ff">2&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(log =&amp;gt; !context.ProtectedRecords
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Any(pr =&amp;gt; pr.AuditLogId == log.Id))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ExecuteDeleteAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これは、&lt;code>NOT EXISTS&lt;/code> サブクエリを含む &lt;code>DELETE&lt;/code> に変換され、まさに手動で記述するものとなります。エンティティがロードされておらず、往復もありません。&lt;/p>
&lt;h2 id="json-列の機能強化">JSON 列の機能強化&lt;/h2>
&lt;p>JSON 列は、最近の EF Core リリースで最も魅力的な機能の 1 つであり、EF Core 9 ではさらに進化しています。&lt;/p>
&lt;h3 id="json-内でのクエリ">JSON 内でのクエリ&lt;/h3>
&lt;p>より優れた翻訳サポートにより、JSON 列内からデータをフィルターして投影できるようになりました。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Order&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> Id { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> CustomerName { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ShippingAddress Address { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } &lt;span style="color:#8b949e;font-style:italic">// Stored as JSON&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;OrderNote&amp;gt; Notes { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } &lt;span style="color:#8b949e;font-style:italic">// Stored as JSON&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ShippingAddress&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Street { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> City { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> State { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> ZipCode { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Country { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">OrderNote&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> DateTime CreatedAt { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Text { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Author { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>DbContext&lt;/code> の構成:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> OnModelCreating(ModelBuilder modelBuilder)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> modelBuilder.Entity&amp;lt;Order&amp;gt;(builder =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.OwnsOne(o =&amp;gt; o.Address, ab =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ab.ToJson();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.OwnsMany(o =&amp;gt; o.Notes, nb =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> nb.ToJson();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これで、JSON に直接クエリできるようになりました。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> newYorkOrders = &lt;span style="color:#ff7b72">await&lt;/span> context.Orders
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(o =&amp;gt; o.Address.State == &lt;span style="color:#a5d6ff">&amp;#34;NY&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OrderByDescending(o =&amp;gt; o.Notes.Count)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Select(o =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> o.CustomerName,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> o.Address.City,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> LatestNote = o.Notes
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OrderByDescending(n =&amp;gt; n.CreatedAt)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Select(n =&amp;gt; n.Text)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .FirstOrDefault()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>EF Core 9 は、SQL Server (または他のプロバイダーの同等のもの) 上で適切な &lt;code>JSON_VALUE&lt;/code> および &lt;code>JSON_QUERY&lt;/code> 呼び出しを生成し、その変換では、以前よりもはるかに広範囲の JSON 要素に対する LINQ 操作がカバーされます。&lt;/p>
&lt;h3 id="json-プロパティの更新">JSON プロパティの更新&lt;/h3>
&lt;p>EF Core 8 の問題点の 1 つは、JSON 列内の 1 つのプロパティを更新すると、JSON ドキュメント全体が書き換えられることです。 EF Core 9 では、JSON マップ型のより詳細な変更追跡によりこれが改善され、可能な場合はよりターゲットを絞った更新が生成されます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> order = &lt;span style="color:#ff7b72">await&lt;/span> context.Orders.FindAsync(orderId);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>order.Address.ZipCode = &lt;span style="color:#a5d6ff">&amp;#34;10001&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> context.SaveChangesAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>サポートされているプロバイダーでは、BLOB 全体を書き換えるのではなく、よりターゲットを絞った JSON 変更を生成できます。&lt;/p>
&lt;h2 id="複合型---id-のない値オブジェクト">複合型 - ID のない値オブジェクト&lt;/h2>
&lt;p>複合型は、ドメイン駆動設計の実践者が待ち望んでいた機能の 1 つです。所有型とは異なり、複合型にはアイデンティティがなく、純粋な値のオブジェクトです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[ComplexType]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">record&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Money&lt;/span>(&lt;span style="color:#ff7b72">decimal&lt;/span> Amount, &lt;span style="color:#ff7b72">string&lt;/span> Currency);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[ComplexType]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">record&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">DateRange&lt;/span>(DateTime Start, DateTime End);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Project&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> Id { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Name { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Money Budget { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> DateRange Timeline { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これらは、別のテーブルやいかなる種類のキーも必要とせずに、親テーブル (&lt;code>Budget_Amount&lt;/code>、&lt;code>Budget_Currency&lt;/code>、&lt;code>Timeline_Start&lt;/code>、&lt;code>Timeline_End&lt;/code>) にフラット化された列として格納されます。&lt;/p>
&lt;p>所有型との主な違い: 複合型は参照ではなく値で比較されます。同じ &lt;code>Amount&lt;/code> および &lt;code>Currency&lt;/code> を持つ 2 つの &lt;code>Money&lt;/code> インスタンスは、どのエンティティに属しているかに関係なく、等しいとみなされます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> expensiveProjects = &lt;span style="color:#ff7b72">await&lt;/span> context.Projects
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(p =&amp;gt; p.Budget.Amount &amp;gt; &lt;span style="color:#a5d6ff">100_000&lt;/span>m &amp;amp;&amp;amp; p.Budget.Currency == &lt;span style="color:#a5d6ff">&amp;#34;USD&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OrderByDescending(p =&amp;gt; p.Budget.Amount)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これは、フラット化された列のフィルタリングに直接変換されます。クリーンで効率的で、まさに期待どおりの結果になります。&lt;/p>
&lt;h2 id="sql-server-の-hierarchyid-サポート">SQL Server の HierarchyId サポート&lt;/h2>
&lt;p>SQL Server の階層データ (組織図、カテゴリ ツリー、ファイル システム) を扱ったことがある場合は、&lt;code>HierarchyId&lt;/code> がこのための組み込み型であることをご存知でしょう。 EF Core 9 は、EF Core 9 に対する最上級のサポートを提供します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Employee&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> Id { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Name { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Title { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> HierarchyId PathFromCeo { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>階層関係を直接クエリできるようになりました。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> managerId = HierarchyId.Parse(&lt;span style="color:#a5d6ff">&amp;#34;/1/3/&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// All direct and indirect reports&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> allReports = &lt;span style="color:#ff7b72">await&lt;/span> context.Employees
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(e =&amp;gt; e.PathFromCeo.IsDescendantOf(managerId))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(e =&amp;gt; e.PathFromCeo != managerId) &lt;span style="color:#8b949e;font-style:italic">// exclude the manager&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OrderBy(e =&amp;gt; e.PathFromCeo)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Direct reports only (one level down)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> directReports = &lt;span style="color:#ff7b72">await&lt;/span> context.Employees
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(e =&amp;gt; e.PathFromCeo.GetAncestor(&lt;span style="color:#a5d6ff">1&lt;/span>) == managerId)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Get an employee&amp;#39;s depth in the hierarchy&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> employeesWithDepth = &lt;span style="color:#ff7b72">await&lt;/span> context.Employees
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Select(e =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> e.Name,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> e.Title,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Level = e.PathFromCeo.GetLevel()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OrderBy(e =&amp;gt; e.Level)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```これらはすべて、&lt;/span>SQL Server &lt;span style="color:#f85149">のネイティブ&lt;/span> &lt;span style="color:#f85149">`&lt;/span>HierarchyId&lt;span style="color:#f85149">`&lt;/span> &lt;span style="color:#f85149">メソッドに変換されます。自己参照外部キーと再帰&lt;/span> CTE &lt;span style="color:#f85149">を使用してツリー構造を実装している場合、これはよりクリーンなアプローチです。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">##&lt;/span> &lt;span style="color:#f85149">コンパイルされたモデルと&lt;/span> AOT &lt;span style="color:#f85149">のサポート&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">パフォーマンスを重視する開発者は、コンパイルされたモデルと事前&lt;/span> (AOT) &lt;span style="color:#f85149">コンパイルのサポートへの継続的な投資を高く評価するでしょう。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">###&lt;/span> &lt;span style="color:#f85149">コンパイルされたモデル&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">コンパイルされたモデルは、&lt;/span>EF Core &lt;span style="color:#f85149">が通常起動時に構築するモデル&lt;/span> &lt;span style="color:#f85149">メタデータを事前に生成します。大規模なモデル&lt;/span> (&lt;span style="color:#f85149">数百のエンティティを想定&lt;/span>) &lt;span style="color:#f85149">の場合、これによりコールド&lt;/span> &lt;span style="color:#f85149">スタート時間を大幅に短縮できます。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```&lt;/span>bash
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet ef dbcontext optimize --output-dir CompiledModels --&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">MyApp.CompiledModels&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>次に、配線します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddDbContext&amp;lt;AppDbContext&amp;gt;(options =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .UseSqlServer(connectionString)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .UseModel(MyApp.CompiledModels.AppDbContextModel.Instance));
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>EF Core 9 では、コンパイルされたモデルがより完全になり、より多くのマッピング機能がサポートされ、より小さな出力が生成されます。約 400 のエンティティを含むモデルの場合、起動時間は数秒からほぼ瞬時に短縮される可能性があります。&lt;/p>
&lt;h3 id="aot-コンパイルの進行状況">AOT コンパイルの進行状況&lt;/h3>
&lt;p>EF Core の完全なネイティブ AOT サポートはまだ進行中ですが、EF Core 9 は大幅に進歩しています。リフレクションの多いコード パスの多くはトリミングしやすいようにリファクタリングされており、コンパイルされたモデルは AOT ストーリーの重要な部分です。 Azure Functions やマイクロサービスなど、コールド スタートが重要なシナリオをターゲットにしている場合、これらの改善は直接関係します。&lt;/p>
&lt;h2 id="cosmos-db-プロバイダーの更新">Cosmos DB プロバイダーの更新&lt;/h2>
&lt;p>Azure Cosmos DB プロバイダーは、EF Core 9 で成熟し続けています。いくつかの注目すべき改善点は次のとおりです。&lt;/p>
&lt;h3 id="パーティションキーの処理">パーティションキーの処理&lt;/h3>
&lt;p>プロバイダーは階層型パーティション キーをサポートし、パーティション キー フィルターをよりインテリジェントに処理するようになりました。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">TenantDocument&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Id { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> TenantId { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } &lt;span style="color:#8b949e;font-style:italic">// Partition key&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Region { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } &lt;span style="color:#8b949e;font-style:italic">// Sub-partition key&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Content { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// This query now correctly uses the partition key for routing&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> docs = &lt;span style="color:#ff7b72">await&lt;/span> context.TenantDocuments
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(d =&amp;gt; d.TenantId == &lt;span style="color:#a5d6ff">&amp;#34;tenant-42&amp;#34;&lt;/span> &amp;amp;&amp;amp; d.Region == &lt;span style="color:#a5d6ff">&amp;#34;us-east&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(d =&amp;gt; d.Content.Contains(&lt;span style="color:#a5d6ff">&amp;#34;important&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="linq-から-nosql-への変換の改善">LINQ から NoSQL への変換の改善&lt;/h3>
&lt;p>&lt;code>Contains&lt;/code>、&lt;code>Any&lt;/code>、入れ子になった配列操作、数学関数のサポートの強化など、より多くの LINQ 操作が Cosmos DB の SQL 言語に変換されるようになりました。以前はクライアント評価にフォールバックされていたクエリがサーバー側で処理されるようになりました。&lt;/p>
&lt;h3 id="ベクトル検索のサポート">ベクトル検索のサポート&lt;/h3>
&lt;p>EF Core 9 では、Cosmos DB によるベクトル類似性検索の早期サポートが導入されています。これは、埋め込みや AI 駆動の検索と統合するアプリケーションを構築する場合に役立ちます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> results = &lt;span style="color:#ff7b72">await&lt;/span> context.Documents
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OrderBy(d =&amp;gt; EF.Functions.VectorDistance(d.Embedding, queryVector))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Take(&lt;span style="color:#a5d6ff">10&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="移行の改善">移行の改善&lt;/h2>
&lt;p>移行により生活の質が向上し、チーム環境での作業の苦痛が軽減されました。&lt;/p>
&lt;h3 id="移行におけるテンポラル-テーブル">移行におけるテンポラル テーブル&lt;/h3>
&lt;p>期間列と履歴テーブルの名前付けが適切にサポートされ、移行でテンポラル テーブルの構成がより適切に処理されるようになりました。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> OnModelCreating(ModelBuilder modelBuilder)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> modelBuilder.Entity&amp;lt;Employee&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToTable(&lt;span style="color:#a5d6ff">&amp;#34;Employees&amp;#34;&lt;/span>, b =&amp;gt; b.IsTemporal(t =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> t.HasPeriodStart(&lt;span style="color:#a5d6ff">&amp;#34;ValidFrom&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> t.HasPeriodEnd(&lt;span style="color:#a5d6ff">&amp;#34;ValidTo&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> t.UseHistoryTable(&lt;span style="color:#a5d6ff">&amp;#34;EmployeeHistory&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="冪等スクリプト">冪等スクリプト&lt;/h3>
&lt;p>&lt;code>Script-Migration&lt;/code> コマンド (およびそれに相当する CLI) は、デフォルトでより適切な冪等スクリプトを生成し、特定の状態に存在するデータに依存するスキーマ変更に関するエッジ ケースの処理が改善されています。&lt;/p>
&lt;h3 id="移行バンドル">移行バンドル&lt;/h3>
&lt;p>移行をデプロイ用のスタンドアロン実行可能ファイルにパッケージ化する移行バンドルは、EF Core 9 ではより優れたエラー報告と一時的な障害に対する再試行ロジックにより信頼性が向上しています。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet ef migrations bundle --self-contained -r linux-x64
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これにより、デプロイメント ターゲットに .NET SDK をインストールしなくても、CI/CD パイプラインで実行できるバイナリが生成されます。&lt;/p>
&lt;h2 id="パフォーマンスのベンチマーク以下は私自身のテストによる大まかなベンチマークですこれらはsql-server-2022-に対して実行されている約-200-のエンティティを含むプロジェクトからのものでbenchmarkdotnet-で測定されました数値は異なりますが相対的な改善は同様であるはずです">パフォーマンスのベンチマーク以下は私自身のテストによる大まかなベンチマークです。これらは、SQL Server 2022 に対して実行されている約 200 のエンティティを含むプロジェクトからのもので、BenchmarkDotNet で測定されました。数値は異なりますが、相対的な改善は同様であるはずです。&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>シナリオ&lt;/th>
&lt;th>EFコア8&lt;/th>
&lt;th>EFコア9&lt;/th>
&lt;th>改善&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>モデルのビルド (コールド スタート)&lt;/td>
&lt;td>1,850ミリ秒&lt;/td>
&lt;td>320ミリ秒&lt;/td>
&lt;td>~5.8 倍高速 (コンパイル)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>単純なクエリ (PK による単一エンティティ)&lt;/td>
&lt;td>0.42ミリ秒&lt;/td>
&lt;td>0.38ミリ秒&lt;/td>
&lt;td>~10% 高速&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>複雑なクエリ (結合 + 集計)&lt;/td>
&lt;td>3.1ミリ秒&lt;/td>
&lt;td>2.4ミリ秒&lt;/td>
&lt;td>~23% 高速&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>一括更新 (10,000 行)&lt;/td>
&lt;td>145ミリ秒&lt;/td>
&lt;td>118ミリ秒&lt;/td>
&lt;td>~19% 高速&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>JSON 列クエリ&lt;/td>
&lt;td>2.8ミリ秒&lt;/td>
&lt;td>1.9ミリ秒&lt;/td>
&lt;td>~32% 高速&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SaveChanges (100 エンティティ)&lt;/td>
&lt;td>48ミリ秒&lt;/td>
&lt;td>41ミリ秒&lt;/td>
&lt;td>~15% 高速&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>コンパイルされたモデルの改善は最も劇的ですが、特に 1 秒あたり数千のクエリを実行する高スループットのシナリオでは、全体的な着実な改善が積み重なっていきます。&lt;/p>
&lt;h2 id="ef-core-8-からのアップグレード">EF Core 8 からのアップグレード&lt;/h2>
&lt;p>EF Core 8 を使用している場合、アップグレード パスは比較的スムーズです。チェックリストは次のとおりです。&lt;/p>
&lt;p>&lt;strong>1.パッケージを更新します:&lt;/strong>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;PackageReference&lt;/span> Include=&lt;span style="color:#a5d6ff">&amp;#34;Microsoft.EntityFrameworkCore&amp;#34;&lt;/span> Version=&lt;span style="color:#a5d6ff">&amp;#34;9.0.0&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;PackageReference&lt;/span> Include=&lt;span style="color:#a5d6ff">&amp;#34;Microsoft.EntityFrameworkCore.SqlServer&amp;#34;&lt;/span> Version=&lt;span style="color:#a5d6ff">&amp;#34;9.0.0&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;PackageReference&lt;/span> Include=&lt;span style="color:#a5d6ff">&amp;#34;Microsoft.EntityFrameworkCore.Tools&amp;#34;&lt;/span> Version=&lt;span style="color:#a5d6ff">&amp;#34;9.0.0&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>2.重大な変更がないか確認してください。&lt;/strong> EF Core 9 のリストは、以前のリリースに比べて比較的短いです。最も注目すべきもの:&lt;/p>
&lt;ul>
&lt;li>以前に廃止された API の一部が削除されました&lt;/li>
&lt;li>特定の &lt;code>GroupBy&lt;/code> クエリの変換方法の変更 (クエリはサーバー側で行われるようになり、クライアントの評価に依存していた場合の動作が変わります)&lt;/li>
&lt;li>移行スキャフォールディングの出力の小さな変更&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>3.コンパイル済みモデルを使用している場合は、再生成します&lt;/strong>。形式が変更されたため、古いコンパイル済みモデルは EF Core 9 では動作しなくなります。&lt;/p>
&lt;p>&lt;strong>4.テスト スイートを実行します。&lt;/strong> 以前にクライアントで評価されたクエリに特に注意してください。クエリはサーバーで評価される可能性があり、通常はその方が優れていますが、データの違いが表面化する可能性があります。&lt;/p>
&lt;p>&lt;strong>5.そのプロバイダーを使用している場合は、Cosmos DB クエリ&lt;/strong>を確認してください。翻訳の改善により、一部のクエリの実行方法が異なります (通常は高速になります)。ただし、結果が同一であることを確認することは価値があります。&lt;/p>
&lt;p>一般的なプロジェクトの最小限のアップグレードは次のようになります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.EntityFrameworkCore --version 9.0.0
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 9.0.0
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.EntityFrameworkCore.Design --version 9.0.0
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet build
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet test
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>すべてがコンパイルされ、テストに合格した場合は、おそらく良好な状態になっています。問題が発生した場合は、EF Core 9 の重大な変更に関するドキュメントに各変更の詳細な移行ガイダンスが記載されています。&lt;/p>
&lt;h2 id="まとめ">まとめ&lt;/h2>
&lt;p>EF Core 9 は革新的なリリースではありません。これは進化的なリリースであり、まさにそれが必要なものでした。 LINQ の機能強化だけでも、ほとんどのプロジェクトのアップグレードが正当化され、JSON 列の機能強化、複合型、&lt;code>HierarchyId&lt;/code> サポートなどの機能により、以前は扱いにくかった、または不可能だったパターンが開かれます。&lt;/p>
&lt;p>私のプロジェクトに最も影響を与えた機能を 3 つ挙げるなら:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>パラメーター化されたプリミティブ コレクション&lt;/strong> — クエリ プランのキャッシュ効率が大規模に重要となるため&lt;/li>
&lt;li>&lt;strong>JSON 列の改善&lt;/strong> — ハイブリッド リレーショナル ドキュメント パターンが非常に便利であるため&lt;/li>
&lt;li>&lt;strong>コンパイル済みモデル&lt;/strong> — 起動時間は開発者の生産性と展開速度に直接影響するためEF Core チームは EF Core 5 以来堅実な軌道を歩んできており、バージョン 9 もその傾向を引き継いでいます。すでに EF Core 8 を使用している場合、アップグレードは低リスクであり、大きなメリットがあります。古いものを使用している場合は、今ほどジャンプするのに最適な時期はありません。&lt;/li>
&lt;/ol>
&lt;p>楽しいコーディングと楽しいクエリ。&lt;/p></content:encoded><category>.NET</category><category>Entity Framework</category><category>Database</category></item><item><title>セマンティック カーネルの入門: C# での AI オーケストレーション</title><link>https://emimontesdeoca.github.io/ja/posts/getting-started-semantic-kernel/</link><pubDate>Sun, 05 Oct 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/getting-started-semantic-kernel/</guid><description>Microsoft のセマンティック カーネルを使用して、プラグインやプランナーからメモリや関数呼び出しに至るまで、AI を活用したアプリケーションを C# で構築する方法を学びます。</description><content:encoded>&lt;p>.NET アプリケーションを構築し、AI 環境の進化を見てきた人なら、おそらく次のことを疑問に思ったことがあるでしょう: &lt;em>コードベースをスパゲッティにせずに大規模な言語モデルを C# プロジェクトに統合する最善の方法は何ですか?&lt;/em> それはまさに Microsoft のセマンティック カーネルが解決する問題であり、昨年これを使用して実稼働アプリケーションを構築することに費やした後、セマンティック カーネルは私の開発者ツールキットの中で最も重要なツールの 1 つになったと言えます。&lt;/p>
&lt;p>この投稿では、中心となる概念の理解から現実世界の AI アシスタントの構築まで、セマンティック カーネルを始めるために必要なすべてのことを説明します。 AI 開発に少しだけ足を踏み入れている場合でも、既存の .NET アプリケーションで LLM 呼び出しを調整するための構造化された方法を探している場合でも、このガイドは役に立ちます。&lt;/p>
&lt;h2 id="セマンティック-カーネルとは何ですか">セマンティック カーネルとは何ですか?&lt;/h2>
&lt;p>セマンティック カーネル (SK) は、Microsoft のオープンソース SDK であり、アプリケーション コードと GPT-4o、Azure OpenAI、またはその他の AI サービスなどの大規模な言語モデルの間の &lt;strong>オーケストレーション レイヤー&lt;/strong> として機能します。これは、従来の C# コードと AI 機能をクリーンで構成可能な方法で組み合わせることができる軽量のミドルウェアと考えてください。&lt;/p>
&lt;p>しかし、なぜ OpenAI API を直接呼び出さないのでしょうか?絶対に可能です。単純な使用例の場合はそれで問題ありません。しかし、次のことが必要になった瞬間に：&lt;/p>
&lt;ul>
&lt;li>ユーザー入力に基づいて &lt;strong>どの関数&lt;/strong>を呼び出すかを AI に決定させます&lt;/li>
&lt;li>&lt;strong>複数の AI 呼び出し&lt;/strong>をパイプライン内の従来のコードと結合します&lt;/li>
&lt;li>&lt;strong>メモリとコンテキスト&lt;/strong>を追加して、AI が以前のインタラクションを記憶できるようにします&lt;/li>
&lt;li>複雑なタスクを推論する &lt;strong>マルチステップ エージェント&lt;/strong>を構築する&lt;/li>
&lt;/ul>
&lt;p>&amp;hellip;車輪の再発明をしていることに気づくでしょう。セマンティック カーネルは、ファーストクラスの .NET サポート、依存関係注入の統合、および C# 開発者にとって自然に感じられるプラグイン アーキテクチャを備え、これらすべてをすぐに提供します。&lt;/p>
&lt;p>このプロジェクトは GitHub の &lt;code>microsoft/semantic-kernel&lt;/code> リポジトリに存在し、C#、Python、Java 用の SDK があります。 C# SDK は最も成熟しており、ここではこれに焦点を当てます。&lt;/p>
&lt;h2 id="コアコンセプト">コアコンセプト&lt;/h2>
&lt;p>コードを書く前に、構成要素を理解しましょう。&lt;/p>
&lt;h3 id="カーネル">カーネル&lt;/h3>
&lt;p>&lt;code>Kernel&lt;/code> は、セマンティック カーネルの中心的なオブジェクトです。これはオーケストレーターであり、AI サービス、プラグイン、構成を結び付けるものです。これを作成し、サービスとプラグインを登録し、それを使用してプロンプトを実行したり、関数を呼び出したりします。 ASP.NET Core での依存関係の挿入に慣れている場合、カーネルは非常に親しみのあるものに感じられるでしょう。カーネルは本質的に AI の強力な機能を備えたサービス コンテナーです。&lt;/p>
&lt;h3 id="プラグインと関数">プラグインと関数&lt;/h3>
&lt;p>&lt;strong>プラグイン&lt;/strong>は、カーネルが呼び出すことができる関連する&lt;strong>関数&lt;/strong>のコレクションです。関数には 2 つの種類があります。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>プロンプト関数&lt;/strong> — LLM に送信される自然言語テンプレートとして定義されます。&lt;/li>
&lt;li>&lt;strong>ネイティブ関数&lt;/strong> — カーネルが検出して呼び出すことができる属性で装飾された通常の C# メソッド&lt;/li>
&lt;/ul>
&lt;p>たとえば、ネイティブ関数 &lt;code>GetCurrentWeather(string city)&lt;/code> と気象データをわかりやすく要約するプロンプト関数を備えた &lt;code>WeatherPlugin&lt;/code> があるとします。### AI コネクタ&lt;/p>
&lt;p>コネクタは、セマンティック カーネルが AI サービスと通信する方法です。最も一般的なものは次のとおりです。&lt;/p>
&lt;ul>
&lt;li>&lt;code>AzureOpenAIChatCompletion&lt;/code> — Azure OpenAI サービス用&lt;/li>
&lt;li>&lt;code>OpenAIChatCompletion&lt;/code> — OpenAI の API を直接使用する場合&lt;/li>
&lt;li>ベクトル検索とメモリ用の埋め込みコネクタ&lt;/li>
&lt;/ul>
&lt;p>これらを起動時にカーネルに登録すれば、その他はすべて正常に動作します。&lt;/p>
&lt;h2 id="プロジェクトのセットアップ">プロジェクトのセットアップ&lt;/h2>
&lt;p>手を汚してみましょう。まず、新しいコンソール アプリケーションを作成します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet new console -n SemanticKernelDemo
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cd SemanticKernelDemo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>次に、セマンティック カーネル NuGet パッケージを追加します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel.Connectors.AzureOpenAI
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Azure OpenAI ではなく OpenAI を直接使用している場合:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel.Connectors.OpenAI
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>メモリと埋め込みのサポートの場合 (これは後で使用します):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.SemanticKernel.Plugins.Memory
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet add package Microsoft.Extensions.VectorData.Abstractions
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>.csproj&lt;/code> は .NET 8 以降をターゲットにする必要があります。 Semantic Kernel の最新バージョンは、最新の .NET 機能を最大限に活用しています。&lt;/p>
&lt;h2 id="最初のカーネル">最初のカーネル&lt;/h2>
&lt;p>最も単純な例から始めましょう。カーネルを作成し、AI サービスに接続し、それに質問します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Build the kernel with Azure OpenAI&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = Kernel.CreateBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: &lt;span style="color:#a5d6ff">&amp;#34;https://your-resource.openai.azure.com/&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: &lt;span style="color:#a5d6ff">&amp;#34;your-api-key&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> kernel = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Invoke a simple prompt&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">await&lt;/span> kernel.InvokePromptAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Explain dependency injection in C# in three sentences.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(result);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>OpenAI を直接使用している場合は、サービス登録を交換します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.AddOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> modelId: &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: &lt;span style="color:#a5d6ff">&amp;#34;your-openai-api-key&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>それだけです。実行すると、依存関係の注入についての簡潔な説明が得られます。しかし、これはほんの表面をなぞっただけです。&lt;/p>
&lt;h3 id="プロンプト-テンプレートの使用">プロンプト テンプレートの使用&lt;/h3>
&lt;p>プロンプト テンプレートを使用すると、ハンドルバー スタイルの構文を使用して変数でプロンプトをパラメータ化できます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> prompt = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a technical writer. Write a brief summary of {{&lt;span style="color:#f85149">$&lt;/span>topic}}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> aimed at developers with {{&lt;span style="color:#f85149">$&lt;/span>experienceLevel}} experience.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Keep it under &lt;span style="color:#a5d6ff">200&lt;/span> words.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> function = kernel.CreateFunctionFromPrompt(prompt);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> arguments = &lt;span style="color:#ff7b72">new&lt;/span> KernelArguments
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [&amp;#34;topic&amp;#34;] = &lt;span style="color:#a5d6ff">&amp;#34;gRPC in .NET&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [&amp;#34;experienceLevel&amp;#34;] = &lt;span style="color:#a5d6ff">&amp;#34;intermediate&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">await&lt;/span> kernel.InvokeAsync(function, arguments);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(result);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ここで、セマンティック カーネルが輝き始めます。再利用可能なプロンプト テンプレートを定義し、バージョンを付けて、より大きなワークフローを構成できます。&lt;/p>
&lt;h2 id="プラグインとネイティブ関数">プラグインとネイティブ関数&lt;/h2>
&lt;p>プラグインは、セマンティック カーネルが AI と既存の C# コードの間のギャップを埋める場所です。ネイティブ関数は、カーネルに公開する通常のメソッドです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.ComponentModel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">TimePlugin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [KernelFunction(&amp;#34;get_current_time&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Gets the current date and time in UTC&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GetCurrentTime()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> DateTime.UtcNow.ToString(&lt;span style="color:#a5d6ff">&amp;#34;yyyy-MM-dd HH:mm:ss UTC&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [KernelFunction(&amp;#34;get_time_in_timezone&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Gets the current time in a specific timezone&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GetTimeInTimezone(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;The IANA timezone identifier, e.g. &amp;#39;America/New_York&amp;#39;&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> timezone)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> tz = TimeZoneInfo.FindSystemTimeZoneById(timezone);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> time = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tz);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> time.ToString(&lt;span style="color:#a5d6ff">&amp;#34;yyyy-MM-dd HH:mm:ss&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>[KernelFunction]&lt;/code> 属性と &lt;code>[Description]&lt;/code> 属性に注目してください。これらは重要です。説明は、AI が関数をいつどのように呼び出すかを理解するために読み取るものです。適切な説明は、ツールを効果的に使用する AI と混乱している AI の違いを生み出します。&lt;/p>
&lt;p>カーネルにプラグインを登録します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = Kernel.CreateBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: &lt;span style="color:#a5d6ff">&amp;#34;https://your-resource.openai.azure.com/&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: &lt;span style="color:#a5d6ff">&amp;#34;your-api-key&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Plugins.AddFromType&amp;lt;TimePlugin&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> kernel = builder.Build();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>サービスを挿入する、より複雑なプラグインを構築することもできます。セマンティック カーネルは &lt;code>Microsoft.Extensions.DependencyInjection&lt;/code> と統合されているため、プラグインは他のサービスと同様にコンストラクターの依存関係を受け取ることができます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">OrderPlugin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> IOrderRepository _repository;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> OrderPlugin(IOrderRepository repository)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _repository = repository;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [KernelFunction(&amp;#34;get_order_status&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Retrieves the status of an order by its ID&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; GetOrderStatus(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;The order ID to look up&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> orderId)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> order = &lt;span style="color:#ff7b72">await&lt;/span> _repository.GetByIdAsync(orderId);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> order &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ? &lt;span style="color:#a5d6ff">$&amp;#34;No order found with ID {orderId}&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> : &lt;span style="color:#a5d6ff">$&amp;#34;Order {orderId}: {order.Status}, placed on {order.CreatedAt:d}&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="関数呼び出しと自動呼び出し">関数呼び出しと自動呼び出し&lt;/h2>
&lt;p>ここからが本当に興味深いことになります。 &lt;strong>関数呼び出し&lt;/strong> (ツール呼び出しとも呼ばれる) を使用すると、会話コンテキストに基づいて、登録された関数のどれを呼び出すかを AI モデルに決定させます。モデルはコードを実行しません。「これらの引数を使用して関数 X を呼び出したい」という構造化されたリクエストを返し、カーネルが実際の呼び出しを処理します。&lt;/p>
&lt;p>自動関数呼び出しを有効にする方法は次のとおりです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Connectors.OpenAI&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = Kernel.CreateBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: &lt;span style="color:#a5d6ff">&amp;#34;https://your-resource.openai.azure.com/&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: &lt;span style="color:#a5d6ff">&amp;#34;your-api-key&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Plugins.AddFromType&amp;lt;TimePlugin&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Plugins.AddFromType&amp;lt;WeatherPlugin&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> kernel = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Enable automatic function calling&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> settings = &lt;span style="color:#ff7b72">new&lt;/span> OpenAIPromptExecutionSettings
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">await&lt;/span> kernel.InvokePromptAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;What time is it in Tokyo and what&amp;#39;s the weather like there?&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> KernelArguments(settings)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(result);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>FunctionChoiceBehavior.Auto()&lt;/code> を使用すると、カーネルは次のことを行います。&lt;/p>
&lt;ol>
&lt;li>利用可能なすべての機能の説明とともにプロンプトを AI に送信します&lt;/li>
&lt;li>AI は、&lt;code>get_time_in_timezone&lt;/code> および &lt;code>get_weather&lt;/code> を呼び出す必要があると判断します。&lt;/li>
&lt;li>カーネルはこれらの関数を自動的に実行します。&lt;/li>
&lt;li>結果は AI に送信されます&lt;/li>
&lt;li>AI は関数の結果を使用して自然言語応答を作成しますこのループは 1 回の呼び出しで複数回発生する可能性があります。AI は必要な情報をすべて収集するために複数の関数を順番に呼び出すことがあります。 &lt;code>FunctionChoiceBehavior.Required()&lt;/code> を使用して、AI に少なくとも 1 つの関数の呼び出しを強制したり、使用を許可する関数の特定のリストを提供したりすることもできます。&lt;/li>
&lt;/ol>
&lt;h3 id="履歴付きチャット完了">履歴付きチャット完了&lt;/h3>
&lt;p>会話型アプリケーションの場合は、 &lt;code>ChatCompletionService&lt;/code> を &lt;code>ChatHistory&lt;/code> オブジェクトで直接使用する必要があります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.ChatCompletion&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> chatService = kernel.GetRequiredService&amp;lt;IChatCompletionService&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> history = &lt;span style="color:#ff7b72">new&lt;/span> ChatHistory();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>history.AddSystemMessage(&lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a helpful developer assistant. You have access to tools
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">for&lt;/span> checking the time and weather. Be concise and friendly.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;);
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">while&lt;/span> (&lt;span style="color:#79c0ff">true&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.Write(&lt;span style="color:#a5d6ff">&amp;#34;You: &amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> input = Console.ReadLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrWhiteSpace(input)) &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.AddUserMessage(input);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> chatService.GetChatMessageContentAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> executionSettings: settings,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> kernel: kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.AddAssistantMessage(response.Content ?? &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;Assistant: {response.Content}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これにより、会話履歴を保持し、必要に応じてプラグインを呼び出すことができる完全にインタラクティブなチャットボットが得られます。&lt;/p>
&lt;h2 id="メモリと埋め込み">メモリと埋め込み&lt;/h2>
&lt;p>AI アプリケーションで最も強力なパターンの 1 つは &lt;strong>検索拡張生成 (RAG)&lt;/strong> です。これは、データをベクトル空間に埋め込み、クエリ時に関連するチャンクを取得することで、AI に独自のデータへのアクセスを提供します。&lt;/p>
&lt;p>セマンティック カーネルは、ベクトル ストアと埋め込みを操作するための抽象化を提供します。開発用にインメモリ ベクター ストアを設定する方法は次のとおりです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.Extensions.VectorData&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Connectors.AzureOpenAI&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Embeddings&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">#pragma&lt;/span> warning disable SKEXP0010
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Create an embedding generation service&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = Kernel.CreateBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureOpenAITextEmbeddingGeneration(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;text-embedding-ada-002&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: &lt;span style="color:#a5d6ff">&amp;#34;https://your-resource.openai.azure.com/&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: &lt;span style="color:#a5d6ff">&amp;#34;your-api-key&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> kernel = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> embeddingService = kernel.GetRequiredService&amp;lt;ITextEmbeddingGenerationService&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Generate embeddings for your documents&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> documents = &lt;span style="color:#ff7b72">new&lt;/span>[]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Semantic Kernel is an open-source SDK for AI orchestration.&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Azure OpenAI provides enterprise-grade AI models.&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Plugins in SK allow you to expose C# methods to AI models.&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> embeddings = &lt;span style="color:#ff7b72">await&lt;/span> embeddingService.GenerateEmbeddingsAsync(documents);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>運用シナリオの場合は、これらの埋め込みを Azure AI Search、Qdrant、Pinecone などの専用のベクター データベースに保存します。セマンティック カーネルには、&lt;code>Microsoft.Extensions.VectorData&lt;/code> 抽象化を介したこれらすべてのコネクタがあります。&lt;/p>
&lt;p>一般的な RAG フローは次のようになります。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>取り込み&lt;/strong>: ドキュメントをチャンク化し、埋め込みを生成し、ベクトル データベースに保存します。&lt;/li>
&lt;li>&lt;strong>取得&lt;/strong>: ユーザーが質問すると、クエリを埋め込んで最も類似したドキュメントを検索します。&lt;/li>
&lt;li>&lt;strong>生成&lt;/strong>: 取得したドキュメントを、ユーザーの質問とともにコンテキストとして LLM に渡します。&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[KernelFunction(&amp;#34;search_knowledge_base&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[Description(&amp;#34;Searches the internal knowledge base for relevant information&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; SearchKnowledgeBase(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;The search query&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> query)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> queryEmbedding = &lt;span style="color:#ff7b72">await&lt;/span> _embeddingService.GenerateEmbeddingAsync(query);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> results = &lt;span style="color:#ff7b72">await&lt;/span> _vectorStore.SearchAsync(queryEmbedding, limit: &lt;span style="color:#a5d6ff">3&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>.Join(&lt;span style="color:#a5d6ff">&amp;#34;\n\n&amp;#34;&lt;/span>, results.Select(r =&amp;gt; r.Text));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>RAG パイプラインをカーネル関数として公開することで、AI はナレッジ ベースをいつ検索する必要があるかを自動的に決定できるため、オーケストレーションをクリーンな状態に保ち、モデルに最適な動作をさせることができます。&lt;/p>
&lt;h2 id="プランナーとエージェント">プランナーとエージェント&lt;/h2>
&lt;p>AI アプリケーションがより複雑になると、&lt;strong>複数ステップのタスクを計画して実行&lt;/strong>する AI が必要になります。ここで、セマンティック カーネルのエージェント フレームワークが登場します。&lt;/p>
&lt;h3 id="基本-チャット完了エージェント">基本: チャット完了エージェント&lt;/h3>
&lt;p>最も単純なエージェント タイプは、チャット完了モデルを命令とプラグインでラップします。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Agents&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">#pragma&lt;/span> warning disable SKEXP0110
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> agent = &lt;span style="color:#ff7b72">new&lt;/span> ChatCompletionAgent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;DevAssistant&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a senior .NET developer assistant. Help users with code
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> reviews, architecture decisions, and debugging. Always provide
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> code examples when relevant. Use your available tools to look up
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> current information when needed.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Arguments = &lt;span style="color:#ff7b72">new&lt;/span> KernelArguments(settings)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> history = &lt;span style="color:#ff7b72">new&lt;/span> ChatHistory();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>history.AddUserMessage(&lt;span style="color:#a5d6ff">&amp;#34;How should I structure a clean architecture project in .NET 8?&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> message &lt;span style="color:#ff7b72">in&lt;/span> agent.InvokeAsync(history))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(message.Content);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.Add(message);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="マルチエージェントのコラボレーション">マルチエージェントのコラボレーション&lt;/h3>
&lt;p>&lt;strong>複数のエージェントが連携している&lt;/strong>場合、物事が非常に強力になります。セマンティック カーネルは、さまざまな専門分野を持つエージェントが協力するグループ チャット パターンをサポートしています。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">#pragma&lt;/span> warning disable SKEXP0110
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> codeReviewer = &lt;span style="color:#ff7b72">new&lt;/span> ChatCompletionAgent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;CodeReviewer&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You review C&lt;span style="color:#f85149">#&lt;/span> code &lt;span style="color:#ff7b72">for&lt;/span> bugs, performance issues, and best practices.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Be specific about what you find and suggest concrete fixes.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> securityAuditor = &lt;span style="color:#ff7b72">new&lt;/span> ChatCompletionAgent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = &lt;span style="color:#a5d6ff">&amp;#34;SecurityAuditor&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Instructions = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You focus exclusively &lt;span style="color:#ff7b72">on&lt;/span> security vulnerabilities &lt;span style="color:#ff7b72">in&lt;/span> code.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Look &lt;span style="color:#ff7b72">for&lt;/span> injection attacks, authentication issues, data exposure,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> and OWASP Top &lt;span style="color:#a5d6ff">10&lt;/span> violations.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> Kernel = kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> groupChat = &lt;span style="color:#ff7b72">new&lt;/span> AgentGroupChat(codeReviewer, securityAuditor)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ExecutionSettings = &lt;span style="color:#ff7b72">new&lt;/span> AgentGroupChatSettings
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> TerminationStrategy = &lt;span style="color:#ff7b72">new&lt;/span> MaximumIterationTerminationStrategy(&lt;span style="color:#a5d6ff">4&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>groupChat.AddChatMessage(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> ChatMessageContent(AuthorRole.User, &lt;span style="color:#a5d6ff">&amp;#34;Review this code: ...&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> message &lt;span style="color:#ff7b72">in&lt;/span> groupChat.InvokeAsync())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;[{message.AuthorName}]: {message.Content}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>このパターンは、さまざまな視点や専門分野が関与する必要がある複雑なタスクに非常に役立ちます。各エージェントは独自のシステム プロンプトで動作し、独自のプラグイン セットを持つことができます。&lt;/p>
&lt;h2 id="実際の例-プロジェクト-ドキュメント-アシスタントの構築">実際の例: プロジェクト ドキュメント アシスタントの構築&lt;/h2>
&lt;p>すべてを実際の例で結び付けてみましょう。ファイルを読み取って質問に答えることで、開発者がコードベースを理解できるように支援する AI アシスタントです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.ChatCompletion&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.SemanticKernel.Connectors.OpenAI&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.ComponentModel&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Define our plugins&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">FileSystemPlugin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> _rootPath;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> FileSystemPlugin(&lt;span style="color:#ff7b72">string&lt;/span> rootPath)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _rootPath = rootPath;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [KernelFunction(&amp;#34;read_file&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Reads the contents of a file from the project directory&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; ReadFile(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Relative path to the file&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> path)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> fullPath = Path.Combine(_rootPath, path);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!File.Exists(fullPath))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#a5d6ff">$&amp;#34;File not found: {path}&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> content = &lt;span style="color:#ff7b72">await&lt;/span> File.ReadAllTextAsync(fullPath);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Truncate very large files&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (content.Length &amp;gt; &lt;span style="color:#a5d6ff">8000&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> content = content[..&lt;span style="color:#a5d6ff">8000&lt;/span>] + &lt;span style="color:#a5d6ff">&amp;#34;\n... [truncated]&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> content;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [KernelFunction(&amp;#34;list_files&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Lists files in a directory, optionally filtered by extension&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> ListFiles(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Relative directory path&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> directory,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;File extension filter like &amp;#39;.cs&amp;#39; or &amp;#39;.json&amp;#39;&amp;#34;)] &lt;span style="color:#ff7b72">string?&lt;/span> extension = &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> fullPath = Path.Combine(_rootPath, directory);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!Directory.Exists(fullPath))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#a5d6ff">$&amp;#34;Directory not found: {directory}&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> files = Directory.GetFiles(fullPath, &lt;span style="color:#a5d6ff">&amp;#34;*.*&amp;#34;&lt;/span>, SearchOption.AllDirectories)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Select(f =&amp;gt; Path.GetRelativePath(_rootPath, f))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Where(f =&amp;gt; extension &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span> || f.EndsWith(extension))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Take(&lt;span style="color:#a5d6ff">50&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>.Join(&lt;span style="color:#a5d6ff">&amp;#34;\n&amp;#34;&lt;/span>, files);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">DocumentationPlugin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [KernelFunction(&amp;#34;generate_summary&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Generates a structured markdown summary for documentation&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GenerateSummaryTemplate(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Name of the component&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> componentName,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Brief description&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> description,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Description(&amp;#34;Key responsibilities as comma-separated values&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> responsibilities)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> items = responsibilities.Split(&lt;span style="color:#a5d6ff">&amp;#39;,&amp;#39;&lt;/span>, StringSplitOptions.TrimEntries);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> bullets = &lt;span style="color:#ff7b72">string&lt;/span>.Join(&lt;span style="color:#a5d6ff">&amp;#34;\n&amp;#34;&lt;/span>, items.Select(r =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;- {r}&amp;#34;&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#a5d6ff">$&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> ## {componentName}
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> {description}
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> ### Responsibilities
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> {bullets}
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> ---
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> *Generated documentation — review and expand as needed.*
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> &amp;#34;&amp;#34;&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Wire it all up&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = Kernel.CreateBuilder();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: &lt;span style="color:#a5d6ff">&amp;#34;https://your-resource.openai.azure.com/&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: &lt;span style="color:#a5d6ff">&amp;#34;your-api-key&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Plugins.AddFromObject(&lt;span style="color:#ff7b72">new&lt;/span> FileSystemPlugin(&lt;span style="color:#a5d6ff">&amp;#34;./src&amp;#34;&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Plugins.AddFromType&amp;lt;DocumentationPlugin&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> kernel = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> chatService = kernel.GetRequiredService&amp;lt;IChatCompletionService&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> history = &lt;span style="color:#ff7b72">new&lt;/span> ChatHistory();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>history.AddSystemMessage(&lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> You are a codebase documentation assistant. You help developers understand
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> projects &lt;span style="color:#ff7b72">by&lt;/span> reading source files and explaining architecture, patterns,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> and design decisions.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> When asked about code, use your tools to read the actual files rather
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> than guessing. Be specific and reference actual code when possible.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Generate documentation artifacts when asked.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&amp;#34;);
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> settings = &lt;span style="color:#ff7b72">new&lt;/span> OpenAIPromptExecutionSettings
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;Documentation Assistant ready. Ask me about your codebase!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(&lt;span style="color:#a5d6ff">&amp;#34;Type &amp;#39;exit&amp;#39; to quit.\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">while&lt;/span> (&lt;span style="color:#79c0ff">true&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.Write(&lt;span style="color:#a5d6ff">&amp;#34;You: &amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> input = Console.ReadLine();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrWhiteSpace(input) || input.Equals(&lt;span style="color:#a5d6ff">&amp;#34;exit&amp;#34;&lt;/span>, StringComparison.OrdinalIgnoreCase))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.AddUserMessage(input);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> chatService.GetChatMessageContentAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> executionSettings: settings,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> kernel: kernel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;\nAssistant: {response.Content}\n&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> history.AddAssistantMessage(response.Content ?? &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>このアシスタントは次のことができます。- &lt;strong>プロジェクト ディレクトリからファイルを一覧表示して読み取る&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>実際のソース ファイルを読んでコードベースに関する &lt;strong>質問に答える&lt;/strong>&lt;/li>
&lt;li>&lt;strong>マークダウン形式でドキュメントを生成&lt;/strong>&lt;/li>
&lt;li>&lt;strong>会話のコンテキストを維持&lt;/strong>して、フォローアップの質問が自然に機能するようにします&lt;/li>
&lt;/ul>
&lt;p>AI は、ユーザーの質問に基づいて、&lt;code>read_file&lt;/code>、&lt;code>list_files&lt;/code>、または &lt;code>generate_summary&lt;/code> をいつ呼び出すかを自動的に決定します。 「OrderService は何をするのですか?」と尋ねてください。ファイルを読み取り、分析し、説明します。 「認証モジュールのドキュメントを生成する」ように依頼すると、ファイルを調べて構造を理解し、フォーマットされた概要を生成します。&lt;/p>
&lt;h2 id="制作のヒント">制作のヒント&lt;/h2>
&lt;p>セマンティック カーネル アプリケーションを出荷する前に、私が苦労して学んだことがいくつかあります。&lt;/p>
&lt;p>&lt;strong>依存関係の挿入を適切に使用します。&lt;/strong> ASP.NET Core アプリでは、カーネルとサービスをインラインで作成するのではなく、DI コンテナーに登録します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddKernel()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddAzureOpenAIChatCompletion(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> deploymentName: &lt;span style="color:#a5d6ff">&amp;#34;gpt-4o&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> endpoint: configuration[&lt;span style="color:#a5d6ff">&amp;#34;AzureOpenAI:Endpoint&amp;#34;&lt;/span>]!,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> apiKey: configuration[&lt;span style="color:#a5d6ff">&amp;#34;AzureOpenAI:ApiKey&amp;#34;&lt;/span>]!
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Plugins.AddFromType&amp;lt;TimePlugin&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddFromType&amp;lt;OrderPlugin&amp;gt;();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>エラーを適切に処理します。&lt;/strong> LLM 呼び出しは失敗したり、タイムアウトになったり、予期しない結果を返したりする可能性があります。呼び出しを try-catch ブロックでラップし、Polly または組み込みの復元機能を使用して再試行ポリシーを実装します。&lt;/p>
&lt;p>&lt;strong>トークンの使用状況を監視します。&lt;/strong> すべてのプロンプト、すべての機能の説明、およびすべてのチャット履歴でトークンが消費されます。フィルターを使用して使用状況をログに記録し、追跡します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>kernel.FunctionInvocationFilters.Add(&lt;span style="color:#ff7b72">new&lt;/span> LoggingFilter());
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>関数の説明は正確にしてください。&lt;/strong> 曖昧な説明は、AI による関数の誤った呼び出しにつながります。 「説明だけを読めば、この機能をいつ、どのように使用するか正確にわかりますか?」と質問して、説明をテストします。&lt;/p>
&lt;h2 id="結論">結論&lt;/h2>
&lt;p>Semantic Kernel は、アプリケーションの構築に関する考え方を根本的に変えるライブラリの 1 つです。これは単なる API ラッパーではなく、保守可能、テスト可能、実稼働対応の方法で従来のコードを使用して AI 機能を構築できるオーケストレーション フレームワークです。&lt;/p>
&lt;p>私が最も気に入っている点は、.NET エコシステムを尊重していることです。これは、依存関係の注入、属性、非同期/待機、インターフェイスなど、すでに知られているパターンを使用し、それらを AI の世界に拡張します。まったく新しいパラダイムを学ぶ必要はありません。 AI をツールキットの別の機能として追加するだけです。&lt;/p>
&lt;p>.NET アプリケーションを構築していて、まだセマンティック カーネルを試していない場合は、今がその時期です。 SDK は安定しており、コミュニティは活発で、シンプルなプロンプト オーケストレーションからマルチエージェントのコラボレーションまで、SDK によって可能になるパターンは、現代の開発者にとって必須のスキルになりつつあります。&lt;/p>
&lt;p>小さなことから始めましょう。カーネルを作成し、プラグインを登録し、AI がコードを呼び出すのを観察します。それがうまくいくと、アプリケーションのあらゆる場所にインテリジェンスを追加できる機会が見え始めるでしょう。&lt;/p>
&lt;p>&lt;a href="https://learn.microsoft.com/semantic-kernel/overview/">公式ドキュメント&lt;/a> と &lt;a href="https://github.com/microsoft/semantic-kernel">GitHub リポジトリ&lt;/a> は、旅を続けるための優れたリソースです。ハッピービルディング！&lt;/p></content:encoded><category>AI</category><category>.NET</category><category>Semantic Kernel</category><category>Azure</category></item><item><title>Blazor でのコンポーネントの継承</title><link>https://emimontesdeoca.github.io/ja/posts/blazor-inherit-components/</link><pubDate>Thu, 04 Sep 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/blazor-inherit-components/</guid><description>ComponentBase と共有基本クラスを使用した継承を通じて、Blazor コンポーネントを拡張および再利用します。</description><content:encoded>&lt;p>私は多数のフォーム ページを含むプロジェクトを構築していましたが、どのページも同じ読み込み状態ロジック、同じエラー処理、同じトースト通知を持っていました。これらすべてをコピーして貼り付けるのは間違っていると感じたので、Blazor でのコンポーネントの継承について調べました。 Blazor コンポーネントは単なる C# クラスであるため、これは非常に簡単であることがわかります。&lt;/p>
&lt;h1 id="基本">基本&lt;/h1>
&lt;p>すべての Blazor コンポーネントは、既定で &lt;code>ComponentBase&lt;/code> から継承します。 &lt;code>ComponentBase&lt;/code> を拡張する独自の基本クラスを作成し、それをコンポーネントに継承させることができます。&lt;/p>
&lt;p>ほとんどのページで状態の読み込みとエラー処理が必要だとします。基本クラスを作成できます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Components&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">abstract&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">PageBase&lt;/span> : ComponentBase
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">bool&lt;/span> IsLoading { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">string?&lt;/span> ErrorMessage { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task LoadDataAsync(Func&amp;lt;Task&amp;gt; action)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IsLoading = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ErrorMessage = &lt;span style="color:#79c0ff">null&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> action();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception ex)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ErrorMessage = ex.Message;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">finally&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IsLoading = &lt;span style="color:#79c0ff">false&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> StateHasChanged();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="基本クラスの使用">基本クラスの使用&lt;/h1>
&lt;p>どのページ コンポーネントでも、&lt;code>ComponentBase&lt;/code> から継承する代わりに、&lt;code>PageBase&lt;/code> から継承します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/users&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@inherits PageBase
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@if (IsLoading)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;spinner&amp;#34;&lt;/span>&amp;gt;&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">else&lt;/span> &lt;span style="color:#ff7b72">if&lt;/span> (ErrorMessage &lt;span style="color:#ff7b72">is&lt;/span> not &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;alert alert-danger&amp;#34;&lt;/span>&amp;gt;@ErrorMessage&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;ul&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @foreach (&lt;span style="color:#ff7b72">var&lt;/span> user &lt;span style="color:#ff7b72">in&lt;/span> users)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;li&amp;gt;@user.Name&amp;lt;/li&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/ul&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> List&amp;lt;User&amp;gt; users = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> LoadDataAsync(&lt;span style="color:#ff7b72">async&lt;/span> () =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> users = &lt;span style="color:#ff7b72">await&lt;/span> Http.GetFromJsonAsync&amp;lt;List&amp;lt;User&amp;gt;&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;api/users&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>@inherits PageBase&lt;/code> ディレクティブが鍵です。これは、デフォルトの &lt;code>ComponentBase&lt;/code> の代わりに基本クラスを使用するように Blazor に指示します。これで、&lt;code>IsLoading&lt;/code>、&lt;code>ErrorMessage&lt;/code>、および &lt;code>LoadDataAsync()&lt;/code> を継承するすべてのページで無料で取得できるようになりました。&lt;/p>
&lt;h1 id="基本クラスにサービスを挿入する">基本クラスにサービスを挿入する&lt;/h1>
&lt;p>基本クラスにサービスを挿入して、すべての子コンポーネントで利用できるようにすることもできます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">abstract&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">PageBase&lt;/span> : ComponentBase
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Inject]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> NavigationManager Navigation { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">default&lt;/span>!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Inject]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> IToastService Toast { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">default&lt;/span>!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">bool&lt;/span> IsLoading { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">string?&lt;/span> ErrorMessage { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> NavigateBack() =&amp;gt; Navigation.NavigateTo(&lt;span style="color:#a5d6ff">&amp;#34;javascript:history.back()&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShowSuccess(&lt;span style="color:#ff7b72">string&lt;/span> message) =&amp;gt; Toast.ShowSuccess(message);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>PageBase&lt;/code> を継承するすべてのコンポーネントは、何も注入しなくても &lt;code>Navigation&lt;/code>、&lt;code>Toast&lt;/code>、&lt;code>NavigateBack()&lt;/code>、および &lt;code>ShowSuccess()&lt;/code> にアクセスできるようになりました。&lt;/p>
&lt;h1 id="汎用基本クラスをさらに深く掘り下げる">汎用基本クラスをさらに深く掘り下げる&lt;/h1>
&lt;p>一般的な CRUD パターンの汎用基本クラスを作成することもできます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">abstract&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CrudPageBase&lt;/span>&amp;lt;T&amp;gt; : PageBase
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> List&amp;lt;T&amp;gt; Items { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> T? SelectedItem { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">abstract&lt;/span> Task&amp;lt;List&amp;lt;T&amp;gt;&amp;gt; FetchItems();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">abstract&lt;/span> Task DeleteItem(T item);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> LoadDataAsync(&lt;span style="color:#ff7b72">async&lt;/span> () =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Items = &lt;span style="color:#ff7b72">await&lt;/span> FetchItems();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnDelete(T item)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> LoadDataAsync(&lt;span style="color:#ff7b72">async&lt;/span> () =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> DeleteItem(item);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Items = &lt;span style="color:#ff7b72">await&lt;/span> FetchItems();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ShowSuccess(&lt;span style="color:#a5d6ff">&amp;#34;Item deleted.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>そうすれば、実際のページは非常にきれいになります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/products&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@inherits CrudPageBase&amp;lt;Product&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>* just the markup, all logic lives &lt;span style="color:#ff7b72">in&lt;/span> the &lt;span style="color:#ff7b72">base&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> *&lt;span style="color:#f85149">@&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> Task&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt; FetchItems()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> =&amp;gt; Http.GetFromJsonAsync&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;api/products&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> Task DeleteItem(Product item)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> =&amp;gt; Http.DeleteAsync(&lt;span style="color:#a5d6ff">$&amp;#34;api/products/{item.Id}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="使用する場合と使用しない場合">使用する場合と使用しない場合&lt;/h1>
&lt;p>コンポーネントの継承は、状態の読み込み、エラー処理、認証チェック、CRUD パターンなどの共有動作に最適です。ただし、継承階層を深くしすぎないでください。2 レベル以上の階層を深くする場合は、(前の投稿の &lt;code>LoadingComponent&lt;/code> アプローチのような) 合成を使用したほうがよいでしょう。&lt;/p>
&lt;p>私は通常、ページの「タイプ」ごとに 1 つの基本クラスに保ちます。通常のページの場合は &lt;code>PageBase&lt;/code>、フォームの場合は &lt;code>FormPageBase&lt;/code> です。それだけです。&lt;/p>
&lt;p>投稿が気に入っていただければ幸いです!ソーシャルメディアで**@emimontesdeoca**までお気軽にご連絡ください。&lt;/p>
&lt;h1 id="リソース">リソース&lt;/h1>
&lt;ul>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/components/#inheritance">ASP.NET Core Razor コンポーネントの継承&lt;/a>&lt;/li>
&lt;/ul></content:encoded><category>.NET</category><category>Blazor</category><category>C#</category></item><item><title>.NET Aspire: クラウドネイティブ アプリケーションを正しい方法で構築する</title><link>https://emimontesdeoca.github.io/ja/posts/dotnet-aspire-cloud-native/</link><pubDate>Wed, 20 Aug 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/dotnet-aspire-cloud-native/</guid><description>.NET Aspire の詳細なガイド。.NET で監視可能で本番環境に対応した分散アプリケーションを構築するための独自のスタックです。</description><content:encoded>&lt;p>.NET で分散アプリケーションを構築したことがある場合は、その詳細をご存知でしょう。 Web API を起動し、バックグラウンド ワーカーを追加し、キャッシュには Redis、永続化には PostgreSQL、おそらくメッセージングには RabbitMQ を投入します。そして突然、ビジネス ロジックの作成よりもインフラストラクチャの配線に多くの時間を費やすことになります。 &lt;code>appsettings.json&lt;/code> ファイルに散在する接続文字列、設定を忘れたヘルスチェック、常に「次のスプリントの問題」である可観測性。&lt;/p>
&lt;p>私はそこに行ったことがある。私が認めたい以上の回数。&lt;/p>
&lt;p>まさに、&lt;strong>.NET Aspire&lt;/strong> が解決するために設計された問題です。実稼働環境で数か月間実行した後、良い点、素晴らしい点、そして欠点など、学んだことを共有したいと思います。&lt;/p>
&lt;h2 id="net-aspire-とは何ですか">.NET Aspire とは何ですか?&lt;/h2>
&lt;p>.NET Aspire は、.NET を使用して、監視可能で本番環境に対応した分散アプリケーションを構築するための独自のスタックです。これは従来の意味でのフレームワークではありません。ASP.NET Core を置き換えたり、新しいプログラミング モデルを強制したりするものではありません。代わりに、すでに知っていることの上に置かれ、クラウドネイティブ アプリを構築する際に常に存在していたギャップを埋めます。&lt;/p>
&lt;p>Aspire はその中核として 4 つのことを提供します。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>AppHost&lt;/strong> — 分散アプリケーション トポロジ全体を定義するプロジェクト。どのサービスが存在し、何に依存し、どのように接続するのか。&lt;/li>
&lt;li>&lt;strong>サービスのデフォルト&lt;/strong> — ヘルスチェック、復元ポリシー、OpenTelemetry などの横断的な懸念事項をすべてのサービスに対して 1 回で構成する共有プロジェクト。&lt;/li>
&lt;li>&lt;strong>コンポーネント&lt;/strong> — Redis、PostgreSQL、RabbitMQ、Azure Storage などのバッキング サービスとの標準化された統合を提供する NuGet パッケージ。&lt;/li>
&lt;li>&lt;strong>開発者ダッシュボード&lt;/strong> — ローカル開発中の分散アプリ全体のログ、トレース、メトリクスを表示するリアルタイム UI。&lt;/li>
&lt;/ol>
&lt;p>哲学はシンプルです。すべての .NET クラウド アプリにこれらの機能が必要な場合、なぜ毎回それらを最初から実装するのでしょうか?&lt;/p>
&lt;h2 id="最初の-aspire-プロジェクトをセットアップする">最初の Aspire プロジェクトをセットアップする&lt;/h2>
&lt;p>始めるのは簡単です。 .NET 8 以降と Aspire ワークロードがインストールされている必要があります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet workload update
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet workload install aspire
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>次に、新しい Aspire スターター プロジェクトを作成します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet new aspire-starter -n MyCloudApp
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これにより、4 つのプロジェクトを含むソリューションが生成されます。&lt;/p>
&lt;ul>
&lt;li>&lt;code>MyCloudApp.AppHost&lt;/code> — オーケストレーター&lt;/li>
&lt;li>&lt;code>MyCloudApp.ServiceDefaults&lt;/code> — 共有構成&lt;/li>
&lt;li>&lt;code>MyCloudApp.ApiService&lt;/code> — サンプル Web API&lt;/li>
&lt;li>&lt;code>MyCloudApp.Web&lt;/code> — Blazor フロントエンド&lt;/li>
&lt;/ul>
&lt;p>AppHost を実行すると、すぐにブラウザーで Aspire ダッシュボードが開き、すべてのサービス、そのログ、正常性が表示されます。 Docker Compose ファイルがありません。手動のポート管理はありません。それはちょうどうまくいきます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet run --project MyCloudApp.AppHost
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>その最初の経験が私を魅了しました。 1 分以内に、可観測性が組み込まれた、完全にオーケストレーションされた分散アプリが完成します。&lt;/p>
&lt;h2 id="apphost-パターン">AppHost パターン&lt;/h2>
&lt;p>AppHost は魔法が生きる場所です。これは、ビルダー パターンを使用して分散アプリケーションのトポロジ (どのようなリソースが存在し、サービスがどのようにリソースに接続するか) を定義する小さなコンソール アプリケーションです。&lt;/p>
&lt;p>実際の AppHost は次のようになります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = DistributedApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Infrastructure resources&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> postgres = builder.AddPostgres(&lt;span style="color:#a5d6ff">&amp;#34;postgres&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithPgAdmin()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddDatabase(&lt;span style="color:#a5d6ff">&amp;#34;catalogdb&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> redis = builder.AddRedis(&lt;span style="color:#a5d6ff">&amp;#34;cache&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> rabbitmq = builder.AddRabbitMQ(&lt;span style="color:#a5d6ff">&amp;#34;messaging&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithManagementPlugin();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Application services&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> catalogApi = builder.AddProject&amp;lt;Projects.CatalogApi&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;catalog-api&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReference(postgres)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReference(redis)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReference(rabbitmq);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> orderApi = builder.AddProject&amp;lt;Projects.OrderApi&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;order-api&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReference(postgres)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReference(rabbitmq);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> frontend = builder.AddProject&amp;lt;Projects.WebFrontend&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;frontend&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithExternalHttpEndpoints()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReference(catalogApi)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReference(orderApi);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Build().Run();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```そのコードを声に出して読んでください。実際にはそれ自体が文書化されています。&lt;/span> &lt;span style="color:#f85149">「カタログ&lt;/span> API &lt;span style="color:#f85149">は&lt;/span> PostgreSQL&lt;span style="color:#f85149">、&lt;/span>Redis&lt;span style="color:#f85149">、&lt;/span>RabbitMQ &lt;span style="color:#f85149">を参照します。フロントエンドはカタログ&lt;/span> API &lt;span style="color:#f85149">と注文&lt;/span> API &lt;span style="color:#f85149">を参照します。」それがコードで表現されたアーキテクチャです。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">注目すべき点がいくつかあります&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">`&lt;/span>WithReference&lt;span style="color:#f85149">`&lt;/span>** &lt;span style="color:#f85149">は面倒な作業を行います。環境変数と構成を介して、接続文字列とサービス&lt;/span> URL &lt;span style="color:#f85149">が使用側プロジェクトに自動的に挿入されます。サービスは&lt;/span> Redis &lt;span style="color:#f85149">が「どこで」実行されているかを知る必要はありません。&lt;/span>Aspire &lt;span style="color:#f85149">がそれを処理します。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">`&lt;/span>WithPgAdmin()&lt;span style="color:#f85149">`&lt;/span>** &lt;span style="color:#f85149">および&lt;/span> **&lt;span style="color:#f85149">`&lt;/span>WithManagementPlugin()&lt;span style="color:#f85149">`&lt;/span>** &lt;span style="color:#f85149">は、実際のサービスとともに&lt;/span> PostgreSQL &lt;span style="color:#f85149">および&lt;/span> RabbitMQ &lt;span style="color:#f85149">の管理&lt;/span> UI &lt;span style="color:#f85149">を起動します。開発中、これらは非常に貴重です。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">`&lt;/span>WithExternalHttpEndpoints()&lt;span style="color:#f85149">`&lt;/span>** &lt;span style="color:#f85149">はサービスを外部からアクセスできるものとしてマークします。これは展開時に重要です。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">###&lt;/span> &lt;span style="color:#f85149">リソース構成&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">きめ細かい制御を使用してリソースを構成できます。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```&lt;/span>csharp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> postgres = builder.AddPostgres(&lt;span style="color:#a5d6ff">&amp;#34;postgres&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithEnvironment(&lt;span style="color:#a5d6ff">&amp;#34;POSTGRES_MAX_CONNECTIONS&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;200&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithDataVolume(&lt;span style="color:#a5d6ff">&amp;#34;postgres-data&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddDatabase(&lt;span style="color:#a5d6ff">&amp;#34;catalogdb&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> redis = builder.AddRedis(&lt;span style="color:#a5d6ff">&amp;#34;cache&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithDataVolume(&lt;span style="color:#a5d6ff">&amp;#34;redis-data&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>データ ボリュームにより、ローカル開発データはコンテナーの再起動後も確実に存続します。細部に至るまで、生活の質は大幅に向上します。&lt;/p>
&lt;h2 id="サービスのデフォルト-縁の下の力持ち">サービスのデフォルト: 縁の下の力持ち&lt;/h2>
&lt;p>&lt;code>ServiceDefaults&lt;/code> プロジェクトは、Aspire の中で最も過小評価されている部分です。これは、ソリューション内のすべてのサービスが参照する共有ライブラリであり、他の方法では忘れたり、一貫性なく実装したりするすべての横断的な懸念事項を構成します。&lt;/p>
&lt;p>ServiceDefaults の一般的な &lt;code>Extensions.cs&lt;/code> は次のようになります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Extensions&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> IHostApplicationBuilder AddServiceDefaults(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span> IHostApplicationBuilder builder)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.ConfigureOpenTelemetry();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.AddDefaultHealthChecks();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.Services.AddServiceDiscovery();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.Services.ConfigureHttpClientDefaults(http =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> http.AddStandardResilienceHandler();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> http.AddServiceDiscovery();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> builder;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> IHostApplicationBuilder ConfigureOpenTelemetry(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span> IHostApplicationBuilder builder)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.Logging.AddOpenTelemetry(logging =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logging.IncludeFormattedMessage = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logging.IncludeScopes = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.Services.AddOpenTelemetry()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithMetrics(metrics =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> metrics.AddAspNetCoreInstrumentation()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddHttpClientInstrumentation()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddRuntimeInstrumentation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithTracing(tracing =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> tracing.AddAspNetCoreInstrumentation()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddHttpClientInstrumentation()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddGrpcClientInstrumentation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.AddOpenTelemetryExporters();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> builder;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> IHostApplicationBuilder AddDefaultHealthChecks(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span> IHostApplicationBuilder builder)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> builder.Services.AddHealthChecks()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddCheck(&lt;span style="color:#a5d6ff">&amp;#34;self&amp;#34;&lt;/span>, () =&amp;gt; HealthCheckResult.Healthy(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [&amp;#34;live&amp;#34;]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> builder;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>次に、各サービスの &lt;code>Program.cs&lt;/code> では、1 行ですべての処理が行われます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddServiceDefaults();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Your service-specific configuration...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> app = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapDefaultEndpoints();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.Run();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>この 1 回の &lt;code>AddServiceDefaults()&lt;/code> 呼び出しで次のことが得られます。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>OpenTelemetry&lt;/strong> による構造化ログ、メトリクス、分散トレース&lt;/li>
&lt;li>&lt;strong>ライブネスおよびレディネスエンドポイントによるヘルスチェック&lt;/strong>&lt;/li>
&lt;li>&lt;strong>サービス検出&lt;/strong> により、サービスが名前で相互に検索できるようになります&lt;/li>
&lt;li>すべての発信 HTTP 呼び出しに対する &lt;strong>復元ポリシー&lt;/strong> (再試行、サーキット ブレーカー、タイムアウト)&lt;/li>
&lt;/ul>
&lt;p>これは、「自分のマシンで動作する」デモと運用準備が整ったシステムを区別するものです。そして、Aspire はそれを後付けではなくデフォルトにしています。&lt;/p>
&lt;h2 id="aspire-コンポーネント">Aspire コンポーネント&lt;/h2>
&lt;p>Aspire コンポーネントは、サービスがバッキング インフラストラクチャに接続する方法を標準化する NuGet パッケージです。これらは単なるクライアント ライブラリではなく、ヘルス チェック、ロギング、トレース、すぐに使える構成可能な復元機能が含まれています。&lt;/p>
&lt;h3 id="レディス">レディス&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddServiceDefaults();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddRedisDistributedCache(&lt;span style="color:#a5d6ff">&amp;#34;cache&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>それだけです。接続文字列は、&lt;code>WithReference&lt;/code> 経由で AppHost から取得されます。このコンポーネントは、Redis によってバックアップされた &lt;code>IDistributedCache&lt;/code> を登録し、ヘルスチェックと OpenTelemetry インストルメンテーションがすでに接続されています。&lt;/p>
&lt;p>Redis を出力キャッシュに使用することもできます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.AddRedisOutputCache(&lt;span style="color:#a5d6ff">&amp;#34;cache&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="entity-framework-core-を使用した-postgresql">Entity Framework Core を使用した PostgreSQL&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.AddNpgsqlDbContext&amp;lt;CatalogDbContext&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;catalogdb&amp;#34;&lt;/span>, settings =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> settings.DisableRetry = &lt;span style="color:#79c0ff">false&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これにより、AppHost で定義された &lt;code>catalogdb&lt;/code> データベースへの接続を使用して &lt;code>DbContext&lt;/code> が登録されます。これには、接続プーリング、ヘルスチェック、および再試行ポリシーが含まれます。&lt;/p>
&lt;h3 id="rabbitmq">RabbitMQ&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.AddRabbitMQClient(&lt;span style="color:#a5d6ff">&amp;#34;messaging&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>完全に構成され、ヘルスチェックされた、RabbitMQ クライアント ライブラリから &lt;code>IConnection&lt;/code> を登録します。&lt;/p>
&lt;h3 id="azure-の統合">Azure の統合&lt;/h3>
&lt;p>Aspire には、Azure サービス用の最上級のコンポーネントもあります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Azure Blob Storage&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureBlobClient(&lt;span style="color:#a5d6ff">&amp;#34;blobs&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Azure Service Bus&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureServiceBusClient(&lt;span style="color:#a5d6ff">&amp;#34;servicebus&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Azure Key Vault&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddAzureKeyVaultClient(&lt;span style="color:#a5d6ff">&amp;#34;secrets&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```パターンは常に同じです。&lt;/span>AppHost &lt;span style="color:#f85149">で&lt;/span> &lt;span style="color:#a5d6ff">1&lt;/span> &lt;span style="color:#f85149">行でリソースを定義し、&lt;/span>&lt;span style="color:#a5d6ff">1&lt;/span> &lt;span style="color:#f85149">行で使用側サービスでそれを使用します。接続の詳細は自動的に表示されます。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">##&lt;/span> &lt;span style="color:#f85149">開発者ダッシュボード&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Aspire &lt;span style="color:#f85149">ダッシュボードは、実際に使用するまでは、あったら便利だと思われる機能の&lt;/span> &lt;span style="color:#a5d6ff">1&lt;/span> &lt;span style="color:#f85149">つです。実際に使用すると、これなしでの作業は考えられなくなります。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>AppHost &lt;span style="color:#f85149">をローカルで実行すると、ダッシュボードが起動して次の情報が表示されます。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">リソースの概要&lt;/span>** &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">すべてのサービスとインフラストラクチャをステータス&lt;/span> &lt;span style="color:#f85149">インジケーターとともに一目で確認できます。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">構造化されたログ&lt;/span>** &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">すべてのサービスからのリアルタイムのログ&lt;/span> &lt;span style="color:#f85149">ストリーミング、フィルタリングと検索が可能&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">分散トレース&lt;/span>** &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">フレーム&lt;/span> &lt;span style="color:#f85149">チャートとして視覚化された、複数のサービスにわたるエンドツーエンドのリクエスト&lt;/span> &lt;span style="color:#f85149">トレース&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">メトリクス&lt;/span>** &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">リアルタイムの&lt;/span> HTTP &lt;span style="color:#f85149">リクエスト&lt;/span> &lt;span style="color:#f85149">レート、エラー&lt;/span> &lt;span style="color:#f85149">レート、レイテンシ、およびカスタム&lt;/span> &lt;span style="color:#f85149">メトリクス&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- **&lt;span style="color:#f85149">コンソール&lt;/span> &lt;span style="color:#f85149">ログ&lt;/span>** &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">各コンテナおよびプロジェクトからの生の&lt;/span> stdout/stderr
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">分散トレースは特に価値があります。リクエストがフロントエンドに到達し、カタログ&lt;/span> API &lt;span style="color:#f85149">を通過し、&lt;/span>Redis &lt;span style="color:#f85149">と&lt;/span> PostgreSQL &lt;span style="color:#f85149">にアクセスすると、チェーン全体が各ホップのタイミングとともに表示されます。ボトルネックがどこにあるのかを推測する必要はもうありません。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">デバッグ中にダッシュボードが最も役立つことがわかりました。複数のターミナル&lt;/span> &lt;span style="color:#f85149">ウィンドウをたどったり、ログ&lt;/span> &lt;span style="color:#f85149">ファイル間を移動したりする代わりに、サービス全体で関連するイベントをリンクする相関&lt;/span> ID &lt;span style="color:#f85149">を使用して、すべてが&lt;/span> &lt;span style="color:#a5d6ff">1&lt;/span> &lt;span style="color:#f85149">か所にまとめられます。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">ダッシュボードはスタンドアロン&lt;/span> &lt;span style="color:#f85149">コンテナーとしても利用できます。つまり、ステージング環境または&lt;/span> CI &lt;span style="color:#f85149">パイプラインで使用できます。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```&lt;/span>bash
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>docker run --rm -p &lt;span style="color:#a5d6ff">18888&lt;/span>:&lt;span style="color:#a5d6ff">18888&lt;/span> \
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> mcr.microsoft.com/dotnet/aspire-dashboard:latest
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="デプロイメント">デプロイメント&lt;/h2>
&lt;p>Aspire は開発中は役に立ちますが、本番環境ではどうなるのでしょうか?ここからが現実的なことになります。&lt;/p>
&lt;h3 id="azure-コンテナー-アプリ">Azure コンテナー アプリ&lt;/h3>
&lt;p>最も簡単なデプロイ パスは、ファーストクラスの Aspire サポートを備えた Azure Container Apps (ACA) です。 Azure Developer CLI を使用して直接デプロイできます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>azd init
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>azd up
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>azd init&lt;/code> コマンドは、Aspire AppHost を検出し、必要なコードとしてのインフラストラクチャを生成します。 &lt;code>azd up&lt;/code> は、AppHost トポロジに基づいて、コンテナー レジストリ、コンテナー アプリ、データベース、Redis インスタンスなどすべてをプロビジョニングします。&lt;/p>
&lt;p>AppHost は基本的に展開マニフェストになります。 「catalog-api は PostgreSQL と Redis に依存する」を定義する同じコードがインフラストラクチャのプロビジョニングを推進します。&lt;/p>
&lt;h3 id="kubernetes">Kubernetes&lt;/h3>
&lt;p>Kubernetes デプロイメントの場合、Aspire はマニフェストを直接生成しませんが、AppHost トポロジは Kubernetes リソースに明確にマップされます。コミュニティ ツール &lt;code>aspirate&lt;/code> (Aspir8) は、AppHost から Helm チャートまたは Kubernetes マニフェストを生成できます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet tool install -g aspirate
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>aspirate generate
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>aspirate apply
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="導入に関する考慮事項">導入に関する考慮事項&lt;/h3>
&lt;p>留意すべき点がいくつかあります:- &lt;strong>接続文字列は環境間で変更されます。&lt;/strong> ローカルでは、Aspire はコンテナーを起動し、接続文字列を自動的に挿入します。運用環境では、マネージド サービスを指します。 Aspire は標準の .NET 構成を使用するため、環境変数と Azure Key Vault は期待どおりに機能します。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>AppHost は運用環境では実行されません。&lt;/strong> これは開発および展開のオーケストレーション ツールです。運用環境では、サービスは環境変数とオーケストレーターを通じて構成され、独立して実行されます。&lt;/li>
&lt;li>&lt;strong>インフラストラクチャ リソースがマネージド サービスになります。&lt;/strong> ローカル PostgreSQL コンテナーが Azure Database for PostgreSQL になります。ローカル Redis コンテナーが Azure Cache for Redis になります。消費するコードは変わりません。&lt;/li>
&lt;/ul>
&lt;h2 id="現実世界のヒント">現実世界のヒント&lt;/h2>
&lt;p>Aspire を実稼働環境でしばらく実行した後、時間を節約できた教訓は次のとおりです。&lt;/p>
&lt;h3 id="1-カスタム-リソース-ライフサイクル-チェックを使用する">1. カスタム リソース ライフサイクル チェックを使用する&lt;/h3>
&lt;p>リソースの準備ができているかどうかをコンテナーの起動だけに頼って判断しないでください。 PostgreSQL は、実際にクエリを処理する準備が整う前に TCP 接続を受け入れる場合があります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> postgres = builder.AddPostgres(&lt;span style="color:#a5d6ff">&amp;#34;postgres&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddDatabase(&lt;span style="color:#a5d6ff">&amp;#34;catalogdb&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithHealthCheck();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Aspire は、リソースに対してヘルスチェックを実行し、実際に準備が整うまで依存サービスを保持できます。&lt;/p>
&lt;h3 id="2-一般的なパターンを拡張機能に抽出する">2. 一般的なパターンを拡張機能に抽出する&lt;/h3>
&lt;p>複数のサービスが同様の構成を共有する場合は、拡張メソッドを作成します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">AppHostExtensions&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> IResourceBuilder&amp;lt;ProjectResource&amp;gt; AddWorkerService(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span> IDistributedApplicationBuilder builder,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> name,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IResourceBuilder&amp;lt;IResourceWithConnectionString&amp;gt; db,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IResourceBuilder&amp;lt;IResourceWithConnectionString&amp;gt; messaging)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> builder.AddProject&amp;lt;Projects.WorkerService&amp;gt;(name)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReference(db)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReference(messaging)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReplicas(&lt;span style="color:#a5d6ff">3&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="3-負荷テストに-withreplicas-を活用する">3. 負荷テストに WithReplicas を活用する&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> catalogApi = builder.AddProject&amp;lt;Projects.CatalogApi&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;catalog-api&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReference(postgres)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReference(redis)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithReplicas(&lt;span style="color:#a5d6ff">5&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>WithReplicas&lt;/code> は、サービスの複数のインスタンスをローカルで起動します。これは、クラスターにデプロイせずに、負荷分散、同時実行性のバグ、分散キャッシュ動作をテストするのに最適です。&lt;/p>
&lt;h3 id="4-機密値にはパラメータを使用する">4. 機密値にはパラメータを使用する&lt;/h3>
&lt;p>ローカル開発の場合でも、資格情報をハードコーディングしないでください。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> dbPassword = builder.AddParameter(&lt;span style="color:#a5d6ff">&amp;#34;db-password&amp;#34;&lt;/span>, secret: &lt;span style="color:#79c0ff">true&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> postgres = builder.AddPostgres(&lt;span style="color:#a5d6ff">&amp;#34;postgres&amp;#34;&lt;/span>, password: dbPassword)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddDatabase(&lt;span style="color:#a5d6ff">&amp;#34;catalogdb&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ローカルで実行している場合、Aspire は値の入力を求めるプロンプトを表示するか、ユーザー シークレットから値を読み取ります。 CI/CD では、環境変数から取得されます。&lt;/p>
&lt;h3 id="5-統合テストが簡単になる">5. 統合テストが簡単になる&lt;/h3>
&lt;p>Aspire には、分散アプリの統合テストを非常に簡単にするテスト パッケージが含まれています。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[Fact]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task CatalogApiReturnsProducts()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> appHost = &lt;span style="color:#ff7b72">await&lt;/span> DistributedApplicationTestingBuilder
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .CreateAsync&amp;lt;Projects.MyCloudApp_AppHost&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> app = &lt;span style="color:#ff7b72">await&lt;/span> appHost.BuildAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> app.StartAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> httpClient = app.CreateHttpClient(&lt;span style="color:#a5d6ff">&amp;#34;catalog-api&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> httpClient.GetAsync(&lt;span style="color:#a5d6ff">&amp;#34;/api/products&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> response.EnsureSuccessStatusCode();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> products = &lt;span style="color:#ff7b72">await&lt;/span> response.Content
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ReadFromJsonAsync&amp;lt;List&amp;lt;Product&amp;gt;&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Assert.NotEmpty(products);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これにより、データベースやメッセージ ブローカーを含む分散アプリケーション「全体」が起動され、それに対してテストが実行され、破棄されます。 CI パイプライン内の実際のインフラストラクチャに対する実際の統合テスト。モックはありません。&lt;/p>
&lt;h3 id="6-リソースの使用状況をローカルで監視する">6. リソースの使用状況をローカルで監視する&lt;/h3>
&lt;p>複数のコンテナをローカルで実行する場合は、リソースの消費に注意してください。管理 UI を備えた PostgreSQL、Redis、および RabbitMQ インスタンスは、簡単に 2 ～ 3 GB の RAM を消費する可能性があります。制約のあるマシンを使用している場合は、より軽量なリソース構成の使用を検討してください。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> redis = builder.AddRedis(&lt;span style="color:#a5d6ff">&amp;#34;cache&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithContainerRuntimeArgs(&lt;span style="color:#a5d6ff">&amp;#34;--memory=256m&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>.NET Aspire は、分散アプリケーションの構築方法を根本的に変えました。革新的な概念を導入しているからではありません。ヘルスチェック、OpenTelemetry、コンテナオーケストレーションは新しいものではありません。しかし、それはそれらを「デフォルト」にしてしまうからです。通常は行う必要がある 100 の小さな決定を必要とし、それらを適切なデフォルトで実装し、必要に応じて上書きできるようにします。私の意見では、AppHost パターンが最大の成果です。分散アプリケーション トポロジを Docker Compose ファイル、Kubernetes マニフェスト、README ドキュメントに散在せずにコードとして表現すると、システムが理解しやすくなります。新しいチーム メンバーは、AppHost で &lt;code>Program.cs&lt;/code> を開いて、アーキテクチャ全体を数分で理解できます。&lt;/p>
&lt;p>.NET を使用して分散アプリケーションを構築している場合は、Aspire を真剣に検討してください。 &lt;code>aspire-starter&lt;/code> テンプレートから始めて、ダッシュボードを探索し、必要に応じてコンポーネントを徐々に採用していきます。初日から全力を尽くす必要はありません。Aspire は追加的な設計になっています。&lt;/p>
&lt;p>最初のスプリントをインフラストラクチャのボイラープレートの配線に費やす時代は終わりました。 Aspire に配管の処理を任せて、実際に重要なこと、つまりアプリケーションに集中できるようにします。&lt;/p></content:encoded><category>.NET</category><category>Azure</category><category>Cloud</category><category>Docker</category></item><item><title>Blazor での認証と承認: 実践ガイド</title><link>https://emimontesdeoca.github.io/ja/posts/blazor-authentication-authorization/</link><pubDate>Sat, 12 Jul 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/blazor-authentication-authorization/</guid><description>ASP.NET ID から OAuth、ロールベースのアクセス、コンポーネントのセキュリティ保護まで、Blazor アプリで認証と承認を実装するための実践的なガイドです。</description><content:encoded>&lt;p>ASP.NET MVC または Razor Pages を使用したことがある場合は、おそらく認証の仕組みについてのメンタル モデルを持っているでしょう。ミドルウェアが要求をインターセプトし、Cookie またはトークンをチェックし、&lt;code>HttpContext.User&lt;/code> を設定して、レースに出発します。 Blazor は、微妙ではあるが重要な方法でそのメンタル モデルを変更します。これらの違いを早い段階で理解していないと、不可能と思われる認証の問題をデバッグすることになります。&lt;/p>
&lt;p>この投稿では、サーバー ホスティング モデルと WebAssembly ホスティング モデルの両方をカバーしながら、Blazor で認証と承認が実際にどのように機能するかを説明したいと思います。基本的なことから、カスタム プロバイダー、外部 OAuth、そして経験豊富な .NET 開発者さえも陥る落とし穴までを見ていきます。&lt;/p>
&lt;h2 id="blazor-の認証が異なる理由">Blazor の認証が異なる理由&lt;/h2>
&lt;p>従来の ASP.NET では、すべてのユーザー操作は HTTP リクエストです。サーバーは資格情報を検証し、Cookie を設定し、後続のすべてのリクエストにその Cookie が含まれます。認証パイプラインは線形で予測可能です。&lt;/p>
&lt;p>Blazor サーバーは、永続的な SignalR 接続を介して動作します。最初の HTTP リクエストでページが読み込まれた後、それ以降のすべての対話は WebSocket 経由で行われます。ボタンがクリックされるたびに新しい HTTP リクエストが送信されることはないため、対話のたびにミドルウェアが再実行されることはありません。 &lt;code>HttpContext&lt;/code> は最初の接続中に使用できますが、回路の存続期間全体を通してこれに依存するとバグが発生します。&lt;/p>
&lt;p>Blazor WebAssembly は完全にブラウザー内で実行されます。サーバー側 &lt;code>HttpContext&lt;/code> はまったくありません。認証状態は API から取得し、クライアント側に保存し、トークン (通常は JWT) を通じて管理する必要があります。サーバーは、トークンの検証が行われる限りにおいてのみクライアントを信頼します。&lt;/p>
&lt;p>これは、Blazor には、ホスティング モデルに関係なく機能する認証状態用の独自の抽象化が必要であることを意味します。その抽象化は &lt;code>AuthenticationStateProvider&lt;/code> です。&lt;/p>
&lt;h2 id="認証状態-財団">認証状態: 財団&lt;/h2>
&lt;p>Blazor の認証システムの中心となるのは &lt;code>AuthenticationStateProvider&lt;/code> です。これは、単一の重要なメソッドを公開する抽象クラスです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">abstract&lt;/span> Task&amp;lt;AuthenticationState&amp;gt; GetAuthenticationStateAsync();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>AuthenticationState&lt;/code> オブジェクトは &lt;code>ClaimsPrincipal&lt;/code> (.NET 全体で使用されるのと同じ ID モデル) をラップします。コンポーネントは Cookie やトークンと直接通信しません。彼らは &lt;code>AuthenticationStateProvider&lt;/code> に現在の状態を尋ねます。&lt;/p>
&lt;p>この状態をコンポーネント ツリー全体で利用できるようにするために、Blazor は &lt;code>CascadingAuthenticationState&lt;/code> を提供します。通常、ルーターを &lt;code>App.razor&lt;/code> またはレイアウトでラップします。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>&amp;lt;CascadingAuthenticationState&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;Router AppAssembly=&amp;#34;@typeof(App).Assembly&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;Found Context=&amp;#34;routeData&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;AuthorizeRouteView RouteData=&amp;#34;@routeData&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> DefaultLayout=&amp;#34;@typeof(MainLayout)&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;NotAuthorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;You&amp;#39;re not authorized to view this page.&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/NotAuthorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/AuthorizeRouteView&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/Found&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;NotFound&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;LayoutView Layout=&amp;#34;@typeof(MainLayout)&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;Page not found.&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/LayoutView&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/NotFound&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/Router&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/CascadingAuthenticationState&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>AuthorizeRouteView&lt;/code> はここで 2 つの役割を果たしています。一致したページ コンポーネントをレンダリングする前にユーザーが認証および許可されているかどうかをチェックし、そうでない場合にはフォールバック UI を提供します。&lt;/p>
&lt;p>統合 Blazor モデルを使用する .NET 8 以降では、これを &lt;code>App.razor&lt;/code> で構成すると、サービス登録で &lt;code>AddCascadingAuthenticationState()&lt;/code> を使用するときにフレームワークがカスケード パラメーターを自動的に処理します。&lt;/p>
&lt;h2 id="aspnet-id-の統合">ASP.NET ID の統合&lt;/h2>
&lt;p>ほとんどのプロジェクトでは、認証を最初から構築する必要はありません。 ASP.NET Identity を使用すると、すぐに使えるユーザー管理、パスワード ハッシュ、2 要素認証、アカウント確認が可能になります。Blazor を使用したセットアップは &lt;code>Program.cs&lt;/code> で始まります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddDbContext&amp;lt;ApplicationDbContext&amp;gt;(options =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.UseSqlServer(builder.Configuration.GetConnectionString(&lt;span style="color:#a5d6ff">&amp;#34;DefaultConnection&amp;#34;&lt;/span>)));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddDefaultIdentity&amp;lt;IdentityUser&amp;gt;(options =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.SignIn.RequireConfirmedAccount = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.Password.RequireDigit = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.Password.RequiredLength = &lt;span style="color:#a5d6ff">8&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddRoles&amp;lt;IdentityRole&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddEntityFrameworkStores&amp;lt;ApplicationDbContext&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddAuthentication(options =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.DefaultScheme = IdentityConstants.ApplicationScheme;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>.NET 8 以降の Blazor Web App テンプレートでは、スキャフォールディングされた Identity UI が Razor コンポーネントを直接使用します。ログイン、登録、およびアカウント管理ページは、Blazor アプリケーションの他の部分と自然に統合されます。Razor ページと Blazor コンポーネントをぎこちなく組み合わせる必要はもうありません。&lt;/p>
&lt;p>&lt;code>ApplicationDbContext&lt;/code> は &lt;code>IdentityDbContext&lt;/code> から継承しているため、移行を実行して ID テーブルを作成する必要があります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ApplicationDbContext&lt;/span> : IdentityDbContext&amp;lt;IdentityUser&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ApplicationDbContext(DbContextOptions&amp;lt;ApplicationDbContext&amp;gt; options)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> : &lt;span style="color:#ff7b72">base&lt;/span>(options) { }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet ef migrations add InitialIdentity
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet ef database update
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="authorizeview-コンポーネント">AuthorizeView コンポーネント&lt;/h2>
&lt;p>認証状態がコンポーネント ツリーを通過すると、&lt;code>AuthorizeView&lt;/code> を使用して条件付きで UI をレンダリングできます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>&amp;lt;AuthorizeView&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;Authorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;Welcome, @context.User.Identity?.Name!&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;a href=&amp;#34;/account/manage&amp;#34;&amp;gt;Manage Account&amp;lt;/a&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;form method=&amp;#34;post&amp;#34; action=&amp;#34;/account/logout&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button type=&amp;#34;submit&amp;#34;&amp;gt;Log Out&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/form&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/Authorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;NotAuthorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;a href=&amp;#34;/account/login&amp;#34;&amp;gt;Log In&amp;lt;/a&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;a href=&amp;#34;/account/register&amp;#34;&amp;gt;Register&amp;lt;/a&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/NotAuthorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/AuthorizeView&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>&amp;lt;Authorized&amp;gt;&lt;/code> 内の &lt;code>context&lt;/code> パラメーターを使用すると &lt;code>AuthenticationState&lt;/code> にアクセスできるため、マークアップ内でクレーム、ロール、ユーザーの ID を直接検査できます。&lt;/p>
&lt;p>&lt;code>AuthorizeView&lt;/code> をロールとポリシーで使用することもできます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>&amp;lt;AuthorizeView Roles=&amp;#34;Admin,Moderator&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;Authorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button @onclick=&amp;#34;DeletePost&amp;#34;&amp;gt;Delete Post&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/Authorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/AuthorizeView&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;AuthorizeView Policy=&amp;#34;CanEditArticles&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;Authorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button @onclick=&amp;#34;EditArticle&amp;#34;&amp;gt;Edit&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/Authorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/AuthorizeView&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>留意すべき点が 1 つあります。&lt;code>AuthorizeView&lt;/code> は UI の問題です。要素を非表示または表示しますが、基礎となるロジックは保護されません。誰かがあなたの API エンドポイントを呼び出すか、あなたのメソッドを直接呼び出すことができる場合、その人は &lt;code>AuthorizeView&lt;/code> を完全にバイパスします。サーバー側でも常に承認を強制します。&lt;/p>
&lt;h2 id="承認-属性">[承認] 属性&lt;/h2>
&lt;p>ページ全体を保護するには、 &lt;code>[Authorize]&lt;/code> 属性を適用します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@page &amp;#34;/admin/dashboard&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@attribute [Authorize(Roles = &amp;#34;Admin&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h1&amp;gt;Admin Dashboard&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;p&amp;gt;Only administrators can see this page.&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>認証されていないユーザーがこのページに移動すると、&lt;code>AuthorizeRouteView&lt;/code> が開始され、前に定義した &lt;code>&amp;lt;NotAuthorized&amp;gt;&lt;/code> テンプレートがレンダリングされます。代わりに、ナビゲーションを使用して &lt;code>NotAuthorized&lt;/code> ケースを処理することで、ログイン ページにリダイレクトできます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-zed" data-lang="zed">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>NotAuthorized&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">@&lt;/span>if&lt;span style="color:#6e7681"> &lt;/span>(&lt;span style="color:#ff7b72;font-weight:bold">!&lt;/span>context.User.Identity&lt;span style="color:#ff7b72;font-weight:bold">?&lt;/span>.IsAuthenticated&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">??&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>true)&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>{&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>RedirectToLogin&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">/&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>}&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>else&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>{&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>You&lt;span style="color:#6e7681"> &lt;/span>don&lt;span style="color:#f85149">&amp;#39;&lt;/span>t&lt;span style="color:#6e7681"> &lt;/span>have&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72">permission&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>to&lt;span style="color:#6e7681"> &lt;/span>access&lt;span style="color:#6e7681"> &lt;/span>this&lt;span style="color:#6e7681"> &lt;/span>page.&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>}&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>NotAuthorized&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>単純な &lt;code>RedirectToLogin&lt;/code> コンポーネントは次のようになります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>inject NavigationManager &lt;span style="color:#f0883e;font-weight:bold">Navigation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> protected override void OnInitialized()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> returnUrl &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> Uri&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>EscapeDataString(&lt;span style="color:#f0883e;font-weight:bold">Navigation&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Uri);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f0883e;font-weight:bold">Navigation&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>NavigateTo(&lt;span style="color:#ff7b72;font-weight:bold">$&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;/account/login?returnUrl={returnUrl}&amp;#34;&lt;/span>, forceLoad: true);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ここでは &lt;code>forceLoad: true&lt;/code> が重要です。サーバー側の認証ミドルウェアがログイン フローを適切に処理できるように、実際の HTTP ナビゲーションが必要です。&lt;/p>
&lt;h2 id="ロールベースおよびポリシーベースの認可">ロールベースおよびポリシーベースの認可&lt;/h2>
&lt;p>ロールは最も単純なモデルです。ユーザーを「管理者」や「編集者」などのグループに割り当て、メンバーシップを確認します。ただし、ポリシーにより、はるかに柔軟な柔軟性が得られます。&lt;/p>
&lt;p>&lt;code>Program.cs&lt;/code> にポリシーを登録します:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddAuthorizationCore(options =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.AddPolicy(&lt;span style="color:#a5d6ff">&amp;#34;CanPublish&amp;#34;&lt;/span>, policy =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> policy.RequireClaim(&lt;span style="color:#a5d6ff">&amp;#34;Permission&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Publish&amp;#34;&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.AddPolicy(&lt;span style="color:#a5d6ff">&amp;#34;MinimumAge&amp;#34;&lt;/span>, policy =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> policy.Requirements.Add(&lt;span style="color:#ff7b72">new&lt;/span> MinimumAgeRequirement(&lt;span style="color:#a5d6ff">18&lt;/span>)));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.AddPolicy(&lt;span style="color:#a5d6ff">&amp;#34;PremiumUser&amp;#34;&lt;/span>, policy =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> policy.RequireRole(&lt;span style="color:#a5d6ff">&amp;#34;Premium&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .RequireClaim(&lt;span style="color:#a5d6ff">&amp;#34;Subscription&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Active&amp;#34;&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>カスタム要件にはハンドラーが必要です。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">MinimumAgeRequirement&lt;/span> : IAuthorizationRequirement
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> MinimumAge { &lt;span style="color:#ff7b72">get&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> MinimumAgeRequirement(&lt;span style="color:#ff7b72">int&lt;/span> minimumAge) =&amp;gt; MinimumAge = minimumAge;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">MinimumAgeHandler&lt;/span> : AuthorizationHandler&amp;lt;MinimumAgeRequirement&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> Task HandleRequirementAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> AuthorizationHandlerContext context,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MinimumAgeRequirement requirement)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> dateOfBirthClaim = context.User.FindFirst(&lt;span style="color:#a5d6ff">&amp;#34;DateOfBirth&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (dateOfBirthClaim &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Task.CompletedTask;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> dateOfBirth = DateOnly.Parse(dateOfBirthClaim.Value);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> age = DateOnly.FromDateTime(DateTime.Today).Year - dateOfBirth.Year;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (age &amp;gt;= requirement.MinimumAge)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> context.Succeed(requirement);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Task.CompletedTask;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ハンドラーを登録します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddSingleton&amp;lt;IAuthorizationHandler, MinimumAgeHandler&amp;gt;();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>コンポーネントでは、動的ロジックが必要な場合に、プログラムで承認をチェックすることもできます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>inject IAuthorizationService AuthorizationService
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>inject AuthenticationStateProvider AuthStateProvider
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private &lt;span style="color:#f0883e;font-weight:bold">bool&lt;/span> canPublish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> protected override async Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> authState &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await AuthStateProvider&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetAuthenticationStateAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> result &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await AuthorizationService&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>AuthorizeAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> authState&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>User, &lt;span style="color:#a5d6ff">&amp;#34;CanPublish&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> canPublish &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> result&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Succeeded;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="外部-oauth-プロバイダー">外部 OAuth プロバイダー&lt;/h2>
&lt;p>ASP.NET 認証ミドルウェアを使用すると、「Google でサインイン」または「GitHub でサインイン」を簡単にサポートできます。 OAuth フローには HTTP リダイレクトが必要なため、これらはサーバー側で構成されます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddAuthentication()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddGoogle(options =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.ClientId = builder.Configuration[&lt;span style="color:#a5d6ff">&amp;#34;Auth:Google:ClientId&amp;#34;&lt;/span>]!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.ClientSecret = builder.Configuration[&lt;span style="color:#a5d6ff">&amp;#34;Auth:Google:ClientSecret&amp;#34;&lt;/span>]!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.Scope.Add(&lt;span style="color:#a5d6ff">&amp;#34;profile&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddMicrosoftAccount(options =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.ClientId = builder.Configuration[&lt;span style="color:#a5d6ff">&amp;#34;Auth:Microsoft:ClientId&amp;#34;&lt;/span>]!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.ClientSecret = builder.Configuration[&lt;span style="color:#a5d6ff">&amp;#34;Auth:Microsoft:ClientSecret&amp;#34;&lt;/span>]!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddGitHub(options =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.ClientId = builder.Configuration[&lt;span style="color:#a5d6ff">&amp;#34;Auth:GitHub:ClientId&amp;#34;&lt;/span>]!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> options.ClientSecret = builder.Configuration[&lt;span style="color:#a5d6ff">&amp;#34;Auth:GitHub:ClientSecret&amp;#34;&lt;/span>]!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>GitHub の場合は、デフォルトの ASP.NET ライブラリに含まれていないため、&lt;code>AspNet.Security.OAuth.GitHub&lt;/code> NuGet パッケージが必要になります。&lt;/p>
&lt;p>次に、ログイン UI は、外部チャレンジをトリガーするリンクを提供します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@page &amp;#34;/account/external-login&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h2&amp;gt;Sign in with an external provider&amp;lt;/h2&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;form method=&amp;#34;post&amp;#34; action=&amp;#34;/api/auth/external-login&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button type=&amp;#34;submit&amp;#34; name=&amp;#34;provider&amp;#34; value=&amp;#34;Google&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Sign in with Google
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button type=&amp;#34;submit&amp;#34; name=&amp;#34;provider&amp;#34; value=&amp;#34;Microsoft&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Sign in with Microsoft
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button type=&amp;#34;submit&amp;#34; name=&amp;#34;provider&amp;#34; value=&amp;#34;GitHub&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Sign in with GitHub
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/form&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>API エンドポイントはチャレンジをトリガーし、コールバックを処理します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/api/auth/external-login&amp;#34;&lt;/span>, (&lt;span style="color:#ff7b72">string&lt;/span> provider, HttpContext context) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> properties = &lt;span style="color:#ff7b72">new&lt;/span> AuthenticationProperties
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RedirectUri = &lt;span style="color:#a5d6ff">&amp;#34;/api/auth/external-callback&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Challenge(properties, [provider]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Blazor の外部認証には常にページ全体のナビゲーションが必要です。サーバーを経由せずに SignalR 回線または WebAssembly アプリ内で OAuth リダイレクトを完了することはできません。&lt;/p>
&lt;h2 id="blazor-webassembly-のトークンベースの認証blazor-webassembly-はクライアント上で実行されるためcookie-ベースの認証は同じようには適用されません代わりに通常はメモリに保存され発信-http-リクエストに添付された-jwt-を使用します">Blazor WebAssembly のトークンベースの認証Blazor WebAssembly はクライアント上で実行されるため、Cookie ベースの認証は同じようには適用されません。代わりに、通常はメモリに保存され、発信 HTTP リクエストに添付された JWT を使用します。&lt;/h2>
&lt;p>フレームワークは、トークンを自動的に添付するための &lt;code>AuthorizationMessageHandler&lt;/code> を提供します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddHttpClient(&lt;span style="color:#a5d6ff">&amp;#34;API&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client =&amp;gt; client.BaseAddress = &lt;span style="color:#ff7b72">new&lt;/span> Uri(&lt;span style="color:#a5d6ff">&amp;#34;https://api.example.com&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddHttpMessageHandler&amp;lt;AuthorizationMessageHandler&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddScoped(sp =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sp.GetRequiredService&amp;lt;IHttpClientFactory&amp;gt;().CreateClient(&lt;span style="color:#a5d6ff">&amp;#34;API&amp;#34;&lt;/span>));
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>独自の API に対して認証するスタンドアロン Blazor WASM アプリの場合は、JWT を解析するカスタム &lt;code>AuthenticationStateProvider&lt;/code> を実装します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">JwtAuthenticationStateProvider&lt;/span> : AuthenticationStateProvider
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> ILocalStorageService _localStorage;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> HttpClient _httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> JwtAuthenticationStateProvider(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ILocalStorageService localStorage,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> HttpClient httpClient)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _localStorage = localStorage;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _httpClient = httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;AuthenticationState&amp;gt; GetAuthenticationStateAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> token = &lt;span style="color:#ff7b72">await&lt;/span> _localStorage.GetItemAsync&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;authToken&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrWhiteSpace(token))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> AuthenticationState(&lt;span style="color:#ff7b72">new&lt;/span> ClaimsPrincipal(&lt;span style="color:#ff7b72">new&lt;/span> ClaimsIdentity()));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _httpClient.DefaultRequestHeaders.Authorization =
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> AuthenticationHeaderValue(&lt;span style="color:#a5d6ff">&amp;#34;Bearer&amp;#34;&lt;/span>, token);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> claims = ParseClaimsFromJwt(token);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> identity = &lt;span style="color:#ff7b72">new&lt;/span> ClaimsIdentity(claims, &lt;span style="color:#a5d6ff">&amp;#34;jwt&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> AuthenticationState(&lt;span style="color:#ff7b72">new&lt;/span> ClaimsPrincipal(identity));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> NotifyAuthStateChanged()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> IEnumerable&amp;lt;Claim&amp;gt; ParseClaimsFromJwt(&lt;span style="color:#ff7b72">string&lt;/span> jwt)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> payload = jwt.Split(&lt;span style="color:#a5d6ff">&amp;#39;.&amp;#39;&lt;/span>)[&lt;span style="color:#a5d6ff">1&lt;/span>];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> padded = payload.Length % &lt;span style="color:#a5d6ff">4&lt;/span> &lt;span style="color:#ff7b72">switch&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">2&lt;/span> =&amp;gt; payload + &lt;span style="color:#a5d6ff">&amp;#34;==&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">3&lt;/span> =&amp;gt; payload + &lt;span style="color:#a5d6ff">&amp;#34;=&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _ =&amp;gt; payload
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> bytes = Convert.FromBase64String(padded);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> json = JsonSerializer.Deserialize&amp;lt;Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, JsonElement&amp;gt;&amp;gt;(bytes);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> json?.Select(kvp =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span> Claim(kvp.Key, kvp.Value.ToString()))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ?? Enumerable.Empty&amp;lt;Claim&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ログインに成功したら、トークンを保存し、認証状態を通知します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">AuthService&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> HttpClient _httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> ILocalStorageService _localStorage;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> JwtAuthenticationStateProvider _authStateProvider;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> AuthService(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> HttpClient httpClient,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ILocalStorageService localStorage,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> AuthenticationStateProvider authStateProvider)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _httpClient = httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _localStorage = localStorage;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _authStateProvider = (JwtAuthenticationStateProvider)authStateProvider;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">bool&lt;/span>&amp;gt; LoginAsync(&lt;span style="color:#ff7b72">string&lt;/span> email, &lt;span style="color:#ff7b72">string&lt;/span> password)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> _httpClient.PostAsJsonAsync(&lt;span style="color:#a5d6ff">&amp;#34;/api/auth/login&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> { Email = email, Password = password });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!response.IsSuccessStatusCode)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#79c0ff">false&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">await&lt;/span> response.Content
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ReadFromJsonAsync&amp;lt;LoginResponse&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> _localStorage.SetItemAsync(&lt;span style="color:#a5d6ff">&amp;#34;authToken&amp;#34;&lt;/span>, result!.Token);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _authStateProvider.NotifyAuthStateChanged();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task LogoutAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> _localStorage.RemoveItemAsync(&lt;span style="color:#a5d6ff">&amp;#34;authToken&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _authStateProvider.NotifyAuthStateChanged();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>注意: JWT を &lt;code>localStorage&lt;/code> に保存すると、XSS 攻撃にさらされます。よりセキュリティの高いアプリケーションの場合は、トークンをメモリ内にのみ保持してリフレッシュ トークンを使用するか、サーバーがトークンを管理し、クライアントが HTTP のみの Cookie を使用するバックエンド対フロントエンド (BFF) パターンを採用することを検討してください。&lt;/p>
&lt;h2 id="blazor-サーバーと-webassembly-セキュリティに関する考慮事項">Blazor サーバーと WebAssembly: セキュリティに関する考慮事項&lt;/h2>
&lt;p>ホスティング モデルはセキュリティ体制を根本的に変えます。&lt;/p>
&lt;p>&lt;strong>Blazor Server&lt;/strong> は、すべてのコンポーネント ロジックをサーバー上に保持します。クライアントには、SignalR 上でレンダリングされた HTML の差分のみが表示されます。これは次のことを意味します。&lt;/p>
&lt;ul>
&lt;li>機密性の高いロジックがサーバーから離れることはありません&lt;/li>
&lt;li>コンポーネントからデータベースや内部サービスに直接アクセスできます。&lt;/li>
&lt;li>認証状態は、初期接続時のサーバーの &lt;code>HttpContext&lt;/code> から取得されます&lt;/li>
&lt;li>回線は認証 Cookie よりも存続できます。ユーザーの Cookie の有効期限が切れても、回線は切断されるまで存続します。&lt;/li>
&lt;li>回線の切断を適切に処理し、再接続時に認証状態を再検証する必要があります。&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Blazor WebAssembly&lt;/strong> は完全にブラウザー内で実行されます。これは次のことを意味します。&lt;/p>
&lt;ul>
&lt;li>すべてのコンポーネント コードはダウンロードして検査可能です&lt;/li>
&lt;li>WASM コンポーネントにはシークレット、接続文字列、または機密のビジネス ロジックを決して入れないでください。&lt;/li>
&lt;li>認証は UX のクライアントにのみ適用されます。実際の強制は API レイヤーで行われる必要があります&lt;/li>
&lt;li>トークンの管理はあなたの責任です&lt;/li>
&lt;li>サーバー プロジェクトが認証を処理し、WASM アプリを提供するホスト型モデルの使用を検討します。&lt;/li>
&lt;/ul>
&lt;p>WebAssembly アプリに対して推奨するパターンは、すべてのコンポーネントを「信頼できない UI」であるかのように扱い、すべての API エンドポイントを未知のクライアントによって呼び出されているかのように扱うことです。クライアントが何をチェックするかに関係なく、サーバー側ですべてを検証します。&lt;/p>
&lt;h2 id="カスタム-authenticationstateprovider-の構築">カスタム AuthenticationStateProvider の構築&lt;/h2>
&lt;p>場合によっては、組み込みプロバイダーがアーキテクチャに適合しないことがあります。おそらく、従来の認証システムと統合しているか、認証状態の変更をポーリングする必要があると考えられます。 Blazor Server のより完全なカスタム プロバイダーは次のとおりです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CustomAuthStateProvider&lt;/span> : AuthenticationStateProvider
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> IHttpContextAccessor _httpContextAccessor;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> IUserService _userService;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> CustomAuthStateProvider(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IHttpContextAccessor httpContextAccessor,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IUserService userService)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _httpContextAccessor = httpContextAccessor;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _userService = userService;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;AuthenticationState&amp;gt; GetAuthenticationStateAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> httpContext = _httpContextAccessor.HttpContext;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (httpContext?.User?.Identity?.IsAuthenticated != &lt;span style="color:#79c0ff">true&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> AuthenticationState(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> ClaimsPrincipal(&lt;span style="color:#ff7b72">new&lt;/span> ClaimsIdentity()));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> userId = httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (userId &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> AuthenticationState(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> ClaimsPrincipal(&lt;span style="color:#ff7b72">new&lt;/span> ClaimsIdentity()));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> user = &lt;span style="color:#ff7b72">await&lt;/span> _userService.GetUserWithClaimsAsync(userId);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (user &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> AuthenticationState(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> ClaimsPrincipal(&lt;span style="color:#ff7b72">new&lt;/span> ClaimsIdentity()));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> claims = &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;Claim&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span>(ClaimTypes.NameIdentifier, user.Id),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span>(ClaimTypes.Name, user.DisplayName),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span>(ClaimTypes.Email, user.Email)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> claims.AddRange(user.Roles.Select(r =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span> Claim(ClaimTypes.Role, r)));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> claims.AddRange(user.Permissions.Select(p =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span> Claim(&lt;span style="color:#a5d6ff">&amp;#34;Permission&amp;#34;&lt;/span>, p)));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> identity = &lt;span style="color:#ff7b72">new&lt;/span> ClaimsIdentity(claims, &lt;span style="color:#a5d6ff">&amp;#34;Custom&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> AuthenticationState(&lt;span style="color:#ff7b72">new&lt;/span> ClaimsPrincipal(identity));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> MarkUserAsAuthenticated(&lt;span style="color:#ff7b72">string&lt;/span> userId)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> MarkUserAsLoggedOut()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> anonymous = &lt;span style="color:#ff7b72">new&lt;/span> ClaimsPrincipal(&lt;span style="color:#ff7b72">new&lt;/span> ClaimsIdentity());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> authState = Task.FromResult(&lt;span style="color:#ff7b72">new&lt;/span> AuthenticationState(anonymous));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> NotifyAuthenticationStateChanged(authState);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>Program.cs&lt;/code> に登録します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddScoped&amp;lt;AuthenticationStateProvider, CustomAuthStateProvider&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddScoped&amp;lt;CustomAuthStateProvider&amp;gt;();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ここでの重要な洞察は &lt;code>NotifyAuthenticationStateChanged&lt;/code> です。これを呼び出すと、カスケード パラメーターの更新がトリガーされ、コンポーネント ツリー内のすべての &lt;code>AuthorizeView&lt;/code> と &lt;code>AuthorizeRouteView&lt;/code> が再評価されます。これは、ページ全体を更新せずに UI をログイン/ログアウト イベントに反応させる方法です。&lt;/p>
&lt;h2 id="よくある落とし穴と解決策">よくある落とし穴と解決策&lt;/h2>
&lt;p>多くのプロジェクトで Blazor 認証を使用した後、最も頻繁に発生する問題は次のとおりです。&lt;/p>
&lt;h3 id="1-blazor-サーバー-コンポーネントでの-httpcontext-の使用httpcontext-は最初の-http-要求中に使用できますがsignalr-の対話中は-null-または無効になります最初のレンダリング後に実行されるコンポーネントに-ihttpcontextaccessor-を挿入しないでください">1. Blazor サーバー コンポーネントでの HttpContext の使用&lt;code>HttpContext&lt;/code> は最初の HTTP 要求中に使用できますが、SignalR の対話中は &lt;code>null&lt;/code> または無効になります。最初のレンダリング後に実行されるコンポーネントに &lt;code>IHttpContextAccessor&lt;/code> を挿入しないでください。&lt;/h3>
&lt;p>&lt;strong>解決策:&lt;/strong> 初期化中に &lt;code>HttpContext&lt;/code> から必要なものを取得し、スコープ指定されたサービスに保存します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">UserContext&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string?&lt;/span> UserId { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string?&lt;/span> AccessToken { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// In a component that renders during the initial HTTP request:&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@inject IHttpContextAccessor HttpContextAccessor
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@inject UserContext UserContext
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> OnInitialized()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> context = HttpContextAccessor.HttpContext;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> UserContext.UserId = context?.User.FindFirstValue(ClaimTypes.NameIdentifier);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> UserContext.AccessToken = context?.Request.Headers.Authorization
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToString().Replace(&lt;span style="color:#a5d6ff">&amp;#34;Bearer &amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="2-ログイン後に認証状態が更新されない">2. ログイン後に認証状態が更新されない&lt;/h3>
&lt;p>ログイン API を呼び出すと成功しますが、UI にはまだ「ログイン」と表示されます。&lt;/p>
&lt;p>&lt;strong>解決策:&lt;/strong> 認証状態が変更された後、&lt;code>AuthenticationStateProvider&lt;/code> で &lt;code>NotifyAuthenticationStateChanged&lt;/code> を呼び出す必要があります。フレームワークは、トークンが保存されたことや Cookie が設定されたことを魔法のように検出しません。&lt;/p>
&lt;h3 id="3-コンポーネントで承認属性が機能しない">3. コンポーネントで承認属性が機能しない&lt;/h3>
&lt;p>&lt;code>[Authorize]&lt;/code> をコンポーネントに追加しても、認証されていないユーザーはブロックされません。&lt;/p>
&lt;p>&lt;strong>解決策:&lt;/strong> &lt;code>App.razor&lt;/code> でプレーン &lt;code>RouteView&lt;/code> の代わりに &lt;code>AuthorizeRouteView&lt;/code> を使用していることを確認してください。標準 &lt;code>RouteView&lt;/code> は認可属性を完全に無視します。&lt;/p>
&lt;h3 id="4-プリレンダリングにより認証状態が中断される">4. プリレンダリングにより認証状態が中断される&lt;/h3>
&lt;p>Blazor WebAssembly でのサーバー側の事前レンダリング中は、使用可能な認証トークンがありません。コンポーネントは非認証としてレンダリングされ、WASM のロード後に認証された状態に切り替わります。&lt;/p>
&lt;p>&lt;strong>解決策:&lt;/strong> &lt;code>@rendermode InteractiveWebAssembly&lt;/code> (プリレンダリングなし) を使用して認証に依存するページのプリレンダリングを無効にするか、読み込み状態を適切に処理します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>&amp;lt;AuthorizeView&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;Authorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;Welcome back, @context.User.Identity?.Name&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/Authorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;Authorizing&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;Loading...&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/Authorizing&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;NotAuthorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;a href=&amp;#34;/login&amp;#34;&amp;gt;Sign in&amp;lt;/a&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/NotAuthorized&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/AuthorizeView&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="5-長時間実行される回線におけるトークンの有効期限">5. 長時間実行される回線におけるトークンの有効期限&lt;/h3>
&lt;p>Blazor サーバー回線は何時間も稼働し続けることができます。トークンまたはセッションの有効期限が切れると、ユーザーは UI で「認証」されたままになりますが、API 呼び出しは失敗し始めます。&lt;/p>
&lt;p>&lt;strong>解決策:&lt;/strong> 定期的なチェックを実装するか、&lt;code>RevalidatingServerAuthenticationStateProvider&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">RevalidatingAuthStateProvider&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> : RevalidatingServerAuthenticationStateProvider
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> IServiceScopeFactory _scopeFactory;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> RevalidatingAuthStateProvider(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ILoggerFactory loggerFactory,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IServiceScopeFactory scopeFactory)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> : &lt;span style="color:#ff7b72">base&lt;/span>(loggerFactory)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _scopeFactory = scopeFactory;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> TimeSpan RevalidationInterval =&amp;gt; TimeSpan.FromMinutes(&lt;span style="color:#a5d6ff">30&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;&lt;span style="color:#ff7b72">bool&lt;/span>&amp;gt; ValidateAuthenticationStateAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> AuthenticationState authenticationState, CancellationToken cancellationToken)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> scope = _scopeFactory.CreateAsyncScope();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> userManager = scope.ServiceProvider
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .GetRequiredService&amp;lt;UserManager&amp;lt;IdentityUser&amp;gt;&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> user = &lt;span style="color:#ff7b72">await&lt;/span> userManager.GetUserAsync(authenticationState.User);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> user &lt;span style="color:#ff7b72">is&lt;/span> not &lt;span style="color:#79c0ff">null&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これにより、ユーザーがまだ存在している (セキュリティ スタンプが変更されていない) ことが 30 分ごとに検証されます。&lt;/p>
&lt;h2 id="結論">結論&lt;/h2>
&lt;p>Blazor での認証と承認には、従来の要求と応答の ASP.NET から考え方を変える必要があります。 &lt;code>AuthenticationStateProvider&lt;/code> の抽象化は、すべてがどのように組み合わされるかを理解するための鍵です。これを一度内面化すれば、残りは自然についてきます。&lt;/p>
&lt;p>ほとんどのアプリケーションでは、ASP.NET ID と組み込みのテンプレートから始めます。これらは、ユーザー管理、パスワードのハッシュ化、トークン生成といった重労働を処理します。要件の拡大に応じて、ポリシーとクレームベースの承認を追加します。ユーザーが外部 OAuth プロバイダーを期待している場合は、外部 OAuth プロバイダーを追加します。&lt;/p>
&lt;p>ホスティング モデルは重要です。Blazor Server は、コードがサーバー上に留まる、より伝統的なセキュリティ体制を提供します。一方、WebAssembly は、クライアントが設計上信頼されていない API ファーストの考え方を推進します。どちらも本質的に安全性が高いわけではなく、脅威モデルが異なるだけです。&lt;/p>
&lt;p>どのアプローチを選択する場合でも、次の黄金律を覚えておいてください。&lt;strong>UI での承認はユーザー エクスペリエンスのため、サーバーでの承認はセキュリティのためです。&lt;/strong> 常に両方を強制してください。&lt;/p></content:encoded><category>Blazor</category><category>.NET</category><category>Security</category><category>Web Development</category></item><item><title>併置された JS ファイルを使用した Blazor での分離された JavaScript</title><link>https://emimontesdeoca.github.io/ja/posts/blazor-isolated-js/</link><pubDate>Wed, 18 Jun 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/blazor-isolated-js/</guid><description>Blazor で併置された JavaScript ファイルを使用して、JS ロジックのスコープを個々のコンポーネントに保ちます。</description><content:encoded>&lt;p>以前に Blazor と JS Interop を使用したことがある場合は、さまざまなコンポーネントのランダムな関数が満載された巨大な &lt;code>app.js&lt;/code> ファイルを作成したことがあるでしょう。機能しますが、すぐに面倒になってしまいます。幸いなことに、JavaScript ファイルを併置するという、よりクリーンなアプローチがあります。&lt;/p>
&lt;h1 id="アイデア">アイデア&lt;/h1>
&lt;p>CSS 分離と同様に、Blazor では &lt;code>.razor.js&lt;/code> ファイルをコンポーネントの隣に配置できます。 JavaScript モジュールは、コンポーネントが実際に必要とする場合にのみ、オンデマンドでロードされます。グローバル スクリプトや汚染はありません。&lt;/p>
&lt;h1 id="セットアップする">セットアップする&lt;/h1>
&lt;p>テキストをクリップボードにコピーする &lt;code>Clipboard.razor&lt;/code> コンポーネントがあるとします。そのすぐ隣に &lt;code>Clipboard.razor.js&lt;/code> という名前のファイルを作成します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-js" data-lang="js">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">export&lt;/span> &lt;span style="color:#ff7b72">function&lt;/span> copyToClipboard(text) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> navigator.clipboard.writeText(text).then(() =&amp;gt; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> console.log(&lt;span style="color:#a5d6ff">&amp;#34;Copied to clipboard!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>export&lt;/code> キーワードに注目してください。これは重要です。 Blazor は、これを標準 ES モジュールとして読み込みます。&lt;/p>
&lt;h1 id="blazor-にモジュールをロードする">Blazor にモジュールをロードする&lt;/h1>
&lt;p>コンポーネントでは、 &lt;code>IJSRuntime&lt;/code> を使用してモジュールをインポートします。パスは規則に従います。ライブラリの場合は &lt;code>./_content/{ASSEMBLY_NAME}/{COMPONENT_PATH}.razor.js&lt;/code>、または現在のプロジェクトの相対パスです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@inject IJSRuntime JS
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@implements IAsyncDisposable
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;button @onclick=&lt;span style="color:#a5d6ff">&amp;#34;Copy&amp;#34;&lt;/span>&amp;gt;Copy to clipboard&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Text { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> IJSObjectReference? module;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnAfterRenderAsync(&lt;span style="color:#ff7b72">bool&lt;/span> firstRender)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (firstRender)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> module = &lt;span style="color:#ff7b72">await&lt;/span> JS.InvokeAsync&amp;lt;IJSObjectReference&amp;gt;(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;import&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;./Components/Clipboard.razor.js&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task Copy()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (module &lt;span style="color:#ff7b72">is&lt;/span> not &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> module.InvokeVoidAsync(&lt;span style="color:#a5d6ff">&amp;#34;copyToClipboard&amp;#34;&lt;/span>, Text);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> ValueTask DisposeAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (module &lt;span style="color:#ff7b72">is&lt;/span> not &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> module.DisposeAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ここで注意すべき点がいくつかあります。&lt;/p>
&lt;ul>
&lt;li>サーバー側のプリレンダリング中に JS 相互運用機能が利用できないため、モジュールを &lt;code>OnAfterRenderAsync&lt;/code> にロードします&lt;/li>
&lt;li>&lt;code>IJSObjectReference&lt;/code> でモジュールへの参照を保持します。&lt;/li>
&lt;li>コンポーネントが破棄されたときにモジュールをクリーンアップするために &lt;code>IAsyncDisposable&lt;/code> を実装します&lt;/li>
&lt;/ul>
&lt;h1 id="なぜこれが良いのか">なぜこれが良いのか&lt;/h1>
&lt;p>以前は、すべてを 1 つの &lt;code>wwwroot/js/app.js&lt;/code> にダンプしていました。それは機能しましたが、関数を見つけるのは面倒で、すべてのページには必要のない JavaScript が読み込まれていました。併置された JS ファイルの場合:&lt;/p>
&lt;ul>
&lt;li>各コンポーネントは独自の JavaScript を所有します&lt;/li>
&lt;li>モジュールは必要な場合にのみ遅延してロードされます&lt;/li>
&lt;li>グローバル関数名が競合しない&lt;/li>
&lt;li>保守と削除が簡単 - コンポーネントを削除すると、コンポーネントと一緒に JS も削除されます。&lt;/li>
&lt;/ul>
&lt;h1 id="パスに関する注意点">パスに関する注意点&lt;/h1>
&lt;p>&lt;code>import&lt;/code> に渡すパスは、スタンドアロンの Blazor アプリにいるか Razor クラス ライブラリにいるかによって異なります。通常の Blazor アプリの場合、パスは &lt;code>wwwroot&lt;/code> に対する相対パスです。 &lt;code>.razor.js&lt;/code> ファイルはビルド時にそこにコピーされるため、プロジェクト内のコンポーネントの場所から参照します。&lt;/p>
&lt;p>モジュールのロード時に 404 が発生する場合は、パスを再確認し、ファイルが &lt;code>.js&lt;/code> だけでなく &lt;code>.razor.js&lt;/code> で終わることを確認してください。&lt;/p>
&lt;p>投稿が気に入っていただければ幸いです!ソーシャルメディアで**@emimontesdeoca**までお気軽にご連絡ください。&lt;/p>
&lt;h1 id="リソース">リソース&lt;/h1>
&lt;ul>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/collocated-js">連結された JS を使用した ASP.NET Core Blazor JavaScript&lt;/a>&lt;/li>
&lt;/ul></content:encoded><category>.NET</category><category>Blazor</category><category>JavaScript</category></item><item><title>.NET 9 および .NET 10 での Blazor の対話性: 完全ガイド</title><link>https://emimontesdeoca.github.io/ja/posts/blazor-interactivity-dotnet-9-10/</link><pubDate>Sun, 15 Jun 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/blazor-interactivity-dotnet-9-10/</guid><description>Blazor のレンダリング モード、ストリーミング SSR、強化されたナビゲーション、.NET 9 と .NET 10 の新しい対話機能について詳しく説明します。</description><content:encoded>&lt;p>過去数年間 Blazor を使用して Web アプリケーションを構築してきた場合は、このフレームワークが &lt;em>長い&lt;/em>進歩を遂げたことをご存知でしょう。 Blazor Server と Blazor WebAssembly のどちらかを選択することから始まったものは、アプリケーション内の各コンポーネントに適切な対話性戦略を選択できるようにする、統合された柔軟なレンダリング モデルに進化しました。&lt;/p>
&lt;p>.NET 8 では、デフォルトとしてのレンダリング モードと静的サーバーサイド レンダリング (SSR) の導入という、根本的な変更が加えられました。現在、.NET 9 と .NET 10 はその基盤の上に構築されており、開発者のエクスペリエンスをよりスムーズにし、エンドユーザーのエクスペリエンスを高速化する改良が加えられています。&lt;/p>
&lt;p>この投稿では、現時点での Blazor のインタラクティブ性の全体像、つまりレンダリング モードの仕組み、ストリーミング SSR がもたらすもの、強化されたナビゲーションとフォーム処理がゲームにどのような変化をもたらすか、最新リリースの新機能について説明したいと思います。新しい Blazor プロジェクトを計画している場合、またはアップグレードを検討している場合、これは、これらすべてを詳しく調べ始めたときに私が入手したかったガイドです。&lt;/p>
&lt;h2 id="簡単に振り返ってみましょう-ここに至るまでの経緯">簡単に振り返ってみましょう: ここに至るまでの経緯&lt;/h2>
&lt;p>.NET 8 より前は、プロジェクト レベルでホスティング モデルにコミットする必要がありました。 Blazor サーバーとは、すべてが SignalR 接続を介してサーバー上で実行されることを意味します。 Blazor WebAssembly とは、すべてがブラウザーで実行されることを意味します。それぞれにトレードオフがあり、それらを組み合わせるのは困難でした。&lt;/p>
&lt;p>.NET 8 では、両方のモデルを統合する単一のプロジェクト テンプレート (Blazor Web App) を導入することで状況が変わりました。重要な概念は &lt;strong>レンダリング モード&lt;/strong>です。&lt;em>コンポーネントごとに&lt;/em>どのようにレンダリングするか、どこでインタラクティブ性を発揮するかを決定します。デフォルトは静的 SSR になりました。これは、コンポーネントがサーバー上でレンダリングし、プレーン HTML をブラウザーに送信することを意味します。SignalR も WebAssembly も使用せず、高速 HTML だけを使用します。&lt;/p>
&lt;p>.NET 9 では、これらの概念が洗練され、開発者のエクスペリエンスが向上し、パフォーマンスが最適化されました。 .NET 10 では、再接続処理の改善、レンダリング モード全体でのコンポーネントの状態の永続化、Blazor スクリプト自体の配信方法の改善により、さらに進化しています。&lt;/p>
&lt;p>すべてを分解してみましょう。&lt;/p>
&lt;h2 id="net-9-のレンダリング-モード">.NET 9 のレンダリング モード&lt;/h2>
&lt;p>最新の Blazor の中心となるのは、レンダリング モードの概念です。知っておくべきモードが 4 つあります。&lt;/p>
&lt;h3 id="1-静的-ssr-デフォルト">1. 静的 SSR (デフォルト)&lt;/h3>
&lt;p>新しい Blazor Web アプリを作成すると、コンポーネントは既定でサーバー上で静的にレンダリングされます。サーバーは Razor コンポーネントを処理し、HTML を生成してブラウザーに送信します。永続的な接続や WebAssembly ランタイムはなく、従来のリクエスト/レスポンスだけです。&lt;/p>
&lt;p>これは、コンテンツの多いページ、主にデータを表示するダッシュボード、またはリアルタイムの操作を必要としないページに最適です。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>page &lt;span style="color:#a5d6ff">&amp;#34;/products&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Our Products&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>foreach (&lt;span style="color:#ff7b72">var&lt;/span> product &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> products)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;product-card&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h3&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Name&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h3&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Description&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>span &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;price&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Price&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToString(&lt;span style="color:#a5d6ff">&amp;#34;C&amp;#34;&lt;/span>)&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>span&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>Product&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> products &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> new();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> protected override async Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> products &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await ProductService&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetAllAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>このコンポーネントはサーバー上でレンダリングされ、HTML が生成され、それだけです。継続的な接続はありません。初期読み込みが速く、SEO に最適で、サーバー リソースの使用量が最小限に抑えられます。&lt;/p>
&lt;h3 id="2-インタラクティブサーバーボタンのクリックの処理ユーザー入力の処理ui-の動的更新などリアルタイムの対話性が必要な場合はinteractive-server-モードを選択できますこれによりブラウザーとサーバーの間に-signalr-接続が確立されui-の更新はその接続を介して行われます">2. インタラクティブサーバーボタンのクリックの処理、ユーザー入力の処理、UI の動的更新など、リアルタイムの対話性が必要な場合は、Interactive Server モードを選択できます。これにより、ブラウザーとサーバーの間に SignalR 接続が確立され、UI の更新はその接続を介して行われます。&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@page &amp;#34;/counter&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@rendermode InteractiveServer
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h1&amp;gt;Counter&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;p&amp;gt;Current count: @currentCount&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;button class=&amp;#34;btn btn-primary&amp;#34; @onclick=&amp;#34;IncrementCount&amp;#34;&amp;gt;Click me&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private int currentCount = 0;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private void IncrementCount()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentCount++;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>必要なのは &lt;code>@rendermode InteractiveServer&lt;/code> ディレクティブだけです。コンポーネントは最初にサーバー上でレンダリングされ、その後、SignalR 接続が確立されて後続の対話が処理されます。 C# イベント ハンドラーはサーバー上で実行され、UI の差分がブラウザーに送信されます。&lt;/p>
&lt;p>&lt;strong>使用する場合:&lt;/strong> インタラクティブ性が必要な場合、コンポーネントはサーバー側のリソース (データベース、API、ファイル システム) にアクセスする必要があり、WebAssembly のダウンロードを待たずに高速な初期読み込みが必要な場合です。&lt;/p>
&lt;h3 id="3-インタラクティブな-webassembly">3. インタラクティブな WebAssembly&lt;/h3>
&lt;p>サーバー接続を維持せずに対話性が必要な場合は、Interactive WebAssembly は、.NET WebAssembly ランタイムを使用してブラウザーでコンポーネント ロジックを直接実行します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>page &lt;span style="color:#a5d6ff">&amp;#34;/search&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>rendermode InteractiveWebAssembly
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Product Search&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>input &lt;span style="color:#f85149">@&lt;/span>bind&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;searchTerm&amp;#34;&lt;/span> &lt;span style="color:#f85149">@&lt;/span>bind:event&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;oninput&amp;#34;&lt;/span> placeholder&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;Search products...&amp;#34;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (filteredProducts&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Any())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>ul&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>foreach (&lt;span style="color:#ff7b72">var&lt;/span> product &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> filteredProducts)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>li&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Name &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Price&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToString(&lt;span style="color:#a5d6ff">&amp;#34;C&amp;#34;&lt;/span>)&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>li&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>ul&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private string searchTerm &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> string&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>Product&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> allProducts &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> new();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private IEnumerable&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>Product&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> filteredProducts &lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span> allProducts
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Where(p &lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span> p&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Name&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Contains(searchTerm, StringComparison&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>OrdinalIgnoreCase));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> protected override async Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> allProducts &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await Http&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetFromJsonAsync&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>Product&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&amp;gt;&lt;/span>(&lt;span style="color:#a5d6ff">&amp;#34;api/products&amp;#34;&lt;/span>) &lt;span style="color:#f85149">??&lt;/span> new();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>トレードオフ: .NET ランタイムとアセンブリの初期ダウンロード コストがかかります。ただし、コンポーネントは一度ロードされると、UI 対話のためのサーバーのラウンドトリップなしで完全にブラウザー内で実行されます。&lt;/p>
&lt;p>&lt;strong>使用する場合:&lt;/strong> 遅延が重要となる高度にインタラクティブなコンポーネント (リッチ テキスト エディター、描画ツール、リアルタイム フィルタリングなど) の場合、サーバーの負荷を軽減したい場合、またはプログレッシブ Web アプリ (PWA) を構築している場合。&lt;/p>
&lt;h3 id="4-インタラクティブオート">4. インタラクティブオート&lt;/h3>
&lt;p>これは実用的な中間点であり、私のお気に入りの機能の 1 つです。 Interactive Auto は、最初の読み込み時に SignalR を介したサーバー側のレンダリングから開始し、バックグラウンドで WebAssembly ランタイムをサイレントにダウンロードします。次回以降のアクセスでは、コンポーネントは WebAssembly 上で実行されます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>page &lt;span style="color:#a5d6ff">&amp;#34;/dashboard&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>rendermode InteractiveAuto
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Dashboard&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>DashboardWidget Title&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;Sales&amp;#34;&lt;/span> Value&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;@salesTotal&amp;#34;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>DashboardWidget Title&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;Users&amp;#34;&lt;/span> Value&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;@activeUsers&amp;#34;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>button &lt;span style="color:#f85149">@&lt;/span>onclick&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;RefreshData&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Refresh&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>button&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private decimal salesTotal;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private &lt;span style="color:#f0883e;font-weight:bold">int&lt;/span> activeUsers;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> protected override async Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> await RefreshData();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private async Task RefreshData()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> data &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await DashboardService&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetSummaryAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> salesTotal &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> data&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>SalesTotal;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> activeUsers &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> data&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ActiveUsers;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これにより、最初のレンダリングが高速になり (WASM ダウンロードを待つ必要がなく)、最終的にコンポーネントがクライアント側で実行されるという、両方の長所が得られます。ユーザーはその変化に気づきません。&lt;/p>
&lt;p>&lt;strong>いつ使用するか:&lt;/strong> 高速な初期ロードと最終的なクライアント側の実行の両方が必要な場合。これは、多くの対話型コンポーネントにとって優れたデフォルトの選択肢です。&lt;/p>
&lt;h2 id="ストリーミング-ssr-両方の長所">ストリーミング SSR: 両方の長所&lt;/h2>
&lt;p>ストリーミング SSR は、単純そうに見えますが、知覚されるパフォーマンスに大きな違いをもたらす機能の 1 つです。アイデアは次のとおりです。HTML を送信する前にすべてのデータが読み込まれるのを待つ代わりに、サーバーはページ シェルをすぐに送信し、データが利用可能になるとコンテンツ更新を &lt;em>ストリーミング&lt;/em> します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>page &lt;span style="color:#a5d6ff">&amp;#34;/reports&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>attribute [StreamRendering]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Monthly Reports&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (reports is null)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;loading-spinner&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Loading reports&lt;span style="color:#ff7b72;font-weight:bold">...&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>table &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;table&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>thead&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Month&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Revenue&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Growth&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>thead&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tbody&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>foreach (&lt;span style="color:#ff7b72">var&lt;/span> report &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> reports)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>report&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Month&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>report&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Revenue&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToString(&lt;span style="color:#a5d6ff">&amp;#34;C&amp;#34;&lt;/span>)&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;@(report.Growth &amp;gt;= 0 ? &amp;#34;&lt;/span>text&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>success&lt;span style="color:#a5d6ff">&amp;#34; : &amp;#34;&lt;/span>text&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>danger&lt;span style="color:#a5d6ff">&amp;#34;)&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>report&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Growth&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToString(&lt;span style="color:#a5d6ff">&amp;#34;P1&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tbody&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>table&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>MonthlyReport&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">?&lt;/span> reports;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> protected override async Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">//&lt;/span> This might take a couple of seconds
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> reports &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await ReportService&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetMonthlyReportsAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>[StreamRendering]&lt;/code> 属性を使用すると、次のことが起こります。&lt;/p>
&lt;ol>
&lt;li>サーバーは、ローディング スピナーを使用して HTML を直ちに送信します。&lt;/li>
&lt;li>&lt;code>OnInitializedAsync&lt;/code> が実行され、データがフェッチされます。&lt;/li>
&lt;li>データが到着すると、サーバーは更新された HTML (テーブル) をブラウザにストリーミングします。&lt;/li>
&lt;li>ブラウザは、ページ全体をリロードせずに DOM にパッチを適用します。&lt;/li>
&lt;/ol>
&lt;p>ユーザーには読み込みインジケーターが &lt;em>即座に&lt;/em>表示され、その後コンテンツが入力されます。JavaScript フレームワークは必要ありません。 WebSocket接続がありません。 HTTP ストリーミングを賢く利用しただけです。&lt;strong>使用する場合:&lt;/strong> レンダリング中にデータをフェッチする静的 SSR ページ。製品リスト ページ、ダッシュボード、レポートなど、初期データの読み込みに数百ミリ秒以上かかる可能性のある場所はどこでもです。&lt;/p>
&lt;p>&lt;strong>使用しない場合:&lt;/strong> データの読み込みが非常に速い場合 (100 ミリ秒未満)、ストリーミングのオーバーヘッドは価値がありません。また、ストリーミング SSR では継続的なインタラクティブ性が提供されません。そのためには、インタラクティブなレンダリング モードが必要です。&lt;/p>
&lt;h2 id="強化されたナビゲーションとフォーム処理">強化されたナビゲーションとフォーム処理&lt;/h2>
&lt;p>最新の Blazor における微妙だが強力な改良点の 1 つは、ナビゲーションの強化です。既定では、Blazor は内部リンクのクリックとフォームの送信をインターセプトし、完全なブラウザー ナビゲーションを実行するのではなく、&lt;code>fetch&lt;/code> 経由で新しいページ コンテンツを取得し、DOM にパッチを適用します。&lt;/p>
&lt;p>これは、静的 SSR ページ間の移動が SPA のように感じられることを意味します。ページ全体がフラッシュすることはなく、スクロール位置が保持され、エクスペリエンスは滑らかに滑らかになります。&lt;/p>
&lt;h3 id="仕組み">仕組み&lt;/h3>
&lt;p>Blazor スクリプト (&lt;code>blazor.web.js&lt;/code>) が読み込まれると、内部リンクのクリックが自動的にインターセプトされます。従来のブラウザ ナビゲーションの代わりに、次のことが可能です。&lt;/p>
&lt;ol>
&lt;li>ターゲット URL に対して &lt;code>fetch&lt;/code> リクエストを実行します。&lt;/li>
&lt;li>HTML 応答を受信します。&lt;/li>
&lt;li>新しいコンテンツを既存の DOM にマージします。&lt;/li>
&lt;li>ブラウザの URL と履歴を更新します。&lt;/li>
&lt;/ol>
&lt;p>これを有効にするために何もする必要はありません。デフォルトでオンになっています。しかし、それを制御することはできます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">&amp;lt;!-- Disable enhanced navigation for a specific link --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#7ee787">a&lt;/span> href&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;/legacy-page&amp;#34;&lt;/span> data-enhance-nav&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;false&amp;#34;&lt;/span>&amp;gt;Legacy Page&amp;lt;/&lt;span style="color:#7ee787">a&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">&amp;lt;!-- Force a full page reload for external-like behavior --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#7ee787">a&lt;/span> href&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;/downloads/report.pdf&amp;#34;&lt;/span> data-enhance-nav&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;false&amp;#34;&lt;/span>&amp;gt;Download Report&amp;lt;/&lt;span style="color:#7ee787">a&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="強化されたフォーム処理">強化されたフォーム処理&lt;/h3>
&lt;p>フォームも同様に扱われます。 Blazor のフォーム処理で &lt;code>EditForm&lt;/code> または標準の &lt;code>&amp;lt;form&amp;gt;&lt;/code> 要素を使用すると、送信はインターセプトされ、&lt;code>fetch&lt;/code> 経由で処理されます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@page &amp;#34;/contact&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;EditForm Model=&amp;#34;contactModel&amp;#34; OnValidSubmit=&amp;#34;HandleSubmit&amp;#34; FormName=&amp;#34;contact&amp;#34; Enhance&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;DataAnnotationsValidator /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&amp;#34;mb-3&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label for=&amp;#34;name&amp;#34;&amp;gt;Name&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;InputText id=&amp;#34;name&amp;#34; @bind-Value=&amp;#34;contactModel.Name&amp;#34; class=&amp;#34;form-control&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;ValidationMessage For=&amp;#34;() =&amp;gt; contactModel.Name&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&amp;#34;mb-3&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label for=&amp;#34;email&amp;#34;&amp;gt;Email&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;InputText id=&amp;#34;email&amp;#34; @bind-Value=&amp;#34;contactModel.Email&amp;#34; class=&amp;#34;form-control&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;ValidationMessage For=&amp;#34;() =&amp;gt; contactModel.Email&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&amp;#34;mb-3&amp;#34;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label for=&amp;#34;message&amp;#34;&amp;gt;Message&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;InputTextArea id=&amp;#34;message&amp;#34; @bind-Value=&amp;#34;contactModel.Message&amp;#34; class=&amp;#34;form-control&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;ValidationMessage For=&amp;#34;() =&amp;gt; contactModel.Message&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button type=&amp;#34;submit&amp;#34; class=&amp;#34;btn btn-primary&amp;#34;&amp;gt;Send&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/EditForm&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [SupplyParameterFromForm]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private ContactModel contactModel { get; set; } = new();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private async Task HandleSubmit()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> await ContactService.SubmitAsync(contactModel);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> contactModel = new ContactModel();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>EditForm&lt;/code> の &lt;code>Enhance&lt;/code> 属性は、Blazor にフォームの送信をインターセプトするように指示します。フォームがサーバーに投稿され、サーバーがそれを処理してページを再レンダリングし、更新された HTML がストリーム バックされます。これらすべてが、完全なページ ナビゲーションを必要とせずに行われます。インタラクティブに感じられますが、完全にサーバーでレンダリングされます。&lt;/p>
&lt;p>&lt;code>[SupplyParameterFromForm]&lt;/code> 属性に注目してください。これは、Blazor が静的 SSR シナリオで投稿されたフォーム データをモデルにバインドする方法です。これは、従来のフォーム ポストと Blazor のコンポーネント モデルの間の橋渡しとなります。&lt;/p>
&lt;h2 id="ページごとおよびコンポーネントごとの対話性">ページごとおよびコンポーネントごとの対話性&lt;/h2>
&lt;p>新しいモデルの最も強力な側面の 1 つは、単一のアプリケーション内でレンダリング モードを混合できることです。製品リスト ページは静的 SSR、ショッピング カートは Interactive Server、製品コンフィギュレータは Interactive WebAssembly にすることができ、すべて同じアプリ内で行うことができます。&lt;/p>
&lt;h3 id="コンポーネント-レベルでのレンダリング-モードの設定">コンポーネント レベルでのレンダリング モードの設定&lt;/h3>
&lt;p>&lt;code>@rendermode&lt;/code> ディレクティブを使用して、コンポーネントにレンダリング モードを直接設定できます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@* This component is interactive via Server *@
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@rendermode InteractiveServer
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h3&amp;gt;Live Chat&amp;lt;/h3&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;!-- chat UI here --&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>または、親のコンポーネントを使用するときに設定することもできます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@page &amp;#34;/product/{Id:int}&amp;#34;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h1&amp;gt;@product?.Name&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;p&amp;gt;@product?.Description&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;!-- This child component gets its own interactive render mode --&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;ProductConfigurator Product=&amp;#34;product&amp;#34; @rendermode=&amp;#34;InteractiveWebAssembly&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;!-- This stays static --&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;ProductReviews ProductId=&amp;#34;Id&amp;#34; /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter] public int Id { get; set; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private Product? product;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> protected override async Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> product = await ProductService.GetByIdAsync(Id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>この例では、ページ自体が静的 SSR です。 &lt;code>ProductConfigurator&lt;/code> コンポーネントは WebAssembly 上で実行され、クライアント側での豊富な対話性を実現します。 &lt;code>ProductReviews&lt;/code> コンポーネントはデータを表示しているだけなので静的なままです。&lt;/p>
&lt;h3 id="レンダリング-モードをグローバルに設定する">レンダリング モードをグローバルに設定する&lt;/h3>
&lt;p>すべてのページをデフォルトでインタラクティブにしたい場合は、&lt;code>App.razor&lt;/code> のルート レベルでレンダリング モードを設定できます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-mysql" data-lang="mysql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>Routes&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">@&lt;/span>rendermode&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;InteractiveServer&amp;#34;&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">/&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">```&lt;/span>&lt;span style="color:#f85149">これにより、サーバー&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">レンダリングを介してすべてのページがインタラクティブになります。必要に応じて、個々のコンポーネントがこれをオーバーライドできます。&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#8b949e;font-style:italic">### 覚えておくべき重要なルール
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#f85149">レンダリング&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">モードを混合する場合は、留意すべき制約がいくつかあります。&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span>&lt;span style="color:#f85149">子コンポーネントは、親よりも「よりインタラクティブな」レンダリング&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">モードを持つことはできません。&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">親が静的な場合、子は静的またはインタラクティブにすることができます。ただし、親が&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>Interactive&lt;span style="color:#6e7681"> &lt;/span>Server&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">の場合、子を&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>Interactive&lt;span style="color:#6e7681"> &lt;/span>WebAssembly&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">にすることはできません&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>(Server&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">または&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>Auto&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">である必要があります&lt;/span>)&lt;span style="color:#f85149">。&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span>&lt;span style="color:#f85149">対話型コンポーネントは、静的な親からスコープ指定されたサービスを直接使用できません。&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">コンポーネントが&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>WebAssembly&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">で実行される場合、サーバー側の&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>DbContext&lt;span style="color:#ff7b72;font-weight:bold">`&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">インスタンスに直接アクセスできません。&lt;/span>API&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">レイヤーが必要になります。&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span>&lt;span style="color:#f85149">状態はレンダリング&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">モード間で自動的に転送されません。&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">コンポーネントがサーバー上で事前レンダリングされてから&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>WebAssembly&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">に切り替わる場合は、状態の永続性を明示的に処理する必要があります。これについては、&lt;/span>.NET&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">10&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">セクションで詳しく説明します。&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#8b949e;font-style:italic">## .NET 10 の新機能
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>.NET&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">10&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">では、信頼性と開発者のエクスペリエンスに重点を置き、段階的な改善アプローチを継続しています。&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>Blazor&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">の対話性のハイライトは次のとおりです。&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#8b949e;font-style:italic">### 再接続エクスペリエンスの向上
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#f85149">運用環境で&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>Interactive&lt;span style="color:#6e7681"> &lt;/span>Server&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">モードを使用したことがある場合は、おそらく再接続オーバーレイに遭遇したことがあるでしょう。&lt;/span>SignalR&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">接続が切断され、ユーザーに「再接続中&lt;/span>...&lt;span style="color:#f85149">」というメッセージが表示される瞬間です。&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>.NET&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">10&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">では、このエクスペリエンスが大幅に向上しています。&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#f85149">再接続ロジックの再試行がよりスマートになりました。固定の再試行間隔の代わりに、状況に適応するバックオフ戦略を使用します。再接続中の&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>UI&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">もさらに洗練され、エクスペリエンスをカスタマイズするためのフックが改善されました。&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">```&lt;/span>razor&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;!&lt;/span>&lt;span style="color:#8b949e;font-style:italic">-- In your App.razor or layout --&amp;gt;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>id&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;components-reconnect-modal&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>class&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;reconnect-visible&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Connection&lt;span style="color:#6e7681"> &lt;/span>lost.&lt;span style="color:#6e7681"> &lt;/span>Attempting&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72">to&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>reconnect...&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>class&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;spinner&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&amp;lt;/&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>class&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;reconnect-failed&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Could&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72">not&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>reconnect&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72">to&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>the&lt;span style="color:#6e7681"> &lt;/span>server.&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>button&lt;span style="color:#6e7681"> &lt;/span>onclick&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;location.reload()&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Reload&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>button&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>class&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;reconnect-rejected&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Your&lt;span style="color:#6e7681"> &lt;/span>session&lt;span style="color:#6e7681"> &lt;/span>has&lt;span style="color:#6e7681"> &lt;/span>expired.&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>a&lt;span style="color:#6e7681"> &lt;/span>href&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#ff7b72">Return&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72">to&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>Home&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>a&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>&lt;span style="color:#ff7b72">div&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>また、フレームワークは一時的な障害に対してより積極的に再接続を試行するようになり、回路状態をより確実に復元できるようになりました。これは、ユーザーにとって「ページをリロードしてください」という瞬間が少なくなることを意味します。&lt;/p>
&lt;h3 id="レンダリング-モード間での永続的なコンポーネントの状態">レンダリング モード間での永続的なコンポーネントの状態&lt;/h3>
&lt;p>これは大きなことです。 .NET 9 では、コンポーネントがサーバー上で事前レンダリングされてから WebAssembly に移行する (またはレンダリング モード間で切り替わる) と、コンポーネントの状態が失われます。コンポーネントは効果的に再初期化されるため、データの重複フェッチや UI のちらつきが発生する可能性があります。&lt;/p>
&lt;p>.NET 10 では、&lt;code>PersistentComponentState&lt;/code> サービスが改善され、レンダリング モードの移行間でよりシームレスに動作するようになりました。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>page &lt;span style="color:#a5d6ff">&amp;#34;/weather&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>rendermode InteractiveWebAssembly
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>inject PersistentComponentState ApplicationState
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Weather Forecast&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (forecasts is null)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Loading&lt;span style="color:#ff7b72;font-weight:bold">...&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>foreach (&lt;span style="color:#ff7b72">var&lt;/span> forecast &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> forecasts)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;forecast-card&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h4&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>forecast&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Date&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToShortDateString()&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h4&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>forecast&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Summary &lt;span style="color:#f85149">—&lt;/span> &lt;span style="color:#f85149">@&lt;/span>forecast&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>TemperatureC&lt;span style="color:#f85149">°&lt;/span>C&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>WeatherForecast&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">?&lt;/span> forecasts;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private PersistingComponentStateSubscription persistingSubscription;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> protected override async Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> persistingSubscription &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> ApplicationState&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>RegisterOnPersisting(PersistData);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72;font-weight:bold">!&lt;/span>ApplicationState&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>TryTakeFromJson&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>WeatherForecast&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&amp;gt;&lt;/span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;weather-forecasts&amp;#34;&lt;/span>, out &lt;span style="color:#ff7b72">var&lt;/span> restored))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">//&lt;/span> Data wasn&lt;span style="color:#a5d6ff">&amp;#39;t persisted from prerendering — fetch it&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> forecasts &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await Http&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetFromJsonAsync&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>WeatherForecast&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&amp;gt;&lt;/span>(&lt;span style="color:#a5d6ff">&amp;#34;api/weather&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> forecasts &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> restored;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private Task PersistData()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ApplicationState&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>PersistAsJson(&lt;span style="color:#a5d6ff">&amp;#34;weather-forecasts&amp;#34;&lt;/span>, forecasts);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Task&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>CompletedTask;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> public void Dispose()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> persistingSubscription&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Dispose();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>.NET 10 の改良により、このパターンはより確実に動作するようになりました。プリレンダリング中にシリアル化された状態は、コンポーネントがインタラクティブ モードで初期化されるときに適切に利用できるため、二重のデータ フェッチやユーザーが目にする可能性のある短いちらつきが回避されます。&lt;/p>
&lt;h3 id="静的-web-アセットとしての-blazor-スクリプト">静的 Web アセットとしての Blazor スクリプト&lt;/h3>
&lt;p>以前のバージョンでは、Blazor JavaScript ファイル (&lt;code>blazor.web.js&lt;/code>) はフレームワークの内部エンドポイントから提供されていました。 .NET 10 では、静的な Web アセットとして配信されます。これは小さな変更のように聞こえるかもしれませんが、実際的な利点があります。- &lt;strong>キャッシュの改善:&lt;/strong> 静的 Web アセットは適切なキャッシュ ヘッダーとフィンガープリントされた URL を取得するため、ブラウザーはそれらをより効果的にキャッシュします。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>CDN フレンドリー:&lt;/strong> これは通常の静的ファイルであるため、CDN はそれをキャッシュし、エッジ ロケーションから提供できます。&lt;/li>
&lt;li>&lt;strong>圧縮:&lt;/strong> 静的な Web アセット圧縮が自動的に適用され、ネットワーク上のスクリプト サイズが削減されます。&lt;/li>
&lt;/ul>
&lt;p>参照方法を変更する必要はありません。更新されたパスはフレームワークによって自動的に処理されます。&lt;/p>
&lt;h2 id="ベスト-プラクティス-適切なレンダリング-モードの選択">ベスト プラクティス: 適切なレンダリング モードの選択&lt;/h2>
&lt;p>いくつかのプロジェクトでこれらすべてのモードを使用した後、私の意思決定フレームワークは次のとおりです。&lt;/p>
&lt;h3 id="静的-ssr-から始める">静的 SSR から始める&lt;/h3>
&lt;p>静的 SSR をデフォルトにします。ほとんどのアプリケーションのほとんどのページは、主にデータの表示に関するものです。製品ページ、ブログ投稿、ユーザー プロファイル、設定ページ - これらにはリアルタイムの対話性は必要ありません。静的 SSR は、最高のパフォーマンス、最小限のリソース使用量、そして最も単純なメンタル モデルを提供します。&lt;/p>
&lt;h3 id="必要な場合にのみインタラクティブ性を追加する">必要な場合にのみインタラクティブ性を追加する&lt;/h3>
&lt;p>ユーザー操作にリアルタイムで応答する必要がある特定のコンポーネントを特定します。 「いいね！」ボタン、チャット ウィジェット、ドラッグ アンド ドロップ インターフェイス - これらには対話性が必要です。しかし、それらを囲むページはおそらくそうではありません。&lt;/p>
&lt;h3 id="インタラクティブ-オートを頼りになるインタラクティブ-モードとして使用する">インタラクティブ オートを頼りになるインタラクティブ モードとして使用する&lt;/h3>
&lt;p>インタラクティブ性が必要な場合は、多くの場合、Interactive Auto がデフォルトの最適な選択になります。これにより、高速な初期読み込み (サーバー レンダリング) と、最終的なクライアント側の実行 (WebAssembly) が実現します。ユーザーは両方の長所を活用でき、コードを一度記述するだけで済みます。&lt;/p>
&lt;h3 id="interactive-server-を特定のケースのために予約する">Interactive Server を特定のケースのために予約する&lt;/h3>
&lt;p>次の場合に Interactive Server を使用します。&lt;/p>
&lt;ul>
&lt;li>コンポーネントはサーバー リソース (データベース、ファイル システム、内部 API) に直接アクセスする必要があります。&lt;/li>
&lt;li>WebAssembly のダウンロード サイズが懸念されるため、自動は使用できません。&lt;/li>
&lt;li>セキュリティ上の理由から (機密データの処理など)、コンポーネントをサーバー上で常に実行する必要があります。&lt;/li>
&lt;/ul>
&lt;h3 id="ストリーミング-ssr-を積極的に使用する">ストリーミング SSR を積極的に使用する&lt;/h3>
&lt;p>静的 SSR ページがデータをフェッチする場合は、&lt;code>[StreamRendering]&lt;/code> を追加します。コストは最小限であり、パフォーマンスの大幅な向上が感じられます。ユーザーは、空白のページを見つめるのではなく、コンテンツが徐々に表示されるのを確認します。&lt;/p>
&lt;h3 id="状態遷移を慎重に処理する">状態遷移を慎重に処理する&lt;/h3>
&lt;p>Interactive Auto を使用している場合、またはプリレンダリングと WebAssembly を組み合わせている場合は、重複したデータのフェッチを避けるために常に &lt;code>PersistentComponentState&lt;/code> を使用してください。ユーザーは、ちらつくコンテンツがないことに感謝するでしょう。&lt;/p>
&lt;h3 id="コンポーネント-ツリーを念頭に置く">コンポーネント ツリーを念頭に置く&lt;/h3>
&lt;p>レンダリング モードの階層ルールを覚えておいてください。インタラクティブな境界が意味をなすようにコンポーネント ツリーを計画します。一般的なパターンは、必要な場所にインタラクティブな「アイランド」が埋め込まれた静的レイアウトを使用することです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>@* Layout: Static SSR *@
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;header&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;NavMenu /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;UserMenu @rendermode=&amp;#34;InteractiveServer&amp;#34; /&amp;gt; @* Needs real-time auth state *@
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/header&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;main&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @Body
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/main&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;footer&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;ChatWidget @rendermode=&amp;#34;InteractiveAuto&amp;#34; /&amp;gt; @* Rich interactivity *@
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/footer&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="結論">結論&lt;/h2>
&lt;p>.NET 9 および .NET 10 の Blazor の対話性モデルは、Web アプリケーションを構築するための成熟した、よく考えられたアプローチを表しています。コンポーネントごとにレンダリング モードを選択できる機能、シームレスな強化されたナビゲーション、ストリーミング SSR、再接続と状態管理の継続的な改善により、幅広いアプリケーションにとって魅力的な選択肢となっています。重要な洞察は、&lt;strong>インタラクティブ性はスペクトルであり、二者択一の選択ではない&lt;/strong>ということです。アプリケーションのほとんどは静的にすることができます。一部の部分ではサーバー主導の対話性が必要です。ブラウザで実行するとメリットが得られるものもあります。 Blazor では、フレームワークと競合することなく、最も細かい粒度 (個々のコンポーネント) でその選択を行うことができるようになりました。&lt;/p>
&lt;p>新しいプロジェクトを開始する場合、私のアドバイスはシンプルです。Blazor Web アプリを作成し、静的 SSR から開始し、データ量の多いページに &lt;code>[StreamRendering]&lt;/code> を追加し、必要な場合にのみ個々のコンポーネントを対話モードに昇格させます。最終的には、高速かつ効率的で、拡張性に優れたアプリケーションが完成します。&lt;/p>
&lt;p>コーディングを楽しんでください。いつものように、質問がある場合はお気軽にお問い合わせください。&lt;/p></content:encoded><category>Blazor</category><category>.NET</category><category>Web Development</category></item><item><title>.NET の最小限の API: 軽量の HTTP API の構築</title><link>https://emimontesdeoca.github.io/ja/posts/minimal-apis-dotnet/</link><pubDate>Thu, 10 Apr 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/minimal-apis-dotnet/</guid><description>ルート ハンドラーやパラメーター バインディングからフィルターや OpenAPI 統合まで、.NET Minimal API を使用してクリーンで高速な HTTP API を構築するための包括的なガイド。</description><content:encoded>&lt;p>ASP.NET Core で API をしばらく構築している場合は、コントローラー ベースのアプローチに精通していることでしょう。コントローラー クラスを作成し、属性で修飾し、コンストラクターを通じてサービスを注入し、ルートを接続します。それは機能します、そしてそれはうまく機能します。しかし、時々、「このリクエストを受け取り、何かをし、応答を返す」ということに相当する多くの儀式を書いているように感じることがあります。&lt;/p>
&lt;p>Minimal API が解決しようとしている問題はまさにそれです。 .NET 6 で導入された Minimal API を使用すると、定型文をほとんど使用せずに HTTP エンドポイントを定義できます。コントローラーも属性もスタートアップ クラスのジャグリングもありません。すっきりとした機能的なスタイルで、ルートからハンドラーへのマッピングを直接行うだけです。&lt;/p>
&lt;p>この投稿では、最初のエンドポイントから大規模なアプリケーションの編成、認証、検証の処理、OpenAPI ドキュメント、パフォーマンスの考慮事項まで、最小限の API について知っておくべきことをすべて説明したいと思います。さあ、始めましょう。&lt;/p>
&lt;h2 id="なぜ最小限の-api-なのか">なぜ最小限の API なのか?&lt;/h2>
&lt;p>本題に入る前に、従来のコントローラーベースのアプローチではなく、最小限の API を選択する &lt;em>理由&lt;/em> について話しましょう。&lt;/p>
&lt;p>&lt;strong>コントローラー&lt;/strong>は、構造化された独自のフレームワークが必要な場合に最適です。これらにより、モデル バインディング、フィルター、コンテンツ ネゴシエーション、および懸念事項の明確な分離がすぐに使用できます。数十人の開発者がいる大規模なエンタープライズ アプリケーションの場合、コントローラーが強制する一貫性は大きな利点となります。&lt;/p>
&lt;p>一方、&lt;strong>最小限の API&lt;/strong> は、次のような場合に威力を発揮します。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>定型文の削減&lt;/strong> — コントローラー クラス、&lt;code>[ApiController]&lt;/code> 属性、個別の起動設定はありません。&lt;/li>
&lt;li>&lt;strong>高速な起動&lt;/strong> — 起動時のリフレクションベースの操作が少なくなります。&lt;/li>
&lt;li>&lt;strong>よりシンプルなメンタル モデル&lt;/strong> — ルートはハンドラーに直接マッピングされます。それでおしまい。&lt;/li>
&lt;li>&lt;strong>マイクロサービス対応&lt;/strong> — API に 5 ～ 10 のエンドポイントがある場合、コントローラーを完全にセットアップするのはやりすぎのように感じることがあります。&lt;/li>
&lt;/ul>
&lt;p>良いニュースは、これは二者択一の決定ではないということです。同じプロジェクト内でコントローラーと最小限の API を混在させることができます。しかし、最小限のアプローチに慣れると、予想よりも頻繁にそれに手を伸ばすようになるかもしれません。&lt;/p>
&lt;h2 id="はじめに">はじめに&lt;/h2>
&lt;p>Minimal API を最初から作成してみましょう。 .NET SDK がインストールされている場合は、次のように簡単です。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet new web -n MyMinimalApi
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cd MyMinimalApi
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これにより、次のような &lt;code>Program.cs&lt;/code> が得られます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> app = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>, () =&amp;gt; &lt;span style="color:#a5d6ff">&amp;#34;Hello World!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.Run();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>それだけです。それは動作する API です。 &lt;code>Startup.cs&lt;/code>、コントローラー クラス、ルーティング設定はありません。 &lt;code>dotnet run&lt;/code> を実行し、&lt;code>http://localhost:5000&lt;/code> を押すと、「Hello World!」が表示されます。戻る。ここでのシンプルさが重要です。&lt;/p>
&lt;p>古典的な todo API を使ってもう少し便利にしてみましょう。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> app = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> todos = &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;Todo&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>, () =&amp;gt; todos);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/todos/{id:int}&amp;#34;&lt;/span>, (&lt;span style="color:#ff7b72">int&lt;/span> id) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> todos.FirstOrDefault(t =&amp;gt; t.Id == id) &lt;span style="color:#ff7b72">is&lt;/span> { } todo
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ? Results.Ok(todo)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> : Results.NotFound());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>, (Todo todo) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> todos.Add(todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/todos/{todo.Id}&amp;#34;&lt;/span>, todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapDelete(&lt;span style="color:#a5d6ff">&amp;#34;/todos/{id:int}&amp;#34;&lt;/span>, (&lt;span style="color:#ff7b72">int&lt;/span> id) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> todo = todos.FirstOrDefault(t =&amp;gt; t.Id == id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (todo &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>) &lt;span style="color:#ff7b72">return&lt;/span> Results.NotFound();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> todos.Remove(todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.NoContent();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.Run();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">record&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Todo&lt;/span>(&lt;span style="color:#ff7b72">int&lt;/span> Id, &lt;span style="color:#ff7b72">string&lt;/span> Title, &lt;span style="color:#ff7b72">bool&lt;/span> IsComplete);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>30 行以内に完全な CRUD があります。それがミニマルアプローチの力です。&lt;/p>
&lt;h2 id="ルート-ハンドラー">ルート ハンドラー&lt;/h2>
&lt;p>ルート ハンドラーは、リクエストがルートと一致した場合に実行される関数です。それらを定義するにはいくつかのオプションがあります。&lt;/p>
&lt;h3 id="ラムダ式">ラムダ式&lt;/h3>
&lt;p>最も一般的なアプローチと、ほとんどの例で見られるものは次のとおりです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/hello&amp;#34;&lt;/span>, () =&amp;gt; &lt;span style="color:#a5d6ff">&amp;#34;Hello!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/hello/{name}&amp;#34;&lt;/span>, (&lt;span style="color:#ff7b72">string&lt;/span> name) =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;Hello, {name}!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="メソッドグループ">メソッドグループ&lt;/h3>
&lt;p>より複雑なロジックの場合は、名前付きメソッドを指定できます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/products&amp;#34;&lt;/span>, GetProducts);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/products&amp;#34;&lt;/span>, CreateProduct);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">static&lt;/span> IResult GetProducts(AppDbContext db)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> =&amp;gt; Results.Ok(db.Products.ToList());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">static&lt;/span> IResult CreateProduct(Product product, AppDbContext db)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> db.Products.Add(product);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> db.SaveChanges();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/products/{product.Id}&amp;#34;&lt;/span>, product);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```これは、ワンライナー以上のものに対して私が好むアプローチです。これにより、ルート&lt;/span> &lt;span style="color:#f85149">マッピング&lt;/span> &lt;span style="color:#f85149">セクションがクリーンで読みやすくなります。実装の詳細を調べなくても、どのエンドポイントが存在するかを一目で確認できます。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">###&lt;/span> &lt;span style="color:#f85149">ローカル関数&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">ローカル関数を使用することもできます。これは、ハンドラーをルート定義の近くに保持したい場合に便利です。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```&lt;/span>csharp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/health&amp;#34;&lt;/span>, CheckHealth);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>IResult CheckHealth()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Some health check logic&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Ok(&lt;span style="color:#ff7b72">new&lt;/span> { Status = &lt;span style="color:#a5d6ff">&amp;#34;Healthy&amp;#34;&lt;/span>, Timestamp = DateTime.UtcNow });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="パラメータのバインド">パラメータのバインド&lt;/h2>
&lt;p>Minimal API について私が本当に感謝している点の 1 つは、パラメーター バインディングがいかに直感的であるかということです。フレームワークは、コンテキストと型に基づいて値をどこから取得するかを判断します。&lt;/p>
&lt;h3 id="ルートパラメータ">ルートパラメータ&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/users/{id:int}&amp;#34;&lt;/span>, (&lt;span style="color:#ff7b72">int&lt;/span> id) =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;User {id}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/files/{*path}&amp;#34;&lt;/span>, (&lt;span style="color:#ff7b72">string&lt;/span> path) =&amp;gt; &lt;span style="color:#a5d6ff">$&amp;#34;File: {path}&amp;#34;&lt;/span>); &lt;span style="color:#8b949e;font-style:italic">// catch-all&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="クエリ文字列パラメータ">クエリ文字列パラメータ&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/search&amp;#34;&lt;/span>, (&lt;span style="color:#ff7b72">string?&lt;/span> query, &lt;span style="color:#ff7b72">int&lt;/span> page = &lt;span style="color:#a5d6ff">1&lt;/span>, &lt;span style="color:#ff7b72">int&lt;/span> pageSize = &lt;span style="color:#a5d6ff">20&lt;/span>) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Results.Ok(&lt;span style="color:#ff7b72">new&lt;/span> { query, page, pageSize }));
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Null 許容型はオプションのパラメーターになります。デフォルト値は期待どおりに機能します。&lt;/p>
&lt;h3 id="リクエスト本文">リクエスト本文&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/orders&amp;#34;&lt;/span>, (Order order) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// &amp;#39;order&amp;#39; is automatically deserialized from the JSON body&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/orders/{order.Id}&amp;#34;&lt;/span>, order);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="ヘッダーとサービスのバインディング">ヘッダーとサービスのバインディング&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/protected&amp;#34;&lt;/span>, (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [FromHeader(Name = &amp;#34;X-Api-Key&amp;#34;)] &lt;span style="color:#ff7b72">string&lt;/span> apiKey,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [FromServices] ILogger&amp;lt;Program&amp;gt; logger) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logger.LogInformation(&lt;span style="color:#a5d6ff">&amp;#34;Request with API key: {Key}&amp;#34;&lt;/span>, apiKey[..&lt;span style="color:#a5d6ff">4&lt;/span>] + &lt;span style="color:#a5d6ff">&amp;#34;****&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Ok(&lt;span style="color:#a5d6ff">&amp;#34;Authorized&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="httpcontext-と-httprequest">HttpContext と HttpRequest&lt;/h3>
&lt;p>下位レベルのアクセスが必要な場合:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/info&amp;#34;&lt;/span>, (HttpContext context) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> userAgent = context.Request.Headers.UserAgent.ToString();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> ip = context.Connection.RemoteIpAddress?.ToString();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Ok(&lt;span style="color:#ff7b72">new&lt;/span> { userAgent, ip });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="bindasync-によるカスタム-バインディング">BindAsync によるカスタム バインディング&lt;/h3>
&lt;p>複合型の場合、静的 &lt;code>BindAsync&lt;/code> メソッドを実装できます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">record&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">PaginationParams&lt;/span>(&lt;span style="color:#ff7b72">int&lt;/span> Page, &lt;span style="color:#ff7b72">int&lt;/span> PageSize)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> ValueTask&amp;lt;PaginationParams?&amp;gt; BindAsync(HttpContext context)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">int&lt;/span>.TryParse(context.Request.Query[&lt;span style="color:#a5d6ff">&amp;#34;page&amp;#34;&lt;/span>], &lt;span style="color:#ff7b72">out&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> page);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">int&lt;/span>.TryParse(context.Request.Query[&lt;span style="color:#a5d6ff">&amp;#34;pageSize&amp;#34;&lt;/span>], &lt;span style="color:#ff7b72">out&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> pageSize);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">new&lt;/span> PaginationParams(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Page: page &amp;gt; &lt;span style="color:#a5d6ff">0&lt;/span> ? page : &lt;span style="color:#a5d6ff">1&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PageSize: pageSize &amp;gt; &lt;span style="color:#a5d6ff">0&lt;/span> ? Math.Min(pageSize, &lt;span style="color:#a5d6ff">100&lt;/span>) : &lt;span style="color:#a5d6ff">20&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> ValueTask.FromResult&amp;lt;PaginationParams?&amp;gt;(result);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/items&amp;#34;&lt;/span>, (PaginationParams pagination, AppDbContext db) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> items = db.Items
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Skip((pagination.Page - &lt;span style="color:#a5d6ff">1&lt;/span>) * pagination.PageSize)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Take(pagination.PageSize)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToList();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Ok(items);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これは信じられないほど強力です。バインディング ロジックを一度定義すると、それをすべてのエンドポイントで再利用できます。&lt;/p>
&lt;h2 id="検証">検証&lt;/h2>
&lt;p>Minimal API がコントローラーに比べてすぐに使える機能が少ない領域の 1 つは、モデルの検証です。 &lt;code>[Required]&lt;/code> または &lt;code>[StringLength]&lt;/code> の自動適用はありません。しかし、それに対処するための明確なパターンがあります。&lt;/p>
&lt;h3 id="手動検証">手動検証&lt;/h3>
&lt;p>最も単純なアプローチ - ハンドラー内で検証するだけです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/users&amp;#34;&lt;/span>, (CreateUserRequest request) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrWhiteSpace(request.Email))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.BadRequest(&lt;span style="color:#ff7b72">new&lt;/span> { Error = &lt;span style="color:#a5d6ff">&amp;#34;Email is required&amp;#34;&lt;/span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (request.Name?.Length &amp;gt; &lt;span style="color:#a5d6ff">100&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.BadRequest(&lt;span style="color:#ff7b72">new&lt;/span> { Error = &lt;span style="color:#a5d6ff">&amp;#34;Name must be 100 characters or less&amp;#34;&lt;/span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Create user...&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/users/{request.Email}&amp;#34;&lt;/span>, request);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="検証ライブラリの使用">検証ライブラリの使用&lt;/h3>
&lt;p>重要なことについては、FluentValidation または同様のライブラリを使用することをお勧めします。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CreateUserValidator&lt;/span> : AbstractValidator&amp;lt;CreateUserRequest&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> CreateUserValidator()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RuleFor(x =&amp;gt; x.Email).NotEmpty().EmailAddress();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RuleFor(x =&amp;gt; x.Name).NotEmpty().MaximumLength(&lt;span style="color:#a5d6ff">100&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RuleFor(x =&amp;gt; x.Age).InclusiveBetween(&lt;span style="color:#a5d6ff">0&lt;/span>, &lt;span style="color:#a5d6ff">150&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これをエンドポイント フィルターを通じてエンドポイントに接続できます。これにより、次のセクションに進みます。&lt;/p>
&lt;h2 id="エンドポイントフィルター">エンドポイントフィルター&lt;/h2>
&lt;p>エンドポイント フィルターは、Minimal API の最も優れた機能の 1 つです。これらはミドルウェアと考えてください。ただし、スコープはパイプライン全体ではなく特定のエンドポイントに限定されます。これらは .NET 7 で導入され、横断的な問題に最適です。&lt;/p>
&lt;h3 id="基本フィルター">基本フィルター&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>, (Todo todo) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Handle the request&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/todos/{todo.Id}&amp;#34;&lt;/span>, todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.AddEndpointFilter(&lt;span style="color:#ff7b72">async&lt;/span> (context, next) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> todo = context.GetArgument&amp;lt;Todo&amp;gt;(&lt;span style="color:#a5d6ff">0&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrEmpty(todo.Title))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.BadRequest(&lt;span style="color:#ff7b72">new&lt;/span> { Error = &lt;span style="color:#a5d6ff">&amp;#34;Title is required&amp;#34;&lt;/span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">await&lt;/span> next(context);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="fluentvalidation-を使用した検証フィルター">FluentValidation を使用した検証フィルター&lt;/h3>
&lt;p>これが本当に強力になるのは、再利用可能な検証フィルターです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ValidationFilter&lt;/span>&amp;lt;T&amp;gt; : IEndpointFilter &lt;span style="color:#ff7b72">where&lt;/span> T : &lt;span style="color:#ff7b72">class&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> IValidator&amp;lt;T&amp;gt; _validator;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ValidationFilter(IValidator&amp;lt;T&amp;gt; validator)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _validator = validator;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> ValueTask&amp;lt;&lt;span style="color:#ff7b72">object?&lt;/span>&amp;gt; InvokeAsync(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> EndpointFilterInvocationContext context,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> EndpointFilterDelegate next)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> model = context.Arguments
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .OfType&amp;lt;T&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .FirstOrDefault();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (model &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.BadRequest(&lt;span style="color:#ff7b72">new&lt;/span> { Error = &lt;span style="color:#a5d6ff">&amp;#34;Request body is required&amp;#34;&lt;/span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">await&lt;/span> _validator.ValidateAsync(model);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!result.IsValid)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> errors = result.Errors
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .GroupBy(e =&amp;gt; e.PropertyName)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToDictionary(g =&amp;gt; g.Key, g =&amp;gt; g.Select(e =&amp;gt; e.ErrorMessage).ToArray());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.ValidationProblem(errors);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">await&lt;/span> next(context);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Usage&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/users&amp;#34;&lt;/span>, (CreateUserRequest request) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Only reached if validation passes&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/users/{request.Email}&amp;#34;&lt;/span>, request);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.AddEndpointFilter&amp;lt;ValidationFilter&amp;lt;CreateUserRequest&amp;gt;&amp;gt;();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="ロギングフィルター">ロギングフィルター&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/products&amp;#34;&lt;/span>, (AppDbContext db) =&amp;gt; db.Products.ToList())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddEndpointFilter(&lt;span style="color:#ff7b72">async&lt;/span> (context, next) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> logger = context.HttpContext
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .RequestServices.GetRequiredService&amp;lt;ILogger&amp;lt;Program&amp;gt;&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> sw = Stopwatch.StartNew();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">await&lt;/span> next(context);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sw.Stop();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logger.LogInformation(&lt;span style="color:#a5d6ff">&amp;#34;Endpoint executed in {Elapsed}ms&amp;#34;&lt;/span>, sw.ElapsedMilliseconds);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> result;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>複数のフィルターをチェーンすることができ、ミドルウェアと同様に追加された順序で実行されます。&lt;/p>
&lt;h2 id="openapi--swagger-の統合">OpenAPI / Swagger の統合&lt;/h2>
&lt;p>優れた API ドキュメントはもはやオプションではありません。ありがたいことに、Minimal API は &lt;code>Microsoft.AspNetCore.OpenApi&lt;/code> パッケージを通じて OpenAPI のファーストクラスのサポートを備えています。&lt;/p>
&lt;h3 id="基本セットアップ">基本セットアップ&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddOpenApi();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> app = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapOpenApi();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>, () =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;Todo&amp;gt;())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithName(&lt;span style="color:#a5d6ff">&amp;#34;GetTodos&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithDescription(&lt;span style="color:#a5d6ff">&amp;#34;Retrieves all todo items&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithTags(&lt;span style="color:#a5d6ff">&amp;#34;Todos&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.Run();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="豊富なエンドポイント-メタデータ">豊富なエンドポイント メタデータ&lt;/h3>
&lt;p>エンドポイントに関する詳細情報を提供できます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>, (Todo todo) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/todos/{todo.Id}&amp;#34;&lt;/span>, todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.WithName(&lt;span style="color:#a5d6ff">&amp;#34;CreateTodo&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.WithDescription(&lt;span style="color:#a5d6ff">&amp;#34;Creates a new todo item&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.WithTags(&lt;span style="color:#a5d6ff">&amp;#34;Todos&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.Accepts&amp;lt;Todo&amp;gt;(&lt;span style="color:#a5d6ff">&amp;#34;application/json&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.Produces&amp;lt;Todo&amp;gt;(StatusCodes.Status201Created)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.Produces(StatusCodes.Status400BadRequest)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.ProducesValidationProblem();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="withopenapi-を使用したカスタマイズ">WithOpenApi を使用したカスタマイズ&lt;/h3>
&lt;p>生成された OpenAPI ドキュメントをきめ細かく制御するには:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/todos/{id:int}&amp;#34;&lt;/span>, (&lt;span style="color:#ff7b72">int&lt;/span> id) =&amp;gt; Results.Ok(&lt;span style="color:#ff7b72">new&lt;/span> Todo(id, &lt;span style="color:#a5d6ff">&amp;#34;Sample&amp;#34;&lt;/span>, &lt;span style="color:#79c0ff">false&lt;/span>)))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithOpenApi(operation =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> operation.Summary = &lt;span style="color:#a5d6ff">&amp;#34;Get a specific todo&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> operation.Description = &lt;span style="color:#a5d6ff">&amp;#34;Retrieves a single todo item by its unique identifier.&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> operation.Parameters[&lt;span style="color:#a5d6ff">0&lt;/span>].Description = &lt;span style="color:#a5d6ff">&amp;#34;The unique identifier of the todo item&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> operation;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これにより、コントローラーの Swashbuckle XML コメントで得られるのと同じレベルのドキュメント制御が提供されますが、より明示的な、コードファーストの方法で実現されます。&lt;/p>
&lt;h2 id="認証と認可">認証と認可&lt;/h2>
&lt;p>Minimal API エンドポイントのセキュリティは、ASP.NET Core の残りの部分と同じパターンに従います。適用方法が異なるだけです。&lt;/p>
&lt;h3 id="基本セットアップcsharp">基本セットアップ```csharp&lt;/h3>
&lt;p>var builder = WebApplication.CreateBuilder(args);&lt;/p>
&lt;p>builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =&amp;gt;
{
options.Authority = &amp;ldquo;&lt;a href="https://your-identity-provider.com">https://your-identity-provider.com&lt;/a>&amp;rdquo;;
options.Audience = &amp;ldquo;your-api&amp;rdquo;;
});&lt;/p>
&lt;p>builder.Services.AddAuthorizationBuilder()
.AddPolicy(&amp;ldquo;AdminOnly&amp;rdquo;, policy =&amp;gt; policy.RequireRole(&amp;ldquo;Admin&amp;rdquo;))
.AddPolicy(&amp;ldquo;PremiumUser&amp;rdquo;, policy =&amp;gt; policy.RequireClaim(&amp;ldquo;subscription&amp;rdquo;, &amp;ldquo;premium&amp;rdquo;));&lt;/p>
&lt;p>var app = builder.Build();&lt;/p>
&lt;p>app.UseAuthentication();
app.UseAuthorization();&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>### エンドポイントへの認可の適用
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>```csharp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>// Require any authenticated user
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&amp;#34;/profile&amp;#34;, (ClaimsPrincipal user) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> return Results.Ok(new
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = user.FindFirstValue(ClaimTypes.Name),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Email = user.FindFirstValue(ClaimTypes.Email)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}).RequireAuthorization();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>// Require a specific policy
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapDelete(&amp;#34;/admin/users/{id}&amp;#34;, (int id) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> // Delete user logic
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> return Results.NoContent();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}).RequireAuthorization(&amp;#34;AdminOnly&amp;#34;);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>// Allow anonymous access
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&amp;#34;/public/health&amp;#34;, () =&amp;gt; Results.Ok(new { Status = &amp;#34;Healthy&amp;#34; }))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AllowAnonymous();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>ClaimsPrincipal&lt;/code> をハンドラー パラメーターに直接挿入する方法に注目してください。残りはフレームワークが処理します。これは、Minimal API を非常にエレガントに感じさせる小さな点の 1 つです。&lt;/p>
&lt;h2 id="大規模な-api-の整理">大規模な API の整理&lt;/h2>
&lt;p>最小限の API を使用する部屋の象は組織です。 &lt;code>Program.cs&lt;/code> に 50 個のエンドポイントがある場合、混乱が生じます。ここでは、物事を管理しやすくするために私が使用しているパターンを示します。&lt;/p>
&lt;h3 id="ルートグループ">ルートグループ&lt;/h3>
&lt;p>ルート グループ (.NET 7 で導入) を使用すると、関連するエンドポイント間で構成を共有できます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> todos = app.MapGroup(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .WithTags(&lt;span style="color:#a5d6ff">&amp;#34;Todos&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .RequireAuthorization();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>todos.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>, (AppDbContext db) =&amp;gt; db.Todos.ToListAsync());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>todos.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/{id:int}&amp;#34;&lt;/span>, (&lt;span style="color:#ff7b72">int&lt;/span> id, AppDbContext db) =&amp;gt; db.Todos.FindAsync(id));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>todos.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>, (Todo todo, AppDbContext db) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> db.Todos.Add(todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> db.SaveChangesAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/todos/{todo.Id}&amp;#34;&lt;/span>, todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>グループ内のすべてのエンドポイントは、&lt;code>/todos&lt;/code> プレフィックス、&lt;code>Todos&lt;/code> タグ、および承認要件を共有します。クリーン。&lt;/p>
&lt;h3 id="拡張メソッド">拡張メソッド&lt;/h3>
&lt;p>これは本当にスケールするパターンです。エンドポイントの各グループを独自の静的クラスに移動します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Endpoints/TodoEndpoints.cs&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">TodoEndpoints&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> RouteGroupBuilder MapTodoEndpoints(&lt;span style="color:#ff7b72">this&lt;/span> WebApplication app)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> &lt;span style="color:#ff7b72">group&lt;/span> = app.MapGroup(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>).WithTags(&lt;span style="color:#a5d6ff">&amp;#34;Todos&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">group&lt;/span>.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>, GetAll);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">group&lt;/span>.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/{id:int}&amp;#34;&lt;/span>, GetById);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">group&lt;/span>.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>, Create);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">group&lt;/span>.MapPut(&lt;span style="color:#a5d6ff">&amp;#34;/{id:int}&amp;#34;&lt;/span>, Update);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">group&lt;/span>.MapDelete(&lt;span style="color:#a5d6ff">&amp;#34;/{id:int}&amp;#34;&lt;/span>, Delete);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">group&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;IResult&amp;gt; GetAll(AppDbContext db)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> =&amp;gt; Results.Ok(&lt;span style="color:#ff7b72">await&lt;/span> db.Todos.ToListAsync());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;IResult&amp;gt; GetById(&lt;span style="color:#ff7b72">int&lt;/span> id, AppDbContext db)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> =&amp;gt; &lt;span style="color:#ff7b72">await&lt;/span> db.Todos.FindAsync(id) &lt;span style="color:#ff7b72">is&lt;/span> { } todo
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ? Results.Ok(todo)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> : Results.NotFound();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;IResult&amp;gt; Create(Todo todo, AppDbContext db)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> db.Todos.Add(todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.SaveChangesAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/todos/{todo.Id}&amp;#34;&lt;/span>, todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;IResult&amp;gt; Update(&lt;span style="color:#ff7b72">int&lt;/span> id, Todo updated, AppDbContext db)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> todo = &lt;span style="color:#ff7b72">await&lt;/span> db.Todos.FindAsync(id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (todo &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>) &lt;span style="color:#ff7b72">return&lt;/span> Results.NotFound();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> todo.Title = updated.Title;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> todo.IsComplete = updated.IsComplete;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.SaveChangesAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Ok(todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;IResult&amp;gt; Delete(&lt;span style="color:#ff7b72">int&lt;/span> id, AppDbContext db)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> todo = &lt;span style="color:#ff7b72">await&lt;/span> db.Todos.FindAsync(id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (todo &lt;span style="color:#ff7b72">is&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>) &lt;span style="color:#ff7b72">return&lt;/span> Results.NotFound();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> db.Todos.Remove(todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.SaveChangesAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.NoContent();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Program.cs — stays clean&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> app = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapTodoEndpoints();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapUserEndpoints();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapOrderEndpoints();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.Run();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>Program.cs&lt;/code> は API の目次になります。各エンドポイント グループは独自のファイル内に存在します。これは、実稼働アプリケーションに推奨するアプローチです。&lt;/p>
&lt;h3 id="カーター図書館">カーター図書館&lt;/h3>
&lt;p>さらに構造を強化したい場合は、Carter ライブラリがモジュールベースのアプローチを提供します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">TodoModule&lt;/span> : ICarterModule
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> AddRoutes(IEndpointRouteBuilder app)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">async&lt;/span> (AppDbContext db) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.Todos.ToListAsync());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">async&lt;/span> (Todo todo, AppDbContext db) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> db.Todos.Add(todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.SaveChangesAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/todos/{todo.Id}&amp;#34;&lt;/span>, todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Carter はすべてのモジュールを自動的に検出して登録します。これは、生の Minimal API アプローチと完全なコントローラーの間の優れた中間点です。&lt;/p>
&lt;h2 id="typedresults-と応答タイプ">TypedResults と応答タイプ&lt;/h2>
&lt;p>.NET 7 以降では、タイプ セーフな応答に &lt;code>Results&lt;/code> の代わりに &lt;code>TypedResults&lt;/code> を使用できるようになりました。これは小さな変更のように思えるかもしれませんが、OpenAPI のドキュメントとテストのしやすさに大きなメリットをもたらします。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/todos/{id:int}&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;Results&amp;lt;Ok&amp;lt;Todo&amp;gt;, NotFound&amp;gt;&amp;gt; (&lt;span style="color:#ff7b72">int&lt;/span> id, AppDbContext db) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> todo = &lt;span style="color:#ff7b72">await&lt;/span> db.Todos.FindAsync(id);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> todo &lt;span style="color:#ff7b72">is&lt;/span> not &lt;span style="color:#79c0ff">null&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ? TypedResults.Ok(todo)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> : TypedResults.NotFound();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>戻り値の型 &lt;code>Results&amp;lt;Ok&amp;lt;Todo&amp;gt;, NotFound&amp;gt;&lt;/code> は、このエンドポイントがどの応答タイプを生成できるかをフレームワーク (および OpenAPI ドキュメント) に明示的に伝えます。基本的なケースに対して推測したり、手動で &lt;code>Produces&amp;lt;&amp;gt;()&lt;/code> を呼び出したりする必要はもうありません。&lt;/p>
&lt;p>複数の可能な結果の場合:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/todos&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;Results&amp;lt;Created&amp;lt;Todo&amp;gt;, ValidationProblem, Conflict&amp;gt;&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (Todo todo, AppDbContext db) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrEmpty(todo.Title))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> TypedResults.ValidationProblem(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">new&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>[]&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> { &lt;span style="color:#a5d6ff">&amp;#34;Title&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">new&lt;/span>[] { &lt;span style="color:#a5d6ff">&amp;#34;Title is required&amp;#34;&lt;/span> } }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">await&lt;/span> db.Todos.AnyAsync(t =&amp;gt; t.Title == todo.Title))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> TypedResults.Conflict();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> db.Todos.Add(todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.SaveChangesAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> TypedResults.Created(&lt;span style="color:#a5d6ff">$&amp;#34;/todos/{todo.Id}&amp;#34;&lt;/span>, todo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>すべての新しいプロジェクトで &lt;code>TypedResults&lt;/code> を使用し始めました。コンパイラは、宣言された戻り値の型と実際に返される値の間の不一致を検出するため、実行時の驚くべき事態がクラス全体で排除されます。&lt;/p>
&lt;h2 id="パフォーマンスに関する考慮事項">パフォーマンスに関する考慮事項&lt;/h2>
&lt;p>Minimal API のセールス ポイントの 1 つはパフォーマンスであり、Minimal API が高速である「理由」を理解することは価値があります。&lt;/p>
&lt;p>&lt;strong>起動時のオーバーヘッドの削減。&lt;/strong> コントローラーは、エンドポイントの検出、モデルのバインド、フィルターの適用をリフレクションに大きく依存します。最小限の API は、ソース ジェネレーター (.NET 7 以降) を使用して、コンパイル時にバインディング コードを生成します。これは、起動時の作業が減り、リクエストごとのメモリ割り当てが少なくなることを意味します。&lt;/p>
&lt;p>&lt;strong>MVC パイプラインなし。&lt;/strong> コントローラーベースの API は、アクションの選択、モデル バインディング、アクション フィルター、結果の実行など、完全な MVC パイプラインを通過します。最小限の API はこれらすべてをスキップし、ルーティングからハンドラーに直接進みます。&lt;/p>
&lt;p>&lt;strong>RequestDelegate のコンパイル。&lt;/strong> フレームワークは、ラムダ式を最適化された &lt;code>RequestDelegate&lt;/code> インスタンスにコンパイルします。結果として得られるコードは、&lt;code>HttpContext&lt;/code> を直接操作する場合に手動で記述するコードに非常に似ています。&lt;/p>
&lt;p>パフォーマンスを最大化するための実践的なヒントをいくつか紹介します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Use AsNoTracking for read-only queries&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/products&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">async&lt;/span> (AppDbContext db) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.Products.AsNoTracking().ToListAsync());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Return results directly — avoid unnecessary allocations&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/count&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">async&lt;/span> (AppDbContext db) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.Products.CountAsync());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Use cancellation tokens for long-running operations&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapGet(&lt;span style="color:#a5d6ff">&amp;#34;/report&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">async&lt;/span> (AppDbContext db, CancellationToken ct) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> db.Orders
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AsNoTracking()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .GroupBy(o =&amp;gt; o.Status)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .Select(g =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span> { Status = g.Key, Count = g.Count() })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToListAsync(ct));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```また、コントローラーと&lt;/span> Minimal API &lt;span style="color:#f85149">の間のパフォーマンスのギャップは、&lt;/span>.NET &lt;span style="color:#f85149">のリリースごとに縮小し続けていることにも言及する価値があります。ほとんどのアプリケーションでは、その違いはボトルネックにはなりません。データベース&lt;/span> &lt;span style="color:#f85149">クエリと外部サービス呼び出しがボトルネックになります。ベンチマークではなく、開発者の経験とプロジェクトのニーズに基づいて選択してください。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">##&lt;/span> &lt;span style="color:#f85149">結論&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">最小限の&lt;/span> API &lt;span style="color:#f85149">は、&lt;/span>.NET &lt;span style="color:#a5d6ff">6&lt;/span> &lt;span style="color:#f85149">で導入されて以来、長い道のりを歩んできました。「&lt;/span>Hello World&lt;span style="color:#f85149">」デモ機能として始まった機能は、運用&lt;/span> API &lt;span style="color:#f85149">の正当な選択肢に成長しました。エンドポイント&lt;/span> &lt;span style="color:#f85149">フィルター、ルート&lt;/span> &lt;span style="color:#f85149">グループ、型指定された結果、および堅牢な&lt;/span> OpenAPI &lt;span style="color:#f85149">サポートにより、適切に構造化された保守可能なサービスを構築するために必要なものがすべて揃っています。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">私のおすすめは？新しい&lt;/span> API &lt;span style="color:#f85149">プロジェクト、特にマイクロサービスや焦点を絞った内部&lt;/span> API &lt;span style="color:#f85149">を開始する場合は、&lt;/span>Minimal API &lt;span style="color:#f85149">を真剣に試してみてください。編成には拡張メソッド&lt;/span> &lt;span style="color:#f85149">パターンを使用し、横断的な問題にはエンドポイント&lt;/span> &lt;span style="color:#f85149">フィルターを利用し、型安全性には&lt;/span> &lt;span style="color:#f85149">`&lt;/span>TypedResults&lt;span style="color:#f85149">`&lt;/span> &lt;span style="color:#f85149">を活用します。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">既存のコントローラーベースのプロジェクトの場合、急いで移行する必要はありません。どちらのアプローチもうまく機能し、並べて使用することもできます。ただし、次回小さなサービスや簡単な内部&lt;/span> API &lt;span style="color:#f85149">を追加する必要がある場合は、コントローラーをスキップして最小限に抑えてください。もう戻れないかもしれない。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">コーディングを楽しんでください&lt;/span>!
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></content:encoded><category>.NET</category><category>API</category><category>Web Development</category></item><item><title>Blazor コンポーネントで分離 CSS を使用する</title><link>https://emimontesdeoca.github.io/ja/posts/blazor-isolated-css/</link><pubDate>Wed, 12 Mar 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/blazor-isolated-css/</guid><description>CSS 分離を使用して CSS スタイルの範囲を個々の Blazor コンポーネントに設定し、グローバルなスタイルの競合を回避します。</description><content:encoded>&lt;p>Blazor を使用するときに常に気になることの 1 つは、コンポーネント間で誤ってスタイルを壊すことが非常に簡単であるということでした。あるコンポーネントにクラスを追加すると、まったく別のページにある別のものが突然表示されなくなってしまいます。 Blazor には、これに対する CSS 分離というソリューションが組み込まれていることがわかりました。&lt;/p>
&lt;h1 id="css-分離とは何ですか">CSS 分離とは何ですか?&lt;/h1>
&lt;p>CSS を分離すると、スタイルの範囲を特定のコンポーネントに限定できます。 Blazor は、ビルド時にコンポーネントごとに一意の属性を生成し、それを CSS セレクターに追加することでこれを実現します。したがって、スタイルは、そのスタイルが属するコンポーネントにのみ適用されます。&lt;/p>
&lt;p>BEM の命名や、特異性に関するクレイジーなハックは必要ありません。クリーンでスコープ指定された CSS だけです。&lt;/p>
&lt;h1 id="使い方">使い方&lt;/h1>
&lt;p>&lt;code>Card.razor&lt;/code> というコンポーネントがあるとします。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#7ee787">div&lt;/span> class&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#7ee787">h3&lt;/span> class&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card-title&amp;#34;&lt;/span>&amp;gt;@Title&amp;lt;/&lt;span style="color:#7ee787">h3&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#7ee787">p&lt;/span> class&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card-body&amp;#34;&lt;/span>&amp;gt;@ChildContent&amp;lt;/&lt;span style="color:#7ee787">p&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/&lt;span style="color:#7ee787">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> public string Title { get; set; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> public RenderFragment ChildContent { get; set; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>分離スタイルを追加するには、同じ名前で拡張子が &lt;code>.razor.css&lt;/code> のファイルを作成するだけです。この場合: &lt;code>Card.razor.css&lt;/code>。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>.&lt;span style="color:#f0883e;font-weight:bold">card&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">border&lt;/span>: &lt;span style="color:#a5d6ff">1&lt;/span>&lt;span style="color:#ff7b72">px&lt;/span> &lt;span style="color:#79c0ff">solid&lt;/span> &lt;span style="color:#d2a8ff;font-weight:bold">var&lt;/span>(&lt;span style="color:#ff7b72;font-weight:bold">--&lt;/span>&lt;span style="color:#79c0ff">color&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>border);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">border-radius&lt;/span>: &lt;span style="color:#a5d6ff">8&lt;/span>&lt;span style="color:#ff7b72">px&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">padding&lt;/span>: &lt;span style="color:#a5d6ff">1&lt;/span>&lt;span style="color:#ff7b72">rem&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">background&lt;/span>: &lt;span style="color:#d2a8ff;font-weight:bold">var&lt;/span>(&lt;span style="color:#ff7b72;font-weight:bold">--&lt;/span>&lt;span style="color:#79c0ff">color&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>bg&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>secondary);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.&lt;span style="color:#f0883e;font-weight:bold">card-title&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">font-size&lt;/span>: &lt;span style="color:#a5d6ff">1.1&lt;/span>&lt;span style="color:#ff7b72">rem&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">font-weight&lt;/span>: &lt;span style="color:#a5d6ff">600&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">margin-bottom&lt;/span>: &lt;span style="color:#a5d6ff">0.5&lt;/span>&lt;span style="color:#ff7b72">rem&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.&lt;span style="color:#f0883e;font-weight:bold">card-body&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">color&lt;/span>: &lt;span style="color:#d2a8ff;font-weight:bold">var&lt;/span>(&lt;span style="color:#ff7b72;font-weight:bold">--&lt;/span>&lt;span style="color:#79c0ff">color&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>&lt;span style="color:#79c0ff">text&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>secondary);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">font-size&lt;/span>: &lt;span style="color:#a5d6ff">0.9&lt;/span>&lt;span style="color:#ff7b72">rem&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>それで終わりです！ Blazor は自動的にそれを選択し、それらのスタイルのスコープを &lt;code>Card&lt;/code> コンポーネントのみに限定します。別の場所に別の &lt;code>.card&lt;/code> クラスがある場合、それは影響を受けません。&lt;/p>
&lt;h1 id="内部での仕組み">内部での仕組み&lt;/h1>
&lt;p>プロジェクトをビルドすると、Blazor によって HTML と CSS が書き換えられます。コンポーネントは &lt;code>b-3x8qz7k2f1&lt;/code> のような一意の属性を取得し、CSS セレクターはその属性を追加します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>.&lt;span style="color:#f0883e;font-weight:bold">card&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">[&lt;/span>&lt;span style="color:#7ee787">b-3x8qz7k2f1&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">]&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">border&lt;/span>: &lt;span style="color:#a5d6ff">1&lt;/span>&lt;span style="color:#ff7b72">px&lt;/span> &lt;span style="color:#79c0ff">solid&lt;/span> &lt;span style="color:#d2a8ff;font-weight:bold">var&lt;/span>(&lt;span style="color:#ff7b72;font-weight:bold">--&lt;/span>&lt;span style="color:#79c0ff">color&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>border);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">/* ... */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>生成された CSS バンドルは &lt;code>{ProjectName}.styles.css&lt;/code> として提供されます。 &lt;code>index.html&lt;/code> または &lt;code>_Host.cshtml&lt;/code> にこれがあることを確認してください:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#7ee787">link&lt;/span> href&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;YourApp.styles.css&amp;#34;&lt;/span> rel&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;stylesheet&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>このリンクが見つからないと、個別のスタイルが表示されず、何が起こっているのか疑問に思って 30 分を費やすことになります。信じてください、私はそこに行ったことがあります。&lt;/p>
&lt;h1 id="子要素をターゲットにする">子要素をターゲットにする&lt;/h1>
&lt;p>留意すべき点の 1 つは、デフォルトでは、分離 CSS は現在のコンポーネントの要素にのみ適用されるということです。子コンポーネント内の要素のスタイルを設定したい場合は、 &lt;code>::deep&lt;/code> コンビネータが必要です。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>::&lt;span style="color:#d2a8ff;font-weight:bold">deep&lt;/span> .&lt;span style="color:#f0883e;font-weight:bold">child-element&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">color&lt;/span>: &lt;span style="color:#79c0ff">red&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これにより、Blazor は子コンポーネント内の一致する要素にもスタイルを適用するように指示されます。子をスタイルする必要があるラッパー コンポーネントがある場合に非常に便利です。&lt;/p>
&lt;p>#いつ使用するか&lt;/p>
&lt;p>私は現在、ほぼすべてのコンポーネントに CSS 分離を使用しています。物事をクリーンかつ予測可能な状態に保ちます。私がこれを使用しないのは、リセット、タイポグラフィー、テーマ変数などの真にグローバルなスタイルの場合だけです。これらは共有 CSS ファイルに入れられます。&lt;/p>
&lt;p>投稿が気に入っていただければ幸いです!ソーシャルメディアで**@emimontesdeoca**までお気軽にご連絡ください。&lt;/p>
&lt;h1 id="リソース">リソース&lt;/h1>
&lt;ul>
&lt;li>&lt;a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/components/css-isolation">ASP.NET Core Blazor CSS 分離&lt;/a>&lt;/li>
&lt;/ul></content:encoded><category>.NET</category><category>Blazor</category><category>CSS</category></item><item><title>セマンティック カーネルでクリスマスの支出をコントロールする</title><link>https://emimontesdeoca.github.io/ja/posts/keeping-christmas-spending-with-semantic-kernel/</link><pubDate>Sat, 28 Dec 2024 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/keeping-christmas-spending-with-semantic-kernel/</guid><description>Semantic Kernel、Azure OpenAI、Blazor を使用して領収書を分析し、クリスマスの支出を追跡します。</description><content:encoded>&lt;h2 id="はじめに">はじめに&lt;/h2>
&lt;p>ホリデーシーズンが近づくと、特にショッピングやギフトの購入が増えるため、出費の管理が課題になることがあります。このブログ投稿では、人工知能を活用して、.NET テクノロジを使用してクリスマスの支出を追跡する方法を検討します。セマンティック カーネルと AI の力でレシートを分析することで、店名、日付、商品リスト、合計金額などの重要な詳細を効率的に抽出できます。このソリューションを使用すると、クリスマスの支出を簡単に監視および管理できるため、領収書を手動で確認する手間をかけずに予算を確実に把握できます。&lt;/p>
&lt;h2 id="calendario-de-adviento-de-inteligencia-artificial-2024-en-español">Calendario de Adviento de Inteligencia Artificial 2024 en Español&lt;/h2>
&lt;p align="center">
&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsdkovc8lzahgm8pvsblk.png"/>
&lt;/p>
&lt;p>このプロジェクトは、AI に特化したオンライン イベントである &lt;strong>Calendario de Adviento de Inteligencia Artificial 2024 en Español&lt;/strong> への参加からインスピレーションを受けました。イベントの詳細については、&lt;a href="https://dev.to/roberto_navarro_mate/calendario-de-adviento-de-inteligencia-artificial-2024-en-espanol-bdb">この Dev.to リンク&lt;/a> でご覧いただけます。&lt;/p>
&lt;h2 id="プロジェクト">プロジェクト&lt;/h2>
&lt;p>このプロジェクトでは、GPT-4 などの強力な AI モデルを利用して画像を処理および分析できるサービスである &lt;strong>Azure OpenAI&lt;/strong> を使用します。このプロセスには、バックエンド API サービスのセットアップから画像アップロードのための Blazor フロントエンドとの統合まで、いくつかの手順が含まれます。また、すべてをシームレスに接続するのに役立つコンポーネントである &lt;strong>.NET Aspire&lt;/strong> も使用します。&lt;/p>
&lt;h2 id="前提条件">前提条件&lt;/h2>
&lt;p>コードに入る前に、次の前提条件を満たしていることを確認してください。&lt;/p>
&lt;ul>
&lt;li>.NET9&lt;/li>
&lt;li>Azure OpenAI アクセス (API キー)&lt;/li>
&lt;li>Visual Studio または Visual Studio Code&lt;/li>
&lt;li>Blazor、HTTP クライアント、API 開発の基本的な知識&lt;/li>
&lt;/ul>
&lt;h2 id="visual-studio-ソリューション">Visual Studio ソリューション&lt;/h2>
&lt;p>最終的には次のようなものになります。私は物事を分けてクールな名前を付けるのが好きなので、次のようになります。&lt;/p>
&lt;p align="center">
&lt;img src="https://imgur.com/gAnGLhM.png">
&lt;/p>
&lt;p>でも、ステップバイステップで物を作っていきましょう！&lt;/p>
&lt;h2 id="ステップ-0-モデル">ステップ 0: モデル&lt;/h2>
&lt;p>Receipt Scanner アプリケーションの中核は、フロントエンド、API、AI サービス間の対話を促進するいくつかの主要なモデルに依存しています。このプロジェクトで使用される主なモデルは次のとおりです。&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>受信リクエストの分析&lt;/strong>&lt;br>
このモデルは、レシートを分析するためのリクエスト構造を表します。これには、処理されるレシート画像のバイト配列を保持する &lt;code>ImageBytes&lt;/code> プロパティが含まれています。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">AnalyzeReceiptRequest&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">byte&lt;/span>[] ImageBytes { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>受信分析結果&lt;/strong>&lt;br>
このモデルは、レシートを処理した後の結果をキャプチャします。レシートから抽出された店舗名、日付、品目、合計金額などの構造化データが保持されます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ReceiptAnalyzeResult&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> DateTime CreatedAt { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ReceiptData Result { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>領収書データ&lt;/strong>&lt;br>
構造化されたレシートデータを保持するモデルです。これには、店名、日付、アイテムのリスト (各アイテムの名前と価格)、レシートの合計金額のプロパティが含まれます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ReceiptData&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Store { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> DateTime? Date { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;ReceiptItem&amp;gt; Items { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">decimal?&lt;/span> Total { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;li>
&lt;p>&lt;strong>受信アイテム&lt;/strong>&lt;br>
領収書の各項目はこのモデルで表されます。アイテム名とその価格が保持されます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ReceiptItem&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Name { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">decimal?&lt;/span> Price { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```これらのモデルは、クライアントとサーバー間でデータを受け渡すための基盤として機能し、情報のスムーズな流れを保証します。&lt;/span> API &lt;span style="color:#f85149">はレシート画像を受け取り、その代わりに、フロントエンドで簡単に使用できる構造化された&lt;/span> JSON &lt;span style="color:#f85149">オブジェクトを処理して返します。&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/li>
&lt;/ul>
&lt;h2 id="ステップ-1-バックエンド-api-サービスのセットアップ">ステップ 1: バックエンド API サービスのセットアップ&lt;/h2>
&lt;p>このアプリケーションを構築する最初のステップは、レシート画像を分析するための API サービスを設定することです。 &lt;strong>Azure OpenAI&lt;/strong> API を使用して、レシート画像から情報を抽出します。すべてがどのように組み合わされるかは次のとおりです。&lt;/p>
&lt;h3 id="ai-サービス---詳しい説明">AI サービス - 詳しい説明&lt;/h3>
&lt;p>AIサービスはレシート分析システムの中核です。 Azure OpenAI の API と通信して画像データを処理し、意味のある洞察を返します。 &lt;strong>AiApiClient&lt;/strong> クラスは、Azure OpenAI API とのすべての対話を処理するクライアントです。&lt;/p>
&lt;h4 id="ai-クライアントの実装">AI クライアントの実装&lt;/h4>
&lt;p>&lt;code>AiApiClient&lt;/code> は、レシート画像 (バイト配列形式) を Azure OpenAI API に送信する重要なコンポーネントです。通信、エラーログ、データ解析を処理します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">AiApiClient&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> HttpClient _httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> ILogger&amp;lt;AiApiClient&amp;gt; _logger;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> AiApiClient(HttpClient httpClient, ILogger&amp;lt;AiApiClient&amp;gt; logger)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _httpClient = httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _logger = logger;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;ReceiptAnalyzeResult?&amp;gt; AnalyzeAsync(&lt;span style="color:#ff7b72">byte&lt;/span>[] imageBytes, CancellationToken cancellationToken = &lt;span style="color:#ff7b72">default&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (imageBytes == &lt;span style="color:#79c0ff">null&lt;/span> || imageBytes.Length == &lt;span style="color:#a5d6ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _logger.LogWarning(&lt;span style="color:#a5d6ff">&amp;#34;ImageBytes is null or empty.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _logger.LogInformation(&lt;span style="color:#a5d6ff">&amp;#34;Sending analyze request with image bytes of length: {Length}&amp;#34;&lt;/span>, imageBytes.Length);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> request = &lt;span style="color:#ff7b72">new&lt;/span> AnalyzeReceiptRequest
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ImageBytes = imageBytes
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> _httpClient.PostAsJsonAsync(&lt;span style="color:#a5d6ff">&amp;#34;/analyze-receipt&amp;#34;&lt;/span>, request, cancellationToken);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!response.IsSuccessStatusCode)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _logger.LogWarning(&lt;span style="color:#a5d6ff">&amp;#34;Failed to analyze receipt. StatusCode: {StatusCode}&amp;#34;&lt;/span>, response.StatusCode);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> analyzeResult = &lt;span style="color:#ff7b72">await&lt;/span> response.Content.ReadFromJsonAsync&amp;lt;ReceiptAnalyzeResult&amp;gt;(cancellationToken: cancellationToken);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (analyzeResult == &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _logger.LogWarning(&lt;span style="color:#a5d6ff">&amp;#34;No content received from AI API service.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _logger.LogInformation(&lt;span style="color:#a5d6ff">&amp;#34;Analysis result received: {AnalyzeResult}&amp;#34;&lt;/span>, analyzeResult);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> analyzeResult;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception ex)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _logger.LogError(ex, &lt;span style="color:#a5d6ff">&amp;#34;An error occurred while analyzing the receipt.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#79c0ff">null&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>コードのこの部分では、次の役割を担う &lt;code>AnalyzeAsync&lt;/code> メソッドを定義します。&lt;/p>
&lt;ol>
&lt;li>画像のバイト配列を Azure OpenAI API に送信します。&lt;/li>
&lt;li>API からのエラーまたは失敗した応答の処理。&lt;/li>
&lt;li>返された JSON データを構造化された結果 (&lt;code>ReceiptAnalyzeResult&lt;/code>) に解析します。&lt;/li>
&lt;/ol>
&lt;p>この機能を専用サービス (AiApiClient) に分離することの利点は次のとおりです。&lt;/p>
&lt;ul>
&lt;li>&lt;strong>エラー処理:&lt;/strong> ネットワークの問題や無効な応答などのエラーを一元的に処理します。&lt;/li>
&lt;li>&lt;strong>ログ:&lt;/strong> システムの動作を監視するためのリクエストと応答の適切なログ。&lt;/li>
&lt;/ul>
&lt;p align="center">
&lt;img src="https://imgur.com/q3EpCSy.png"/>
&lt;/p>
&lt;h3 id="api-サービス---リクエストとレスポンスの処理">API サービス - リクエストとレスポンスの処理&lt;/h3>
&lt;p>&lt;strong>API サービス&lt;/strong> は、フロントエンド Blazor アプリケーションと AI サービスの間の仲介者として機能します。このサービスは、画像データを受け取り、AI サービスに渡し、分析結果をクライアントに返す役割を果たします。&lt;/p>
&lt;h4 id="api-エンドポイント">API エンドポイント&lt;/h4>
&lt;p>このステップでは、レシート画像を受け入れ、それを AI サービスに転送して処理し、結果をクライアントに返すための単純な API エンドポイントを定義します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">ReceiptScanner.Shared.Clients&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">ReceiptScanner.Shared.Models&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Add service defaults &amp;amp; Aspire client integrations.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.AddServiceDefaults();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Add services to the container.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddProblemDetails();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Register AiApiClient with HttpClient&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddHttpClient&amp;lt;AiApiClient&amp;gt;(client =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client.BaseAddress = &lt;span style="color:#ff7b72">new&lt;/span> Uri(&lt;span style="color:#a5d6ff">&amp;#34;https+http://aiservice&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddOpenApi();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> app = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Configure the HTTP request pipeline.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseExceptionHandler();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">if&lt;/span> (app.Environment.IsDevelopment())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> app.MapOpenApi();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// POST endpoint to analyze receipt&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapPost(&lt;span style="color:#a5d6ff">&amp;#34;/api/analyze-receipt&amp;#34;&lt;/span>, &lt;span style="color:#ff7b72">async&lt;/span> (AnalyzeReceiptRequest request, AiApiClient aiApiClient, ILogger&amp;lt;Program&amp;gt; logger) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (request.ImageBytes == &lt;span style="color:#79c0ff">null&lt;/span> || request.ImageBytes.Length == &lt;span style="color:#a5d6ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logger.LogWarning(&lt;span style="color:#a5d6ff">&amp;#34;ImageBytes is null or empty.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.BadRequest(&lt;span style="color:#a5d6ff">&amp;#34;ImageBytes is required.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logger.LogInformation(&lt;span style="color:#a5d6ff">&amp;#34;Received analyze receipt request with image bytes of length: {Length}&amp;#34;&lt;/span>, request.ImageBytes.Length);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> result = &lt;span style="color:#ff7b72">await&lt;/span> aiApiClient.AnalyzeAsync(request.ImageBytes);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (result == &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logger.LogWarning(&lt;span style="color:#a5d6ff">&amp;#34;Failed to analyze the receipt.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Problem(&lt;span style="color:#a5d6ff">&amp;#34;Unable to process the receipt at this time. Please try again later.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logger.LogInformation(&lt;span style="color:#a5d6ff">&amp;#34;Analysis completed successfully. Result: {Result}&amp;#34;&lt;/span>, result);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Ok(result);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception ex)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> logger.LogError(ex, &lt;span style="color:#a5d6ff">&amp;#34;An error occurred while processing the receipt.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Results.Problem(&lt;span style="color:#a5d6ff">&amp;#34;An error occurred while processing the receipt. Please try again later.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapDefaultEndpoints();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.Run();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>このエンドポイント:&lt;/p>
&lt;ol>
&lt;li>リクエスト本文の一部としてレシート画像を受け入れます。&lt;/li>
&lt;li>&lt;code>AiService&lt;/code> endopint メソッドを呼び出して、処理のために画像を Azure OpenAI に送信します。&lt;/li>
&lt;li>分析結果をクライアントに返します。&lt;/li>
&lt;/ol>
&lt;p align="center">
&lt;img src="https://imgur.com/u9mQrpq.png"/>
&lt;/p>
&lt;h2 id="ステップ-2-blazor-フロントエンドのセットアップ">ステップ 2: Blazor フロントエンドのセットアップ&lt;/h2>
&lt;p>バックエンドのセットアップが完了したので、&lt;strong>Blazor フロントエンド&lt;/strong>に注目してみましょう。ここで、ユーザーは分析のためにレシート画像をアップロードし、結果を確認できます。&lt;/p>
&lt;h3 id="blazor-ページの実装">Blazor ページの実装&lt;/h3>
&lt;p>Blazor ページには、ユーザーが複数のレシート画像をアップロードして、表に表示される分析結果を確認できるシンプルなインターフェイスが提供されます。ページのコードは次のとおりです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>page &lt;span style="color:#a5d6ff">&amp;#34;/analyzer&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>using ReceiptScanner&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Shared&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Clients
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>using ReceiptScanner&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Shared&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Models
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>using System&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Globalization
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>inject ApiServiceClient ApiClient
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>inject ILogger&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>Program&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> Logger
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>attribute [StreamRendering]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>rendermode InteractiveServer
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>PageTitle&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Receipt Analyzer&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>PageTitle&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h1 &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;text-center my-4&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Receipt Analyzer&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h1&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;container&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;lead text-center mb-4&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Upload receipt images below to extract their data&lt;span style="color:#ff7b72;font-weight:bold">.&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;!--&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">File&lt;/span> Upload Section &lt;span style="color:#ff7b72;font-weight:bold">--&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card mb-4&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card-body&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>InputFile OnChange&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;HandleFileSelected&amp;#34;&lt;/span> multiple &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;form-control mb-3&amp;#34;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>button &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;btn btn-primary w-100&amp;#34;&lt;/span> &lt;span style="color:#f85149">@&lt;/span>onclick&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;ProcessReceipts&amp;#34;&lt;/span> disabled&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;@(!hasFiles)&amp;#34;&lt;/span> type&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;button&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>span &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;@(!processing ? &amp;#34;&lt;/span>d&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>none&lt;span style="color:#a5d6ff">&amp;#34; : &amp;#34;&amp;#34;) spinner-border spinner-border-sm&amp;#34;&lt;/span> role&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;status&amp;#34;&lt;/span> aria&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>hidden&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;true&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&amp;lt;/&lt;/span>span&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (processing)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>span&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Processing&lt;span style="color:#ff7b72;font-weight:bold">...&amp;lt;/&lt;/span>span&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>span&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Process Receipts&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>span&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>button&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;!--&lt;/span> Uploaded Images Preview &lt;span style="color:#ff7b72;font-weight:bold">--&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (fileBytesList&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Any())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card mb-4&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card-header&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h5 &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;mb-0&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Uploaded Receipt Images&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h5&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card-body&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;row&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>foreach (&lt;span style="color:#ff7b72">var&lt;/span> fileBytes &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> fileBytesList)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;col-12 col-md-4 mb-3&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>img src&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;@($&amp;#34;&lt;/span>data:image&lt;span style="color:#ff7b72;font-weight:bold">/&lt;/span>jpeg;base64,{Convert&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToBase64String(fileBytes)}&lt;span style="color:#a5d6ff">&amp;#34;)&amp;#34;&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;img-fluid rounded&amp;#34;&lt;/span> alt&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;Uploaded receipt&amp;#34;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;!--&lt;/span> Processing Indicator &lt;span style="color:#ff7b72;font-weight:bold">--&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (processing)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;alert alert-info text-center&amp;#34;&lt;/span> role&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;alert&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>strong&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Processing receipts&lt;span style="color:#ff7b72;font-weight:bold">...&amp;lt;/&lt;/span>strong&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> Please wait &lt;span style="color:#ff7b72">while&lt;/span> we analyze the uploaded files&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;!--&lt;/span> Analysis Results Section &lt;span style="color:#ff7b72;font-weight:bold">--&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (analyzedReceipts &lt;span style="color:#ff7b72;font-weight:bold">!=&lt;/span> null &lt;span style="color:#ff7b72;font-weight:bold">&amp;amp;&amp;amp;&lt;/span> analyzedReceipts&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Any())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card-header&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h5 &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;mb-0&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Analysis Results&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h5&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;card-body&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>table &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;table table-striped table-bordered&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>thead&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Store&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Date&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Total&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Items&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>thead&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tbody&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>foreach (&lt;span style="color:#ff7b72">var&lt;/span> receipt &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> analyzedReceipts)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>(receipt&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Result&lt;span style="color:#f85149">?&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Store &lt;span style="color:#f85149">??&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;Unknown&amp;#34;&lt;/span>)&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>(receipt&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Result&lt;span style="color:#f85149">?&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Date&lt;span style="color:#f85149">?&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToString() &lt;span style="color:#f85149">??&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;Unknown&amp;#34;&lt;/span>)&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>(receipt&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Result&lt;span style="color:#f85149">?&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Total&lt;span style="color:#f85149">?&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToString(&lt;span style="color:#a5d6ff">&amp;#34;C&amp;#34;&lt;/span>, ci) &lt;span style="color:#f85149">??&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;Unknown&amp;#34;&lt;/span>)&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>ul &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;list-unstyled&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (receipt&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Result&lt;span style="color:#f85149">?&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Items is &lt;span style="color:#ff7b72;font-weight:bold">not&lt;/span> null)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>foreach (&lt;span style="color:#ff7b72">var&lt;/span> item &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> receipt&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Result&lt;span style="color:#f85149">?&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Items&lt;span style="color:#ff7b72;font-weight:bold">!&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>li&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&amp;lt;&lt;/span>strong&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>item&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Name&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>strong&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span> &lt;span style="color:#f85149">@&lt;/span>item&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Price&lt;span style="color:#f85149">?&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToString(&lt;span style="color:#a5d6ff">&amp;#34;C&amp;#34;&lt;/span>, ci)&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>li&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>ul&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tbody&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>table&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span> &lt;span style="color:#ff7b72">if&lt;/span> (processed &lt;span style="color:#ff7b72;font-weight:bold">&amp;amp;&amp;amp;&lt;/span> (analyzedReceipts &lt;span style="color:#ff7b72;font-weight:bold">==&lt;/span> null &lt;span style="color:#ff7b72;font-weight:bold">||&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">!&lt;/span>analyzedReceipts&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Any()))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>div &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;alert alert-warning text-center&amp;#34;&lt;/span> role&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;alert&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>strong&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>No results found&lt;span style="color:#ff7b72;font-weight:bold">.&amp;lt;/&lt;/span>strong&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> Please try again with different images &lt;span style="color:#ff7b72;font-weight:bold">or&lt;/span> ensure they are clear &lt;span style="color:#ff7b72;font-weight:bold">and&lt;/span> legible&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>div&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private &lt;span style="color:#f0883e;font-weight:bold">bool&lt;/span> hasFiles;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private &lt;span style="color:#f0883e;font-weight:bold">bool&lt;/span> processing;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private &lt;span style="color:#f0883e;font-weight:bold">bool&lt;/span> processed;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>byte[]&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> fileBytesList &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> new();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>ReceiptAnalyzeResult&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> analyzedReceipts &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> new();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CultureInfo ci &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> new CultureInfo(&lt;span style="color:#a5d6ff">&amp;#34;es-es&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private async Task HandleFileSelected(InputFileChangeEventArgs e)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> try
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fileBytesList&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Clear();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> foreach (&lt;span style="color:#ff7b72">var&lt;/span> file &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> e&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetMultipleFiles())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> memoryStream &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> new MemoryStream();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> await file&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>OpenReadStream()&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>CopyToAsync(memoryStream);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> fileBytesList&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Add(memoryStream&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>ToArray());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> hasFiles &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> fileBytesList&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Any();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> catch (Exception ex)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Logger&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>LogError(ex, &lt;span style="color:#a5d6ff">&amp;#34;Error while handling file upload.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private async Task ProcessReceipts()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72;font-weight:bold">!&lt;/span>hasFiles)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> processing &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> true;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> analyzedReceipts&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Clear();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> try
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> foreach (&lt;span style="color:#ff7b72">var&lt;/span> fileBytes &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> fileBytesList)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> result &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await ApiClient&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>AnalyzeReceiptAsync(fileBytes);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (result &lt;span style="color:#ff7b72;font-weight:bold">!=&lt;/span> null)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> analyzedReceipts&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Add(result);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> catch (Exception ex)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Logger&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>LogError(ex, &lt;span style="color:#a5d6ff">&amp;#34;Error while processing receipts.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> finally
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> processing &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> false;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> processed &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> true;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```このページでは、レシートをアップロードし、分析結果を店名、日付、合計金額、商品リストの表で表示します。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p align&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;center&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>img src&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;https://imgur.com/BLswKhm.png&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">## ステップ 3: .NET Aspire&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p align&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;center&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>img src&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;https://imgur.com/ja56RWN.png&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">### .NET Aspire とは何ですか?&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>NET Aspire &lt;span style="color:#f85149">は、監視可能な実稼働対応アプリを構築するための強力なツール、テンプレート、パッケージのセットです。&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>NET Aspire &lt;span style="color:#f85149">は、特定のクラウドネイティブの問題を処理する&lt;/span> NuGet &lt;span style="color:#f85149">パッケージのコレクションを通じて提供されます。クラウド&lt;/span> &lt;span style="color:#f85149">ネイティブ&lt;/span> &lt;span style="color:#f85149">アプリは、多くの場合、単一のモノリシック&lt;/span> &lt;span style="color:#f85149">コード&lt;/span> &lt;span style="color:#f85149">ベースではなく、相互接続された小さな部分またはマイクロサービスで構成されます。クラウドネイティブ&lt;/span> &lt;span style="color:#f85149">アプリは通常、データベース、メッセージング、キャッシュなどの多数のサービスを消費します。サポートの詳細については、&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>NET Aspire &lt;span style="color:#f85149">サポート&lt;/span> &lt;span style="color:#f85149">ポリシーを参照してください。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">分散アプリケーションは、異なるホスト上で実行されるコンテナなど、複数のノードにわたって計算リソースを使用するアプリケーションです。このようなノードは、ユーザーに応答を配信するためにネットワーク境界を越えて通信する必要があります。クラウド&lt;/span> &lt;span style="color:#f85149">ネイティブ&lt;/span> &lt;span style="color:#f85149">アプリは、クラウド&lt;/span> &lt;span style="color:#f85149">インフラストラクチャのスケーラビリティ、復元力、管理容易性を最大限に活用する、特定の種類の分散アプリです。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">このプロジェクトで&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">**.&lt;/span>NET Aspire&lt;span style="color:#ff7b72;font-weight:bold">**&lt;/span> &lt;span style="color:#f85149">を使用すると、システム全体の品質が向上する次のようないくつかの利点が得られます。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">### 1. **集中ログ**&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>NET Aspire &lt;span style="color:#f85149">は、アプリケーション全体のログを自動的に統合します。つまり、サービスごとにログを手動で構成する必要はありません。これにより、ログの一貫性が確保され、一元的な場所に保存されるため、デバッグと監視がはるかに簡単になります。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">たとえば、`&lt;/span>AiApiClient&lt;span style="color:#f85149">`&lt;/span> &lt;span style="color:#f85149">クラスはログを使用して、&lt;/span>AI &lt;span style="color:#f85149">サービスに送信された画像バイト、&lt;/span>API &lt;span style="color:#f85149">応答、分析プロセス中に発生したエラーを記録します。&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```&lt;/span>csharp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>_logger&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>LogInformation(&lt;span style="color:#a5d6ff">&amp;#34;Sending analyze request with image bytes of length: {Length}&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> imageBytes&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Length);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p align="center">
&lt;img src="https://imgur.com/5NS416X.png">
&lt;/p>
&lt;h3 id="2-自動メトリクス収集">2. &lt;strong>自動メトリクス収集&lt;/strong>&lt;/h3>
&lt;p>.NET Aspire は、応答時間、リクエスト数、エラー率などの重要なアプリケーション メトリックも自動的に追跡し、レポートします。これは、アプリケーションのパフォーマンスを理解し、ボトルネックや問題を迅速に検出するのに役立ちます。&lt;/p>
&lt;p align="center">
&lt;img src="https://imgur.com/SGawOY3.png">
&lt;/p>
&lt;h3 id="3-パフォーマンスの向上">3. &lt;strong>パフォーマンスの向上&lt;/strong>&lt;/h3>
&lt;p>.NET Aspire は HTTP 呼び出しを最適化するため、応答時間を短縮し、不必要なリソースの消費を削減します。接続プーリング、リクエストの再試行、インテリジェントなルーティングなどの機能を提供します。&lt;/p>
&lt;h3 id="4-シームレスな統合">4. &lt;strong>シームレスな統合&lt;/strong>&lt;/h3>
&lt;p>.NET Aspire は、さまざまなサービス (このプロジェクトの AI サービスや API サービスなど) の統合を簡素化し、展開プロセスを合理化します。 Aspire がインフラストラクチャ関連のタスクを処理してくれるため、低レベルの構成について心配する必要はありません。&lt;/p>
&lt;p align="center">
&lt;img src="https://imgur.com/OSHhWVb.png">
&lt;/p>
&lt;h3 id="結論ai-はもはや単なるバズワードや-sf-映画で見られるものではありませんこのプロジェクトで私たちが取り組んだ問題領収書から構造化データを抽出する問題など今日の現実世界の問題を積極的に解決しています-azure-openainet-aspireblazor-の助けを借りて時間がかかりエラーが発生しやすい手動タスクを自動化できます-ai-は単にチャットしたりchatgpt-のようなプロンプトに応答したりするだけではありません画像を解釈し貴重な情報を抽出し実用的な洞察を数秒で提供します">結論AI はもはや単なるバズワードや SF 映画で見られるものではありません。このプロジェクトで私たちが取り組んだ問題、領収書から構造化データを抽出する問題など、今日の現実世界の問題を積極的に解決しています。 &lt;strong>Azure OpenAI&lt;/strong>、&lt;strong>.NET Aspire&lt;/strong>、&lt;strong>Blazor&lt;/strong> の助けを借りて、時間がかかりエラーが発生しやすい手動タスクを自動化できます。 AI は単にチャットしたり、ChatGPT のようなプロンプトに応答したりするだけではありません。画像を解釈し、貴重な情報を抽出し、実用的な洞察を数秒で提供します。&lt;/h3>
&lt;p>領収書分析には &lt;strong>Azure OpenAI&lt;/strong>、ログ記録とメトリクスとのシームレスな統合には &lt;strong>.NET Aspire&lt;/strong> を使用することで、強力でスケーラブルなソリューションを作成しました。ビジネス プロセスを合理化し、退屈なタスクを自動化し、精度を向上させる AI の可能性は非常に大きく、これはその応用例の一例にすぎません。&lt;/p>
&lt;p>この投稿は、&lt;strong>Calendario de Adviento de Inteligencia Artificial 2024 en Español&lt;/strong> の一部です。このイベントは、現実世界の AI アプリケーションを紹介し、スペイン語圏のテクノロジー コミュニティに最新のトレンドを啓蒙するイベントです。 AI とその可能性についてさらに詳しく知りたい場合は、このイベントから始めるのが最適です。&lt;/p>
&lt;p>AI は私たちの働き方を変革しつつあり、このプロジェクトは可能性を垣間見るだけです。 AI の真の力は、レシートの処理、画像の分析、傾向の予測など、実際の問題を解決する能力にあります。私たちは表面をなぞっただけです。&lt;/p>
&lt;h3 id="ソースコード">ソースコード&lt;/h3>
&lt;p>このプロジェクトの完全なソース コードは &lt;a href="https://github.com/emimontesdeoca/ReceiptScannerPoc">GitHub&lt;/a> で入手できます。自由にダウンロードして、AI サービスと API サービスがどのように連携するかを調べて、独自のユースケースに合わせて調整してください。問題が発生した場合、または改善のアイデアがある場合は、ためらわずに問題を作成するか、プル リクエストを送信してください。貢献はいつでも歓迎されており、あなたのフィードバックはこのプロジェクトをさらに改善するのに役立ちます。&lt;/p>
&lt;p>コーディングを楽しんでください!&lt;/p></content:encoded><category>.NET</category><category>Blazor</category><category>Azure</category><category>NuGet</category><category>Docker</category><category>AI</category></item><item><title>コンテナを使用してクリスマス プレゼントの価格を追跡しますか?</title><link>https://emimontesdeoca.github.io/ja/posts/monitoring-prices-containers/</link><pubDate>Thu, 12 Dec 2024 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/monitoring-prices-containers/</guid><description>Docker コンテナー、Blazor、.NET API バックエンドを使用して、クリスマス ギフトの価格監視を自動化します。</description><content:encoded>&lt;p>クリスマスが近づいてきました。愛する人への完璧な贈り物を見つけるという楽しい仕事がやって来ます。あなたも私と同じなら、お得に買い物をするのが大好きかもしれませんが、ホリデー シーズン中に人気商品の価格が高騰する中を歩き回るのは、本物のそりに乗っているような気分になるかもしれません。エキサイティングではありますが、少し大変です。今年は、毎日の価格チェックに夢中になるのではなく、自分の技術スキルを有効に活用してプロセスを自動化することにしました。&lt;/p>
&lt;p align="center">
&lt;img src="https://imgur.com/7K7QC08.png" />
&lt;/p>
&lt;p>ソフトウェア開発者として、またテクノロジーを活用して日常の問題を解決することに情熱を注いでいた私は、価格チェックのプロセスを自動化する方法を見つけました。価格を比較するために毎日複数のオンライン ストアを手動で訪問するのではなく、それを実行できるシステムを作成することにしました。これにより、時間を節約できるだけでなく、毎日のチェックのストレスを感じることなく、最良の取引を確実に入手できるようになります。&lt;/p>
&lt;h2 id="価格監視を自動化する理由">価格監視を自動化する理由&lt;/h2>
&lt;p>価格監視の自動化は、サンタの妖精のチームを24時間体制で働かせて、何もせずに最高の取引を確実に入手できるようにするようなものです。気に入っていただける理由は次のとおりです:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>高価なアイテム&lt;/strong>: クリスマス プレゼントは、特にガジェットやおもちゃがリストにある場合、かなり高価になる可能性があることは誰もが知っています。これらでお金を節約することは、在庫にある最後のツリートップを見つけるのと同じくらい満足できることです。&lt;/li>
&lt;li>&lt;strong>毎日のチェック&lt;/strong>: 毎日価格をチェックする時間 (または忍耐力) がある人はいるでしょうか?私じゃないよ！&lt;/li>
&lt;li>&lt;strong>自動化&lt;/strong>: ホリデー シーズンの寄付の精神を受け入れ、効率性という贈り物を自分自身に与えましょう。&lt;/li>
&lt;li>&lt;strong>精度&lt;/strong>: 自動化により、価格チェックがルドルフの鼻のように正確であることが保証されます。&lt;/li>
&lt;/ol>
&lt;h2 id="コアテクノロジー">コアテクノロジー&lt;/h2>
&lt;p>独自のサンタの小さなヘルパーを構築するために、いくつかの主要なテクノロジーを使用することにしました。&lt;/p>
&lt;h3 id="コンテナ">コンテナ&lt;/h3>
&lt;p>コンテナはソフトウェアのクリスマスラッピングのようなものです。アプリケーションをすべての機能 (依存関係) とバンドルして、すべてが新雪のそりのようにスムーズに動作するようにします。これらのコンテナを作成するには Docker が頼りになります。&lt;/p>
&lt;h3 id="ブレザー">ブレザー&lt;/h3>
&lt;p>Blazor は、.NET を使用してインタラクティブな Web アプリケーションを構築するための優れたフレームワークです。それは、一般的なクリスマスキャロルを、カスタマイズされ、効率的で、とても楽しい、独自のホリデー プレイリストに置き換えるようなものです。&lt;/p>
&lt;h3 id="docker-compose">Docker-Compose&lt;/h3>
&lt;p>Docker-Compose は、北極の運営のマネージャーです。これは、API や Blazor フロントエンドなどのすべてのサービスを完全に調和して実行するのに役立ちます。それを私たちのホリデー交響楽団の指揮者だと考えてください。&lt;/p>
&lt;h2 id="ステップバイステップガイド">ステップバイステップガイド&lt;/h2>
&lt;p>さあ、サンタさんのワークショップに飛び込んで、このプロジェクトを実現させましょう!&lt;/p>
&lt;h3 id="ステップ-1-api-の作成">ステップ 1: API の作成&lt;/h3>
&lt;h4 id="11-プロジェクトのセットアップ">1.1 プロジェクトのセットアップ&lt;/h4>
&lt;p>コーディング用のサンタ帽をかぶって、新しい ASP.NET Core Web API プロジェクトをセットアップします。ターミナルを開いて (アドベントカレンダーを開くのと似ています)、次のコマンドを実行します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet new webapi -o PriceMonitorApi
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cd PriceMonitorApi
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>このコマンドは、&lt;code>PriceMonitorApi&lt;/code> という名前の新しいディレクトリを作成し、基本的な Web API プロジェクトをセットアップします。ジンジャーブレッドハウスの頑丈な土台を作るようなものだと想像してください。&lt;/p>
&lt;h4 id="12-httpclient-の追加とライブラリのスクレイピング次にhttpclient-と-html-を解析するライブラリを追加しますこれは価格データを取得して読み取るための信頼できるそりになります">1.2 HttpClient の追加とライブラリのスクレイピング次に、&lt;code>HttpClient&lt;/code> と HTML を解析するライブラリを追加します。これは、価格データを取得して読み取るための信頼できるそりになります。&lt;/h4>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet add package HtmlAgilityPack
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>HtmlAgilityPack は、HTML ドキュメントの解析を支援するエルフです。&lt;/p>
&lt;h4 id="13-モデルの作成">1.3 モデルの作成&lt;/h4>
&lt;p>模型はおもちゃの設計図のようなものです。製品情報を表すモデルを作成しましょう。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorApi.Models&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ProductInfo&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Title { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">decimal&lt;/span> Price { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> DateTime Date { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>データに最適なテンプレートを作成しました。&lt;/p>
&lt;h4 id="14-スクレーパー-サービスの実装">1.4 スクレーパー サービスの実装&lt;/h4>
&lt;p>私たちのスクレイパー サービスはサンタさんの小さなヘルパーのようなもので、私たちのために情報を取得して処理します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">HtmlAgilityPack&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorApi.Models&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Globalization&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorApi.Services&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ScraperService&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> HttpClient _httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ScraperService(HttpClient httpClient)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _httpClient = httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;ProductInfo&amp;gt; ScrapeProductInfoAsync(&lt;span style="color:#ff7b72">string&lt;/span> url)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> _httpClient.GetStringAsync(url);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> htmlDoc = &lt;span style="color:#ff7b72">new&lt;/span> HtmlDocument();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> htmlDoc.LoadHtml(response);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> title = htmlDoc.DocumentNode.SelectSingleNode(&lt;span style="color:#a5d6ff">&amp;#34;//h1[@class=&amp;#39;product-title&amp;#39;]&amp;#34;&lt;/span>).InnerText.Trim();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> priceString = htmlDoc.DocumentNode.SelectSingleNode(&lt;span style="color:#a5d6ff">&amp;#34;//span[@class=&amp;#39;product-price&amp;#39;]&amp;#34;&lt;/span>).InnerText.Trim();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">decimal&lt;/span>.TryParse(priceString, NumberStyles.Currency, CultureInfo.InvariantCulture, &lt;span style="color:#ff7b72">out&lt;/span> &lt;span style="color:#ff7b72">var&lt;/span> price))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> ProductInfo
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Title = title,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Price = price,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Date = DateTime.UtcNow
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> Exception(&lt;span style="color:#a5d6ff">&amp;#34;Unable to parse price&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>このスニペットは、&lt;code>HtmlAgilityPack&lt;/code> をホリデーの魔法の杖に変えます。&lt;/p>
&lt;h4 id="15-api-コントローラーの作成">1.5 API コントローラーの作成&lt;/h4>
&lt;p>データのゲートキーパーのように機能するコントローラーを作成しましょう。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Mvc&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorApi.Models&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorApi.Services&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorApi.Controllers&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [ApiController]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Route(&amp;#34;api/[controller]&lt;span style="color:#a5d6ff">&amp;#34;)]
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ScraperController&lt;/span> : ControllerBase
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> ScraperService _scraperService;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ScraperController(ScraperService scraperService)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _scraperService = scraperService;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [HttpGet(&amp;#34;productinfo&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;ActionResult&amp;lt;ProductInfo&amp;gt;&amp;gt; GetProductInfo(&lt;span style="color:#ff7b72">string&lt;/span> url)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> productInfo = &lt;span style="color:#ff7b72">await&lt;/span> _scraperService.ScrapeProductInfoAsync(url);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Ok(productInfo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception ex)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> BadRequest(&lt;span style="color:#ff7b72">new&lt;/span> { Message = ex.Message });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>私たちのコントローラーは、サンタさんが煙突から下りてくるよりも早くデータが配信されることを保証します。&lt;/p>
&lt;h4 id="16-diコンテナへのサービスの登録">1.6 DIコンテナへのサービスの登録&lt;/h4>
&lt;p>最後に、&lt;code>ScraperService&lt;/code> を登録して、必要なときに利用できるようにします。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>services.AddHttpClient&amp;lt;ScraperService&amp;gt;();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これで、クリスマスイブのサンタのそりのように、API の準備が整いました。&lt;/p>
&lt;hr>
&lt;h3 id="ステップ-2-blazor-アプリケーションの作成">ステップ 2: Blazor アプリケーションの作成&lt;/h3>
&lt;p>Blazor を使用すると、プロジェクトをクリスマス ツリーのように飾り、視覚的に魅力的でインタラクティブにすることができます。&lt;/p>
&lt;h4 id="21-blazor-プロジェクトのセットアップ">2.1 Blazor プロジェクトのセットアップ&lt;/h4>
&lt;p>次に、価格監視そりのインターフェイスとして機能する Blazor プロジェクトを作成します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>dotnet new blazor -o PriceMonitorBlazor
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>cd PriceMonitorBlazor
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>このコマンドは、基本的な Blazor WebAssembly プロジェクトをセットアップするための休日の魔法をいくつか散りばめています。&lt;/p>
&lt;h4 id="22-モデルの追加">2.2 モデルの追加&lt;/h4>
&lt;p>オーナメントを設定するのと同じように、Blazor プロジェクトに &lt;code>ProductInfo&lt;/code> モデルを追加します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorBlazor.Models&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ProductInfo&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Title { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">decimal&lt;/span> Price { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> DateTime Date { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="23-api-呼び出し用のサービスの作成">2.3 API 呼び出し用のサービスの作成&lt;/h4>
&lt;p>API からデータを取得するサービスを作成します。これをオンライン ショッピングの友達と考えてください。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorBlazor.Models&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Net.Http&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Net.Http.Json&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Threading.Tasks&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorBlazor.Services&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ScraperService&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> HttpClient _httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ScraperService(HttpClient httpClient)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _httpClient = httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;ProductInfo&amp;gt; GetProductInfoAsync(&lt;span style="color:#ff7b72">string&lt;/span> url)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> _httpClient.GetFromJsonAsync&amp;lt;ProductInfo&amp;gt;(&lt;span style="color:#a5d6ff">$&amp;#34;https://localhost:5001/api/scraper/productinfo?url={url}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> response;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="24-diコンテナへのサービスの登録">2.4 DIコンテナへのサービスの登録&lt;/h4>
&lt;p>&lt;code>ScraperService&lt;/code> がコンポーネントに挿入できるように、&lt;code>ScraperService&lt;/code> が登録されていることを確認してください。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddScoped&amp;lt;ScraperService&amp;gt;();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="25-blazor-コンポーネントの作成">2.5 Blazor コンポーネントの作成&lt;/h3>
&lt;p>&lt;code>Pages/Index.razor&lt;/code> を更新して、楽しくインタラクティブなインターフェイスを追加します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>page &lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>using PriceMonitorBlazor&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Models
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>using PriceMonitorBlazor&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Services
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>inject ScraperService ScraperService
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h3&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Price Monitor&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h3&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>label &lt;span style="color:#ff7b72">for&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;urlInput&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Product URL:&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>label&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>input id&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;urlInput&amp;#34;&lt;/span> &lt;span style="color:#f85149">@&lt;/span>bind&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;productUrl&amp;#34;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>button &lt;span style="color:#f85149">@&lt;/span>onclick&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;FetchProductInfo&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Fetch Price&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>button&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (productInfos&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Any())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>table &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;table&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>thead&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Title&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Price&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Date&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>thead&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tbody&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>foreach (&lt;span style="color:#ff7b72">var&lt;/span> product &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> productInfos)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Title&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Price&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Date&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tbody&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>table&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private string productUrl;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>ProductInfo&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> productInfos &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> new List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>ProductInfo&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private async Task FetchProductInfo()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72;font-weight:bold">!&lt;/span>string&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>IsNullOrEmpty(productUrl))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> productInfo &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await ScraperService&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetProductInfoAsync(productUrl);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> productInfos&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Add(productInfo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これは、お祭りのディスプレイをセットアップするようなもので、アプリをインタラクティブで楽しいものにします。&lt;/p>
&lt;hr>
&lt;h3 id="ステップ-3-docker-compose-を使用してすべてを接続する">ステップ 3: Docker-Compose を使用してすべてを接続する&lt;/h3>
&lt;p>それでは、Docker-Compose を使用してすべてを接続し、プロジェクトを十分に油を注いだそりに変えてみましょう。&lt;/p>
&lt;h4 id="31-dockerfile-の作成">3.1 Dockerfile の作成&lt;/h4>
&lt;p>API プロジェクトと Blazor プロジェクトの両方の Dockerfile を作成します。&lt;/p>
&lt;p>&lt;strong>PriceMonitorApi/Dockerfile&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">FROM&lt;/span>&lt;span style="color:#a5d6ff"> mcr.microsoft.com/dotnet/aspnet:6.0 AS base&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">WORKDIR&lt;/span>&lt;span style="color:#a5d6ff"> /app&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">EXPOSE&lt;/span>&lt;span style="color:#a5d6ff"> 80&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">EXPOSE&lt;/span>&lt;span style="color:#a5d6ff"> 443&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">FROM&lt;/span>&lt;span style="color:#a5d6ff"> mcr.microsoft.com/dotnet/sdk:6.0 AS build&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">WORKDIR&lt;/span>&lt;span style="color:#a5d6ff"> /src&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">COPY&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">[&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorApi/PriceMonitorApi.csproj&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorApi/&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">]&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">RUN&lt;/span> dotnet restore &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorApi/PriceMonitorApi.csproj&amp;#34;&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">COPY&lt;/span> . .&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">WORKDIR&lt;/span>&lt;span style="color:#a5d6ff"> &amp;#34;/src/PriceMonitorApi&amp;#34;&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">RUN&lt;/span> dotnet build &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorApi.csproj&amp;#34;&lt;/span> -c Release -o /app/build&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">FROM&lt;/span>&lt;span style="color:#a5d6ff"> build AS publish&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">RUN&lt;/span> dotnet publish &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorApi.csproj&amp;#34;&lt;/span> -c Release -o /app/publish&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">FROM&lt;/span>&lt;span style="color:#a5d6ff"> base AS final&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">WORKDIR&lt;/span>&lt;span style="color:#a5d6ff"> /app&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">COPY&lt;/span> --from&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>publish /app/publish .&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">ENTRYPOINT&lt;/span> [&lt;span style="color:#a5d6ff">&amp;#34;dotnet&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorApi.dll&amp;#34;&lt;/span>]&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>PriceMonitorBlazor/Dockerfile&lt;/strong>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-dockerfile" data-lang="dockerfile">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">FROM&lt;/span>&lt;span style="color:#a5d6ff"> mcr.microsoft.com/dotnet/aspnet:6.0 AS base&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">WORKDIR&lt;/span>&lt;span style="color:#a5d6ff"> /app&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">EXPOSE&lt;/span>&lt;span style="color:#a5d6ff"> 80&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">EXPOSE&lt;/span>&lt;span style="color:#a5d6ff"> 443&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">FROM&lt;/span>&lt;span style="color:#a5d6ff"> mcr.microsoft.com/dotnet/sdk:6.0 AS build&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">WORKDIR&lt;/span>&lt;span style="color:#a5d6ff"> /src&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">COPY&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">[&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorBlazor/PriceMonitorBlazor.csproj&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorBlazor/&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">]&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">RUN&lt;/span> dotnet restore &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorBlazor/PriceMonitorBlazor.csproj&amp;#34;&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">COPY&lt;/span> . .&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">WORKDIR&lt;/span>&lt;span style="color:#a5d6ff"> &amp;#34;/src/PriceMonitorBlazor&amp;#34;&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">RUN&lt;/span> dotnet build &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorBlazor.csproj&amp;#34;&lt;/span> -c Release -o /app/build&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">FROM&lt;/span>&lt;span style="color:#a5d6ff"> build AS publish&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">RUN&lt;/span> dotnet publish &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorBlazor.csproj&amp;#34;&lt;/span> -c Release -o /app/publish&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">FROM&lt;/span>&lt;span style="color:#a5d6ff"> base AS final&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">WORKDIR&lt;/span>&lt;span style="color:#a5d6ff"> /app&lt;/span>&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">COPY&lt;/span> --from&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>publish /app/publish .&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">&lt;/span>&lt;span style="color:#ff7b72">ENTRYPOINT&lt;/span> [&lt;span style="color:#a5d6ff">&amp;#34;dotnet&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;PriceMonitorBlazor.dll&amp;#34;&lt;/span>]&lt;span style="color:#f85149">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="32-docker-composeyml-の作成">3.2 docker-compose.yml の作成&lt;/h4>
&lt;p>すべてのドット (クリスマス ライト) を 1 つの &lt;code>docker-compose.yml&lt;/code> ファイルで接続します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">version&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">&amp;#39;3.4&amp;#39;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">services&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">api&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">image&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">pricemonitorapi&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">build&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">context&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">./PriceMonitorApi&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">dockerfile&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Dockerfile&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">ports&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">&amp;#34;5000:80&amp;#34;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">networks&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">price-monitor-network&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">blazor&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">image&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">pricemonitorblazor&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">build&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">context&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">./PriceMonitorBlazor&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">dockerfile&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Dockerfile&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">ports&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">&amp;#34;5001:80&amp;#34;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">depends_on&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">api&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">networks&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">price-monitor-network&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">networks&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">price-monitor-network&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">driver&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">bridge&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="図表">図表&lt;/h3>
&lt;p>以下は、さまざまなコンポーネントとデータ フローを視覚化するのに役立つ図です。これは、サンタの世界で実際に何が起こっているのかを理解するのに役立ちます。&lt;/p>
&lt;h4 id="システムアーキテクチャ図">システムアーキテクチャ図&lt;/h4>
&lt;p>&lt;img src="https://imgur.com/nE0hSJ4.png" alt="Imgur">&lt;/p>
&lt;h4 id="データ-フロー図">データ フロー図&lt;/h4>
&lt;p>&lt;img src="https://imgur.com/hJXv4Jw.png" alt="Imgur">&lt;/p>
&lt;h4 id="シーケンス図">シーケンス図&lt;/h4>
&lt;p>&lt;img src="https://imgur.com/FH6VzuH.png" alt="Imgur">&lt;/p>
&lt;h4 id="docker-セットアップのコンポーネント図">Docker セットアップのコンポーネント図&lt;/h4>
&lt;p>&lt;img src="https://imgur.com/efQ9WuT.png" alt="Imgur">&lt;/p>
&lt;h3 id="ステップ-4-url-管理更新自動定期更新このデモではメモリに保存するだけですが実際のアプリケーションではデータベースを使用しますこの楽しいプロジェクトではメモリ内ストレージにこだわりましょう">ステップ 4: URL 管理、更新、自動定期更新このデモではメモリに保存するだけですが、実際のアプリケーションではデータベースを使用します。この楽しいプロジェクトでは、メモリ内ストレージにこだわりましょう。&lt;/h3>
&lt;h4 id="41-url-を管理するためのサービスの変更">4.1 URL を管理するためのサービスの変更&lt;/h4>
&lt;p>まず、サービスが URL の追加と取得、および製品情報の取得を処理できることを確認します。&lt;/p>
&lt;p>&lt;code>Services/ScraperService.cs&lt;/code> を更新します:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorBlazor.Models&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Collections.Generic&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Net.Http&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Net.Http.Json&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Threading.Tasks&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">PriceMonitorBlazor.Services&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ScraperService&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> HttpClient _httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; urls = &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ScraperService(HttpClient httpClient)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _httpClient = httpClient;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;ProductInfo&amp;gt; GetProductInfoAsync(&lt;span style="color:#ff7b72">string&lt;/span> url)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> response = &lt;span style="color:#ff7b72">await&lt;/span> _httpClient.GetFromJsonAsync&amp;lt;ProductInfo&amp;gt;(&lt;span style="color:#a5d6ff">$&amp;#34;http://localhost:5000/api/scraper/productinfo?url={url}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> response;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> AddUrl(&lt;span style="color:#ff7b72">string&lt;/span> url)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!urls.Contains(url))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> urls.Add(url);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; GetUrls()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> urls;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ClearUrls()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> urls.Clear();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h4 id="42-url-管理と更新のための-blazor-コンポーネントの更新">4.2 URL 管理と更新のための Blazor コンポーネントの更新&lt;/h4>
&lt;p>次に、&lt;code>Pages/Index.razor&lt;/code> を更新して、URL 管理、更新、自動定期更新を追加します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>page &lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>using PriceMonitorBlazor&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Models
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>using PriceMonitorBlazor&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Services
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>inject ScraperService ScraperService
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>h3&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Price Monitor&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>h3&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>label &lt;span style="color:#ff7b72">for&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;urlInput&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Product URL:&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>label&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>input id&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;urlInput&amp;#34;&lt;/span> &lt;span style="color:#f85149">@&lt;/span>bind&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;productUrl&amp;#34;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>button &lt;span style="color:#f85149">@&lt;/span>onclick&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;AddUrl&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Add URL&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>button&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>button &lt;span style="color:#f85149">@&lt;/span>onclick&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;RefreshPrices&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Refresh All Prices&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>button&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>p&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>&lt;span style="color:#ff7b72">if&lt;/span> (productInfos&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Any())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>table &lt;span style="color:#ff7b72">class&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;table&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>thead&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Title&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Price&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>Date&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>th&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>thead&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tbody&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>foreach (&lt;span style="color:#ff7b72">var&lt;/span> product &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> productInfos)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Title&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Price&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>&lt;span style="color:#f85149">@&lt;/span>product&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Date&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>td&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tr&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>tbody&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;/&lt;/span>table&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private string productUrl;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>ProductInfo&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span> productInfos &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> new List&lt;span style="color:#ff7b72;font-weight:bold">&amp;lt;&lt;/span>ProductInfo&lt;span style="color:#ff7b72;font-weight:bold">&amp;gt;&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private &lt;span style="color:#f0883e;font-weight:bold">Timer&lt;/span> _timer;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> protected override void OnInitialized()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> StartTimer();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private void StartTimer()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72;font-weight:bold">//&lt;/span> Set up the timer to call RefreshPrices every minute (&lt;span style="color:#a5d6ff">60000&lt;/span> ms)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _timer &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> new &lt;span style="color:#f0883e;font-weight:bold">Timer&lt;/span>(async _ &lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span> await InvokeAsync(RefreshPrices), null, TimeSpan&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Zero, TimeSpan&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>FromMinutes(&lt;span style="color:#a5d6ff">1&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private void AddUrl()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72;font-weight:bold">!&lt;/span>string&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>IsNullOrEmpty(productUrl))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ScraperService&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>AddUrl(productUrl);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> productUrl &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> string&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Empty;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> private async Task RefreshPrices()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> productInfos&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Clear();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> urls &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> ScraperService&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetUrls();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> foreach (&lt;span style="color:#ff7b72">var&lt;/span> url &lt;span style="color:#ff7b72;font-weight:bold">in&lt;/span> urls)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> productInfo &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> await ScraperService&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>GetProductInfoAsync(url);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> productInfos&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Add(productInfo);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> StateHasChanged();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>この更新されたコンポーネントでは次のようになります。&lt;/p>
&lt;ul>
&lt;li>&lt;code>Timer&lt;/code> を使用して、&lt;code>RefreshPrices&lt;/code> メソッドを毎分定期的に呼び出します。&lt;/li>
&lt;li>&lt;code>StartTimer&lt;/code> メソッドは、タイマーを初期化してすぐに開始し、その後 60 秒ごとにトリガーします。&lt;/li>
&lt;li>&lt;code>OnInitialized&lt;/code> ライフサイクル メソッドは、コンポーネントが初期化されて定期更新を開始するときに &lt;code>StartTimer&lt;/code> を呼び出します。&lt;/li>
&lt;/ul>
&lt;h4 id="43-ソリューションの実行">4.3 ソリューションの実行&lt;/h4>
&lt;p>新しい機能を備えた更新された Blazor アプリケーションを実行するには、Docker コンテナーを再構築して再起動します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>docker-compose build
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>docker-compose up
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ブラウザーに &lt;code>http://localhost:5001&lt;/code> を読み込んだ後、Blazor アプリは手動更新と URL 管理に加えて、商品価格を 1 分ごとに自動的に更新するようになります。&lt;/p>
&lt;h2 id="結論">結論&lt;/h2>
&lt;p>この価格監視システムの構築は非常に楽しいものでした。毎日の価格チェックのストレスから解放されただけでなく、最新の Web テクノロジーの魔法を見せてくれました。&lt;/p>
&lt;h1 id="フェスティブテックカレンダー2024">フェスティブテックカレンダー2024&lt;/h1>
&lt;p align="center">
&lt;img src="https://festivetechcalendar.com/assets/images/Heading.png" />
&lt;/p>
&lt;p>私はこの投稿を &lt;strong>Festive Tech Calendar 2024&lt;/strong> イベントの一環として作成しました。このイベントでは、テクノロジー愛好家、イノベーター、デジタル夢想家が集まり、知識を共有し、お祭り気分とテクノロジーの驚異の融合を祝います。この取り組みは、学び、つながるだけでなく、恩返しすることも目的としています。&lt;/p>
&lt;p>&lt;strong>Festive Tech Calendar 2024&lt;/strong> は今年、Beatson Cancer Charity をサポートしています。 Beatson Cancer Charity は、がんに苦しむ人々、その家族、そして彼らの世話をする医療従事者を支援することに専念しています。彼らの素晴らしい仕事についての詳細は、&lt;a href="https://www.beatsoncancercharity.org/">https://www.beatsoncancercharity.org/&lt;/a> でご覧いただけます。&lt;/p>
&lt;p>よくある質問とイベントの詳細については、Festive Tech Calendar Web サイト (&lt;a href="https://festivetechcalendar.com">https://festivetechcalendar.com&lt;/a>) をご覧ください。&lt;/p>
&lt;h1 id="ホーホーホー">&lt;strong>ホーホーホー!&lt;/strong>&lt;/h1>
&lt;p>このプロジェクトの作成は、お祭り騒ぎのテクノロジー コミュニティに貢献し、同時に大きな目的をサポートする素晴らしい方法でした。&lt;/p>
&lt;p>このガイドがお役に立ち、テクノロジーを使用して日常業務を簡素化するさらなる方法を探求するきっかけになれば幸いです。&lt;/p>
&lt;p>ご質問がある場合、またはさらにサポートが必要な場合は、お気軽にお問い合わせください。&lt;/p>
&lt;p>コーディングを楽しんでください!&lt;/p></content:encoded><category>.NET</category><category>Blazor</category><category>Docker</category><category>API</category></item><item><title>カスタム ValidationAttribute と Blazor 検証</title><link>https://emimontesdeoca.github.io/ja/posts/custom-validationattribute-blazor/</link><pubDate>Fri, 29 Mar 2024 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/custom-validationattribute-blazor/</guid><description>データ注釈を使用した Blazor フォーム検証用の再利用可能なカスタム ValidationAttribute クラスを作成します。</description><content:encoded>&lt;h1 id="すべてをカスタマイズ">すべてをカスタマイズ&lt;/h1>
&lt;p>おそらく私のすべての投稿でご覧になっていると思いますが、カスタム属性、カスタム例外処理、サービス コレクション インジェクションなどに関する投稿をすでに書いているため、私はすべてをできるだけクリーンに保つよう努めています。&lt;/p>
&lt;p>時間が経つにつれて、この種のコーディング方法により、私と私のチームが残業を改善し、問題を見つけやすくなり、コードを可能な限り分離する方法が得られることに気づきました。&lt;/p>
&lt;p>はい、この素晴らしい話の後で、私は最近いつものように Blazor に取り組んでおり、何年も開発を続けた結果、カスタム検証属性を作成できることがわかりました。&lt;/p>
&lt;p>うん、面白いね、何年も経ったのに…&lt;/p>
&lt;h1 id="カスタム検証属性">カスタム検証属性&lt;/h1>
&lt;p>このアイデアは実際に仕事から生まれました。私たちは常にすべての場所で検証を行っていますが、同じ検証プロセスを必要とするフィールドがいくつかあったので、そこに何かがあるかもしれないと考えました&amp;hellip;カスタム検証属性のようなものです。&lt;/p>
&lt;p>そこで Microsoft のドキュメントを調べたところ、次のようにカスタム検証属性を作成してプロパティに割り当てることができることがわかりました。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">StringLengthRangeAttribute&lt;/span> : ValidationAttribute
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> Minimum { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> Maximum { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> StringLengthRangeAttribute()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span>.Minimum = &lt;span style="color:#a5d6ff">0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span>.Maximum = &lt;span style="color:#ff7b72">int&lt;/span>.MaxValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">bool&lt;/span> IsValid(&lt;span style="color:#ff7b72">object&lt;/span> &lt;span style="color:#ff7b72">value&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> strValue = &lt;span style="color:#ff7b72">value&lt;/span> &lt;span style="color:#ff7b72">as&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrEmpty(strValue))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">int&lt;/span> len = strValue.Length;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> len &amp;gt;= &lt;span style="color:#ff7b72">this&lt;/span>.Minimum &amp;amp;&amp;amp; len &amp;lt;= &lt;span style="color:#ff7b72">this&lt;/span>.Maximum;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>次のような単純なクラスで使用します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[Required]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[StringLengthRange(Minimum = 10, ErrorMessage = &amp;#34;Must be &amp;gt;10 characters.&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[StringLengthRange(Maximum = 20)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[Required]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[StringLengthRange(Minimum = 10, Maximum = 20)]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="カスタムバリデーター">カスタムバリデーター&lt;/h1>
&lt;p>そこで、特定のビジネスケースに必要なバリデーターを用意しました。これは、最初の 20 文字をカウントし、9 つの数字とハイフンを省略し、通常は国コードとなる 2 文字で終わります。つまり、次のようなものになります。 &lt;strong>123456789-123456789-ES&lt;/strong>&lt;/p>
&lt;p>私は最終的にこのようなものを思いつきました、それは本当に単純ですが、うまくいきます:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.ComponentModel.DataAnnotations&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Text.RegularExpressions&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">SpecialStringValidatorAttribute&lt;/span> : ValidationAttribute
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">const&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> TotalLength = &lt;span style="color:#a5d6ff">22&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">const&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Pattern = &lt;span style="color:#a5d6ff">@&amp;#34;^(\d{10})-(\d{10})-([A-Za-z]{2})$&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> ValidationResult IsValid(&lt;span style="color:#ff7b72">object&lt;/span> &lt;span style="color:#ff7b72">value&lt;/span>, ValidationContext validationContext)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> strValue = &lt;span style="color:#ff7b72">value&lt;/span> &lt;span style="color:#ff7b72">as&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrEmpty(strValue))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (strValue.Length != TotalLength)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> ValidationResult(&lt;span style="color:#a5d6ff">$&amp;#34;The string must be {TotalLength} characters long.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!Regex.IsMatch(strValue, Pattern))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> ValidationResult(&lt;span style="color:#a5d6ff">&amp;#34;The string must follow the pattern: 1234567890-1234567890-AB&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> ValidationResult.Success;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> ValidationResult(&lt;span style="color:#a5d6ff">&amp;#34;The string cannot be null or empty.&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="テスト">テスト&lt;/h2>
&lt;p>念のため、彼らのためにいくつかのテストも書きました。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">SpecialStringValidatorTests&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Theory]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [InlineData(&amp;#34;1234567890-1234567890-AB&amp;#34;, true)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [InlineData(&amp;#34;1234567890-1234567890-XY&amp;#34;, true)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [InlineData(&amp;#34;1234567890-1234567890-A1&amp;#34;, false)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [InlineData(&amp;#34;1234567890-1234567890-A&amp;#34;, false)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [InlineData(&amp;#34;1234567890-123456789-AB&amp;#34;, false)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [InlineData(&amp;#34;1234567890-1234567890&amp;#34;, false)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [InlineData(&amp;#34;1234567890-1234567890-ABCDE&amp;#34;, false)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> SpecialStringValidatorTest(&lt;span style="color:#ff7b72">string&lt;/span> input, &lt;span style="color:#ff7b72">bool&lt;/span> expectedResult)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Arrange&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> validator = &lt;span style="color:#ff7b72">new&lt;/span> SpecialStringValidatorAttribute();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Act&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> result = validator.IsValid(input);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Assert&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Assert.Equal(expectedResult, result);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>そして実行すると、次の結果が得られました。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>Microsoft &lt;span style="color:#ff7b72;font-weight:bold">(&lt;/span>R&lt;span style="color:#ff7b72;font-weight:bold">)&lt;/span> Test Execution Command Line Tool Version 16.9.1
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Copyright &lt;span style="color:#ff7b72;font-weight:bold">(&lt;/span>c&lt;span style="color:#ff7b72;font-weight:bold">)&lt;/span> Microsoft Corporation. All rights reserved.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Starting test execution, please wait...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>A total of &lt;span style="color:#a5d6ff">1&lt;/span> test files matched the specified pattern.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Test run in progress...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Passed! - SpecialStringValidatorTests.SpecialStringValidatorTest&lt;span style="color:#ff7b72;font-weight:bold">(&lt;/span>input: &lt;span style="color:#a5d6ff">&amp;#34;1234567890-1234567890-AB&amp;#34;&lt;/span>, expectedResult: True&lt;span style="color:#ff7b72;font-weight:bold">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Passed! - SpecialStringValidatorTests.SpecialStringValidatorTest&lt;span style="color:#ff7b72;font-weight:bold">(&lt;/span>input: &lt;span style="color:#a5d6ff">&amp;#34;1234567890-1234567890-XY&amp;#34;&lt;/span>, expectedResult: True&lt;span style="color:#ff7b72;font-weight:bold">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Passed! - SpecialStringValidatorTests.SpecialStringValidatorTest&lt;span style="color:#ff7b72;font-weight:bold">(&lt;/span>input: &lt;span style="color:#a5d6ff">&amp;#34;1234567890-1234567890-A1&amp;#34;&lt;/span>, expectedResult: False&lt;span style="color:#ff7b72;font-weight:bold">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Passed! - SpecialStringValidatorTests.SpecialStringValidatorTest&lt;span style="color:#ff7b72;font-weight:bold">(&lt;/span>input: &lt;span style="color:#a5d6ff">&amp;#34;1234567890-1234567890-A&amp;#34;&lt;/span>, expectedResult: False&lt;span style="color:#ff7b72;font-weight:bold">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Passed! - SpecialStringValidatorTests.SpecialStringValidatorTest&lt;span style="color:#ff7b72;font-weight:bold">(&lt;/span>input: &lt;span style="color:#a5d6ff">&amp;#34;1234567890-123456789-AB&amp;#34;&lt;/span>, expectedResult: False&lt;span style="color:#ff7b72;font-weight:bold">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Passed! - SpecialStringValidatorTests.SpecialStringValidatorTest&lt;span style="color:#ff7b72;font-weight:bold">(&lt;/span>input: &lt;span style="color:#a5d6ff">&amp;#34;1234567890-1234567890&amp;#34;&lt;/span>, expectedResult: False&lt;span style="color:#ff7b72;font-weight:bold">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Passed! - SpecialStringValidatorTests.SpecialStringValidatorTest&lt;span style="color:#ff7b72;font-weight:bold">(&lt;/span>input: &lt;span style="color:#a5d6ff">&amp;#34;1234567890-1234567890-ABCDE&amp;#34;&lt;/span>, expectedResult: False&lt;span style="color:#ff7b72;font-weight:bold">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Test Run Successful.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Total tests: &lt;span style="color:#a5d6ff">7&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Passed: &lt;span style="color:#a5d6ff">7&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Total time: 1.7296 Seconds
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="カスタム検証と-blazor">カスタム検証と Blazor&lt;/h1>
&lt;p>この関数が使用できることがわかったので、それを Blazor に実装するのは明らかに良い考えですよね?&lt;/p>
&lt;p>前に示したモデル &lt;code>Person&lt;/code> を使用するこのフォームがあると仮定しましょう。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@using Models
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;PageTitle&amp;gt;Home&amp;lt;/PageTitle&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h1&amp;gt;Hello, world!&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;EditForm Model=@Person FormName=&lt;span style="color:#a5d6ff">&amp;#34;PersonForm&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;DataAnnotationsValidator/&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;ValidationSummary/&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;form-group&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label &lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Name&amp;#34;&lt;/span>&amp;gt;Name&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;InputText @bind-Value=Person.Name class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> id=&lt;span style="color:#a5d6ff">&amp;#34;Name&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;ValidationMessage For=&lt;span style="color:#a5d6ff">&amp;#34;() =&amp;gt; Person.Name&amp;#34;&lt;/span>/&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;form-group&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label &lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;MySpecialString&amp;#34;&lt;/span>&amp;gt;My special &lt;span style="color:#ff7b72">string&lt;/span>&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;InputText @bind-Value=Person.MySpecialString class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> id=&lt;span style="color:#a5d6ff">&amp;#34;Name&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;ValidationMessage For=&lt;span style="color:#a5d6ff">&amp;#34;() =&amp;gt; Person.MySpecialString&amp;#34;&lt;/span>/&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;form-group&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label &lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Age&amp;#34;&lt;/span>&amp;gt;Age&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;InputNumber @bind-Value=Person.Age class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> id=&lt;span style="color:#a5d6ff">&amp;#34;Age&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;ValidationMessage For=&lt;span style="color:#f85149">@&lt;/span>(() =&amp;gt; Person.Age) /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;input type=&lt;span style="color:#a5d6ff">&amp;#34;submit&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;btn btn-primary&amp;#34;&lt;/span> &lt;span style="color:#ff7b72">value&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Save&amp;#34;&lt;/span>/&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/EditForm&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Person Person = &lt;span style="color:#ff7b72">new&lt;/span> Person();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これを実行するとすぐに、次のエラーが表示されます。&lt;/p>
&lt;img src="https://i.imgur.com/psfpzdr.png">
&lt;p>そして、必要なものをそのまま入力すると、次のことがわかります。&lt;/p>
&lt;img src="https://i.imgur.com/RPoUq02.png">
&lt;p>正直に言うと、少なくともこれらのフォームを検証するロジックをカスタム検証属性に移動する必要があることは明らかです。これにより、そのログインのコードを 1 か所に自由に保存できるようになり、後で API や別のアプリケーションで使用できるようになります。&lt;/p>
&lt;p>気に入っていただければ幸いです。ご質問がある場合、または連絡したい場合は、ためらわずにご連絡ください。&lt;/p></content:encoded><category>.NET</category><category>Blazor</category></item><item><title>.NET API でのカスタム例外処理</title><link>https://emimontesdeoca.github.io/ja/posts/custom-exception-handler-api/</link><pubDate>Sun, 01 Oct 2023 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/custom-exception-handler-api/</guid><description>カスタム例外処理ミドルウェアを構築して、.NET API からクリーンなエラー応答を返します。</description><content:encoded>&lt;p>例外は悪いことです、私たちは知っていますよね?しかし、それらを処理しなければならない場合はどうすればよいでしょうか?&lt;/p>
&lt;p>たとえば API で例外が発生すると、ユーザーが受け取る応答から削除したい可能性のある多くの情報を含むスタック メッセージが表示されます。&lt;/p>
&lt;p>デモ用に、ドットネット API を作成し、例外をスローするメソッドを追加しました。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[HttpGet]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[Route(&amp;#34;GetWithoutExceptionHandler&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> Task GetWithoutExceptionHandler()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> Exception(&lt;span style="color:#a5d6ff">&amp;#34;This is a custom exception!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>例外をスローすると、次のようになります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>System.Exception: This &lt;span style="color:#ff7b72">is&lt;/span> a custom exception!
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at CustomExceptionHandleDemo.Controllers.WeatherForecastController.GetWithoutExceptionHandler()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at lambda_method16(Closure , Object , Object[] )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.&amp;lt;InvokeActionMethodAsync&amp;gt;g__Logged|&lt;span style="color:#a5d6ff">12_1&lt;/span>(ControllerActionInvoker invoker)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.&amp;lt;InvokeNextActionFilterAsync&amp;gt;g__Awaited|&lt;span style="color:#a5d6ff">10_0&lt;/span>(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State&amp;amp; next, Scope&amp;amp; scope, Object&amp;amp; state, Boolean&amp;amp; isCompleted)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>--- End of stack trace &lt;span style="color:#ff7b72">from&lt;/span> previous location ---
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.&amp;lt;InvokeFilterPipelineAsync&amp;gt;g__Awaited|&lt;span style="color:#a5d6ff">20_0&lt;/span>(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.&amp;lt;InvokeAsync&amp;gt;g__Logged|&lt;span style="color:#a5d6ff">17_1&lt;/span>(ResourceInvoker invoker)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.&amp;lt;InvokeAsync&amp;gt;g__Logged|&lt;span style="color:#a5d6ff">17_1&lt;/span>(ResourceInvoker invoker)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Routing.EndpointMiddleware.&amp;lt;Invoke&amp;gt;g__AwaitRequestTask|&lt;span style="color:#a5d6ff">6_0&lt;/span>(Endpoint endpoint, Task requestTask, ILogger logger)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これは正常に見えますが、これは例外から得られるものですが、私にとっては、多くの情報が表示されます。ライブラリなどがあれば、クライアント、プロジェクト、その他のものに関する賢明な情報を、何かを見ようとしている人に表示できます。&lt;/p>
&lt;p>Swagger では次のように表示されます。&lt;/p>
&lt;img src="https://imgur.com/fUujR3s.png"/>
&lt;h3 id="カスタム例外の作成">カスタム例外の作成&lt;/h3>
&lt;p>どのような例外がスローされるか (この場合は &lt;code>Exception&lt;/code>] であることがわかっているため、このステップを回避することもできますが、私にとっては、スローするものをより詳細に制御できるため、カスタム例外を持つ方が良いと考えています。&lt;/p>
&lt;p>この場合、&lt;code>Exception&lt;/code> を継承するオブジェクト &lt;code>CustomException&lt;/code> を作成しました。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">CustomExceptionHandleDemo.Exceptions&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CustomException&lt;/span> : Exception
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Constructor for &amp;lt;see cref=&amp;#34;CustomException&amp;#34;/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> CustomException() { }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Constructor for &amp;lt;see cref=&amp;#34;CustomException&amp;#34;/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param cref=&amp;#34;string&amp;#34; name=&amp;#34;message&amp;#34;&amp;gt;Parameter for message&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> CustomException(&lt;span style="color:#ff7b72">string&lt;/span> message) : &lt;span style="color:#ff7b72">base&lt;/span>(message) { }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Constructor for &amp;lt;see cref=&amp;#34;CustomException&amp;#34;/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param cref=&amp;#34;string&amp;#34; name=&amp;#34;message&amp;#34;&amp;gt;Parameter for message&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param cref=&amp;#34;Exception&amp;#34; name=&amp;#34;inner&amp;#34;&amp;gt;Parameter for inner&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> CustomException(&lt;span style="color:#ff7b72">string&lt;/span> message, Exception inner) : &lt;span style="color:#ff7b72">base&lt;/span>(message, inner) { }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>カスタム例外を作成したら、&lt;code>Exception&lt;/code> の代わりに &lt;code>CustomException&lt;/code> をスローするようにメソッドを更新しましょう。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[HttpGet]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[Route(&amp;#34;GetWithoutExceptionHandler&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> Task GetWithoutExceptionHandler()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> CustomException(&lt;span style="color:#a5d6ff">&amp;#34;This is a custom exception!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>現時点では何も変わりませんが、スタック トレースでは、スローされたオブジェクトが &lt;code>Exception&lt;/code> ではなく &lt;code>CustomException&lt;/code> であることが示されます。スタック トレースの先頭を見てください。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>CustomExceptionHandleDemo.Exceptions.CustomException: This &lt;span style="color:#ff7b72">is&lt;/span> a custom exception!
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at CustomExceptionHandleDemo.Controllers.WeatherForecastController.GetWithoutExceptionHandler()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at lambda_method24(Closure , Object , Object[] )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.AwaitableResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.&amp;lt;InvokeActionMethodAsync&amp;gt;g__Logged|&lt;span style="color:#a5d6ff">12_1&lt;/span>(ControllerActionInvoker invoker)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.&amp;lt;InvokeNextActionFilterAsync&amp;gt;g__Awaited|&lt;span style="color:#a5d6ff">10_0&lt;/span>(ControllerActionInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State&amp;amp; next, Scope&amp;amp; scope, Object&amp;amp; state, Boolean&amp;amp; isCompleted)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>--- End of stack trace &lt;span style="color:#ff7b72">from&lt;/span> previous location ---
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.&amp;lt;InvokeFilterPipelineAsync&amp;gt;g__Awaited|&lt;span style="color:#a5d6ff">20_0&lt;/span>(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.&amp;lt;InvokeAsync&amp;gt;g__Logged|&lt;span style="color:#a5d6ff">17_1&lt;/span>(ResourceInvoker invoker)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.&amp;lt;InvokeAsync&amp;gt;g__Logged|&lt;span style="color:#a5d6ff">17_1&lt;/span>(ResourceInvoker invoker)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Routing.EndpointMiddleware.&amp;lt;Invoke&amp;gt;g__AwaitRequestTask|&lt;span style="color:#a5d6ff">6_0&lt;/span>(Endpoint endpoint, Task requestTask, ILogger logger)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="exceptionfilterattribute-の作成">ExceptionFilterAttribute の作成&lt;/h3>
&lt;p>Microsoft は、例外がスローされた後に例外を処理する方法を提供してくれました。詳細については、&lt;a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.exceptionfilterattribute?view=aspnetcore-7.0">ここ&lt;/a> で確認できます。&lt;/p>
&lt;p>しかし、これまでのところ、彼らが私たちに提供しているドキュメントは次のとおりです。&lt;/p>
&lt;blockquote>
&lt;p>アクションが例外をスローした後に非同期で実行される抽象フィルター。サブクラスは OnException(ExceptionContext) または OnExceptionAsync(ExceptionContext) をオーバーライドする必要がありますが、両方をオーバーライドすることはできません。&lt;/p>&lt;/blockquote>
&lt;p>それでは、作成しましょう。 &lt;code>CustomExceptionFilterAttribute&lt;/code> を作成し、その中で &lt;code>OnException&lt;/code> をオーバーライドします。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Mvc.Filters&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Mvc&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">CustomExceptionHandleDemo.Exceptions&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">CustomExceptionHandleDemo.ExceptionFilterAttributes&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CustomExceptionFilterAttribute&lt;/span> : ExceptionFilterAttribute
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// OnException&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param cref=&amp;#34;ExceptionContext&amp;#34; name=&amp;#34;context&amp;#34;&amp;gt;Parameter for context&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> OnException(ExceptionContext context)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (context.Exception &lt;span style="color:#ff7b72">is&lt;/span> CustomException)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> context.HttpContext.Response.StatusCode = &lt;span style="color:#a5d6ff">500&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> context.Result = &lt;span style="color:#ff7b72">new&lt;/span> ObjectResult(context.Exception.Message);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ご覧のとおり、&lt;code>ExceptionContext&lt;/code> を調べています。例外が &lt;code>CustomException&lt;/code> のタイプである場合、何らかの処理を行います。&lt;/p>
&lt;p>これは、返される応答とステータス コードを更新しています。&lt;/p>
&lt;p>ステータス コードを更新するには、&lt;code>context.HttpContext.Response.StatusCode&lt;/code> を更新する必要があります。結果を返すには、&lt;code>ActionResult&lt;/code> から継承したオブジェクトを与えて &lt;code>context.Result&lt;/code> を更新する必要があります。&lt;/p>
&lt;p>これはフィルターなので、&lt;code>[CustomExceptionFilter]&lt;/code> を追加して何かを追加する必要があることを意味します。&lt;/p>
&lt;h3 id="フィルターの使用">フィルターの使用&lt;/h3>
&lt;p>ここで、持っているメソッドを複製し、このフィルターを追加してアクションを実行しましょう。API エンドポイントは次のようになります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">CustomExceptionHandleDemo.ExceptionFilterAttributes&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">CustomExceptionHandleDemo.Exceptions&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Mvc&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">CustomExceptionHandleDemo.Controllers&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [ApiController]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Route(&amp;#34;[controller]&lt;span style="color:#a5d6ff">&amp;#34;)]
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">WeatherForecastController&lt;/span> : ControllerBase
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> WeatherForecastController()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [HttpGet]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Route(&amp;#34;GetWithoutExceptionHandler&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Task GetWithoutExceptionHandler()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> CustomException(&lt;span style="color:#a5d6ff">&amp;#34;This is a custom exception!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [HttpGet]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Route(&amp;#34;GetWithExceptionHandler&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [CustomExceptionFilter]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Task GetWithExceptionHandler()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> CustomException(&lt;span style="color:#a5d6ff">&amp;#34;This is a custom exception!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ご覧のとおり、 &lt;code>GetWithExceptionHandler&lt;/code> という新しいメソッドがあり、 &lt;code>GetWithoutExceptionHandler&lt;/code> と同じロジックを持っていますが、この場合はフィルター &lt;code>[CustomExceptionFilter]&lt;/code> をメソッドに追加しています。&lt;/p>
&lt;p>メソッドを実行した後の結果は次のとおりです。スタック トレースが表示されなくなったため、画像を表示します。&lt;/p>
&lt;p>&lt;img src="https://imgur.com/a8TSAy2.png"/>これにより、カスタム例外、つまりスローおよび例外をメソッドで使用したときに何が起こるかをオーバーライドするフィルターを作成しました。&lt;/p>
&lt;p>これは、ログを記録したり、何が、いつ、どこでエラーが発生したかを知るなど、さまざまな目的に使用できます。&lt;/p></content:encoded><category>.NET</category><category>API</category></item><item><title>何かを注入するためのより良い方法</title><link>https://emimontesdeoca.github.io/ja/posts/custom-iservicecollection-services/</link><pubDate>Mon, 04 Sep 2023 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/custom-iservicecollection-services/</guid><description>クリーンな IServiceCollection 拡張メソッドを使用して、.NET 依存関係注入の登録を整理します。</description><content:encoded>&lt;p>サービス、リポジトリ、属性など、アプリケーションに挿入するものを構築するときは常に、実際にアプリケーションにサービスを追加する必要があるステップがあります。&lt;/p>
&lt;p>これは常に同じです。&lt;code>Program.cs&lt;/code> に移動し、サービスを挿入するためにファイルの一部に &lt;code>builder.Services.AddScoped&amp;lt;MyService&amp;gt;();&lt;/code> を追加します。&lt;/p>
&lt;p>次のようなもの:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Add services to the container.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddRazorPages();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Repositories&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddScoped&amp;lt;ARepository&amp;gt;(); &lt;span style="color:#8b949e;font-style:italic">// 👀&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddScoped&amp;lt;BRepository&amp;gt;(); &lt;span style="color:#8b949e;font-style:italic">// 👀&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Services&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddScoped&amp;lt;AService&amp;gt;(); &lt;span style="color:#8b949e;font-style:italic">// 👀&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddScoped&amp;lt;BService&amp;gt;(); &lt;span style="color:#8b949e;font-style:italic">// 👀&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddScoped&amp;lt;CService&amp;gt;(); &lt;span style="color:#8b949e;font-style:italic">// 👀&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddScoped&amp;lt;DService&amp;gt;(); &lt;span style="color:#8b949e;font-style:italic">// 👀&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> app = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Configure the HTTP request pipeline.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">if&lt;/span> (!app.Environment.IsDevelopment())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> app.UseExceptionHandler(&lt;span style="color:#a5d6ff">&amp;#34;/Error&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> app.UseHsts();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseHttpsRedirection();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseStaticFiles();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseRouting();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseAuthorization();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapRazorPages();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.Run();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>つまり、それは機能しますが、私は好き嫌いがあり、あまり好きではありません。&lt;/p>
&lt;p>実行したい依存関係の注入が複数あるとします。それらは実際には同じものではありません。私の場合、これは、すべてのリポジトリを含むライブラリ、すべてのサービスを含む別のライブラリ、そして最後に属性を含む別のライブラリを持つようなものになる可能性があります。&lt;/p>
&lt;p>その場合、&lt;code>Program.cs&lt;/code> に追加しなければならない行の量を想像できますか。&lt;/p>
&lt;p>いくつかのサービスを含むライブラリがあるとします。すべてのサービスを含めたい場合は、&lt;code>IServiceCollection&lt;/code> を操作する必要があります。&lt;/p>
&lt;p>そこで、&lt;code>IServiceCollection&lt;/code> を返す &lt;code>AddServices&lt;/code> という &lt;code>static&lt;/code> メソッドを持つ &lt;code>static class&lt;/code> を作成します。&lt;/p>
&lt;p>この場合、名前は &lt;code>IServiceCollectionServicesExtensions&lt;/code> になります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// IServiceCollectionServicesExtensions class&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">IServiceCollectionServicesExtensions&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// AddCoreServices&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param cref=&amp;#34;IServiceCollection&amp;#34; name=&amp;#34;services&amp;#34;&amp;gt;Parameter for &amp;lt;see cref=&amp;#34;IServiceCollection&amp;#34;/&amp;gt;&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;returns&amp;gt;An object of type &amp;lt;see cref=&amp;#34;IServiceCollection&amp;#34;/&amp;gt;&amp;lt;/returns&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> IServiceCollection AddServices(&lt;span style="color:#ff7b72">this&lt;/span> IServiceCollection services)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> services
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddScoped&amp;lt;AService&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddScoped&amp;lt;BService&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddScoped&amp;lt;CService&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddScoped&amp;lt;DService&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> services;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>それだけでなく、このサービスで使用されているいくつかのリポジトリを含む別のライブラリもあるので、同じようにしましょう。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// IServiceCollectionServicesExtensions class&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">IServiceCollectionServicesExtensions&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// AddCoreServices&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param cref=&amp;#34;IServiceCollection&amp;#34; name=&amp;#34;services&amp;#34;&amp;gt;Parameter for &amp;lt;see cref=&amp;#34;IServiceCollection&amp;#34;/&amp;gt;&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;returns&amp;gt;An object of type &amp;lt;see cref=&amp;#34;IServiceCollection&amp;#34;/&amp;gt;&amp;lt;/returns&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> IServiceCollection AddRepositories(&lt;span style="color:#ff7b72">this&lt;/span> IServiceCollection services)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> services
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddScoped&amp;lt;ARepository&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .AddScoped&amp;lt;BRepository&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> services;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これで、注入するリポジトリとサービス メソッドが作成されましたが、それらはどのように使用するのでしょうか?&lt;/p>
&lt;p>&lt;code>Program.cs&lt;/code> に戻り、以下を追加しましょう。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Add services to the container.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddRazorPages();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Repositories&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddRepositories(); &lt;span style="color:#8b949e;font-style:italic">// 👀&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Services&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddServices(); &lt;span style="color:#8b949e;font-style:italic">// 👀&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> app = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Configure the HTTP request pipeline.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">if&lt;/span> (!app.Environment.IsDevelopment())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> app.UseExceptionHandler(&lt;span style="color:#a5d6ff">&amp;#34;/Error&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> app.UseHsts();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseHttpsRedirection();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseStaticFiles();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseRouting();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseAuthorization();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapRazorPages();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.Run();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これでかなりすっきりしましたね。これで、いくつかのサービスとリポジトリを正常に挿入できましたが、見た目はさらに良くなり、実際に外部ライブラリに注入したものが得られました。&lt;/p></content:encoded><category>.NET</category><category>Docker</category></item><item><title>.NET API の属性による依存関係の注入</title><link>https://emimontesdeoca.github.io/ja/posts/api-di-attributes/</link><pubDate>Tue, 22 Aug 2023 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/api-di-attributes/</guid><description>ActionAttribute の代わりに TypeFilterAttribute を使用して、.NET API アクション フィルターで依存関係の挿入を有効にします。</description><content:encoded>&lt;p>依存関係の注入は、おそらく現時点で .NET にある最高の機能の 1 つです。どのような場合でも、これを使用しないわけはありません。そのため、あなたが私と同じであれば、作成するすべての実装にそれを追加したいと思うでしょう。&lt;/p>
&lt;p>公式 Microsoft の &lt;a href="https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-3.1">ドキュメント&lt;/a> によるフィルター:&lt;/p>
&lt;blockquote>
&lt;p>ASP.NET Core のフィルターを使用すると、要求処理パイプラインの特定のステージの前後でコードを実行できます。&lt;/p>
&lt;p>組み込みフィルターは次のようなタスクを処理します。&lt;/p>
&lt;ul>
&lt;li>認可。ユーザーが認可されていないリソースへのアクセスを防止します。&lt;/li>
&lt;li>応答キャッシュ。要求パイプラインを短絡して、キャッシュされた応答を返します。&lt;/li>
&lt;/ul>
&lt;p>横断的な問題に対処するためにカスタム フィルターを作成できます。横断的な問題の例としては、エラー処理、キャッシュ、構成、認可、ロギングなどが挙げられます。フィルターはコードの重複を避けます。&lt;/p>&lt;/blockquote>
&lt;p>私は API をよく使うのですが、すべてのリクエスト、またはほぼすべてのリクエストを実行する必要があるものがいくつかあります。そのため、理想的には API に加えて依存関係の注入を実行したいと考えています。&lt;/p>
&lt;p>しかし、これは時々ちょっとしたトリックです。&lt;code>ActionAttribute&lt;/code> から継承したい場合は希望どおりに動作しないため、&lt;code>TypeFilterAttribute&lt;/code> を操作する必要があります。これにより、&lt;code>OnActionExecutionAsync&lt;/code> をオーバーライドするときに処理を行うことができます。&lt;/p>
&lt;p>通常、ログを記録するためにこのフィルターを作成するので、それを例として使用します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// LoggedQueryAttribute class&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">LoggedQueryTypeFilterAttribute&lt;/span> : TypeFilterAttribute
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Constructor for &amp;lt;see cref=&amp;#34;LoggedQueryTypeFilterAttribute&amp;#34;/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> LoggedQueryTypeFilterAttribute() : &lt;span style="color:#ff7b72">base&lt;/span>(&lt;span style="color:#ff7b72">typeof&lt;/span>(LoggedQueryFilter))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// LoggedQueryFilter class&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">LoggedQueryFilter&lt;/span> : IAsyncActionFilter
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;see cref=&amp;#34;_loggingService&amp;#34;/&amp;gt; object&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> LoggingService _loggingService;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Constructor for &amp;lt;see cref=&amp;#34;LoggedQueryFilter&amp;#34;/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param cref=&amp;#34;LoggingService&amp;#34; name=&amp;#34;loggingService&amp;#34;&amp;gt;Parameter for loggingService&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> LoggedQueryFilter(LoggingService loggingService)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _loggingService = loggingService;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// OnActionExecutionAsync&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param cref=&amp;#34;ActionExecutingContext&amp;#34; name=&amp;#34;context&amp;#34;&amp;gt;Parameter for context&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param cref=&amp;#34;ActionExecutionDelegate&amp;#34; name=&amp;#34;next&amp;#34;&amp;gt;Parameter for next&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Get properties&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> properties = (Request)context.ActionArguments.First().Value!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Get call from context&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> call = context.HttpContext.Request.Path.Value!;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Logging&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _loggingService.LogCustomEvent(call);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Continue call&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> next();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ロジックは非常に単純で、&lt;code>context.ActionArguments.First().Value&lt;/code> で &lt;code>context&lt;/code> オブジェクトにアクセスして本体を取得し、&lt;code>context.HttpContext.Request.Path.Value&lt;/code> でメソッド呼び出しも取得します。&lt;/p>
&lt;p>次に、サービスからメソッドを呼び出すだけです。この場合は &lt;code> _loggingService.LogCustomEvent(call)&lt;/code> です。&lt;/p>
&lt;p>次に、パイプラインを続行する必要があるため、&lt;code>await next();&lt;/code> を呼び出す必要があります。&lt;/p>
&lt;p>これは属性に関するものですが、この属性を実際にメソッドに含める必要があります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[LoggedQueryTypeFilterAttribute]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> ActionResult&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; TestFilter()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Ok(&lt;span style="color:#a5d6ff">&amp;#34;Hello world!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>気に入っていただければ幸いです。ご質問がある場合、または連絡したい場合は、ためらわずにご連絡ください。&lt;/p></content:encoded><category>.NET</category></item><item><title>外部ライブラリを使用した Swagger ドキュメント</title><link>https://emimontesdeoca.github.io/ja/posts/swagger-libraries-documentation/</link><pubDate>Fri, 17 Feb 2023 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/swagger-libraries-documentation/</guid><description>Swagger を有効にして、外部 .NET クラス ライブラリで定義されたモデルから XML ドキュメントを表示します。</description><content:encoded>&lt;p>将来の場合に備えて、複数のライブラリを使用してコードを分割することは良いことです。個人的には、ロジックを分割して他のプロジェクトやプロジェクトの一部で再利用できるのであれば、そうするのが大好きです。&lt;/p>
&lt;p>少なくとも私にとっては、&lt;code>Console application&lt;/code>、&lt;code>Blazor application&lt;/code>、または &lt;code>API&lt;/code> で参照および使用できるように、&lt;code>Entity Framework&lt;/code> を含むデータ モデルを分割することが必須です。&lt;/p>
&lt;p>私がどのように作業を行っているかの例を見てみましょう。これは、いくつかのアプリケーションとすべてのモデルを含む共有ライブラリを含むプロジェクトのスクリーンショットです。&lt;/p>
&lt;img src="https://imgur.com/gdg2nn3.png">
&lt;p>これは、各プロパティとクラスに関する &lt;code>summary&lt;/code> のドキュメントを含む &lt;code>API&lt;/code> プロジェクトから移動したクラスの例です。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">CustomLibrariesDocumentation.Models&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// WeatherForecast class&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">WeatherForecast&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Gets or sets the Date&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> DateOnly Date { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Gets or sets the TemperatureC&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> TemperatureC { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Gets or sets the TemperatureF&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> TemperatureF =&amp;gt; &lt;span style="color:#a5d6ff">32&lt;/span> + (&lt;span style="color:#ff7b72">int&lt;/span>)(TemperatureC / &lt;span style="color:#a5d6ff">0.5556&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Gets or sets the Summary&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string?&lt;/span> Summary { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これを &lt;code>Console application&lt;/code> に参照し、モデルの使用を開始すると、ドキュメントが表示されます。これは Visual Studio の標準機能であるため、次のように正常に動作します。&lt;/p>
&lt;img src="https://imgur.com/Djf1MeO.png">
&lt;p>しかし、&lt;code>API&lt;/code> を参照して &lt;code>Swagger&lt;/code> に移動すると、概要ドキュメントは存在しません。&lt;/p>
&lt;img src="https://imgur.com/3Dg2Xpm.png">
&lt;p>これをどうやって修正すればよいのでしょうか？&lt;/p>
&lt;p>まず、ライブラリで XML コメントを有効にする必要があります。これを行うには、プロジェクト設定を更新し、「:」を有効にする必要があります。&lt;/p>
&lt;img src="https://imgur.com/R1j8SfX.png">
&lt;p>これにより、ここに示すように、ファイル拡張子 &lt;code>xml&lt;/code> で終わるいくつかのファイルがビルド フォルダーに生成されます。&lt;/p>
&lt;img src="https://imgur.com/O8ywjr2.png">
&lt;p>これが完了したので、これらのファイルを読み込むために &lt;code>Program.cs&lt;/code> にいくつかの要素を追加する必要があります。これは、デフォルトでは、そのファイルが存在するプロジェクトの XML 定義のみをロードするためです。&lt;/p>
&lt;p>&lt;code>AddSwaggerGen&lt;/code> 内にある &lt;code>IncludeXmlComments&lt;/code> と呼ばれるメソッドを使用します。&lt;/p>
&lt;p>すべての &lt;code>xml&lt;/code> ファイルがある場合、それらを Swagger に強制的にロードするという考えです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>builder.Services.AddSwaggerGen(s =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Comments&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> allXmlFiles = Directory.GetFiles(AppContext.BaseDirectory, &lt;span style="color:#a5d6ff">&amp;#34;*.xml&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">string&lt;/span> xmlFiles &lt;span style="color:#ff7b72">in&lt;/span> allXmlFiles)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> s.IncludeXmlComments(xmlFiles);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>簡単に言えば、ビルド ディレクトリから &lt;code>xml&lt;/code> ファイルを取得し、 &lt;code>IncludeXmlComments&lt;/code> メソッドを使用して追加します。&lt;/p>
&lt;p>ここで、&lt;code>API&lt;/code> を再度ロードし、ドキュメントが表示されるかどうかを確認します。&lt;/p>
&lt;img src="https://imgur.com/uNVNswn.png">
&lt;p>そして、ドキュメントを見ることができることがわかります。&lt;/p>
&lt;p>お役に立てば幸いです。ご質問がございましたら、お気軽にお問い合わせください。&lt;/p></content:encoded><category>.NET</category><category>Blazor</category><category>API</category></item><item><title>Git のローカル ブランチをクリーンアップする</title><link>https://emimontesdeoca.github.io/ja/posts/cleanup-local-branches/</link><pubDate>Mon, 30 Jan 2023 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/cleanup-local-branches/</guid><description>単一の PowerShell コマンドを使用して、メイン ブランチを除くすべてのローカル Git ブランチを削除します。</description><content:encoded>&lt;p>地元の支店が多すぎる状態になったことはありますか?私たちは機能、バグ、雑用ごとにブランチを使用するため、このようなことがよく起こります。&lt;/p>
&lt;p>結局このようなものになり、しばらくすると非常に汚くなります&lt;/p>
&lt;img src="https://imgur.com/W3OGJE7.png">
&lt;p>それは本当にイライラするので、このクリーンアップを行うためにコンソールで実行できる簡単なコマンドを共有します。&lt;/p>
&lt;p>このコマンドは、&lt;a href="https://stackoverflow.com/users/529612/robert-corvus">Robert Corvus&lt;/a> による次の &lt;a href="https://stackoverflow.com/a/56671336/7823470">answer&lt;/a> の Stack Overflow で見つかりました。これは Powerhsell で実行されるバージョンです。&lt;/p>
&lt;p>&lt;strong>このコマンドを実行するときは、変更内容が失われる可能性があるので注意してください&lt;/strong>&lt;/p>
&lt;p>実行する前に、&lt;code>MY_MASTER_BRANCH_NAME&lt;/code> をメイン ブランチに更新することを忘れないでください。メイン ブランチは、私が使用している &lt;code>master&lt;/code>、またはデフォルトで提供される &lt;code>main&lt;/code> と呼ばれる新しいブランチです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-powershell" data-lang="powershell">&lt;span style="display:flex;">&lt;span>git branch | %{ &lt;span style="color:#79c0ff">$_&lt;/span>.&lt;span style="color:#79c0ff">Trim&lt;/span>() } | ?{ &lt;span style="color:#79c0ff">$_&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">-ne&lt;/span> &lt;span style="color:#a5d6ff">&amp;#39;MY_MASTER_BRANCH_NAME&amp;#39;&lt;/span> } | %{ git branch -D &lt;span style="color:#79c0ff">$_&lt;/span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>このコマンドを実行すると、次のような出力が得られます&lt;/p>
&lt;img src="https://imgur.com/VJn89OZ.png">
&lt;p>お役に立てば幸いです!&lt;/p></content:encoded><category>Git</category></item><item><title>ASP.NET Core 7 での ID のルートの更新</title><link>https://emimontesdeoca.github.io/ja/posts/identity-url-change/</link><pubDate>Mon, 23 Jan 2023 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/identity-url-change/</guid><description>デフォルトの ASP.NET Core ID ログインをカスタマイズし、ID ページをスキャフォールディングすることで URL を登録します。</description><content:encoded>&lt;p>Microsoft がなぜこれほど珍しい名前を付けたのか不思議に思いませんか?彼らは実際にはそれほど素晴らしいことをしていないのではないかといつも思っていますが、まあ、それがそういうものなのです！&lt;/p>
&lt;p>これの良い点は、開発中にほとんどすべてを変更できることです。&lt;/p>
&lt;p>ページにアクセスし、登録またはログイン プロセスを実行するだけで、これが ASP.NET Web プロジェクトであることにすぐに気づいたことはありますか?&lt;/p>
&lt;img src="https://imgur.com/8NMNHGp.png">
&lt;p>作成したプロジェクトにはデフォルトでログインと登録プロセスを実行するための URL が設定されているため、これはよく起こります (他のページもたくさんあります)。&lt;/p>
&lt;p>そこで、このチュートリアルでは、プロジェクトの見栄えを良くするために、これらの URL を更新する方法を説明します。&lt;/p>
&lt;h2 id="デフォルトの動作">デフォルトの動作&lt;/h2>
&lt;p>Blazor プロジェクトを作成し、それを Identity で使用することにすると、次のように表示されます。&lt;/p>
&lt;img align="center" src="https://i.imgur.com/2W8Oou9.png">
&lt;p>そして、ログインまたは登録プロセスを実行しようとすると、&lt;code>/Identity/Account/Login&lt;/code> または &lt;code>/Identity/Account/Register&lt;/code> のいずれかになります。&lt;/p>
&lt;p>しかし、これらの URL を実際に異なるものに更新できると言ったらどうなるでしょうか?&lt;/p>
&lt;h2 id="login--および-register-ページのスキャフォールディング">&lt;code>Login &lt;/code> および &lt;code>Register&lt;/code> ページのスキャフォールディング&lt;/h2>
&lt;p>これらのページを更新するために、Microsoft はページを非表示にしますが、すぐにページを足場にして、必要な変更を行うことができます。&lt;/p>
&lt;p>これを行うには、次のように、プロジェクトのコンテキスト メニューで &lt;code>Add Scaffolded Item&lt;/code> に移動する必要があります。&lt;/p>
&lt;img src="https://imgur.com/F3C4C9b.png">
&lt;p>次にモーダルがポップアップするので、&lt;code>Identity&lt;/code> を 2 回選択して、&lt;code>Add&lt;/code> をクリックする必要があります。&lt;/p>
&lt;img src="https://imgur.com/tZUqUlY.png">
&lt;p>このモーダルの後に、さらに別のモーダルがポップアップ表示され、アイデンティティ全体から更新するページを選択できます。更新できるページはたくさんありますが、ここでは &lt;code>Account/Login&lt;/code> と &lt;code>Account/Register&lt;/code> に焦点を当てます。&lt;/p>
&lt;img src="https://imgur.com/BS6ZLas.png">
&lt;p>ここで少し動作させてから、ソリューション エクスプローラーを確認すると、いくつかの新しいファイルが見つかります。&lt;/p>
&lt;img src="https://imgur.com/5lWHLyI.png">
&lt;p>これらの新しいファイルは、ID を追加するためにプロジェクトを選択したときに、ASP.NET がプロジェクトに追加するログイン ページと登録ページです。&lt;/p>
&lt;h2 id="urlを更新しています">URLを更新しています&lt;/h2>
&lt;p>お気づきかと思いますが、拡張子が &lt;code>cshtml&lt;/code> であるため、このファイルは razor ファイルです。そのため、ディレクティブを使用してページの URL を更新するだけです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/login&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@model LoginModel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ViewData[&lt;span style="color:#a5d6ff">&amp;#34;Title&amp;#34;&lt;/span>] = &lt;span style="color:#a5d6ff">&amp;#34;Log in&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h1&amp;gt;@ViewData[&lt;span style="color:#a5d6ff">&amp;#34;Title&amp;#34;&lt;/span>]&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;row&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;col-md-4&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;section&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;form id=&lt;span style="color:#a5d6ff">&amp;#34;account&amp;#34;&lt;/span> method=&lt;span style="color:#a5d6ff">&amp;#34;post&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;h2&amp;gt;Use a local account to log &lt;span style="color:#ff7b72">in&lt;/span>.&amp;lt;/h2&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;hr /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div asp-validation-summary=&lt;span style="color:#a5d6ff">&amp;#34;ModelOnly&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;text-danger&amp;#34;&lt;/span> role=&lt;span style="color:#a5d6ff">&amp;#34;alert&amp;#34;&lt;/span>&amp;gt;&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;form-floating mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;input asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Email&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> autocomplete=&lt;span style="color:#a5d6ff">&amp;#34;username&amp;#34;&lt;/span> aria-required=&lt;span style="color:#a5d6ff">&amp;#34;true&amp;#34;&lt;/span> placeholder=&lt;span style="color:#a5d6ff">&amp;#34;name@example.com&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Email&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-label&amp;#34;&lt;/span>&amp;gt;Email&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;span asp-validation-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Email&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;text-danger&amp;#34;&lt;/span>&amp;gt;&amp;lt;/span&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;form-floating mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;input asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Password&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> autocomplete=&lt;span style="color:#a5d6ff">&amp;#34;current-password&amp;#34;&lt;/span> aria-required=&lt;span style="color:#a5d6ff">&amp;#34;true&amp;#34;&lt;/span> placeholder=&lt;span style="color:#a5d6ff">&amp;#34;password&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Password&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-label&amp;#34;&lt;/span>&amp;gt;Password&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;span asp-validation-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Password&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;text-danger&amp;#34;&lt;/span>&amp;gt;&amp;lt;/span&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;checkbox mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.RememberMe&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-label&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;input class=&lt;span style="color:#a5d6ff">&amp;#34;form-check-input&amp;#34;&lt;/span> asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.RememberMe&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @Html.DisplayNameFor(m =&amp;gt; m.Input.RememberMe)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button id=&lt;span style="color:#a5d6ff">&amp;#34;login-submit&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;submit&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;w-100 btn btn-lg btn-primary&amp;#34;&lt;/span>&amp;gt;Log &lt;span style="color:#ff7b72">in&lt;/span>&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;a id=&lt;span style="color:#a5d6ff">&amp;#34;forgot-password&amp;#34;&lt;/span> asp-page=&lt;span style="color:#a5d6ff">&amp;#34;./ForgotPassword&amp;#34;&lt;/span>&amp;gt;Forgot your password?&amp;lt;/a&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;a asp-page=&lt;span style="color:#a5d6ff">&amp;#34;./Register&amp;#34;&lt;/span> asp-route-returnUrl=&lt;span style="color:#a5d6ff">&amp;#34;@Model.ReturnUrl&amp;#34;&lt;/span>&amp;gt;Register &lt;span style="color:#ff7b72">as&lt;/span> a &lt;span style="color:#ff7b72">new&lt;/span> user&amp;lt;/a&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;a id=&lt;span style="color:#a5d6ff">&amp;#34;resend-confirmation&amp;#34;&lt;/span> asp-page=&lt;span style="color:#a5d6ff">&amp;#34;./ResendEmailConfirmation&amp;#34;&lt;/span>&amp;gt;Resend email confirmation&amp;lt;/a&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/form&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/section&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;col-md-6 col-md-offset-2&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;section&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;h3&amp;gt;Use another service to log &lt;span style="color:#ff7b72">in&lt;/span>.&amp;lt;/h3&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;hr /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> ((Model.ExternalLogins?.Count ?? &lt;span style="color:#a5d6ff">0&lt;/span>) == &lt;span style="color:#a5d6ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> There are no external authentication services configured. See &lt;span style="color:#ff7b72">this&lt;/span> &amp;lt;a href=&lt;span style="color:#a5d6ff">&amp;#34;https://go.microsoft.com/fwlink/?LinkID=532715&amp;#34;&lt;/span>&amp;gt;article
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> about setting up &lt;span style="color:#ff7b72">this&lt;/span> ASP.NET application to support logging &lt;span style="color:#ff7b72">in&lt;/span> via external services&amp;lt;/a&amp;gt;.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;form id=&lt;span style="color:#a5d6ff">&amp;#34;external-account&amp;#34;&lt;/span> asp-page=&lt;span style="color:#a5d6ff">&amp;#34;./ExternalLogin&amp;#34;&lt;/span> asp-route-returnUrl=&lt;span style="color:#a5d6ff">&amp;#34;@Model.ReturnUrl&amp;#34;&lt;/span> method=&lt;span style="color:#a5d6ff">&amp;#34;post&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-horizontal&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @foreach (&lt;span style="color:#ff7b72">var&lt;/span> provider &lt;span style="color:#ff7b72">in&lt;/span> Model.ExternalLogins!)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button type=&lt;span style="color:#a5d6ff">&amp;#34;submit&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;btn btn-primary&amp;#34;&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;provider&amp;#34;&lt;/span> &lt;span style="color:#ff7b72">value&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;@provider.Name&amp;#34;&lt;/span> title=&lt;span style="color:#a5d6ff">&amp;#34;Log in using your @provider.DisplayName account&amp;#34;&lt;/span>&amp;gt;@provider.DisplayName&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/form&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/section&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@section Scripts {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#ff7b72">partial&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;_ValidationScriptsPartial&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ご覧のとおり、ほとんどの内容は同じですが、クラスの最初の行を見ると、以前のものを更新しています。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>に&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/login&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>まあ、簡単でしたね。それでは、簡単なテストを行って、それが機能するかどうかを確認してみましょう。&lt;/p>
&lt;p>まず最初に、最初に表示されているデフォルトのページに移動して、それがまだ機能しているかどうかを確認してみましょう。&lt;/p>
&lt;img src="https://imgur.com/ngbRNaG.png">
&lt;p>それは違います！それでは、新しい URL &lt;code>/login&lt;/code> をテストしてみましょう。&lt;/p>
&lt;img src="https://imgur.com/R067PnF.png">
&lt;p>そしてそれはうまくいきます!!&lt;/p>
&lt;p>次に、レジスタに対して同じことを行い、そのページを更新して、必要なパスを &lt;code>@page&lt;/code> ディレクティブに追加して、テストを行ってみましょう。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/register&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@model RegisterModel
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">@&lt;/span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ViewData[&lt;span style="color:#a5d6ff">&amp;#34;Title&amp;#34;&lt;/span>] = &lt;span style="color:#a5d6ff">&amp;#34;Register&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h1&amp;gt;@ViewData[&lt;span style="color:#a5d6ff">&amp;#34;Title&amp;#34;&lt;/span>]&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;row&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;col-md-4&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;form id=&lt;span style="color:#a5d6ff">&amp;#34;registerForm&amp;#34;&lt;/span> asp-route-returnUrl=&lt;span style="color:#a5d6ff">&amp;#34;@Model.ReturnUrl&amp;#34;&lt;/span> method=&lt;span style="color:#a5d6ff">&amp;#34;post&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;h2&amp;gt;Create a &lt;span style="color:#ff7b72">new&lt;/span> account.&amp;lt;/h2&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;hr /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div asp-validation-summary=&lt;span style="color:#a5d6ff">&amp;#34;ModelOnly&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;text-danger&amp;#34;&lt;/span> role=&lt;span style="color:#a5d6ff">&amp;#34;alert&amp;#34;&lt;/span>&amp;gt;&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;form-floating mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;input asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Email&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> autocomplete=&lt;span style="color:#a5d6ff">&amp;#34;username&amp;#34;&lt;/span> aria-required=&lt;span style="color:#a5d6ff">&amp;#34;true&amp;#34;&lt;/span> placeholder=&lt;span style="color:#a5d6ff">&amp;#34;name@example.com&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Email&amp;#34;&lt;/span>&amp;gt;Email&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;span asp-validation-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Email&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;text-danger&amp;#34;&lt;/span>&amp;gt;&amp;lt;/span&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;form-floating mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;input asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Password&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> autocomplete=&lt;span style="color:#a5d6ff">&amp;#34;new-password&amp;#34;&lt;/span> aria-required=&lt;span style="color:#a5d6ff">&amp;#34;true&amp;#34;&lt;/span> placeholder=&lt;span style="color:#a5d6ff">&amp;#34;password&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Password&amp;#34;&lt;/span>&amp;gt;Password&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;span asp-validation-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.Password&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;text-danger&amp;#34;&lt;/span>&amp;gt;&amp;lt;/span&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;form-floating mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;input asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.ConfirmPassword&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> autocomplete=&lt;span style="color:#a5d6ff">&amp;#34;new-password&amp;#34;&lt;/span> aria-required=&lt;span style="color:#a5d6ff">&amp;#34;true&amp;#34;&lt;/span> placeholder=&lt;span style="color:#a5d6ff">&amp;#34;password&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;label asp-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.ConfirmPassword&amp;#34;&lt;/span>&amp;gt;Confirm Password&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;span asp-validation-&lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;Input.ConfirmPassword&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;text-danger&amp;#34;&lt;/span>&amp;gt;&amp;lt;/span&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button id=&lt;span style="color:#a5d6ff">&amp;#34;registerSubmit&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;submit&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;w-100 btn btn-lg btn-primary&amp;#34;&lt;/span>&amp;gt;Register&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/form&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;col-md-6 col-md-offset-2&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;section&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;h3&amp;gt;Use another service to register.&amp;lt;/h3&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;hr /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f85149">@&lt;/span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> ((Model.ExternalLogins?.Count ?? &lt;span style="color:#a5d6ff">0&lt;/span>) == &lt;span style="color:#a5d6ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> There are no external authentication services configured. See &lt;span style="color:#ff7b72">this&lt;/span> &amp;lt;a href=&lt;span style="color:#a5d6ff">&amp;#34;https://go.microsoft.com/fwlink/?LinkID=532715&amp;#34;&lt;/span>&amp;gt;article
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> about setting up &lt;span style="color:#ff7b72">this&lt;/span> ASP.NET application to support logging &lt;span style="color:#ff7b72">in&lt;/span> via external services&amp;lt;/a&amp;gt;.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;form id=&lt;span style="color:#a5d6ff">&amp;#34;external-account&amp;#34;&lt;/span> asp-page=&lt;span style="color:#a5d6ff">&amp;#34;./ExternalLogin&amp;#34;&lt;/span> asp-route-returnUrl=&lt;span style="color:#a5d6ff">&amp;#34;@Model.ReturnUrl&amp;#34;&lt;/span> method=&lt;span style="color:#a5d6ff">&amp;#34;post&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-horizontal&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @foreach (&lt;span style="color:#ff7b72">var&lt;/span> provider &lt;span style="color:#ff7b72">in&lt;/span> Model.ExternalLogins!)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;button type=&lt;span style="color:#a5d6ff">&amp;#34;submit&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;btn btn-primary&amp;#34;&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;provider&amp;#34;&lt;/span> &lt;span style="color:#ff7b72">value&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;@provider.Name&amp;#34;&lt;/span> title=&lt;span style="color:#a5d6ff">&amp;#34;Log in using your @provider.DisplayName account&amp;#34;&lt;/span>&amp;gt;@provider.DisplayName&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/form&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/section&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@section Scripts {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#ff7b72">partial&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;_ValidationScriptsPartial&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>上部を &lt;code>@page &amp;quot;/login&amp;quot;&lt;/code> で更新し、それが機能するかどうかをテストします。&lt;/p>
&lt;img src="https://imgur.com/M7KakaF.png">
&lt;p>も動作します!!&lt;/p>
&lt;h2 id="以上ですこの-url-を更新する方法を学んでいただければ幸いです主な理由は一部のプロジェクトで-url-を特定の方法で実行すると-id-が違って見えるためですそれは最悪です-笑">以上ですこの URL を更新する方法を学んでいただければ幸いです。主な理由は、一部のプロジェクトで URL を特定の方法で実行すると ID が違って見えるためです。それは最悪です (笑)。&lt;/h2>
&lt;p>何か必要なことがあれば、私にツイートするかメールを送ってください。手を差し伸べようと思います。&lt;/p></content:encoded><category>.NET</category><category>Blazor</category></item><item><title>.NET 6 Core API のカスタム属性</title><link>https://emimontesdeoca.github.io/ja/posts/custom-attributes-net-6-core-api/</link><pubDate>Fri, 09 Dec 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/custom-attributes-net-6-core-api/</guid><description>カスタム ActionFilterAttribute クラスを作成して、.NET 6 Core API の要求ヘッダーを検証します。</description><content:encoded>&lt;p>カスタム属性は非常に使いやすく、私はごく最近使い始めました。カスタム属性を使用すると、カスタム属性を 1 つ作成し、コントローラー、クラス、またはメソッド自体で再利用できるからです。&lt;/p>
&lt;p>これらは、ヘッダーのチェックなどのセキュリティ関連の作業を行う場合や、絶対に必要なパラメーターの値を確認する場合に非常に役立ちます。&lt;/p>
&lt;p>私の場合は、.NET Core API プロジェクトでこれを使用し、すべてのリクエストに特定のヘッダーが含まれているかどうかを確認します。&lt;/p>
&lt;h1 id="headercheckattribute">HeaderCheckAttribute&lt;/h1>
&lt;p>クールな .NET Core API を作成したら、フォルダーを使用するのが好きなので、内容を保存するフォルダーを作成しましょう。&lt;/p>
&lt;img src="https://i.imgur.com/i2VKbZN.png"/>
&lt;p>次に、ロジックを &lt;code>HeaderCheckAttribute&lt;/code> クラスに追加します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Mvc&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Mvc.Filters&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">DotNet6CustomAttribute.Attributes&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">HeaderCheckAttribute&lt;/span> : ActionFilterAttribute
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> OnActionExecuting(ActionExecutingContext context)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Get all headers&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> headers = context.HttpContext.Request.Headers;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Check if headers has x-dotnet-6-custom-attribute&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (!headers.ContainsKey(&lt;span style="color:#a5d6ff">&amp;#34;x-dotnet-6-custom-attribute&amp;#34;&lt;/span>))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> context.Result = &lt;span style="color:#ff7b72">new&lt;/span> BadRequestObjectResult(&lt;span style="color:#a5d6ff">&amp;#34;The header x-dotnet-6-custom-attribute is missing&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span> &lt;span style="color:#ff7b72">if&lt;/span> (&lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrEmpty(headers[&lt;span style="color:#a5d6ff">&amp;#34;x-dotnet-6-custom-attribute&amp;#34;&lt;/span>]))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> context.Result = &lt;span style="color:#ff7b72">new&lt;/span> BadRequestObjectResult(&lt;span style="color:#a5d6ff">&amp;#34;The header x-dotnet-6-custom-attribute can&amp;#39;t be null or empty&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">base&lt;/span>.OnActionExecuting(context);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>基本的なロジックは、まずキー &lt;code>x-dotnet-6-custom-attribute&lt;/code> を持つヘッダーをチェックし、存在する場合は値があるかどうかをチェックします。&lt;/p>
&lt;p>これらの式が両方とも true の場合、特定のメッセージを含む &lt;code>BadRequestObjectResult&lt;/code> が返されます。&lt;/p>
&lt;h1 id="コントローラーに追加する">コントローラーに追加する&lt;/h1>
&lt;p>このロジックは複数の場所に追加することも、コントローラー全体に直接追加することも、一部のメソッドに追加することもできます。最初にメソッドに追加し、次にコントローラー全体に追加します。&lt;/p>
&lt;p>それでは、&lt;code>WeatherForecastController&lt;/code> クラスをそれらで装飾しましょう。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">DotNet6CustomAttribute.Attributes&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Mvc&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">DotNet6CustomAttribute.Controllers&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [ApiController]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Route(&amp;#34;[controller]&lt;span style="color:#a5d6ff">&amp;#34;)]
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">&lt;/span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">WeatherForecastController&lt;/span> : ControllerBase
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>[] Summaries = &lt;span style="color:#ff7b72">new&lt;/span>[]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;Freezing&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Bracing&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Chilly&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Cool&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Mild&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Warm&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Balmy&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Hot&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Sweltering&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Scorching&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> ILogger&amp;lt;WeatherForecastController&amp;gt; _logger;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> WeatherForecastController(ILogger&amp;lt;WeatherForecastController&amp;gt; logger)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _logger = logger;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [HttpGet]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Route(&amp;#34;GetWeatherForecastWithCheck&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [HeaderCheckAttribute]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> IEnumerable&amp;lt;WeatherForecast&amp;gt; GetWithCheck()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Enumerable.Range(&lt;span style="color:#a5d6ff">1&lt;/span>, &lt;span style="color:#a5d6ff">5&lt;/span>).Select(index =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span> WeatherForecast
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Date = DateTime.Now.AddDays(index),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> TemperatureC = Random.Shared.Next(-&lt;span style="color:#a5d6ff">20&lt;/span>, &lt;span style="color:#a5d6ff">55&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Summary = Summaries[Random.Shared.Next(Summaries.Length)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToArray();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [HttpGet]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Route(&amp;#34;GetWeatherForecastWithoutCheck&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> IEnumerable&amp;lt;WeatherForecast&amp;gt; GetWithoutCheck()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Enumerable.Range(&lt;span style="color:#a5d6ff">1&lt;/span>, &lt;span style="color:#a5d6ff">5&lt;/span>).Select(index =&amp;gt; &lt;span style="color:#ff7b72">new&lt;/span> WeatherForecast
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Date = DateTime.Now.AddDays(index),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> TemperatureC = Random.Shared.Next(-&lt;span style="color:#a5d6ff">20&lt;/span>, &lt;span style="color:#a5d6ff">55&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Summary = Summaries[Random.Shared.Next(Summaries.Length)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToArray();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>プロジェクトを実行しましょう!&lt;/p>
&lt;img src="https://i.imgur.com/ZvO4LnE.png"/>
&lt;p>そこには &lt;code>GetWeatherForecastWithCheck&lt;/code> と &lt;code>GetWeatherForecastWithoutCheck&lt;/code> という 2 つの関数があります。そのうちの 1 つは失敗し、もう 1 つは失敗しません。しかし、Swagger で確認してみましょう。&lt;/p>
&lt;img src="https://i.imgur.com/x1yRb9j.png"/>
&lt;img src="https://i.imgur.com/XiO4GC9.png">
&lt;p>ご覧のとおり、一方はメッセージとともに 400 エラーを返し、もう一方は値を返します。これを完全にテストするために、Postman を実行してヘッダーを追加し、&lt;code>GetWeatherForecastWithCheck&lt;/code> を使用してデータも確認できるようにします。&lt;/p>
&lt;p>#郵便屋さん&lt;/p>
&lt;p>Postman で実行中、ヘッダーを追加すると、ヘッダーは提供していますが、値が存在しないため、エラー メッセージが変更されていることがわかります。&lt;/p>
&lt;img src="https://i.imgur.com/JHP8ZXZ.png"/>
&lt;p>それに値を追加すると、最終的に値が得られます。&lt;/p>
&lt;img src="https://i.imgur.com/jxt6xFe.png"/>
&lt;p>#それだけです&lt;/p>
&lt;p>それはそれです！かなりシンプルですよね？これで、属性を作成してメソッドとコントローラーに割り当てる方法がわかりました。&lt;/p>
&lt;p>彼らと一緒に楽しんでください！&lt;/p>
&lt;h1 id="コード">コード&lt;/h1>
&lt;p>このプロジェクト全体は Github にあり、&lt;a href="https://github.com/emimontesdeoca/dotnet-6-attribute-post">ここ&lt;/a> で見つけることができます。&lt;/p>
&lt;p>問題や質問がある場合は、ソーシャル メディアで @emimontesdeoca までお気軽にご連絡ください (Twitter では実際には &lt;code>@emimontesdeocaa&lt;/code> で最後に &lt;code>aa&lt;/code> が 2 つ付いています)。私のソーシャルのほとんどはブログのヘッダーからも見つけることができます。&lt;/p>
&lt;p>投稿が気に入っていただければ幸いです!キャー！&lt;/p></content:encoded><category>.NET</category><category>API</category></item><item><title>Blazor でコンポーネントの読み込みを処理する</title><link>https://emimontesdeoca.github.io/ja/posts/loading-component-blazor/</link><pubDate>Tue, 19 Jul 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/loading-component-blazor/</guid><description>RenderFragment と ChildContent を使用して、Blazor で再利用可能な読み込みスピナー ラッパー コンポーネントを構築します。</description><content:encoded>&lt;p>Blazor は素晴らしく、本当に素晴らしく、特にボックスの読み込みなどの非同期処理を行う場合、見栄えが良くなります。&lt;/p>
&lt;p>ページ、状態、コンポーネントなどの読み込みを処理する方法について、さまざまな方法を試してきました。そして、最終的に、希望どおりに実行する完璧な方法を見つけたと思います。&lt;/p>
&lt;h1 id="アイデア">アイデア&lt;/h1>
&lt;p>各ページまたはコンポーネントの読み込みロジックを書き直す代わりに、&lt;code>ChildComponent&lt;/code> を使用して親コンポーネントを構築します。これにより、複数回再利用するだけの変更が得られます。&lt;/p>
&lt;h1 id="loadingcomponent-コード">LoadingComponent コード&lt;/h1>
&lt;p>コードは非常にシンプルですが、やるべきことはそれほど多くありません。読み込みプロパティを備えた基本的な &lt;code>if&lt;/code> と、内部のトグル関数だけで完了です。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@if (_loaded)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;text-center&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;spinner-border&amp;#34;&lt;/span> role=&lt;span style="color:#a5d6ff">&amp;#34;status&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;span class=&lt;span style="color:#a5d6ff">&amp;#34;sr-only&amp;#34;&lt;/span>&amp;gt;&amp;lt;/span&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @ChildContent
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">bool&lt;/span> _loaded = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [Parameter]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> RenderFragment? ChildContent { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ToggleLoad(&lt;span style="color:#ff7b72">bool&lt;/span> state)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _loaded = state;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="使用法">使用法&lt;/h1>
&lt;p>使用方法は非常に簡単です。テストのために新しいページを使用し、先ほど作成した &lt;code>LoadingComponent&lt;/code> コンポーネント内にコンテンツを配置します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/loading&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@using LoadingBoxes.Components
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;LoadingBoxes.Components.LoadingComponent @ref=loadingComponent&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> This &lt;span style="color:#ff7b72">is&lt;/span> some content
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/LoadingBoxes.Components.LoadingComponent&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> LoadingComponent loadingComponent;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// We are going to simulate some load&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> Task.Delay(&lt;span style="color:#a5d6ff">5000&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> loadingComponent.ToggleLoad(&lt;span style="color:#79c0ff">false&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>見た目はこんな感じ&lt;/p>
&lt;img src="https://i.gyazo.com/cb892d796f396d43d5c54e30e1e87568.gif"/>
&lt;h1 id="さらに面白い例">さらに面白い例&lt;/h1>
&lt;p>複数のコンポーネントがあり、それぞれに読み込み時間が設定されているとします。その上で見栄えの良いものを構築できます。&lt;/p>
&lt;p>&lt;code>FakeLoadingComponent&lt;/code> という再利用できる偽の読み込みコンポーネントを作成しましょう。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@using LoadingBoxes.Components
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;LoadingBoxes.Components.LoadingComponent @ref=loadingComponent&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> It took @ellapsedTime seconds!
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/LoadingBoxes.Components.LoadingComponent&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> LoadingComponent loadingComponent = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> ellapsedTime = &lt;span style="color:#a5d6ff">0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> Random rnd = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">protected&lt;/span> &lt;span style="color:#ff7b72">override&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task OnInitializedAsync()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> random = rnd.Next(&lt;span style="color:#a5d6ff">0&lt;/span>, &lt;span style="color:#a5d6ff">5000&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// We are going to simulate some load&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> Task.Delay(random);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ellapsedTime = random/ &lt;span style="color:#a5d6ff">1000&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> loadingComponent.ToggleLoad(&lt;span style="color:#79c0ff">false&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> StateHasChanged();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>次に、複数の &lt;code>FakeLoadingComponent&lt;/code> コンポーネントを使用して &lt;code>Loading&lt;/code> ページを更新し、結果を確認します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/loading&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@using LoadingBoxes.Components
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;FakeLoadingComponent/&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;FakeLoadingComponent/&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;FakeLoadingComponent/&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;FakeLoadingComponent/&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;FakeLoadingComponent/&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これで見た目がかなり良くなりました&lt;/p>
&lt;img src="https://i.gyazo.com/e427ca31af410314762446979d1c3739.gif">
&lt;p>それで終わりです！&lt;/p>
&lt;p>問題や質問がある場合は、ソーシャル メディアで @emimontesdeoca までお気軽にご連絡ください (Twitter では実際には &lt;code>@emimontesdeocaa&lt;/code> で最後に &lt;code>aa&lt;/code> が 2 つ付いています)。私のソーシャルのほとんどはブログのヘッダーからも見つけることができます。&lt;/p>
&lt;p>投稿が気に入っていただければ幸いです!&lt;/p></content:encoded><category>.NET</category><category>Blazor</category></item><item><title>証明書から有効期限を取得する</title><link>https://emimontesdeoca.github.io/ja/posts/expiration-date-certificate/</link><pubDate>Fri, 27 May 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/expiration-date-certificate/</guid><description>C# HttpClient と X509Certificate2 を使用して、プログラムで SSL 証明書の有効期限を取得します。</description><content:encoded>&lt;p>弊社が使用しているドメインの証明書の現在のステータスを確認するのに役立つ Web アプリがいくつかあります。&lt;/p>
&lt;p>1 日に 1 回実行され、確認する必要があるいくつかのドメインをチェックする Azure 関数を作成しました。これは、証明書の有効期限が 30 日未満の場合に電子メールを送信する、非常にシンプルなコンソール アプリケーションです。&lt;/p>
&lt;p>関数がどのように動作するかのロジック自体は示されませんが、証明書の基本チェックを行う関数と取得するデータを示したいと思います。&lt;/p>
&lt;h2 id="関数">関数&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;X509Certificate2&amp;gt; CheckCertificateAsync(&lt;span style="color:#ff7b72">string&lt;/span> urlPath)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> certificate = &lt;span style="color:#ff7b72">new&lt;/span> X509Certificate2();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> httpClientHandler = &lt;span style="color:#ff7b72">new&lt;/span> HttpClientHandler
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ServerCertificateCustomValidationCallback = (request, cert, chain, policyErrors) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> certificate = &lt;span style="color:#ff7b72">new&lt;/span> X509Certificate2(cert);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">HttpClient&lt;/span> httpClient = &lt;span style="color:#ff7b72">new&lt;/span> HttpClient(httpClientHandler);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> httpClient.SendAsync(&lt;span style="color:#ff7b72">new&lt;/span> HttpRequestMessage(HttpMethod.Head, urlPath));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> certificate;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>このメソッド &lt;code>CheckCertificateAsync&lt;/code> は &lt;code>X509Certificate2&lt;/code> 証明書を返します。これにより、有効期限の確認など、さまざまな操作が可能になります。&lt;/p>
&lt;h2 id="シリアル化された結果">シリアル化された結果&lt;/h2>
&lt;p>これは &lt;code>certificate&lt;/code> オブジェクトのシリアル化された値です。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Archived&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Extensions&amp;#34;&lt;/span>:[
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;KeyUsages&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">160&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Critical&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;2.5.29.15&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;Key Usage&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;AwIFoA==&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;EnhancedKeyUsages&amp;#34;&lt;/span>:[
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;1.3.6.1.5.5.7.3.1&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;Server Authentication&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;1.3.6.1.5.5.7.3.2&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;Client Authentication&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Critical&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;2.5.29.37&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;Enhanced Key Usage&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;MBQGCCsGAQUFBwMBBggrBgEFBQcDAg==&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;CertificateAuthority&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;HasPathLengthConstraint&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;PathLengthConstraint&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">0&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Critical&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;2.5.29.19&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;Basic Constraints&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;MAA=&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;SubjectKeyIdentifier&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;634E1585565AA49402C21642A4A5979A38025797&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Critical&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;2.5.29.14&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;Subject Key Identifier&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;BBRjThWFVlqklALCFkKkpZeaOAJXlw==&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Critical&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;2.5.29.35&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;Authority Key Identifier&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;MBaAFBQusxe3WFbLrlAJQOYfr52LFMLG&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Critical&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;1.3.6.1.5.5.7.1.1&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;Authority Information Access&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;MEcwIQYIKwYBBQUHMAGGFWh0dHA6Ly9yMy5vLmxlbmNyLm9yZzAiBggrBgEFBQcwAoYWaHR0cDovL3IzLmkubGVuY3Iub3JnLw==&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Critical&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;2.5.29.17&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;Subject Alternative Name&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;MB6CHGJsb2cuZW1pbGlhbm9tb250ZXNkZW9jYS5jb20=&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Critical&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;2.5.29.32&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;Certificate Policies&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;MEMwCAYGZ4EMAQIBMDcGCysGAQQBgt8TAQEBMCgwJgYIKwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5cHQub3Jn&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Critical&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;1.3.6.1.4.1.11129.2.4.2&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;SCT List&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;BIH0APIAdwBByMqx3yJGShDGoToJQodeTjGLGwPr60vHaPCQYpYG9gAAAYCeJIwYAAAEAwBIMEYCIQCG8sf4iBitUjNCc1dsxVd5mdRQCKapRqqnTHKxSKHjHgIhAJFGNXEZkCHKygT1T7bE4orpd6p2l1+GmifMEIuRsgHbAHcARqVV63X6kSAwtaKJafTzfREsQXS+/Um4havy/HD+bUcAAAGAniSMNgAABAMASDBGAiEAoxv1LBn/vfyR7s67kRLB/n1tq3eicuA/8/V0S2YzQCYCIQDXaS3FZbdIVNxQvKxPFxM1awBO/sGxBXafz0lspOoWSA==&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;HasPrivateKey&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;PrivateKey&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">null&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;IssuerName&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Name&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;CN=R3, O=Let&amp;#39;s Encrypt, C=US&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">null&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">null&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJSMw==&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;NotAfter&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;2022-08-05T10:50:35+01:00&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;NotBefore&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;2022-05-07T10:50:36+01:00&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;PublicKey&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;EncodedKeyValue&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;1.2.840.113549.1.1.1&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;RSA&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;MIIBCgKCAQEAq8cbDO3GAfjqqbPPCBdPost8NMRmEubv85gXecll7mZMH5qSfTPuB/ouFWL3tPMf1U8usWeoSUK/48yatzBGwmj1KKlkaW9MS2QkydztRp+kH8LvbzbQvGknuOLWGHBALLT17o/3DYxuA5LnXdY+vLvJWygQoFr2N/XhnhUjcm6OaQEJpIykydfbBQGQSEuQIIw4egpgdHkYJjCOYAsXuSSggN8/FADTCec0RzVjfFTSoJ3hV9HLE9M8MCSXjuo0AJ/MbAxq91S8XmDcRjHCCd7Zw+NjHo8cxZCQ6NqGvn3xwx8ahmmbC+CyDEcIyJJZK2Yv+qE4oS8QZfaX/RaHMwIDAQAB&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;EncodedParameters&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;1.2.840.113549.1.1.1&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;RSA&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;BQA=&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Key&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Key&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Algorithm&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Algorithm&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;RSA&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;AlgorithmGroup&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;AlgorithmGroup&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;RSA&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;ExportPolicy&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">0&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Handle&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;IsInvalid&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;IsClosed&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;IsEphemeral&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;IsMachineKey&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;KeyName&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">null&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;KeySize&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">2048&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;KeyUsage&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">16777215&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;ParentWindowHandle&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Provider&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Provider&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;Microsoft Software Key Storage Provider&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;ProviderHandle&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;IsInvalid&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;IsClosed&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">false&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;UIPolicy&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;ProtectionLevel&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">0&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">null&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Description&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">null&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;UseContext&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">null&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;CreationTitle&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">null&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;UniqueName&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">null&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;LegalKeySizes&amp;#34;&lt;/span>:[
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;MinSize&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">512&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;MaxSize&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">16384&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;SkipSize&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">64&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;KeyExchangeAlgorithm&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;RSA&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;SignatureAlgorithm&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;RSA&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;KeySize&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">2048&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;1.2.840.113549.1.1.1&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;RSA&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;MIIFQDCCBCigAwIBAgISBNwTmwP/RTcrEeIgAdMrpaFtMA0GCSqGSIb3DQEBCwUAMDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJSMzAeFw0yMjA1MDcwOTUwMzZaFw0yMjA4MDUwOTUwMzVaMCcxJTAjBgNVBAMTHGJsb2cuZW1pbGlhbm9tb250ZXNkZW9jYS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrxxsM7cYB+Oqps88IF0+iy3w0xGYS5u/zmBd5yWXuZkwfmpJ9M+4H+i4VYve08x/VTy6xZ6hJQr/jzJq3MEbCaPUoqWRpb0xLZCTJ3O1Gn6Qfwu9vNtC8aSe44tYYcEAstPXuj/cNjG4Dkudd1j68u8lbKBCgWvY39eGeFSNybo5pAQmkjKTJ19sFAZBIS5AgjDh6CmB0eRgmMI5gCxe5JKCA3z8UANMJ5zRHNWN8VNKgneFX0csT0zwwJJeO6jQAn8xsDGr3VLxeYNxGMcIJ3tnD42MejxzFkJDo2oa+ffHDHxqGaZsL4LIMRwjIklkrZi/6oTihLxBl9pf9FoczAgMBAAGjggJZMIICVTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFGNOFYVWWqSUAsIWQqSll5o4AleXMB8GA1UdIwQYMBaAFBQusxe3WFbLrlAJQOYfr52LFMLGMFUGCCsGAQUFBwEBBEkwRzAhBggrBgEFBQcwAYYVaHR0cDovL3IzLm8ubGVuY3Iub3JnMCIGCCsGAQUFBzAChhZodHRwOi8vcjMuaS5sZW5jci5vcmcvMCcGA1UdEQQgMB6CHGJsb2cuZW1pbGlhbm9tb250ZXNkZW9jYS5jb20wTAYDVR0gBEUwQzAIBgZngQwBAgEwNwYLKwYBBAGC3xMBAQEwKDAmBggrBgEFBQcCARYaaHR0cDovL2Nwcy5sZXRzZW5jcnlwdC5vcmcwggEGBgorBgEEAdZ5AgQCBIH3BIH0APIAdwBByMqx3yJGShDGoToJQodeTjGLGwPr60vHaPCQYpYG9gAAAYCeJIwYAAAEAwBIMEYCIQCG8sf4iBitUjNCc1dsxVd5mdRQCKapRqqnTHKxSKHjHgIhAJFGNXEZkCHKygT1T7bE4orpd6p2l1+GmifMEIuRsgHbAHcARqVV63X6kSAwtaKJafTzfREsQXS+/Um4havy/HD+bUcAAAGAniSMNgAABAMASDBGAiEAoxv1LBn/vfyR7s67kRLB/n1tq3eicuA/8/V0S2YzQCYCIQDXaS3FZbdIVNxQvKxPFxM1awBO/sGxBXafz0lspOoWSDANBgkqhkiG9w0BAQsFAAOCAQEAjSEID5MWonbSiyHbmPYWO8ImCCOjkLGxgY8WJODbrWxFy+xU44UwrWOCkqYZUlv2LRmPqSyZDrIeeHK9VMbGh71oXX+XovikgAr6PpI0Mp897nPWj0XvOBaSYG0s+f+CXMtyt0tWCsQOcl+iT82+Ja71f8gbVL6l7xESewEE78pTKEH8EqD22r8VSD7FNICD8EYQr13v3AuVWObSU/R8Td6SrSVEknw1HgJS4e9nvmrMxBGKOJ+aWrAGiUydehg8M9o2gbGckMhz6D7cwB5l618cYaXKkW1dEOYZHl++qUj1/VPK+FNkiDZOPVNN//PbZuOLwAUIlZvhqGWX5/9PBg==&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;SerialNumber&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;04DC139B03FF45372B11E22001D32BA5A16D&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;SignatureAlgorithm&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;1.2.840.113549.1.1.11&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;sha256RSA&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;SubjectName&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Name&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;CN=blog.emilianomontesdeoca.com&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Oid&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Value&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">null&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;FriendlyName&amp;#34;&lt;/span>:&lt;span style="color:#79c0ff">null&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;RawData&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;MCcxJTAjBgNVBAMTHGJsb2cuZW1pbGlhbm9tb250ZXNkZW9jYS5jb20=&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Thumbprint&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;28CF960F772ABFF22AA193C291492C27F8E13D4D&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Version&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">3&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Handle&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;value&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">2658150705632&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Issuer&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;CN=R3, O=Let&amp;#39;s Encrypt, C=US&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;Subject&amp;#34;&lt;/span>:&lt;span style="color:#a5d6ff">&amp;#34;CN=blog.emilianomontesdeoca.com&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="有効期限">有効期限&lt;/h2>
&lt;p>有効期限を知るには、このオブジェクト内にある &lt;code>NotAfter&lt;/code> と &lt;code>NotBefore&lt;/code> を確認する必要があります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;NotAfter&amp;#34;&lt;/span>&lt;span style="color:#f85149">:&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;2022-08-05T10:50:35+01:00&amp;#34;&lt;/span>&lt;span style="color:#f85149">,&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;NotBefore&amp;#34;&lt;/span>&lt;span style="color:#f85149">:&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;2022-05-07T10:50:36+01:00&amp;#34;&lt;/span>&lt;span style="color:#f85149">,&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="コンソール-アプリケーション">コンソール アプリケーション&lt;/h2>
&lt;p>次のスニペットは、.NET 6 上に構築された単純なコンソール アプリケーションであり、次の結果が生成され、必要な認定を確認できます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Newtonsoft.Json&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Net&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Security.Cryptography.X509Certificates&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Text.Json&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> url = &lt;span style="color:#a5d6ff">&amp;#34;https://blog.emilianomontesdeoca.com/&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task&amp;lt;X509Certificate2&amp;gt; CheckCertificateAsync(&lt;span style="color:#ff7b72">string&lt;/span> urlPath)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> certificate = &lt;span style="color:#ff7b72">new&lt;/span> X509Certificate2();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> httpClientHandler = &lt;span style="color:#ff7b72">new&lt;/span> HttpClientHandler
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ServerCertificateCustomValidationCallback = (request, cert, chain, policyErrors) =&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> certificate = &lt;span style="color:#ff7b72">new&lt;/span> X509Certificate2(cert);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">HttpClient&lt;/span> httpClient = &lt;span style="color:#ff7b72">new&lt;/span> HttpClient(httpClientHandler);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> httpClient.SendAsync(&lt;span style="color:#ff7b72">new&lt;/span> HttpRequestMessage(HttpMethod.Head, urlPath));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> certificate;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> cert = &lt;span style="color:#ff7b72">await&lt;/span> CheckCertificateAsync(url);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> serializedValue = JsonConvert.SerializeObject(cert);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.WriteLine(serializedValue);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Console.ReadLine();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="デモプロジェクト">デモプロジェクト&lt;/h2>
&lt;p>コンソール アプリケーションは、私の Github の &lt;a href="https://github.com/emimontesdeoca/expiration-date-certificate">expiration-date-certificate&lt;/a> というリポジトリにあります。&lt;/p></content:encoded><category>.NET</category><category>Azure</category></item><item><title>Blazor の要素にフォーカスする</title><link>https://emimontesdeoca.github.io/ja/posts/focus-element-blazor/</link><pubDate>Thu, 05 May 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/focus-element-blazor/</guid><description>JavaScript 相互運用機能と要素参照を使用して、Blazor コンポーネントの HTML 要素にフォーカスを設定します。</description><content:encoded>&lt;p>最近行った Wordlzor ゲームに取り組んだ後、非常に単純な機能を追加する必要がありました。それは、ゲームに入るときにゲーム全体にフォーカスするというものです。&lt;/p>
&lt;p>ユーザーはスクリーン キーボードを使用するだけでなく、実際にゲーム上で入力できるため、これを行う必要がありました。&lt;/p>
&lt;h2 id="javascript-ファイル">JavaScript ファイル&lt;/h2>
&lt;p>これを行うには、関数を保持する &lt;code>app.js&lt;/code> という Javascript ファイルを作成する必要があります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-js" data-lang="js">&lt;span style="display:flex;">&lt;span>window.FocusElement &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> (element) =&amp;gt; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> element.focus();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>それが完了したら、スクリプトを &lt;code>index.html&lt;/code> ファイルに含める必要があります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#7ee787">script&lt;/span> src&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;js/app.js&amp;#34;&lt;/span>&amp;gt;&amp;lt;/&lt;span style="color:#7ee787">script&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="ブレザーコンポーネント">ブレザーコンポーネント&lt;/h2>
&lt;p>スクリプトを初期化したら、注目する要素を見つける必要があるため、どのコンポーネントでもその要素をオブジェクトに参照する必要があります。&lt;/p>
&lt;p>そこで、blazor コンポーネントに &lt;code> を含む div を追加しましょう。 @ref&lt;/code> プロパティ:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-html" data-lang="html">&lt;span style="display:flex;">&lt;span>&amp;lt;&lt;span style="color:#7ee787">div&lt;/span> &lt;span style="color:#f85149">@&lt;/span>ref&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">@elementToFocus&lt;/span> tabindex&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;0&amp;#34;&lt;/span> &amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> This is my focus component
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/&lt;span style="color:#7ee787">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>次に、コンポーネント ロジックでプロパティを &lt;code>ElementReference&lt;/code> として持つことができます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Reference to element&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> ElementReference elementToFocus;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="jsinterop-の挿入">JSInterop の挿入&lt;/h2>
&lt;p>ここで、JavaScript ファイルにある関数を呼び出すには、&lt;code>JSInterop&lt;/code> を使用する必要があります。&lt;/p>
&lt;p>まず最初に、次の構文を使用してコンポーネントにそれを注入する必要があります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[Inject]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> IJSRuntime JSRuntime { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>サービスが挿入されると、関数を呼び出す &lt;code>InvokeVoidAsync&lt;/code> など、そのサービスが持つメソッドを呼び出すことができるようになります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task Focus()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Focus when initializing&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> JSRuntime.InvokeVoidAsync(&lt;span style="color:#a5d6ff">&amp;#34;window.FocusElement&amp;#34;&lt;/span>, elementToFocus);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>そして、関数 &lt;code>Focus()&lt;/code> を呼び出すと、作成した要素にフォーカスされます。明らかに、要素自体をリファクタリングしてパラメータとして渡すことができます。&lt;/p>
&lt;p>私がそれをどのように実装したかを見たい場合は、&lt;a href="https://github.com/emimontesdeoca/Wordlzor">Wordlzor&lt;/a> ソース コードを見てください。命令モーダルを閉じるときのアラートとフォーカスに使用しました。&lt;/p></content:encoded><category>.NET</category><category>Blazor</category></item><item><title>GitHub Actions で VS Database プロジェクトから *.dacpac ファイルを生成する</title><link>https://emimontesdeoca.github.io/ja/posts/generate-dacpacs-github-actions/</link><pubDate>Mon, 18 Apr 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/generate-dacpacs-github-actions/</guid><description>GitHub Actions パイプラインを使用して、Visual Studio データベース プロジェクトからの dacpac ファイルの生成を自動化します。</description><content:encoded>&lt;p>データベースを操作する必要があり、アジャイル開発を行いたい場合は、テーブル、SP、関数を追加または変更するたびに、それがコンパイルされ、データベースのデプロイメント ファイルが生成されるフローが必要です。&lt;/p>
&lt;p>私はこのアプローチを長い間行ってきました。ローカルで変更を行い、データベース プロジェクトと比較して、変更をコミットしてプッシュすると、データベースに送信される bacpac ファイルが生成されます。&lt;/p>
&lt;p>手動で行うと人的エラーが発生する可能性があり、データベースでは非常に悪いニュースとなる可能性があります。&lt;/p>
&lt;h1 id="データベースプロジェクト">データベースプロジェクト&lt;/h1>
&lt;p>Visual Studio でデータベース プロジェクトを作成し、データベースをインポートすると、次のようになります。&lt;/p>
&lt;img src="https://i.gyazo.com/c0e11b14c707db66b8dbb591031cc527.png" />
&lt;p>このプロジェクトでコンパイルを実行すると、すべての構造を含む &lt;code>dacpac&lt;/code> ファイルが生成されます&lt;/p>
&lt;img src="https://i.gyazo.com/c883f3e329c7deb033564f7b5e9be7d4.png" />
&lt;h1 id="github-パイプライン">Github パイプライン&lt;/h1>
&lt;p>コードをリポジトリに公開したので、このプロジェクトをコンパイルし、この &lt;code>dacpac&lt;/code> ファイルを生成し、ダウンロードするか別のステップで使用できる場所に保存するアクションを作成する必要があります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">DacpacGithubActions project build&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">on&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">push&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">branches&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">main&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">jobs&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">build&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">runs-on&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">windows-latest&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">steps&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">uses&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">actions/checkout@v2&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Setup MSBuild&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">uses&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">microsoft/setup-msbuild@v1&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Navigate to Workspace&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">run&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">cd $GITHUB_WORKSPACE&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Create Build Directory&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">run&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">mkdir artifacts&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Build Solution&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">run&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>|&lt;span style="color:#a5d6ff">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> msbuild.exe /t:DacpacGithubActions /p:DebugSymbols=false /p:DebugType=None /p:DeployOnBuild=true /p:WebPublishMethod=FileSystem /p:OutDir=&amp;#34;../artifacts&amp;#34;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff"> &lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">Upload artifact&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">uses&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">actions/upload-artifact@v1.0.0&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">with&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">DacpacGithubActionsArtifacts&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">path&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;../artifacts&amp;#34;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>このアクションは一連の処理を実行し、ソリューションを構築して、その結果を事前に作成した &lt;code>artifacts&lt;/code> フォルダーに配置し、そのフォルダーからアーティファクトにファイルをアップロードします。&lt;/p>
&lt;h1 id="パイプラインの実行">パイプラインの実行&lt;/h1>
&lt;p>ビルドをトリガーすると、結果は次のようになります。&lt;/p>
&lt;img src="https://i.gyazo.com/9fe0b7a44be0f07bcc41fe0862183a54.png" />
&lt;p>実際にアーティファクトをダウンロードしてコンテンツを確認すると、これは将来、継続的デプロイメント ステップを実装するときに必要となる &lt;code>dacpac&lt;/code> ファイルです。&lt;/p>
&lt;img src="https://i.gyazo.com/287a392df0c5e966958262644e335149.png" />
&lt;h1 id="コード">コード&lt;/h1>
&lt;p>このプロジェクト全体は Github にあり、&lt;a href="https://github.com/emimontesdeoca/dacpac-github-actions">ここ&lt;/a> で見つけることができます。&lt;/p>
&lt;p>問題や質問がある場合は、ソーシャル メディアで @emimontesdeoca までお気軽にご連絡ください (Twitter では実際には &lt;code>@emimontesdeocaa&lt;/code> で最後に &lt;code>aa&lt;/code> が 2 つ付いています)。私のソーシャルのほとんどはブログのヘッダーからも見つけることができます。&lt;/p>
&lt;p>投稿が気に入っていただければ幸いです!キャー！&lt;/p></content:encoded><category>CI/CD</category></item><item><title>Blazor の Javascript Interop を使用してテーマを切り替える</title><link>https://emimontesdeoca.github.io/ja/posts/blazor-toggle-darkmode/</link><pubDate>Fri, 01 Apr 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/blazor-toggle-darkmode/</guid><description>JavaScript Interop と CSS データ属性を使用して、Blazor で明るいテーマと暗いテーマの切り替えを実装します。</description><content:encoded>&lt;p>Web ページを開くたびにフラッシュバックに悩まされている私のような人のために、JavaScript を使用してライト モードとダーク モードを切り替え、JavaScript Interop を使用して Blazor から呼び出す方法に関する非常に簡単な解決策を思いつきました。&lt;/p>
&lt;img src="https://media-exp1.licdn.com/dms/image/C4D22AQEYLTFA1e7i9A/feedshare-shrink_800/0/1641493691714?e=1651708800&amp;v=beta&amp;t=j6RxTrY--qUwxOcbt8Xh4QE9nYYF1zlJBMZP9dDLuC8" />
&lt;h1 id="親属性の設定">親属性の設定&lt;/h1>
&lt;p>まず第一に、親要素を識別子で識別する必要があります。これは、その識別子の値に応じて色を変更するためです。&lt;/p>
&lt;p>これを Javascript で行うには、次のコマンドを実行する必要があります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-js" data-lang="js">&lt;span style="display:flex;">&lt;span>document.documentElement.setAttribute(&lt;span style="color:#a5d6ff">&amp;#39;data-theme&amp;#39;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#39;YOUR_IDFENTIFIER&amp;#39;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>この場合、スタイルのタイプごとに &lt;code>light&lt;/code> と &lt;code>dark&lt;/code> という 2 つの識別子が必要になります。&lt;/p>
&lt;h2 id="ロジックを処理するための-javascript-ファイルの作成">ロジックを処理するための Javascript ファイルの作成&lt;/h2>
&lt;p>識別子を更新する関数ができたので、そのロジックを実行する関数を含む Javascript ファイルを作成しましょう。私たちのファイルは &lt;code>app.js&lt;/code> という名前になります&lt;/p>
&lt;img src="https://i.gyazo.com/c481aa0ed8329e8592832e9da2921cea.png" />
&lt;p>このファイルには、前に説明したコードを呼び出す関数が含まれます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-js" data-lang="js">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> toggleTheme &lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span> &lt;span style="color:#ff7b72">function&lt;/span> (identifier) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> document.documentElement.setAttribute(&lt;span style="color:#a5d6ff">&amp;#39;data-theme&amp;#39;&lt;/span>, identifier);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="blazor-への-javascript-ファイルの追加">Blazor への Javascript ファイルの追加&lt;/h2>
&lt;p>スクリプトが配置されている場所にスクリプトを追加して、Blazor アプリケーションにスクリプトを追加します。&lt;/p>
&lt;p>&lt;code>&amp;lt;script src=&amp;quot;~/js/app.js&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;&lt;/code>&lt;/p>
&lt;h1 id="サービスの作成">サービスの作成&lt;/h1>
&lt;p>Javascript コードが完成したので、&lt;code>ThemeToggleService&lt;/code> という名前のサービスを作成する必要があります。&lt;/p>
&lt;img src="https://i.gyazo.com/e6480acaf46eef68f65b16a0738683ce.png" />
&lt;p>このサービスは、&lt;code>light&lt;/code> テーマと &lt;code>dark&lt;/code> テーマを切り替えるロジックを処理します。&lt;/p>
&lt;h2 id="jsinterop-の挿入">JSInterop の挿入&lt;/h2>
&lt;p>Javascipt を呼び出すには、JSInterop を呼び出す必要があるため、注入するプロパティを作成する必要があります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Components&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.JSInterop&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">BlazorDarkmodeToggle.Data&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> IJSRuntime jSRuntime;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ThemeToggleService(IJSRuntime jSRuntime)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span>.jSRuntime = jSRuntime;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="識別子を設定する関数">識別子を設定する関数&lt;/h2>
&lt;p>サービスを作成したので、識別子を設定する関数を作成しましょう。この関数はページ内の &lt;code>data-theme&lt;/code> を更新するだけです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Components&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.JSInterop&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">BlazorDarkmodeToggle.Data&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ThemeToggleService&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">readonly&lt;/span> IJSRuntime jSRuntime;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> ThemeToggleService(IJSRuntime jSRuntime)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span>.jSRuntime = jSRuntime;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">bool&lt;/span> IsLightTheme { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; } = &lt;span style="color:#79c0ff">true&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GetIdentifier =&amp;gt; IsLightTheme ? &lt;span style="color:#a5d6ff">&amp;#34;light&amp;#34;&lt;/span> : &lt;span style="color:#a5d6ff">&amp;#34;dark&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task ToggleTheme() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IsLightTheme = !IsLightTheme;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> jSRuntime.InvokeVoidAsync(&lt;span style="color:#a5d6ff">&amp;#34;toggleTheme&amp;#34;&lt;/span>, GetIdentifier);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="スタートアップにサービスを追加">スタートアップにサービスを追加&lt;/h2>
&lt;p>すべてのコンポーネントとページでサービスを利用できるようにするには、サービスを &lt;code>Program.cs&lt;/code> ファイルに追加する必要があります。&lt;/p>
&lt;p>&lt;code>builder.Services.AddSingleton&amp;lt;ThemeToggleService&amp;gt;();&lt;/code>&lt;/p>
&lt;p>ファイルは次のようになります&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">BlazorDarkmodeToggle.Data&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Components&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.AspNetCore.Components.Web&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> builder = WebApplication.CreateBuilder(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Add services to the container.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddRazorPages();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddServerSideBlazor();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddSingleton&amp;lt;WeatherForecastService&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>builder.Services.AddSingleton&amp;lt;ThemeToggleService&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> app = builder.Build();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Configure the HTTP request pipeline.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">if&lt;/span> (!app.Environment.IsDevelopment())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> app.UseExceptionHandler(&lt;span style="color:#a5d6ff">&amp;#34;/Error&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> app.UseHsts();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseHttpsRedirection();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseStaticFiles();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.UseRouting();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapBlazorHub();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.MapFallbackToPage(&lt;span style="color:#a5d6ff">&amp;#34;/_Host&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.Run();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これは、Blazor &lt;code>Server&lt;/code> を使用するか Blazor &lt;code>WebAssembly&lt;/code> を使用するかによって変わる可能性があることに注意してください。&lt;/p>
&lt;h1 id="css-をベイクする">CSS をベイクする&lt;/h1>
&lt;p>コードの一部が準備できたので、CSS の作業を開始する必要があります。私たちがしなければならないことは、CSSの色、背景などを処理するすべてのCSSプロパティを変数に設定することです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>:&lt;span style="color:#d2a8ff;font-weight:bold">root&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#79c0ff">--background-color&lt;/span>:&lt;span style="color:#a5d6ff">#ffffff&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#79c0ff">--text-color&lt;/span>:&lt;span style="color:#a5d6ff">#000000&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これを実行すると、識別子を使用して識別子を簡単に変更でき、他の値のいずれかを使用します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">[&lt;/span>&lt;span style="color:#7ee787">data-theme&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">=&lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;dark&amp;#34;&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">]&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#79c0ff">--background-color&lt;/span>: &lt;span style="color:#a5d6ff">#000000&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#79c0ff">--text-color&lt;/span>: &lt;span style="color:#a5d6ff">#ffffff&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>クールなアニメーションのフェードについては、&lt;code>transition&lt;/code> プロパティをすべてに追加するだけなので、見栄えが良くなります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">*&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">transition&lt;/span>: &lt;span style="color:#79c0ff">all&lt;/span> &lt;span style="color:#a5d6ff">250&lt;/span>&lt;span style="color:#ff7b72">ms&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>次に、この変数をいくつかの CSS クラスに割り当てて、Blazor ページで確認できるようにします。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-css" data-lang="css">&lt;span style="display:flex;">&lt;span>.&lt;span style="color:#f0883e;font-weight:bold">app-background&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">background-color&lt;/span>: &lt;span style="color:#d2a8ff;font-weight:bold">var&lt;/span>(&lt;span style="color:#ff7b72;font-weight:bold">--&lt;/span>background&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>&lt;span style="color:#79c0ff">color&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">width&lt;/span>: &lt;span style="color:#a5d6ff">200&lt;/span>&lt;span style="color:#ff7b72">px&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">height&lt;/span>: &lt;span style="color:#a5d6ff">200&lt;/span>&lt;span style="color:#ff7b72">px&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.&lt;span style="color:#f0883e;font-weight:bold">app-text&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">color&lt;/span>: &lt;span style="color:#d2a8ff;font-weight:bold">var&lt;/span>(&lt;span style="color:#ff7b72;font-weight:bold">--&lt;/span>&lt;span style="color:#79c0ff">text&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>&lt;span style="color:#79c0ff">color&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>head&lt;/code> にファイルを追加して、Blazor アプリケーションに忘れずに追加してください。&lt;/p>
&lt;p>&lt;code>&amp;lt;link href=&amp;quot;css/app.css&amp;quot; rel=&amp;quot;stylesheet&amp;quot; /&amp;gt;&lt;/code>&lt;/p>
&lt;h1 id="blazor-からの呼び出し">Blazor からの呼び出し&lt;/h1>
&lt;p>では、これをどのようにテストするのでしょうか?シンプルにするために、 &lt;code>app-background&lt;/code> クラスの div を追加するだけで、その中に &lt;code>app-text&lt;/code> クラスの &lt;code>p&lt;/code> が含まれます。&lt;code>Pages&lt;/code> フォルダーの下に &lt;code>Theme.razor&lt;/code> というページを作成し、そのコードを追加しましょう。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/theme&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@using BlazorDarkmodeToggle.Data
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@inject ThemeToggleService ThemeToggleService
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;button class=&lt;span style="color:#a5d6ff">&amp;#34;btn btn-primary&amp;#34;&lt;/span> @onclick=Toggle&amp;gt;Toggle theme&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;br /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;app-background&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;p class=&lt;span style="color:#a5d6ff">&amp;#34;app-text&amp;#34;&lt;/span>&amp;gt;Hello world!&amp;lt;/p&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task Toggle()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> &lt;span style="color:#ff7b72">this&lt;/span>.ThemeToggleService.ToggleTheme();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>/theme&lt;/code> に移動してこれをテストできますが、ナビゲーションバーにも追加しましょう&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;nav-item px-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;NavLink class=&lt;span style="color:#a5d6ff">&amp;#34;nav-link&amp;#34;&lt;/span> href=&lt;span style="color:#a5d6ff">&amp;#34;theme&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;span class=&lt;span style="color:#a5d6ff">&amp;#34;oi oi-list-rich&amp;#34;&lt;/span> aria-hidden=&lt;span style="color:#a5d6ff">&amp;#34;true&amp;#34;&lt;/span>&amp;gt;&amp;lt;/span&amp;gt; Theme togle
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/NavLink&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="テスト">テスト&lt;/h1>
&lt;img src="https://i.gyazo.com/a4894edb345cfd8c9f0cf2de869dba32.gif" />
&lt;p>ご覧のとおり、テーマ、背景、フォントの色の変更を切り替えるとすぐに、これがいかに強力であるかがわかります。CSS でこの変数を使用してページ全体を実際に開発すると、さまざまなテーマを設定し、それらを切り替えるだけで済みます。&lt;/p>
&lt;p>なんと素晴らしいことでしょう!!&lt;/p>
&lt;h1 id="コード">コード&lt;/h1>
&lt;p>このプロジェクト全体は Github にあり、&lt;a href="https://github.com/emimontesdeoca/BlazorDarkmodeToggle">ここ&lt;/a> で見つけることができます。&lt;/p>
&lt;p>問題や質問がある場合は、ソーシャル メディアで @emimontesdeoca までお気軽にご連絡ください (Twitter では実際には &lt;code>@emimontesdeocaa&lt;/code> で最後に &lt;code>aa&lt;/code> が 2 つ付いています)。私のソーシャルのほとんどはブログのヘッダーからも見つけることができます。&lt;/p>
&lt;p>投稿が気に入っていただければ幸いです!&lt;/p>
&lt;h1 id="リソース">リソース&lt;/h1>
&lt;ul>
&lt;li>&lt;a href="https://docs.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-6.0">ASP.NET Core Blazor の .NET メソッドから JavaScript 関数を呼び出す&lt;/a>&lt;/li>
&lt;/ul></content:encoded><category>.NET</category><category>Blazor</category><category>Docker</category></item><item><title>Blazor での Azure Blob Storage へのファイルのアップロード</title><link>https://emimontesdeoca.github.io/ja/posts/uploading-files-az-blob-blazor/</link><pubDate>Wed, 23 Mar 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/uploading-files-az-blob-blazor/</guid><description>ネイティブ HTML5 ファイル入力を使用して、Blazor アプリから Azure Blob Storage にファイルをアップロードします。</description><content:encoded>&lt;p>Azure Blob Storage サービスは、Azure エコシステムで最も使用されているサービスの 1 つであり、非常に安価でありながら大量のファイルをクラウドにアップロードできます。また、直観的な Web を備えており、直接リンクでアクセスしたりダウンロードしたりできます。&lt;/p>
&lt;p>全体的に非常に優れたサービスで、私は仕事と個人のプロジェクトの両方でこれを使用してきましたが、正直に言って、最も優れている点は、それを機能させる方法がシンプルであることです。&lt;/p>
&lt;p>私が Azure Blob Storage を使用したほとんどのケースはバックエンドで、操作などの結果として得られたものを直接アップロードしています。したがって、Web サイトなどからファイルをアップロードするようなことは実際には行っていません。不運！&lt;/p>
&lt;p>私は Blazor のファンなので、HTML5 からのネイティブ ファイル入力を使用して Azure Blob Storage にファイルをアップロードする方法を紹介します。&lt;/p>
&lt;h1 id="前提条件">前提条件&lt;/h1>
&lt;ul>
&lt;li>Azure アカウント (お持ちでない場合は、&lt;a href="aka.ms/%E7%84%A1%E6%96%99">ここ&lt;/a> に $$$ を集めてアクセスしてください)。&lt;/li>
&lt;li>IDE (VS、Code、どれでも動作します)&lt;/li>
&lt;li>.NET Core 3.0以降&lt;/li>
&lt;/ul>
&lt;h1 id="azure-blob-storage-の作成">Azure Blob Storage の作成&lt;/h1>
&lt;p>Azure portal に移動し、ストレージ アカウントの作成に進みましょう。&lt;/p>
&lt;p>まず &lt;code>Storage accounts&lt;/code> を検索し、結果をクリックします。&lt;/p>
&lt;img src="https://i.gyazo.com/dfc7db88129cd5e2a5015a7bfd846685.png" />
&lt;p>ロードしたら、&lt;code>Create&lt;/code> をクリックします。&lt;/p>
&lt;p>多数の手順が記載されたフォームが表示されるので、先に進んで入力してください。&lt;/p>
&lt;img src="https://i.gyazo.com/2293db6fea38aa8b9c61980d088c56c4.png" />
&lt;p>必要な設定をすべて入力したら、検証に合格してリソースを作成します。&lt;/p>
&lt;img src="https://i.gyazo.com/f416db52c958744f8e9ca1dbe904a790.png" />
&lt;p>作成したら、&lt;code>Go to resource&lt;/code> をクリックします。 API を操作できるようにする接続文字列を取得します。&lt;/p>
&lt;img src="https://i.gyazo.com/713702e57e8c18db16ec214ea999507f.png" />
&lt;h1 id="リソースキーを取得する">リソースキーを取得する&lt;/h1>
&lt;p>新しく作成したリソースに到達したら、&lt;code>Security + networking&lt;/code> の下の &lt;code>Security + networking&lt;/code> に移動します。ロードすると、&lt;code>Connection string&lt;/code> と &lt;code>Key&lt;/code> が表示されます。後で必要になるため、これらをコピーします。&lt;/p>
&lt;img src="https://i.gyazo.com/87af79ff72e5973f3fbcdf22547d2c54.png" />
&lt;h1 id="クールな-blazor-サーバー-プロジェクトの作成">クールな Blazor サーバー プロジェクトの作成&lt;/h1>
&lt;p>このチュートリアルでは Visual Studio 2022 を使用しますが、前に述べたように、他の IDE を使用することもでき、&lt;code>dotnet&lt;/code> CLI を使用してプロジェクトを作成するだけです。&lt;/p>
&lt;p>それでは、Visual Studio を使用して、わずかな手順で Blazor プロジェクトをすばやく作成してみましょう。&lt;/p>
&lt;img src="https://i.gyazo.com/fab7bbabc9601132c8554c0b83ff4f58.png" />
&lt;img src="https://i.gyazo.com/dd015b5c8f1a46c2df0fa5af7cfc08e4.png" />
&lt;img src="https://i.gyazo.com/e7225a77fb3a91c2fb5d39877165e9b8.png" />
&lt;p>作成したものを実行すると、次のようになります。これは、ごく普通の Blazor アプリケーションです。&lt;/p>
&lt;img src="https://i.gyazo.com/95785a6c6cc68050d9989c489df0f599.png" />
&lt;p>プロジェクトを作成したので、いくつかの UI 作業を行いましょう。リソースからのキー、ファイルの &lt;code>InputFile&lt;/code>、何かを実行するボタン、およびアクションの結果として生じるメッセージを入力できるように、大量の入力を含む新しいページができます。&lt;/p>
&lt;h2 id="モデルの作成">モデルの作成&lt;/h2>
&lt;p>これを適切に行うにはいくつかのモデルが必要です。まず、接続文字列、コンテナ名、ファイルを処理する &lt;code>BlobRequest&lt;/code> クラスを用意します。&lt;/p>
&lt;p>また、すべてをきれいに保つために、&lt;code>Name&lt;/code> と &lt;code>Data&lt;/code> を保存する &lt;code>BlobFile&lt;/code> というクラスを作成します。&lt;/p>
&lt;p>&lt;code>Models&lt;/code> という名前のフォルダーを作成したので、すべてが分離されました。&lt;/p>
&lt;p>授業はとてもシンプルです。```csharp
using System.ComponentModel.DataAnnotations;&lt;/p>
&lt;p>namespace UploadingFilesAzBlobBlazor.Models {
public class BlobRequest
{
[Required(ErrorMessage = &amp;ldquo;Connection string is required&amp;rdquo;)]
public string? ConnectionString { get; set; }
[Required(ErrorMessage = &amp;ldquo;Container name is required&amp;rdquo;)]
public string? Container { get; set; }
public List&lt;BlobFile>? Files { get; set; }
}
}&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-gdscript3" data-lang="gdscript3">&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f85149">```&lt;/span>csharp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>namespace UploadingFilesAzBlobBlazor&lt;span style="color:#ff7b72;font-weight:bold">.&lt;/span>Models
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> public &lt;span style="color:#ff7b72">class&lt;/span> BlobFile
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> public string&lt;span style="color:#f85149">?&lt;/span> Name { get; set; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> public byte[]&lt;span style="color:#f85149">?&lt;/span> Data { get; set; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="ui-の変更">UI の変更&lt;/h2>
&lt;p>モデルを作成したので、それを使って UI に魔法をかけてみましょう。&lt;/p>
&lt;p>まず、&lt;code>Pages&lt;/code> フォルダーの下に &lt;code>Upload.razor&lt;/code> という名前のページを作成します。&lt;/p>
&lt;p>このページでは、&lt;code>BlobRequest&lt;/code> クラスを満たす小さなフォームを追加するので、&lt;code>ConnectionString&lt;/code>、&lt;code>Container&lt;/code>、および &lt;code>Files&lt;/code> の入力を追加する必要があります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@page &lt;span style="color:#a5d6ff">&amp;#34;/upload&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@using UploadingFilesAzBlobBlazor.Models
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@inject IJSRuntime JsRuntime
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;PageTitle&amp;gt; Upload&amp;lt;/PageTitle&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;h1&amp;gt; Azure blob storage uploader!&amp;lt;/h1&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;EditForm Model=&lt;span style="color:#a5d6ff">&amp;#34;@modal&amp;#34;&lt;/span> OnValidSubmit=&lt;span style="color:#a5d6ff">&amp;#34;@HandleValidSubmit&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;DataAnnotationsValidator /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;ValidationSummary /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;label &lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;connectionString&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-label&amp;#34;&lt;/span>&amp;gt; Connection &lt;span style="color:#ff7b72">string&lt;/span>&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;InputText class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> id=&lt;span style="color:#a5d6ff">&amp;#34;connectionString&amp;#34;&lt;/span> @bind-Value=&lt;span style="color:#a5d6ff">&amp;#34;modal.ConnectionString&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;label &lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;container&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-label&amp;#34;&lt;/span>&amp;gt; Container name&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;InputText class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> id=&lt;span style="color:#a5d6ff">&amp;#34;container&amp;#34;&lt;/span> @bind-Value=&lt;span style="color:#a5d6ff">&amp;#34;modal.Container&amp;#34;&lt;/span> /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;label &lt;span style="color:#ff7b72">for&lt;/span>=&lt;span style="color:#a5d6ff">&amp;#34;formFileMultiple&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-label&amp;#34;&lt;/span>&amp;gt; Files&amp;lt;/label&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;InputFile OnChange=&lt;span style="color:#a5d6ff">&amp;#34;OnInputFileChange&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;form-control&amp;#34;&lt;/span> id=&lt;span style="color:#a5d6ff">&amp;#34;formFileMultiple&amp;#34;&lt;/span> multiple /&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;div class=&lt;span style="color:#a5d6ff">&amp;#34;mb-3&amp;#34;&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;button type=&lt;span style="color:#a5d6ff">&amp;#34;submit&amp;#34;&lt;/span> class=&lt;span style="color:#a5d6ff">&amp;#34;btn btn-primary&amp;#34;&lt;/span>&amp;gt; Submit&amp;lt;/button&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/div&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&amp;lt;/EditForm&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@code {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> BlobRequest modal = &lt;span style="color:#ff7b72">new&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> EditContext editContext;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> IReadOnlyList &amp;lt;IBrowserFile&amp;gt; selectedFiles;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> OnInputFileChange(InputFileChangeEventArgs e)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> selectedFiles = e.GetMultipleFiles();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span> .StateHasChanged();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task HandleValidSubmit() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// This is where we are going to upload stuff ! &lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> JsRuntime.InvokeVoidAsync(&lt;span style="color:#a5d6ff">&amp;#34;alert&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Files uploaded!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これは少しのコードですが、基本的にはフォームをレンダリングし、検証を処理します。&lt;/p>
&lt;img src="https://i.gyazo.com/00d0ad6d23fa93bdb0fb354218018c5f.png"/>
&lt;p>アップロードするボタンを押して問題がなければ、警告が表示されます。&lt;/p>
&lt;img src="https://i.gyazo.com/27a3ff831fa9936bac21d8aa8ff60936.gif"/>
&lt;h2 id="azure-blob-storage-へのアップロード">Azure Blob Storage へのアップロード&lt;/h2>
&lt;p>ほとんどの UI が完成したので、Azure Blob Storage API を処理し、ファイルをアップロードできるようにするパッケージをインストールしましょう。とてもシンプルです。&lt;/p>
&lt;h3 id="nuget-パッケージを追加する">NuGet パッケージを追加する&lt;/h3>
&lt;p>プロジェクトから NuGet パッケージを管理し、&lt;code>Azure.Storage.Blob&lt;/code> を追加しましょう。&lt;/p>
&lt;img src="https://i.gyazo.com/c6ecd7b13c0a63a50f38342407acee49.png"/>
&lt;h3 id="azure-blob-ストアにアップロードする">Azure BLOB ストアにアップロードする&lt;/h3>
&lt;p>ここで、実際にファイルをアップロードするためのロジックを実行しましょう。通常、接続文字列と app.config のキーを使用しますが、このチュートリアルのために、入力を使用し、そこにデータを提供します。&lt;/p>
&lt;p>まず、Azure Blob Storage クラスの &lt;code>using&lt;/code> を追加しましょう&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>@using Azure.Storage.Blobs
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>@using Azure.Storage.Blobs.Models
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>次に、&lt;code>HandleValidSubmit&lt;/code> メソッドに取り組みます。これは、BLOB ストレージに接続し、コンテナーが存在しない場合は作成して、ファイルをアップロードします。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task HandleValidSubmit()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Instantiate container&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> container = &lt;span style="color:#ff7b72">new&lt;/span> BlobContainerClient(modal.ConnectionString, modal.Container);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Create container if not exists&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> createResponse = &lt;span style="color:#ff7b72">await&lt;/span> container.CreateIfNotExistsAsync();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Set access policy&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (createResponse != &lt;span style="color:#79c0ff">null&lt;/span> &amp;amp;&amp;amp; createResponse.GetRawResponse().Status == &lt;span style="color:#a5d6ff">201&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> container.SetAccessPolicyAsync(Azure.Storage.Blobs.Models.PublicAccessType.Blob);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// For each file that we have uploaded&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> file &lt;span style="color:#ff7b72">in&lt;/span> selectedFiles)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// New blob&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> blob = container.GetBlobClient(file.Name);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Delete any blob with the same name&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> blob.DeleteIfExistsAsync(Azure.Storage.Blobs.Models.DeleteSnapshotsOption.IncludeSnapshots);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Create a file stream and use the UploadSync method to upload the Blob.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> fileStream = file.OpenReadStream())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> blob.UploadAsync(fileStream, &lt;span style="color:#ff7b72">new&lt;/span> BlobHttpHeaders { ContentType = file.ContentType });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Display success message&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> JsRuntime.InvokeVoidAsync(&lt;span style="color:#a5d6ff">&amp;#34;alert&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;Files uploaded!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception e)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Display error message&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> JsRuntime.InvokeVoidAsync(&lt;span style="color:#a5d6ff">&amp;#34;alert&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;An error ocurred!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="コードのテスト">コードのテスト&lt;/h3>
&lt;p>ここでコードをテストします。テストするには、フォームに記入して送信をクリックするだけです。&lt;/p>
&lt;img src="https://i.gyazo.com/4ce26ec6487038f3bc70c8e977b16215.png"/>
&lt;p>アラートが表示され、ファイルが Azure Blob Storage にアップロードされるはずです。Azure ポータルにアクセスして確認してください。&lt;/p>
&lt;img src="https://i.gyazo.com/8c070489d4aa6a9f917d685a2ba670f3.png"/>
&lt;p>ご覧のとおり、コンテナーも作成されました。コンテナー内に入ると、アップロードしたファイルが表示されます。&lt;/p>
&lt;img src="https://i.gyazo.com/166076b94e065578a3aaa4012d3b5c37.png"/>
&lt;p>頑張ってね！&lt;/p>
&lt;h3 id="リファクタリング">リファクタリング&lt;/h3>
&lt;p>すべてがうまくいったので、コードを &lt;code>.razor&lt;/code> ページから &lt;code>.razor.cs&lt;/code> クラスに移動して、見栄えを良くしましょう。&lt;/p>
&lt;p>Visual Studio 2022 を使用している場合、&lt;code>@code&lt;/code> にマウスを移動すると電球が表示され、&lt;code>Extract block to code behind&lt;/code> のオプションが表示され、そのとおりに動作します。&lt;/p>
&lt;img src="https://i.gyazo.com/1a4b52c9adf1728860b1965bc9b11bdd.png"/>
&lt;p>これで、すべてが分離されて完了します。&lt;/p>
&lt;h1 id="コード">コード&lt;/h1>
&lt;p>このプロジェクト全体は Github にあり、&lt;a href="https://github.com/emimontesdeoca/UploadingFilesAzBlobBlazor">ここ&lt;/a> で見つけることができます。&lt;/p>
&lt;p>問題や質問がある場合は、ソーシャル メディアで @emimontesdeoca までお気軽にご連絡ください (Twitter では実際には &lt;code>@emimontesdeocaa&lt;/code> で最後に &lt;code>aa&lt;/code> が 2 つ付いています)。私のソーシャルのほとんどはブログのヘッダーからも見つけることができます。&lt;/p>
&lt;p>投稿が気に入っていただければ幸いです!キャー！&lt;/p>
&lt;h1 id="リソース">リソース&lt;/h1>
&lt;ul>
&lt;li>&lt;a href="https://www.nuget.org/packages/Azure.Storage.Blobs">https://www.nuget.org/packages/Azure.Storage.Blobs&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://docs.microsoft.com/en-us/azure/storage/blobs/storage-quickstart-blobs-dotnet">https://docs.microsoft.com/en-us/azure/storage/blobs/storage-quickstart-blobs-dotnet&lt;/a>&lt;/li>
&lt;/ul></content:encoded><category>.NET</category><category>Blazor</category><category>Azure</category><category>NuGet</category><category>Docker</category></item><item><title>Pimcore のコア コントローラーをオーバーライドする</title><link>https://emimontesdeoca.github.io/ja/posts/override-method-pimcore-core-bundles/</link><pubDate>Thu, 10 Dec 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/override-method-pimcore-core-bundles/</guid><description>サービス設定を含むカスタム Symfony バンドルを作成して、コア Pimcore コントローラーをオーバーライドします。</description><content:encoded>&lt;p>私はしばらくの間 Pimcore プロジェクトに取り組んでいますが、これまで PHP や Symfony に触れたことはなかったので、これは大きな挑戦でした。&lt;/p>
&lt;p>ドキュメントは素晴らしい、つまり真剣に素晴らしいのですが、言語やフレームワークの初心者向けに作られていないように見えるので、私がやったことはすべてメモを取る必要がありました。&lt;/p>
&lt;p>バンドルを使用して Pimcore を拡張できることを知ってから、私はコントローラーや JavaScript メソッドなどをオーバーライドするのに何時間も費やしてきました。&lt;/p>
&lt;p>そこでこの記事では、コントローラーをオーバーライドする方法を説明します。 JavaScript ファイルをオーバーライドする方法は後ほど説明します 😎。&lt;/p>
&lt;h1 id="バンドルの作成">バンドルの作成&lt;/h1>
&lt;p>まず最初にバンドルを作成する必要があります。これは &lt;a href="https://pimcore.com/docs/pimcore/current/Development_Documentation/Extending_Pimcore/Bundle_Developers_Guide/index.html">Pimcore ドキュメント&lt;/a> に記載されているように非常に簡単です。プロジェクト フォルダーで &lt;code>bin/console pimcore:generate:bundle --namespace=EmiDemo/EmiDemoBundle&lt;/code> を実行するだけです。&lt;/p>
&lt;p>いくつかの質問が表示されますが、心配する必要はありません。&lt;/p>
&lt;div style="text-align:center">&lt;img src="https://i.gyazo.com/9aa04169e668506d18638388d0061910.png" />&lt;/div>
&lt;p>&lt;code>src&lt;/code> フォルダーの下に、前に宣言した名前空間を持つ新しいフォルダーが作成され、その中にバンドルに必要なすべてのファイルが含まれます。&lt;/p>
&lt;div style="text-align:center">&lt;img src="https://i.gyazo.com/4d3329c303bb43012faaae43290c613b.png" />&lt;/div>
&lt;p>また、プラグインは Pimcore 管理サイトによって検出されますが、無効になっているため、使用を開始するには有効にする必要があります。&lt;/p>
&lt;div style="text-align:center">&lt;img src="https://i.gyazo.com/e0170db9111df69a00bdb3f10473c9a7.png" />&lt;/div>
&lt;h1 id="コントローラーからメソッドをオーバーライドする">コントローラーからメソッドをオーバーライドする&lt;/h1>
&lt;p>コントローラーでメソッドをオーバーライドするには、明らかに最初に更新するアクションを見つける必要があります。これは簡単そうに見えますが、私が通常行う方法を紹介します (チームメイト &lt;a href="https://twitter.com/cesabreu">Cesar&lt;/a> から学びました)。&lt;/p>
&lt;p>まず、コントローラーがアクションを実行すると思われるページに移動します。私の場合は、アセットを開いたときに読み込まれるページを確認したいと思います。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/df3833858806b14a39f50a0707a19dcd">&lt;img src="https://i.gyazo.com/df3833858806b14a39f50a0707a19dcd.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>次に、コンソールを開いてネットワークタブに移動し、同じ手順をもう一度実行してアクションを見つけてください。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/5fbd9496d585d145bea3f9a3b950de73">&lt;img src="https://i.gyazo.com/5fbd9496d585d145bea3f9a3b950de73.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>この情報により、アセットをロードするアクションが &lt;code>http://localhost/admin/asset/get-data-by-id?_dc=1607601778450&amp;amp;id=2&amp;amp;type=image&lt;/code> であることがわかります。そこから、コントローラーのアクションが &lt;code>get-data-by-id&lt;/code> であることがわかります。&lt;/p>
&lt;h2 id="コアコントローラーでアクションを見つける">コアコントローラーでアクションを見つける&lt;/h2>
&lt;p>私にとって、これを行う最も簡単な方法は、Visual Studio Code 内のすべてのファイルを検索することです。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/96cbeea01a1174d45e5a263a997c882a">&lt;img src="https://i.gyazo.com/96cbeea01a1174d45e5a263a997c882a.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>おそらく複数のものが見つかるので、調べてどれが 1 つであるかを決定する必要があります。この場合、アセットを使用しているため、&lt;code>AssetController.php&lt;/code> を使用したいことは明らかです。&lt;/p>
&lt;h2 id="コアコントローラーを変更する">コアコントローラーを変更する&lt;/h2>
&lt;p>それは開発者によって異なりますが、コントローラーをオーバーライドしてそこから開発する方が速いため、私は通常は行いません。ただし、最初にコア バンドルのコントローラーを更新して、変更が機能するかどうかを確認することをお勧めします。&lt;/p>
&lt;p>私たちの場合は、メソッドの先頭でメッセージを返して、それが機能することを確認するだけです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-php" data-lang="php">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#79c0ff">$this&lt;/span>&lt;span style="color:#ff7b72;font-weight:bold">-&amp;gt;&lt;/span>adminJson([&lt;span style="color:#a5d6ff">&amp;#39;success&amp;#39;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span> &lt;span style="color:#ff7b72">false&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#39;message&amp;#39;&lt;/span> &lt;span style="color:#ff7b72;font-weight:bold">=&amp;gt;&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;Overriding the getDataByIdAction in the core!!&amp;#34;&lt;/span>]);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;a href="https://gyazo.com/32139d67b9e5369d5ab38daed1b229ea">&lt;img src="https://i.gyazo.com/32139d67b9e5369d5ab38daed1b229ea.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>次に、アセットをリロードしてネットワークタブを確認しましょう&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/d1653f36e7bece9da6232ede0a431c05">&lt;img src="https://i.gyazo.com/d1653f36e7bece9da6232ede0a431c05.png" alt="Gyazo からの画像">&lt;/a>そこにあるものをそのままにしておきます。そうすれば、後で、コア メッセージの代わりにバンドル メッセージを使用していることがわかります。&lt;/p>
&lt;p>次に、これを一時ファイルにコピーして、何を行ったかを追跡しましょう。&lt;/p>
&lt;h2 id="それらの変更をバンドルに移動します">それらの変更をバンドルに移動します&lt;/h2>
&lt;p>これらの変更をバンドルに移動するには、まずコア コントローラーからいくつかのものが必要です。&lt;/p>
&lt;ul>
&lt;li>名前空間&lt;/li>
&lt;li>輸入品&lt;/li>
&lt;li>コントローラー名&lt;/li>
&lt;/ul>
&lt;p>&lt;a href="https://gyazo.com/416b3a527be397aaf6d9f89073c02428">&lt;img src="https://i.gyazo.com/416b3a527be397aaf6d9f89073c02428.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>そして明らかに、オーバーライドしたいメソッド &lt;code>getDataByIdAction&lt;/code> です。&lt;/p>
&lt;h2 id="コントローラーを公開する">コントローラーを公開する&lt;/h2>
&lt;p>コントローラーを公開する必要があります。これを行うには、ファイル &lt;code>/src/EmiDemo/EmiDemoBundle/Resources/config/pimcore/routing.yml&lt;/code> に移動して追加する必要があります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">options&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">expose&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#79c0ff">true&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>また、&lt;code>prefix&lt;/code> をオーバーライドするコントローラー、この場合は &lt;code>admin&lt;/code> コントローラーに変更する必要があります。&lt;/p>
&lt;p>それで最終的にはこんな感じになります&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">emi_demo_emi_demo&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">resource&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;@EmiDemoEmiDemoBundle/Controller/&amp;#34;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">type&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">annotation&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">prefix&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">/admin&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">options&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">expose&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#79c0ff">true&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="defaultcontrollerphp">DefaultController.php&lt;/h2>
&lt;p>ファイルにインポートを追加し、オーバーライドするコントローラーでコントローラーを拡張します。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/b8d798436ed111d4676312f8aeba443d">&lt;img src="https://i.gyazo.com/b8d798436ed111d4676312f8aeba443d.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>次に、コア コントローラーからメソッドをコピーし、この場合は返すメッセージを更新します。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/dcdd9f8166e4ba8820cb8e285c43dec8">&lt;img src="https://i.gyazo.com/dcdd9f8166e4ba8820cb8e285c43dec8.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>覚えているかもしれませんが、コア コントローラーにはすでにメッセージ &lt;code>Overriding the getDataByIdAction in the core!!&lt;/code> があり、現在は &lt;code>Overriding the getDataByIdAction in the bundle!!&lt;/code> が表示されているはずです。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/8e16b1de2a40292c843c51576acf43c4">&lt;img src="https://i.gyazo.com/8e16b1de2a40292c843c51576acf43c4.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>何も返さないで、以前と同じようにページが表示されることを確認してみましょう。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/dda8df5f6d721f5ba25d6a056ac7f9bf">&lt;img src="https://i.gyazo.com/dda8df5f6d721f5ba25d6a056ac7f9bf.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>return ステートメントをコメントアウトしてリロードします&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/2fdbff7f45c38fb2bc56a3fc73661077">&lt;img src="https://i.gyazo.com/2fdbff7f45c38fb2bc56a3fc73661077.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;h2 id="以上です">以上です&lt;/h2>
&lt;p>この小さなデモでは、既存の Pimcore コア コントローラーをいかに簡単にオーバーライドできるかがわかります。いくつかの手順に従わなければなりませんが、難しいことは何もありません。変更を加えている間は、コントローラーを公開し、キャッシュを時々クリアするようにしてください。コントローラーがキャッシュされてしまい、コントローラーが機能していないのにキャッシュされているだけだと思い込んでしまうことがあります。&lt;/p>
&lt;p>JavaScript ファイルをオーバーライドする方法について疑問がある場合は、別のチュートリアルで説明します 😁。&lt;/p></content:encoded></item><item><title>Windows 10 で Apple Magic Keyboard 2 を構成する</title><link>https://emimontesdeoca.github.io/ja/posts/configure-apple-keyboard-windows/</link><pubDate>Wed, 11 Nov 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/configure-apple-keyboard-windows/</guid><description>ドライバをインストールし、Windows 10 で Apple Magic Keyboard 2 が正しく動作するように設定します。</description><content:encoded>&lt;p>&lt;a href="https://gyazo.com/9c8641cdd22bd528b2141bad1322c74a">&lt;img src="https://i.gyazo.com/9c8641cdd22bd528b2141bad1322c74a.jpg" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>つい昨日、メカニカル キーボードを 5 つほど持っていますが、Apple Magic Keyboard 2 を購入しました。試してみたかったし、ワイヤレスだったからです。&lt;/p>
&lt;p>私のメイン OS は Windows 10 で、気に入っているので変更したくありません。そのことを念頭に置いて、キーボードを完璧に動作させるにはいくつかの作業が必要であることがわかりました。私は Apple がどのように機能するのか、そして彼らが自社のデバイスを自社のエコシステムにどのように維持したいのかを知っています。&lt;/p>
&lt;h2 id="問題">問題&lt;/h2>
&lt;p>キーボードをペアリングすると、次のことがわかります。&lt;/p>
&lt;p>※ファンクションキーが効かない&lt;/p>
&lt;ul>
&lt;li>いくつかのキーが間違ってマッピングされています (これはスペイン語版で発生しました)&lt;/li>
&lt;/ul>
&lt;h2 id="ドキュメント">ドキュメント&lt;/h2>
&lt;p>これを機能させるには、Web からたくさんの情報を読む必要がありましたが、次の 2 つのリンクが役に立ちました。&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.bluetoothgoodies.com/info/apple-devices/">Windows で Apple Magic Keyboard/Mouse/Trackpad を最大限に活用する&lt;/a>&lt;/li>
&lt;li>&lt;a href="https://superuser.com/questions/82826/how-do-i-use-my-f-1-f12-keys-without-pressing-fn-on-windows-7-using-bootcamp-o">Macbook Pro で BootCamp を使用している Windows 7 で FN を押さずに f-1 ～ f12 キーを使用するにはどうすればよいですか?&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="apple-キーボードドライバーのインストール">Apple キーボードドライバーのインストール&lt;/h2>
&lt;p>この手順の一部は、前述のドキュメントからのものです。&lt;/p>
&lt;ol>
&lt;li>&lt;a href="https://www.7-zip.org/">7zip&lt;/a> をお持ちでない場合は、コンピュータにインストールします。&lt;/li>
&lt;li>&lt;a href="https://www.python.org/downloads/">Python (バージョン 2.x)&lt;/a> がない場合は、コンピューターにインストールします。
&lt;ul>
&lt;li>重要: Python の最新バージョンは 3.x です。ただし、准将スクリプトはバージョン 3.x と互換性がないため、バージョン 2.x が必要です。&lt;/li>
&lt;li>(オプション) デフォルトでは、インストーラーは python.exe を PATH に追加しません。必要に応じて、このオプションを有効にする必要があります。 (右側のスクリーンショットを参照)&lt;/li>
&lt;li>すでに別のバージョンの Python を使用している場合は、このオプションを有効にしたくない可能性があります。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>brigadier (Boot Camp の最新バージョンのダウンロードに役立つ Python スクリプト) をダウンロードします。&lt;/li>
&lt;li>次のリンクを右クリックし、「名前を付けてリンク先を保存」を使用してファイルを保存してください。 &lt;a href="https://raw.githubusercontent.com/timsutton/brigadier/master/brigadier">https://raw.githubusercontent.com/timsutton/brigadier/master/brigadier&lt;/a>&lt;/li>
&lt;li>コマンド プロンプト ウィンドウ (別名 DOS ボックス) を開き、准将スクリプトをダウンロードしたディレクトリに変更します。&lt;/li>
&lt;li>准将スクリプトが「brigadier.txt」として保存されていると仮定して、次のコマンドを実行してください。
&lt;ul>
&lt;li>Python バージョン 2.x が PATH にある場合: python brigadier.txt &amp;ndash;model=MacBook13,2&lt;/li>
&lt;li>それ以外の場合: [Python バージョン 2.x へのパス]\python.exe brigadier.txt &amp;ndash;model=MacBook13,2&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Bootcamp からすべてのドライバーを含む大きなバンドルがダウンロードされます&lt;/li>
&lt;li>&lt;code>BootCamp&lt;/code> という名前のフォルダーを作成し、その中に &lt;code>BootCamp-xxx-yyyyyy\BootCamp\Drivers\Apple\BootCamp.msi&lt;/code> と &lt;code>BootCamp-xxx-yyyyyy\BootCamp\Drivers\Apple\AppleKeyboardMagic2&lt;/code> をコピーします。&lt;/li>
&lt;li>管理者用パワーシェルを実行し、&lt;code>BootCamp.msi&lt;/code> を実行すると、いくつかのものがインストールされますが、&lt;code>AppleKeyboardMagic2&lt;/code> フォルダーの内容を使用してドライバーを更新する必要があります。&lt;/li>
&lt;li>デバイスマネージャーを起動します (&lt;code>devmgmt.msc&lt;/code>)&lt;/li>
&lt;li>&lt;code>Human Interface Devices&lt;/code> ノードを展開します&lt;/li>
&lt;li>&lt;code>Bluetooth HID Device&lt;/code> を探します&lt;/li>
&lt;li>&lt;code>AppleKeyboardMagic2&lt;/code> フォルダーの内容を使用してドライバーを更新します&lt;/li>
&lt;li>コンピュータを再起動します&lt;/li>
&lt;/ol>
&lt;p>Bluetooth キーボードが Apple キーボードとして検出されるようになったはずです。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/278f6bd3e419d6688ccfadf6918ff309">&lt;img src="https://i.gyazo.com/278f6bd3e419d6688ccfadf6918ff309.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;h3 id="fn-キーの動作を更新すべてを適切にインストールした場合はfn-キーがデフォルトで有効になっていることがわかりますこれはf5-ボタンを実際に押すにはfn--f5-を押す必要があることを意味します">FN キーの動作を更新すべてを適切にインストールした場合は、FN キーがデフォルトで有効になっていることがわかります。これは、&lt;code>F5&lt;/code> ボタンを実際に押すには、&lt;code>fn&lt;/code> + &lt;code>F5&lt;/code> を押す必要があることを意味します。&lt;/h3>
&lt;p>これを修正するために、ドキュメントセクションに記載されている解決策を見つけました。これは、regedit のエントリを変更することで機能します。&lt;/p>
&lt;ol>
&lt;li>regeditを開きます&lt;/li>
&lt;li>&lt;code>HKEY_CURRENT_USER\SOFTWARE\Apple Inc.\Apple Keyboard Support&lt;/code> に移動します&lt;/li>
&lt;li>&lt;code>OSXFnBehavior&lt;/code> を作成または更新し、&lt;code>0&lt;/code> に設定します&lt;/li>
&lt;li>コンピュータを再起動します&lt;/li>
&lt;/ol>
&lt;h3 id="キーマッピングを更新する">キーマッピングを更新する&lt;/h3>
&lt;p>マッピングに問題がある場合は、&lt;a href="https://www.randyrants.com/category/sharpkeys/">SharpKeys&lt;/a> を使用して更新できます。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/ee0301205ffeddaae4241db40002864d">&lt;img src="https://i.gyazo.com/ee0301205ffeddaae4241db40002864d.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>使い方は非常に簡単ですが、レジストリが更新されるため、更新を有効にするために必ずログアウトするかコンピュータを再起動する必要があります。&lt;/p>
&lt;p>私の場合、&lt;code>Windows&lt;/code>、&lt;code>alt&lt;/code>、&lt;code>º&lt;/code>、および &lt;code>&amp;lt;&amp;gt;&lt;/code> キーを更新する必要がありました。&lt;/p></content:encoded></item><item><title>コンパイルされたライブラリを NuGet パッケージに追加する</title><link>https://emimontesdeoca.github.io/ja/posts/adding-compiled-dll-to-nuget/</link><pubDate>Thu, 01 Oct 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/adding-compiled-dll-to-nuget/</guid><description>コンパイルされたライブラリ参照を nuspec ファイルに含めることにより、NuGet パッケージで欠落している DLL を修正します。</description><content:encoded>&lt;p>つい先日、アップロードした nuget パッケージからのものであるため、存在するはずのライブラリが欠落している &lt;code>NullReferenceException&lt;/code> に関するバグを発見しました。&lt;/p>
&lt;p>基本的に、いくつかのプロジェクトを参照するライブラリ クラスをコンパイルしました。ビルドフェーズでは、&lt;code>dll&lt;/code> ファイルを &lt;code>bin&lt;/code> フォルダーに追加しますが、それを nuget パッケージとして &lt;code>packaging&lt;/code> して別のソリューションにインストールすると、参照し、bin フォルダーにコピーする必要がある &lt;code>dll&lt;/code> ファイルがそこにありません。&lt;/p>
&lt;h3 id="解決方法">解決方法&lt;/h3>
&lt;p>この問題の解決は非常に簡単です。 &lt;code>nuspec&lt;/code> ファイルを更新し、コピーするライブラリごとに &lt;code>files&lt;/code> 部分に &lt;code>file&lt;/code> を追加する必要があります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;utf-8&amp;#34;?&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;package&lt;/span> &lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;metadata&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;id&amp;gt;&lt;/span>$id$&lt;span style="color:#7ee787">&amp;lt;/id&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;version&amp;gt;&lt;/span>$version$&lt;span style="color:#7ee787">&amp;lt;/version&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;title&amp;gt;&lt;/span>$title$&lt;span style="color:#7ee787">&amp;lt;/title&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;authors&amp;gt;&lt;/span>$author$&lt;span style="color:#7ee787">&amp;lt;/authors&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;owners&amp;gt;&lt;/span>$author$&lt;span style="color:#7ee787">&amp;lt;/owners&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;requireLicenseAcceptance&amp;gt;&lt;/span>false&lt;span style="color:#7ee787">&amp;lt;/requireLicenseAcceptance&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;license&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;expression&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>MIT&lt;span style="color:#7ee787">&amp;lt;/license&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;projectUrl&amp;gt;&lt;/span>$projectUrl$&lt;span style="color:#7ee787">&amp;lt;/projectUrl&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;iconUrl&amp;gt;&lt;/span>$projectIconUrl$&lt;span style="color:#7ee787">&amp;lt;/iconUrl&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;description&amp;gt;&lt;/span>$description$&lt;span style="color:#7ee787">&amp;lt;/description&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;releaseNotes&amp;gt;&amp;lt;/releaseNotes&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;copyright&amp;gt;&lt;/span>$copyright$&lt;span style="color:#7ee787">&amp;lt;/copyright&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;tags&amp;gt;&amp;lt;/tags&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/metadata&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;files&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;file&lt;/span> src=&lt;span style="color:#a5d6ff">&amp;#34;bin\Release\MyFile1.dll&amp;#34;&lt;/span> target=&lt;span style="color:#a5d6ff">&amp;#34;lib\net47&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;file&lt;/span> src=&lt;span style="color:#a5d6ff">&amp;#34;bin\Release\MyFile2.dll&amp;#34;&lt;/span> target=&lt;span style="color:#a5d6ff">&amp;#34;lib\net47&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;file&lt;/span> src=&lt;span style="color:#a5d6ff">&amp;#34;bin\Release\MyFile3.dll&amp;#34;&lt;/span> target=&lt;span style="color:#a5d6ff">&amp;#34;lib\net47&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;file&lt;/span> src=&lt;span style="color:#a5d6ff">&amp;#34;bin\Release\MyFile4.dll&amp;#34;&lt;/span> target=&lt;span style="color:#a5d6ff">&amp;#34;lib\net47&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/files&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;/package&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div></content:encoded><category>NuGet</category></item><item><title>WSL ホームディレクトリにターミナルロード bash を作成する</title><link>https://emimontesdeoca.github.io/ja/posts/bash-straight-to-wsl-machine-with-terminal/</link><pubDate>Tue, 22 Sep 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/bash-straight-to-wsl-machine-with-terminal/</guid><description>.bashrc を介して Linux ホーム ディレクトリで WSL セッションを直接開くように Windows ターミナルを構成します。</description><content:encoded>&lt;p>WSL が発売された日から開発とテストに WSL を愛用してきましたが、ターミナル アプリが削除されて、これ以上に幸せなことはありません。見た目も良く、完璧に動作し、パフォーマンスも優れています。&lt;/p>
&lt;p>提供されるコンソールを使用する代わりに、WSL ディストリビューションでそれを使用しないのは愚かです。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/245fb4ff4fbd5297b2a8d9917dbee236">&lt;img src="https://i.gyazo.com/245fb4ff4fbd5297b2a8d9917dbee236.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>このコンソールは明らかに優れていますが、ターミナルを一度使用すると、もう戻ることはできません。とても良くなりました、&lt;em>c o l o r s&lt;/em> も。&lt;/p>
&lt;p>ここで、ターミナルを起動し、新しいタブを開いて WSL ディストリビューションをロードすると、マウントされたユーザー フォルダーがロードされます。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/dac1264f7b8dbae1f64e769b64c551da">&lt;img src="https://i.gyazo.com/dac1264f7b8dbae1f64e769b64c551da.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>&lt;code>cd ~&lt;/code> を実行するだけでは、ディストリビューション ユーザー フォルダーが読み込まれるだけなので、これは実際には問題ではありません。&lt;/p>
&lt;p>問題は、これを何度もやりたくないので、ファイルを更新して単独で実行しましょう。&lt;/p>
&lt;h2 id="bashrc">.bashrc&lt;/h2>
&lt;p>ターミナルまたはコンソールを起動し、プロファイル フォルダーに移動し、お気に入りのテキスト エディターを使用して &lt;code>.bashrc&lt;/code> ファイルを編集します。&lt;/p>
&lt;p>&lt;code>cd ~&lt;/code> をファイルの末尾の最後の命令の前に追加します。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/b601aba59b9e877bc3926ab9ceb2b98c">&lt;img src="https://i.gyazo.com/b601aba59b9e877bc3926ab9ceb2b98c.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>保存し、ターミナルで WSL コンソールを再度開くと、プロファイル フォルダーが表示されるはずです。&lt;/p>
&lt;p>1 つの WSL インストールに対して &lt;code>bashrc&lt;/code> ファイルを更新するため、これをすべての WSL インストールに対して行う必要があることに注意してください。&lt;/p></content:encoded></item><item><title>ExpandoObject を使用した動的オブジェクト生成</title><link>https://emimontesdeoca.github.io/ja/posts/dynamic-object-generation-expando-object/</link><pubDate>Fri, 06 Mar 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/dynamic-object-generation-expando-object/</guid><description>ExpandoObject を使用して、柔軟なデータ エクスポートのためにランタイム定義のプロパティを持つオブジェクトを動的に作成します。</description><content:encoded>&lt;p>独自に定義された列を含む Excel ファイルを、動的列を含む新しいファイルに変換する必要がありましたが、重大なパフォーマンスの問題を発生させずに適切に変換する方法について少し混乱しました。&lt;/p>
&lt;p>私は、.NET &lt;a href="https://docs.microsoft.com/en-us/dotnet/api/system.dynamic.expandoobject?view=netframework-4.8">ExpandoObject&lt;/a> を使用するチュートリアルを思いつきました。&lt;a href="https://www.oreilly.com/content/building-c-objects-dynamically/">ここ&lt;/a> では、オブジェクトを作成して動的メンバーを追加できます。&lt;/p>
&lt;h2 id="expandoobject">ExpandoObject&lt;/h2>
&lt;p>Microsoft の定義は次のとおりです。&lt;/p>
&lt;blockquote>
&lt;p>実行時にメンバーを動的に追加および削除できるオブジェクトを表します。&lt;/p>&lt;/blockquote>
&lt;p>そして、いくつかのコメントがあります。&lt;/p>
&lt;blockquote>
&lt;p>ExpandoObject クラスを使用すると、実行時にインスタンスのメンバーを追加および削除したり、これらのメンバーの値を設定および取得したりすることができます。このクラスは動的バインディングをサポートしているため、sampleObject.GetAttribute(&amp;ldquo;sampleMember&amp;rdquo;) のような複雑な構文の代わりに、sampleObject.sampleMember のような標準構文を使用できます。&lt;/p>&lt;/blockquote>
&lt;h2 id="現在の動作">現在の動作&lt;/h2>
&lt;p>&lt;code>ExcelExportService&lt;/code> がありますが、これは &lt;code>List&amp;lt;T&amp;gt;&lt;/code> (この場合は &lt;code>ExcelItem&lt;/code>) を渡すことによって、 &lt;code>Reflection&lt;/code> を使用して &lt;code>xlsx&lt;/code> ファイルを作成します。&lt;/p>
&lt;p>これまでのところ、コードは次のようになります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">ExcelItem&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> Id { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Name { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Surname { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Age { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> CreatedAt { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> UpdatedAt { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>ExcelItem&lt;/code> は、Excel ファイルの生成に使用されるすべてのプロパティを含むオブジェクトです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">byte&lt;/span>[] GetExcelBytes() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> List&amp;lt;ExcelItem&amp;gt; excelItems = &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;ExcelItem&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ExcelExportService exportService = &lt;span style="color:#ff7b72">new&lt;/span> ExcelExportService();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> items &lt;span style="color:#ff7b72">in&lt;/span> itemsToExcel)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> excelItems.Add(&lt;span style="color:#ff7b72">new&lt;/span> ExcelItem()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Id = item.Id,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Name = item.Name,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Surname = item.Surname,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Age = item.Age,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CreatedAt = item.CreatedAt
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> UpdatedAt = item.UpdatedAt
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> exportService.ExportToExcel(excelItems);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>このアプローチは完全に機能し、&lt;code>xlsx&lt;/code> ファイルは各列が各プロパティとして問題なく生成されます。たとえば、ファイルに保存するには、この &lt;code>GetExcelBytes&lt;/code> メソッドを次のように使用します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>File.WriteAllBytes(&lt;span style="color:#a5d6ff">&amp;#34;path-to-somewhere/my-file.xlsx&amp;#34;&lt;/span>, GetExcelBytes());
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="問題">問題&lt;/h2>
&lt;p>ここで行っていることは &lt;code>ExcelItem&lt;/code> オブジェクトのすべてのプロパティを使用することであるため、実際に必要なのはすべてを使用するのではなく、そのうちの 2 ～ 3 個だけを使用することだけです。&lt;/p>
&lt;p>また、コードを変更したくないという要件もあります。すべては &lt;code>administrator&lt;/code> によって行われ、どの列を表示するかを決定するのですが、彼はコード内のプロパティを知っているかどうかはわかりません。&lt;/p>
&lt;p>TLDR: すべては動的である必要があります。プロパティを持つオブジェクトがあり、生成されたファイルにはそのオブジェクトの &lt;code>N&lt;/code> プロパティがあることを確認する必要がありますが、明らかにハードコーディングされていません。&lt;/p>
&lt;h2 id="動的列">動的列&lt;/h2>
&lt;p>列のリストを生成するためにオブジェクトのどのプロパティを使用するかを設定したいため、変更は &lt;code>dynamic&lt;/code> オブジェクトを使用することになります。&lt;/p>
&lt;p>たとえば、&lt;em>大量&lt;/em> のような多くのプロパティを持つオブジェクトがあり、変更を加えるたびに &lt;code>ExcelItem&lt;/code> オブジェクトを変更するのはあまり望ましくなく、その &lt;code>ExcelItem&lt;/code> の生成に使用される &lt;code>ColumnExcelItem&lt;/code> テーブルを作成します。&lt;/p>
&lt;h3 id="データベース構造">データベース構造&lt;/h3>
&lt;p>この定義を次のようにしてデータベースに保存します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>Id PropertyName
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>---------------------
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>1 Name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>2 Surname
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>3 Age
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>ExcelItem&lt;/code> オブジェクトの既存のプロパティよりも値が少なく、&lt;code>6&lt;/code> プロパティがあり、データベースには &lt;code>3&lt;/code> のみがリストされていることに注意してください。&lt;/p>
&lt;p>&lt;code>PropertyName&lt;/code> は、動的割り当てに使用するオブジェクトのプロパティ名と一致する必要があることにも注意してください。&lt;/p>
&lt;h2 id="オブジェクトの構築">オブジェクトの構築&lt;/h2>
&lt;p>データベース テーブルを作成したので、それを取得してコード内で使用できるようにするためのリポジトリを構築する必要があります。この部分のチュートリアルは作成しません。新しい &lt;code>GetExcelBytes()&lt;/code> 関数の最初のところでスキップします。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">byte&lt;/span>[] GetExcelBytes(List&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; columns) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> List&amp;lt;ExcelItem&amp;gt; excelItems = &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;ExcelItem&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> List&amp;lt;&lt;span style="color:#ff7b72">dynamic&lt;/span>&amp;gt; objectsToExcel = &lt;span style="color:#ff7b72">new&lt;/span> List&amp;lt;&lt;span style="color:#ff7b72">dynamic&lt;/span>&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ExcelExportService exportService = &lt;span style="color:#ff7b72">new&lt;/span> ExcelExportService();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> item &lt;span style="color:#ff7b72">in&lt;/span> itemsToExcel)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">dynamic&lt;/span> newObject = &lt;span style="color:#ff7b72">new&lt;/span> ExpandoObject();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> col &lt;span style="color:#ff7b72">in&lt;/span> columns)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">this&lt;/span>.AddProperty(newObject, col, product.GetType().GetProperty(col).GetValue(item, &lt;span style="color:#79c0ff">null&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> objectsToExcel.Add(newObject);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> exportService.ExportToExcel(objectsToExcel);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ここで、&lt;code>ExcelItem&lt;/code> のいくつかのプロパティを持つオブジェクトを作成しますが、完全に動的です。 6 つのプロパティをすべて使用するのではなく、送信したいプロパティのうち 3 つだけを使用します。&lt;/p>
&lt;p>ここで、この &lt;code>ExcelItem&lt;/code> オブジェクトに 200 個のプロパティがあると仮定しましょう。&lt;em>クレイジー&lt;/em>ですが、それは起こり得ることです。&lt;/p>
&lt;p>私がしなければならない唯一のことは、Excel ファイルにレンダリングしたいプロパティを &lt;code>ColumnExcelItem&lt;/code> テーブルに挿入することだけです。&lt;/p>
&lt;h2 id="件">件&lt;/h2>
&lt;p>この例では 1 つのケースを使用しましたが、一部のユーザーに対して異なるレポートを作成したいとします。 &lt;code>admin&lt;/code> ロールがすべてのプロパティを取得する必要があるとします。その場合、データベースに次のような構造を作成します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>ReportingTemplates
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Id PropertyName
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>---------------------
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>1 Admin
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>2 Users
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>ReportingTemplateItems
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Id TemplateId PropertyName
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>---------------------
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>1 1 Id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>2 1 Name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>3 1 Surname
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>4 1 Age
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>5 1 CreatedAt
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>6 1 UpdatedAt
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>7 2 Name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>8 2 Surname
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>9 2 Age
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>次に、テンプレートを取得することで、コードに関係なく、すべて動的にさまざまな列を作成できます。&lt;/p>
&lt;p>ロール &lt;code>Admins&lt;/code> を持つユーザーがいる場合、ファイルには列 &lt;code>Id&lt;/code>、&lt;code>Name&lt;/code>、&lt;code>Surname&lt;/code>、&lt;code>Age&lt;/code>、&lt;code>CreatedAt&lt;/code>、&lt;code>UpdatedAt&lt;/code> の列が含まれます。&lt;/p>
&lt;p>&lt;code>Users&lt;/code> ロールを使用して生成した場合、&lt;code>Name&lt;/code>、&lt;code>Age&lt;/code> が含まれます。 &lt;code>Surname&lt;/code>。&lt;/p>
&lt;h2 id="結論">結論&lt;/h2>
&lt;p>これは、.NET で &lt;code>dynamic&lt;/code> がどのように機能するかを学ぶための優れた方法であり、ユーザーごとに異なるロールやテンプレートが必要で、ハードコーディングに時間を費やすことにあまり興味がない場合に、非常に優れたソリューションです。&lt;/p></content:encoded><category>.NET</category></item><item><title>TravisCI を使用した NuGet パッケージの継続的配信</title><link>https://emimontesdeoca.github.io/ja/posts/automated-nuget-deployment-travis-ci/</link><pubDate>Tue, 21 Jan 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/automated-nuget-deployment-travis-ci/</guid><description>Travis CI 継続的デリバリー パイプラインを使用して、NuGet パッケージのコンパイル、テスト、公開を自動化します。</description><content:encoded>&lt;p>最近、&lt;code>master&lt;/code> ブランチにプッシュした、またはプッシュしようとしたコードを自動的にテストするツールとして Travis CI を使用する方法に関する &lt;a href="https://emimontesdeoca.github.io/2020/ci-dotnet-core-and-travis-ci/">チュートリアル&lt;/a> を行いました。それをコンパイルしてテストし、最終的にその変更のステータスを報告します。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/e4c3f9019fdb8a8d80b2649f4c4bbbde">&lt;img src="https://i.gyazo.com/e4c3f9019fdb8a8d80b2649f4c4bbbde.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>しかし、&lt;strong>より良い&lt;/strong> にし続けることはできます。たとえば、最後のチュートリアルで作成されたコア ライブラリを &lt;a href="https://www.nuget.org/">NuGet&lt;/a> フィードを使用して公開し、誰もが自分のライブラリで簡単に操作できるようにしたいと考えています。&lt;/p>
&lt;p>&lt;strong>しかし、どのようにして?&lt;/strong> そのパッケージをどのようにコンパイル、テストし、フィードに公開して、全員がプロジェクトにダウンロードできるようにするのでしょうか?&lt;/p>
&lt;p>さて、これがこのチュートリアルです 😁!&lt;/p>
&lt;p>＃ 新着情報？&lt;/p>
&lt;p>コードに関してはほとんど変更はありません。変更する必要があるのは、パッキング、公開、そしてそれを自動化する方法です。&lt;/p>
&lt;h1 id="netコアcli">.NETコアCLI&lt;/h1>
&lt;p>&lt;a href="https://docs.microsoft.com/en-gb/dotnet/core/tools/?tabs=netcore2x">.NET Core CLI&lt;/a> を使用します。前のチュートリアルでは、&lt;code>dotnet restore&lt;/code>、&lt;code>dotnet build&lt;/code>、&lt;code>dotnet test&lt;/code> などのコマンドで使用しました。&lt;/p>
&lt;p>ここで、新しいコマンドのセットを使用してみます。&lt;/p>
&lt;h1 id="dotnet-nuget">&lt;code>dotnet nuget&lt;/code>&lt;/h1>
&lt;p>このツール セットのドキュメントは &lt;a href="https://docs.microsoft.com/en-gb/nuget/reference/dotnet-commands">ここ&lt;/a> で確認できますが、簡単な説明は次のとおりです。&lt;/p>
&lt;blockquote>
&lt;p>&lt;code>dotnet nuget&lt;/code> は、パッケージのインストール、復元、削除、公開を含む必須ツールのセットです。&lt;/p>&lt;/blockquote>
&lt;h1 id="nuget-フィード">NuGet フィード&lt;/h1>
&lt;p>&lt;a href="https://www.nuget.org/">NuGet は .NET のパッケージ マネージャーです&lt;/a>。 NuGet クライアント ツールは、パッケージを作成および使用する機能を提供します。 NuGet ギャラリーは、すべてのパッケージ作成者と利用者が使用する中央のパッケージ リポジトリです。&lt;/p>
&lt;p>NuGet は無料なので、サインアップして API キーを取得し、パッケージのアップロードを開始できます。これらはアップロードされ、検証されてから、&lt;strong>全員&lt;/strong>にリストされます。&lt;/p>
&lt;h1 id="apiキーを取得する">APIキーを取得する&lt;/h1>
&lt;p>パッケージをアップロードするには、サインアップ後に API キーを取得する必要があるため、&lt;a href="https://www.nuget.org/account/apikeys">API キー ページ&lt;/a> に移動して、新しい API キーを作成します。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/7509632471a35fb6b5b909699dec2520">&lt;img src="https://i.gyazo.com/7509632471a35fb6b5b909699dec2520.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>&lt;strong>API キーの有効期限は 365 日であることに注意してください。&lt;/strong>&lt;/p>
&lt;p>次に、キーをコピーしてどこかに保存します。ページに記載されているように、再度見ることはできなくなり、保存しなかった場合は再生成する必要があります。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/2feb401c84034706d12a59f03bd30136">&lt;img src="https://i.gyazo.com/2feb401c84034706d12a59f03bd30136.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;h1 id="nugetパッケージを生成する">NuGetパッケージを生成する&lt;/h1>
&lt;p>&lt;code>API&lt;/code> キーを取得したので、次のステップはソリューションから NuGet パッケージを生成することです。クラス ライブラリがあるソリューションを開きます。&lt;/p>
&lt;p>&lt;a href="https://emimontesdeoca.github.io/2020/ci-dotnet-core-and-travis-ci/">最後のチュートリアル&lt;/a> のソリューションを使用します。これには、&lt;code>CalculatorCLI.Core&lt;/code> という .NET Core ライブラリ クラスがあります。&lt;/p>
&lt;p>&lt;strong>プロジェクト プロパティ&lt;/strong>、&lt;strong>パッケージ&lt;/strong>に移動すると、パッケージのバージョン、作成者、説明など、パッケージに関する多くの情報が表示されます。&lt;/p>
&lt;p>次にこれを入力します。実際にはすべてを入力する必要はありませんが、重要で必須のものは &lt;code>Package id&lt;/code>、&lt;code>Package version&lt;/code>、&lt;code>Authors&lt;/code>、および &lt;code>Description&lt;/code> です。また、自分でファイルをアップロードしようとすると、&lt;code>License&lt;/code> が要求されるため、それも追加する必要があります。&lt;a href="https://gyazo.com/6becdace2915c40fa2091ed9916fe517">&lt;img src="https://i.gyazo.com/6becdace2915c40fa2091ed9916fe517.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>次に保存し、プロジェクト内で右クリックして &lt;code>Pack&lt;/code> を選択します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">1&lt;/span>&amp;gt;&lt;span style="color:#ff7b72;font-weight:bold">------&lt;/span> Build started: Project: CalculatorCLI.Core, Configuration: Debug Any CPU &lt;span style="color:#ff7b72;font-weight:bold">------&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">1&lt;/span>&amp;gt;CalculatorCLI.Core &lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>&amp;gt; D:&lt;span style="color:#f85149">\&lt;/span>Development&lt;span style="color:#f85149">\&lt;/span>Personal&lt;span style="color:#f85149">\&lt;/span>CalculatorCLI&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>demo&lt;span style="color:#f85149">\&lt;/span>CalculatorCLI&lt;span style="color:#f85149">\&lt;/span>CalculatorCLI.Core&lt;span style="color:#f85149">\&lt;/span>bin&lt;span style="color:#f85149">\&lt;/span>Debug&lt;span style="color:#f85149">\&lt;/span>netstandard2&lt;span style="color:#a5d6ff">.1&lt;/span>&lt;span style="color:#f85149">\&lt;/span>CalculatorCLI.Core.dll
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a5d6ff">1&lt;/span>&amp;gt;Successfully created &lt;span style="color:#ff7b72">package&lt;/span> &lt;span style="color:#f85149">&amp;#39;&lt;/span>D:&lt;span style="color:#f85149">\&lt;/span>Development&lt;span style="color:#f85149">\&lt;/span>Personal&lt;span style="color:#f85149">\&lt;/span>CalculatorCLI&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>demo&lt;span style="color:#f85149">\&lt;/span>CalculatorCLI&lt;span style="color:#f85149">\&lt;/span>CalculatorCLI.Core&lt;span style="color:#f85149">\&lt;/span>bin&lt;span style="color:#f85149">\&lt;/span>Debug&lt;span style="color:#f85149">\&lt;/span>CalculatorCLI.Core&lt;span style="color:#a5d6ff">.1.0.0.1&lt;/span>.nupkg&lt;span style="color:#f85149">&amp;#39;&lt;/span>.
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72;font-weight:bold">==========&lt;/span> Build: &lt;span style="color:#a5d6ff">1&lt;/span> succeeded, &lt;span style="color:#a5d6ff">0&lt;/span> failed, &lt;span style="color:#a5d6ff">0&lt;/span> up&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>to&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>date, &lt;span style="color:#a5d6ff">0&lt;/span> skipped &lt;span style="color:#ff7b72;font-weight:bold">==========&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>出力には、すべてが正常であれば &lt;code>Successfully created package&lt;/code> が出力されることが示されます。&lt;/p>
&lt;h1 id="dotnet-を使用する">&lt;code>dotnet&lt;/code> を使用する&lt;/h1>
&lt;p>前のチュートリアルと同様に、ソリューションを構築してテストするためにさまざまなコマンドを使用しました。ここで、パッケージをパックして公開するためのコマンドをさらに追加します。&lt;/p>
&lt;p>それらのコマンドは次のとおりです。&lt;/p>
&lt;ol>
&lt;li>&lt;code>dotnet pack -c &amp;lt;configuration&amp;gt;&lt;/code>&lt;/li>
&lt;li>&lt;code>dotnet nuget push &amp;lt;package&amp;gt; &amp;lt;k &amp;lt;apikey&amp;gt; -s &amp;lt;source&amp;gt;&lt;/code>&lt;/li>
&lt;/ol>
&lt;h1 id="デプロイメントスクリプト">デプロイメントスクリプト&lt;/h1>
&lt;p>ビルドの方法を変更する予定であるため、環境に応じて異なるファイルを用意し、それぞれのファイルにビルド定義を含めるのが最善のアイデアであると考えています。&lt;/p>
&lt;p>そこで、リポジトリのルートに 3 つの &lt;code>bash&lt;/code> スクリプトを含む &lt;code>scripts&lt;/code> という名前のフォルダーを作成しましょう。&lt;/p>
&lt;p>&lt;code>scripts\compile.sh&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">#!/bin/sh
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#a5d6ff">&amp;#34;Restoring...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet restore &lt;span style="color:#a5d6ff">&amp;#34;.\CalculatorCLI\CalculatorCLI.sln&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#a5d6ff">&amp;#34;Building...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet build &lt;span style="color:#a5d6ff">&amp;#34;.\CalculatorCLI\CalculatorCLI.sln&amp;#34;&lt;/span> -c Release
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>scripts\test.sh&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">#!/bin/sh
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#a5d6ff">&amp;#34;Testing...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet test &lt;span style="color:#a5d6ff">&amp;#34;.\CalculatorCLI\CalculatorCLI.sln&amp;#34;&lt;/span> -c Release -v n
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>scripts\push.sh&lt;/code>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">#!/bin/sh
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#a5d6ff">&amp;#34;Packing...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet pack ./CalculatorCLI/CalculatorCLI.Core/CalculatorCLI.Core.csproj -c Release
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#a5d6ff">&amp;#34;Pushing...&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>dotnet nuget push ./CalculatorCLI/CalculatorCLI.Core/bin/Release/*.nupkg -s &lt;span style="color:#a5d6ff">&amp;#34;https://nuget.org&amp;#34;&lt;/span> -k &lt;span style="color:#79c0ff">$NUGET_API_KEY&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="travisyml-ファイルの改善">&lt;code>.travis.yml&lt;/code> ファイルの改善&lt;/h1>
&lt;p>ソース コードが更新され、使用する必要があるコマンドがわかったので、継続的デリバリーと継続的インテグレーションを統合する必要があります。これにより、コーディング、テスト、レビュー、プッシュ以外に何もする必要がなくなります。&lt;/p>
&lt;p>&lt;code>.travis.yml&lt;/code> ファイルに対してこれから行うことは次のとおりです。&lt;/p>
&lt;ol>
&lt;li>別の &lt;code>stages&lt;/code> を追加します&lt;/li>
&lt;li>各 &lt;code>stage&lt;/code> はブランチに依存します&lt;/li>
&lt;li>ビルドがマスター上にある場合は、パッケージを更新する必要があることを意味するため、パッケージを NuGet フィードにプッシュします。&lt;/li>
&lt;li>ビルドがプル リクエストの場合、ビルドがコンパイルおよびテストされるかどうかを確認しますが、公開するつもりはありません。&lt;/li>
&lt;/ol>
&lt;h2 id="ステージ">ステージ&lt;/h2>
&lt;p>彼らのドキュメントから:&lt;/p>
&lt;blockquote>
&lt;p>ビルド設定 (.travis.yml ファイル) で条件を指定することで、ビルド、ステージ、ジョブをフィルターで除外したり拒否したりできます。&lt;/p>&lt;/blockquote>
&lt;p>TravisCI &lt;code>stages&lt;/code> に関する詳細は、&lt;a href="https://docs.travis-ci.com/user/conditional-builds-stages-jobs">ドキュメント ページ&lt;/a> で見つけることができます。&lt;/p>
&lt;p>したがって、ファイルを変更してステージを追加すると、次のようになります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">language&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">csharp&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">sudo&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">required&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">mono&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">none &lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">dotnet&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">3.0&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">os&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">linux&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">jobs&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">include&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">stage&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">compile&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">script&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">bash scripts/compile.sh&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">stage&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">test&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">script&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">bash scripts/test.sh&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#7ee787">stage&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">deploy-prod&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">if&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">branch = master AND type = push&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">name&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">&amp;#34;Deploy to NuGet&amp;#34;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#7ee787">script&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">bash scripts/push.sh&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ご覧のとおり、3 つの異なるステージ &lt;code>compile&lt;/code>、&lt;code>test&lt;/code>、および &lt;code>push&lt;/code> があり、最後のステージのみが &lt;code>branch&lt;/code> が &lt;code>master&lt;/code> の場合に実行され、それに対して &lt;code>pull request&lt;/code> ではなく、&lt;code>push&lt;/code> のみが実行されます。&lt;/p>
&lt;p>これについて理解できない場合は、ドキュメントを参照してください。非常に簡単に説明されています。&lt;/p>
&lt;p>それを念頭に置くと、すべての &lt;code>pull request&lt;/code> は NuGet フィードに何もプッシュしません。&lt;/p>
&lt;h1 id="apiキーを環境変数として設定する">APIキーを環境変数として設定する&lt;/h1>
&lt;p>リポジトリ ビルドの設定に移動し、&lt;code>NUGET_API_KEY&lt;/code> という名前の新しい環境変数を追加します。値は NuGet ページからコピーした API キーです。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/68a4bcc4ce57eccd6bb25b7d62901c84">&lt;img src="https://i.gyazo.com/68a4bcc4ce57eccd6bb25b7d62901c84.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;h2 id="pull-request-を作成します">&lt;code>pull request&lt;/code> を作成します&lt;/h2>
&lt;p>すべての設定が完了したので、&lt;code>pull request&lt;/code> を作成し、そのプル リクエストのコンパイルで最後の &lt;code>stage&lt;/code> が無視されているかどうかを確認します。&lt;/p>
&lt;h2 id="ビルドを確認する">ビルドを確認する&lt;/h2>
&lt;p>プル リクエストを行うとすぐに、ビルドが TravisCI ダッシュボードのキューに入れられます。ご覧のとおり、そこには 3 つではなく 2 つの異なるビルドがあることがわかります。&lt;a href="https://gyazo.com/11a209e72a121aef82a5b5a80d77a2f0">&lt;img src="https://i.gyazo.com/11a209e72a121aef82a5b5a80d77a2f0.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>ここで &lt;code>pull request&lt;/code> を受け入れ、ビルドを再度チェックして、2 つではなく 3 つのジョブがあることを確認しましょう。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/46dad20d00defb8c14279332a946e73e">&lt;img src="https://i.gyazo.com/46dad20d00defb8c14279332a946e73e.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>ビルドがキューに入れられていることがわかるとすぐに、最後に &lt;code>Deploy-prod&lt;/code> を含む 3 つのジョブが表示されます。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/d24344e42f2b45225acb618ac030fd8f">&lt;img src="https://i.gyazo.com/d24344e42f2b45225acb618ac030fd8f.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>このビルド全体が完了すると、パッケージがソースにプッシュされたことがログですぐに確認できます。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/77b1e44b4f9059cb7266fb18fbace8a7">&lt;img src="https://i.gyazo.com/77b1e44b4f9059cb7266fb18fbace8a7.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>そして明らかに、NuGet ページで確認できます。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/09421094730ea2e087cc5da639069ec4">&lt;img src="https://i.gyazo.com/09421094730ea2e087cc5da639069ec4.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/a7905ddfd6665bb6e8e62e160f21630d">&lt;img src="https://i.gyazo.com/a7905ddfd6665bb6e8e62e160f21630d.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>#それだけです&lt;/p>
&lt;p>このチュートリアルでは、NuGet パッケージの配信を自動化する方法について学びました。 &lt;code>TravisCI&lt;/code>、&lt;code>stages&lt;/code>、&lt;code>dotnet nuget&lt;/code> などのツールを使用しました。&lt;/p>
&lt;p>私の意見では、それが適切に設定するには時間がかかり、場合によっては非常に複雑すぎる可能性があるとしてもです。すべてを完璧かつ自動化するために、この種のテクニックに時間を投資する価値があります。&lt;/p>
&lt;p>ソース コードは &lt;code>.travis.yml&lt;/code> ファイルで [ここ] (&lt;a href="https://github.com/emimontesdeoca/CalculatorCLI-demo">https://github.com/emimontesdeoca/CalculatorCLI-demo&lt;/a>) にあります。ご質問がある場合は、私の &lt;a href="https://twitter.com/emimontesdeocaa">twitter&lt;/a> までお気軽にご連絡ください。&lt;/p></content:encoded><category>.NET</category><category>NuGet</category><category>CI/CD</category></item><item><title>TravisCI を使用した .NET Core 3.0 プロジェクトの継続的統合</title><link>https://emimontesdeoca.github.io/ja/posts/ci-dotnet-core-and-travis-ci/</link><pubDate>Tue, 14 Jan 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/ci-dotnet-core-and-travis-ci/</guid><description>自動化されたビルドとテストを備えた Travis CI を使用して、.NET Core プロジェクトの継続的統合をセットアップします。</description><content:encoded>&lt;p>先週末、私はさまざまなリポジトリで行ってきたスクレイパー・チェッカー・ダウンローダー・プロジェクトを適切に開始したいと決心しました。&lt;/p>
&lt;p>さらに別のプロジェクトを開始した後、これは、CI/CD、プル リクエスト、ドキュメント、Readme のバッジなど、私がこれまで見てきたクールで実際にベスト プラクティスであるすべてを使用して、&lt;strong>本当にクール&lt;/strong> のようにクールでなければなりませんでした。&lt;/p>
&lt;p>そして充実した週末を過ごした後、最終的に &lt;a href="https://github.com/Dramarr">Dramarr&lt;/a> というさまざまなソースから番組をスクラップしてダウンロードするツール セットを作成することになりました。&lt;/p>
&lt;p>組織内にはさまざまなリポジトリがあり、それらのほとんどは、プル リクエストやマスター ブランチにマージされるときにコンパイル、テスト、デプロイされるライブラリです。&lt;/p>
&lt;p>それが CI/CD または継続的インテグレーション/継続的デリバリーと呼ばれるものです。&lt;/p>
&lt;p>ただし、このチュートリアルでは CI についてのみ説明します。&lt;/p>
&lt;h1 id="継続的インテグレーション">継続的インテグレーション&lt;/h1>
&lt;p>＃＃それは何ですか？&lt;/p>
&lt;p>Martin Fowler の &lt;a href="https://martinfowler.com/articles/continuousIntegration.html">ブログ&lt;/a> から引用したもので、これが私が読んだ中で最も優れた説明です。&lt;/p>
&lt;blockquote>
&lt;p>継続的インテグレーションは、チームのメンバーが自分の作業を頻繁に統合するソフトウェア開発手法であり、通常は各人が少なくとも毎日統合するため、1 日に複数の統合が行われます。
統合エラーをできるだけ早く検出するために、各統合は自動ビルド (テストを含む) によって検証されます。&lt;/p>&lt;/blockquote>
&lt;h2 id="ツール">ツール&lt;/h2>
&lt;p>ワークフローを CI/CD と統合するためのツールは多数ありますが、このチュートリアルでは、&lt;a href="https://github.com/">Github&lt;/a> を使用してコードを保存し、&lt;a href="https://travis-ci.org/">TravisCI&lt;/a> ツールを使用して CI をセットアップします。言語とフレームワークに関しては、C# と新しい .NET Core 3.0 を使用します。&lt;/p>
&lt;h1 id="要件">要件&lt;/h1>
&lt;p>これを機能させるには、次の 3 つの簡単なことが必要です。&lt;/p>
&lt;ol>
&lt;li>Visual Studio 2019の最新バージョン
2.Githubアカウント&lt;/li>
&lt;li>Github アカウントにリンクされた Travis-CI アカウント&lt;/li>
&lt;/ol>
&lt;h1 id="プロジェクト">プロジェクト&lt;/h1>
&lt;p>このチュートリアルでは、簡単な計算機を使用します。すべてをテストするためのライブラリ、コマンド ライン ツール、およびテスト プロジェクトを作成します。&lt;/p>
&lt;p>このテスト プロジェクトは、CI のセットアップ時にも実行されます。つまり、将来コードに変更を加え、最初に作成したテストが合格しなかった場合は、通知を受け取るか、単純にプル リクエストを拒否することができます。&lt;/p>
&lt;h2 id="github-リポジトリの作成">Github リポジトリの作成&lt;/h2>
&lt;p>まず、Github リポジトリを作成するので、Github にアクセスしてリポジトリを作成し、ローカル環境にクローンを作成します。この新しいリポジトリを &lt;code>CalculatorCLI-demo&lt;/code> と呼ぶことにしました。&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/0657eb2bdeb3c331b9e4585d7deed5ef.png" >
&lt;h2 id="ソリューションの作成">ソリューションの作成&lt;/h2>
&lt;p>次に、クローンされたリポジトリのルート フォルダーに &lt;code>CalculatorCLI&lt;/code> という名前の空のソリューションを作成しましょう。&lt;/p>
&lt;h2 id="コアライブラリ">コアライブラリ&lt;/h2>
&lt;p>実際のプロジェクトと同様に、ライブラリを生成する別のプロジェクトにロジックを保存するので、それを作成しましょう。&lt;/p>
&lt;p>&lt;code>Class Library (.NET Standard)&lt;/code> を作成し、&lt;code>CalculatorCLI.Core&lt;/code> という名前を付けます。&lt;/p>
&lt;h3 id="net-core-のバージョンプロジェクトを作成したらすぐにプロジェクトのプロパティに移動しnet-core-30-でビルドされたプロジェクトと互換性を持たせるためにtarget-framework-を-net-standard-21-に変更します">NET Core のバージョンプロジェクトを作成したらすぐに、プロジェクトのプロパティに移動し、&lt;code>.NET Core 3.0&lt;/code> でビルドされたプロジェクトと互換性を持たせるために、&lt;code>Target framework&lt;/code> を &lt;code>.NET Standard 2.1&lt;/code> に変更します。&lt;/h3>
&lt;h3 id="コード">コード&lt;/h3>
&lt;p>チュートリアルのために、操作を処理する簡単なクラスを作成しましょう。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">ConsoleCalculator.Core&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">enum&lt;/span> OperatorsEnum
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ADD,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> SUBSTRACT,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MULTIPLY,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> DIVIDE
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Operation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> OperatorsEnum OperatorEnum { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> LeftValue { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> RightValue { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Operation(&lt;span style="color:#ff7b72">string&lt;/span> operatorString, &lt;span style="color:#ff7b72">int&lt;/span> leftValue, &lt;span style="color:#ff7b72">int&lt;/span> rightValue)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">switch&lt;/span> (operatorString)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;+&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> OperatorEnum = OperatorsEnum.ADD;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;-&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> OperatorEnum = OperatorsEnum.SUBSTRACT;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;*&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> OperatorEnum = OperatorsEnum.MULTIPLY;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> OperatorEnum = OperatorsEnum.DIVIDE;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">default&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> Exception(&lt;span style="color:#a5d6ff">&amp;#34;Operator invalid&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> LeftValue = leftValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RightValue = rightValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> DoOperation()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">switch&lt;/span> (OperatorEnum)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> OperatorsEnum.ADD:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> LeftValue + RightValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> OperatorsEnum.SUBSTRACT:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> LeftValue - RightValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> OperatorsEnum.MULTIPLY:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> LeftValue * RightValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> OperatorsEnum.DIVIDE:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> LeftValue / RightValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">default&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> Exception(&lt;span style="color:#a5d6ff">&amp;#34;Operator is not valid&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="cli">CLI&lt;/h2>
&lt;p>コアプロジェクトができたので、アプリケーションを作成しましょう。この場合、引数を受け取り、結果を出力する単純なコンソール アプリケーションになります。&lt;/p>
&lt;p>それでは、新しい &lt;code>Console App (.NET Core)&lt;/code> を作成しましょう。これに &lt;code>CalculatorCLI.CLI&lt;/code> という名前を付けました。&lt;/p>
&lt;h3 id="net-core-のバージョン">NET Core のバージョン&lt;/h3>
&lt;p>前に行ったように、プロジェクトを作成したらすぐにプロジェクトのプロパティに移動し、&lt;code>Target framework&lt;/code> を &lt;code>.NET Core 3.0&lt;/code> に変更します (まだ変更されていない場合)。&lt;/p>
&lt;p>次に、&lt;code>ConsoleCLI.Core&lt;/code> への参照を新しく作成したプロジェクトに追加します。&lt;/p>
&lt;h3 id="コード-1">コード&lt;/h3>
&lt;p>次にコードですが、以前よりも簡単になりました。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">ConsoleCalculator.Core&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Text.RegularExpressions&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">ConsoleCalculator.CLI&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Program&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> Main(&lt;span style="color:#ff7b72">string&lt;/span>[] args)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (args.Length == &lt;span style="color:#a5d6ff">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PrintUsage();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> joinedArgs = &lt;span style="color:#ff7b72">string&lt;/span>.Join(&lt;span style="color:#a5d6ff">&amp;#34; &amp;#34;&lt;/span>, args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> regex = &lt;span style="color:#a5d6ff">@&amp;#34;-op [\+\-\*\/] -l [-0-9]+ -r [-0-9]+&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (Regex.IsMatch(joinedArgs, regex))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">int&lt;/span> _left = Int32.Parse(args[&lt;span style="color:#a5d6ff">3&lt;/span>]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">int&lt;/span> _right = Int32.Parse(args[&lt;span style="color:#a5d6ff">5&lt;/span>]);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> _operator = args[&lt;span style="color:#a5d6ff">1&lt;/span>];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> _operation = &lt;span style="color:#ff7b72">new&lt;/span> Operation(_operator, _left, _right);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> _result = _operation.DoOperation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;Result is: {_result}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">else&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> PrintUsage();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> PrintUsage()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;Welcome to ConsoleCalculator!&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;-op Operator, it must be +,-,*,/&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;-l Left number&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;-r Left number&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;Example usage: -op + -l 5 -r 6&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>このアプリケーションはコマンドなどから使用するので、それを機能させるには、いくつかのパラメーターを渡して呼び出す必要があります。たとえば:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>ConsoleCalculator.CLI.exe -op + -l &lt;span style="color:#a5d6ff">10&lt;/span> -r &lt;span style="color:#a5d6ff">20&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これは次のようになります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>ConsoleCalculator.CLI.exe -operator + -leftValue &lt;span style="color:#a5d6ff">10&lt;/span> -rightValue &lt;span style="color:#a5d6ff">20&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>そのコードは非常に単純で、特定の正規表現パターンに一致しない場合、それは間違った呼び出しとなり、&lt;code>PrintUsage()&lt;/code> が呼び出されます。これは、数値以外の値を入力した場合、正規表現で設定されているため、計算すら試行されないことを意味します。&lt;/p>
&lt;p>つまり、次のように呼び出すと、次のようになります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>ConsoleCalculator.CLI.exe -operator + -leftValue asdfg -rightValue ghjk
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>操作ロジック内に侵入することはなく、値を &lt;code>TryParse&lt;/code> するなどの将来のチェックを保存します。&lt;/p>
&lt;h2 id="テスト">テスト&lt;/h2>
&lt;p>コア ライブラリとコマンド ラインはありますが、CI でやりたいことなので、ここでテストする必要があります。&lt;/p>
&lt;p>それでは、新しい &lt;code>MSTest Test Project (.NET Core)&lt;/code> を作成し、 &lt;code>CalculatorCLI.Tests&lt;/code> という名前を付けましょう。&lt;/p>
&lt;h3 id="net-core-のバージョン-1">NET Core のバージョン&lt;/h3>
&lt;p>前に行ったように、プロジェクトを作成したらすぐにプロジェクトのプロパティに移動し、&lt;code>Target framework&lt;/code> を &lt;code>.NET Core 3.0&lt;/code> に変更します (まだ変更されていない場合)。&lt;/p>
&lt;p>次に、&lt;code>ConsoleCLI.Core&lt;/code> および &lt;code>ConsoleCLI.Core&lt;/code> への参照を、新しく作成したテスト プロジェクトに追加します。&lt;/p>
&lt;h3 id="コード-2">コード&lt;/h3>
&lt;p>テストを 2 つの異なるファイル &lt;code>CoreTests.cs&lt;/code> と &lt;code>CLITests.cs&lt;/code> に分割します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">CalculatorCLI.Core&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.VisualStudio.TestTools.UnitTesting&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Collections.Generic&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Text&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">CalculatorCLI.Tests&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestClass]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CoreTests&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> _left = &lt;span style="color:#a5d6ff">2&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> _right = &lt;span style="color:#a5d6ff">2&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldAdd()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> expectedResult = &lt;span style="color:#a5d6ff">4&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> operation = &lt;span style="color:#ff7b72">new&lt;/span> Operation(&lt;span style="color:#a5d6ff">&amp;#34;+&amp;#34;&lt;/span>, _left, _right);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> functionResult = operation.DoOperation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Assert.AreEqual(functionResult, expectedResult);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldSubstract()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> expectedResult = &lt;span style="color:#a5d6ff">0&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> operation = &lt;span style="color:#ff7b72">new&lt;/span> Operation(&lt;span style="color:#a5d6ff">&amp;#34;-&amp;#34;&lt;/span>, _left, _right);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> functionResult = operation.DoOperation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Assert.AreEqual(functionResult, expectedResult);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldMultiply()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> expectedResult = &lt;span style="color:#a5d6ff">4&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> operation = &lt;span style="color:#ff7b72">new&lt;/span> Operation(&lt;span style="color:#a5d6ff">&amp;#34;*&amp;#34;&lt;/span>, _left, _right);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> functionResult = operation.DoOperation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Assert.AreEqual(functionResult, expectedResult);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldDivide()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> expectedResult = &lt;span style="color:#a5d6ff">1&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> operation = &lt;span style="color:#ff7b72">new&lt;/span> Operation(&lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>, _left, _right);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> functionResult = operation.DoOperation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Assert.AreEqual(functionResult, expectedResult);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [ExpectedException(typeof(System.DivideByZeroException))]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldThrowExceptionForDivideByZero()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> operation = &lt;span style="color:#ff7b72">new&lt;/span> Operation(&lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>, _left, &lt;span style="color:#a5d6ff">0&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> operation.DoOperation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [ExpectedException(typeof(System.Exception), &amp;#34;Operator invalid&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldThrowExceptionForWrongOperator()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> operation = &lt;span style="color:#ff7b72">new&lt;/span> Operation(&lt;span style="color:#a5d6ff">&amp;#34;text&amp;#34;&lt;/span>, _left, &lt;span style="color:#a5d6ff">0&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> operation.DoOperation();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.VisualStudio.TestTools.UnitTesting&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Collections.Generic&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System.Text&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">CalculatorCLI.Tests&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestClass]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CLITests&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> _left = &lt;span style="color:#a5d6ff">&amp;#34;2&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> _right = &lt;span style="color:#a5d6ff">&amp;#34;2&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldAdd()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> args = &lt;span style="color:#ff7b72">new&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>[] { &lt;span style="color:#a5d6ff">&amp;#34;-op&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;+&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-l&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;45&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-r&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;96&amp;#34;&lt;/span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CalculatorCLI.CLI.Program.Main(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldSubstract()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> args = &lt;span style="color:#ff7b72">new&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>[] { &lt;span style="color:#a5d6ff">&amp;#34;-op&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-l&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;45&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-r&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;96&amp;#34;&lt;/span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CalculatorCLI.CLI.Program.Main(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldMultiply()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> args = &lt;span style="color:#ff7b72">new&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>[] { &lt;span style="color:#a5d6ff">&amp;#34;-op&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;*&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-l&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;45&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-r&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;96&amp;#34;&lt;/span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CalculatorCLI.CLI.Program.Main(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> ShouldDivide()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> args = &lt;span style="color:#ff7b72">new&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span>[] { &lt;span style="color:#a5d6ff">&amp;#34;-op&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-l&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;45&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;-r&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;96&amp;#34;&lt;/span> };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> CalculatorCLI.CLI.Program.Main(args);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>すべてを作成すると、最終的に次のようなソリューションになります。&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/ffecc23a14d796af9a46dbb390c0d072.png" />
&lt;p>これでテストを実行できるようになりました。Visual Studio の &lt;code>Test Explorer&lt;/code> に移動してテストを実行してください。&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/17d7ac9b2f1e7fb2666a68fc87882eed.png" />
&lt;p>#トラヴィスCI&lt;/p>
&lt;p>まだお持ちでない場合は、TravisCI はホスト型の継続的統合および展開システムです。&lt;/p>
&lt;p>ここで行う必要がある手順がいくつかありますが、最初に、プロジェクトをビルドしてテストするために、TravisCI エージェントがリッスンできるように Github リポジトリをリンクします。&lt;/p>
&lt;h2 id="リポジトリを有効にする">リポジトリを有効にする&lt;/h2>
&lt;p>これを行うには、Travis CI ページにログインしてリポジトリに移動し、作成したプロジェクトをフィルタリングして、リポジトリ名の横にあるスライダーをクリックして有効にします。&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/28b366dddd3f5caa9100ca6b6d200764.png" >
&lt;h2 id="travisyml-を作成するプロジェクトのルートに-travisyml-というファイルを作成する必要がありますこれはドキュメントに記載されているように-ためです">.travis.yml を作成するプロジェクトのルートに &lt;code>.travis.yml&lt;/code> というファイルを作成する必要があります。これは、&lt;a href="https://docs.travis-ci.com/user/tutorial/">ドキュメントに記載されているように&lt;/a> ためです。&lt;/h2>
&lt;blockquote>
&lt;p>Travis は、.travis.yml ファイルを追加した後にプッシュしたコミットでのみビルドを実行します。&lt;/p>&lt;/blockquote>
&lt;p>そこで、次の行を含む &lt;code>.travis.yml&lt;/code> ファイルをリポジトリのルートに作成します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yml" data-lang="yml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">language&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">csharp&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">sudo&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">required&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">mono&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">none &lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">dotnet&lt;/span>:&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#a5d6ff">3.0&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">os&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">linux&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">before_script&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">dotnet restore &amp;#34;.\CalculatorCLI\CalculatorCLI.sln&amp;#34;&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681">&lt;/span>&lt;span style="color:#7ee787">script&lt;/span>:&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">dotnet build &amp;#34;.\CalculatorCLI\CalculatorCLI.sln&amp;#34; -c Release&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#6e7681"> &lt;/span>- &lt;span style="color:#a5d6ff">dotnet test &amp;#34;.\CalculatorCLI\CalculatorCLI.sln&amp;#34; -c Release -v n&lt;/span>&lt;span style="color:#6e7681">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>.travis.yml&lt;/code> ファイルがどのように機能するかという構文には立ち入りませんが、これが何をしているのかを確認してみましょう。&lt;/p>
&lt;ol>
&lt;li>言語が &lt;code>csharp&lt;/code> になるように設定します。&lt;/li>
&lt;li>&lt;code>.NET Core 3.0&lt;/code> は Linux でネイティブに実行されるため、&lt;code>mono&lt;/code> は使用しません。&lt;/li>
&lt;li>&lt;code>dotnet&lt;/code> バージョンを &lt;code>3.0&lt;/code> に設定します。&lt;/li>
&lt;li>&lt;code>os&lt;/code> を設定します。デフォルトでは &lt;code>linux&lt;/code> ですが、とにかく追加しました。&lt;/li>
&lt;li>これで、主要なロジックの前に実行される &lt;code>before_script&lt;/code> ができました。ここで、私が設定したのは、ソリューションに対して &lt;code>dotnet restore&lt;/code> を実行して、後ですべてが完全に読み込まれるようにすることでした。&lt;/li>
&lt;li>&lt;code>script&lt;/code> では、ソリューションに対して &lt;code>dotnet builld&lt;/code> および &lt;code>dotnet test&lt;/code> を実行します。これにより、コンパイルが完了したことを確認してからテストを実行します。&lt;/li>
&lt;/ol>
&lt;p>ああ、これで終わりです！&lt;/p>
&lt;h1 id="マスターにアップロード">マスターにアップロード&lt;/h1>
&lt;p>あとは、すべてをマスターするだけです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-cmd" data-lang="cmd">&lt;span style="display:flex;">&lt;span>git add --all
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>git commit -m &lt;span style="color:#a5d6ff">&amp;#34;Initial files&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>git push
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="継続的インテグレーションをチェックする">継続的インテグレーションをチェックする&lt;/h1>
&lt;p>リポジトリ ページまたは TravisCI ダッシュボードの両方で実行した &lt;code>master&lt;/code> へのプッシュの CI ステータスを確認できます。&lt;/p>
&lt;h2 id="進行中">進行中&lt;/h2>
&lt;img align="center" src="https://i.gyazo.com/977ea42a90adccf0736464b6603867a5.png" >
&lt;br/>
&lt;br/>
&lt;img align="center" src="https://i.gyazo.com/52a5f9356df5436c862b7df6fe66a9f4.png" >
&lt;h2 id="完了しました">完了しました&lt;/h2>
&lt;img align="center" src="https://i.gyazo.com/c3d3521925a3e20bcf55bf5f6a2a711d.png" >
&lt;br/>
&lt;br/>
&lt;img align="center" src="https://i.gyazo.com/8c749c2ce44837a39fc5cd3e8838a798.png" >
&lt;p>#壊してみよう&lt;/p>
&lt;p>これがどれほど強力であるかを確認するために、コードを分割し、コア ライブラリを変更して失敗させてみましょう。&lt;/p>
&lt;h2 id="コードの変更">コードの変更&lt;/h2>
&lt;p>したがって、&lt;code>Operation.cs&lt;/code> に移動して、一部のテストを中断するような変更を加えてください。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">System&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">namespace&lt;/span> &lt;span style="color:#ff7b72">CalculatorCLI.Core&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">enum&lt;/span> OperatorsEnum
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ADD,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> SUBSTRACT,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> MULTIPLY,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> DIVIDE
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Operation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> OperatorsEnum OperatorEnum { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> LeftValue { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> RightValue { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Operation(&lt;span style="color:#ff7b72">string&lt;/span> operatorString, &lt;span style="color:#ff7b72">int&lt;/span> leftValue, &lt;span style="color:#ff7b72">int&lt;/span> rightValue)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">switch&lt;/span> (operatorString)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;+&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> OperatorEnum = OperatorsEnum.SUBSTRACT;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;-&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> OperatorEnum = OperatorsEnum.SUBSTRACT;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;*&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> OperatorEnum = OperatorsEnum.MULTIPLY;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;/&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> OperatorEnum = OperatorsEnum.DIVIDE;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">default&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> Exception(&lt;span style="color:#a5d6ff">&amp;#34;Operator invalid&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> LeftValue = leftValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> RightValue = rightValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">int&lt;/span> DoOperation()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">switch&lt;/span> (OperatorEnum)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> OperatorsEnum.ADD:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> LeftValue + RightValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> OperatorsEnum.SUBSTRACT:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> LeftValue - RightValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> OperatorsEnum.MULTIPLY:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> LeftValue * RightValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> OperatorsEnum.DIVIDE:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> LeftValue / RightValue;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">default&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">throw&lt;/span> &lt;span style="color:#ff7b72">new&lt;/span> Exception(&lt;span style="color:#a5d6ff">&amp;#34;Operator is not valid&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>そして、ケースを追加に変更したため、テストを再度実行すると、失敗します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;+&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> OperatorEnum = OperatorsEnum.SUBSTRACT;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">break&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>予想どおり、&lt;code>ShouldAdd&lt;/code> のケースでは失敗しました。&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/a57d0a0f8c07cc1ab6e4f55a8466cbbd.png" >
&lt;p>次に、この変更をコミットしてマスターにプッシュし、TravisCI エージェントからの結果を待ちます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>git add --all
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>git commit -m &lt;span style="color:#a5d6ff">&amp;#34;Breaking changes&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>git push
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="ビルドする">ビルドする&lt;/h2>
&lt;p>次に、TravisCI ログに移動しましょう。統合テストが失敗し、ビルド ステータスがエラーになっているため、プロジェクトが正常に中断されたことがわかります。&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/397befe9b7f5a32b6e97511733296b00.png" >
&lt;p>ログの最後にエラー自体が表示されます。&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/e5cbf32c5dd7a08bf6628d81edff3130.png" >
&lt;h2 id="もう一度修正しましょう">もう一度修正しましょう!&lt;/h2>
&lt;p>ここで、これまでの操作を元に戻してコードをマスターにプッシュし、新しいビルドのステータスを確認します。&lt;/p>
&lt;p>テストは正常に合格しています:&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/02deb8fcb4fca618ff2d79f1c27c6df5.png" >
&lt;p>そしてビルドも成功です。&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/38a227f0634c6040c6608f8c51f36cd3.png" >
&lt;h1 id="結論">結論&lt;/h1>
&lt;p>&lt;strong>本当に非常に強力です&lt;/strong>。CI と CD は昔から存在していましたが、現在では、どれだけ小さくてもシンプルでも、すべてのプロジェクトで実行するのが非常に簡単です。私の観点からは、誰もが少なくとも自分のプロジェクトごとに CI を設定する必要があります。これは良い習慣であり、適切な &lt;code>tests&lt;/code>] と CI を設定していれば発生しないはずのデバッグやエラーの発見にかかる時間を最終的に節約できるからです。&lt;/p>
&lt;p>#それだけです&lt;/p>
&lt;p>TravisCI を使用し、コードを Github に保存することで、すべてのビルドで継続的統合を行う .NET Core 3.0 ソリューションを作成する方法については以上です。&lt;/p>
&lt;p>このプロジェクトのソース コードは &lt;a href="https://github.com/emimontesdeoca/CalculatorCLI-demo">ここ&lt;/a> で見つけることができます。&lt;/p></content:encoded><category>.NET</category><category>CI/CD</category></item><item><title>インメモリキャッシュに対する私の見解</title><link>https://emimontesdeoca.github.io/ja/posts/my-take-on-in-memory-cache/</link><pubDate>Tue, 03 Sep 2019 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/my-take-on-in-memory-cache/</guid><description>C# とジェネリックを使用して、有効期限をサポートするカスタムのメモリ内キャッシュ実装を構築します。</description><content:encoded>&lt;p>私は大量のデータを処理するものに取り組んできました。やっていくうちに、その大部分は決して変わらない、少なくとも一定期間は変わらないことに気づきました。&lt;/p>
&lt;p>そこで、個人用のキャッシュ リポジトリを作成すると便利だと思いました。もちろん、これは新しいことではありません。数週間前、アプリケーション キャッシュの管理方法について、&lt;a href="https://nickcraver.com/]">Nick Craver&lt;/a> によって書かれた StackOverflow の &lt;a href="https://nickcraver.com/blog/2019/08/06/stack-overflow-how-we-do-app-caching/#in-memory--redis-cache">投稿&lt;/a> でこれについて読みました。&lt;/p>
&lt;p>また、私は常にキャッシュを扱いたいと思っていました。キャッシュがどのように機能するのか、ロジック、そしてどうすればキャッシュを機能させることができるのか、&lt;em>だから&amp;hellip; やるべきではありません!&lt;/em>&lt;/p>
&lt;h2 id="フロー">フロー&lt;/h2>
&lt;p>ユーザーが値を要求したときに、キャッシュを制御するクラスがどのように動作するかを示す簡単なフローを次に示します。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/830d5a91089c3344c8b406c66ea547b8">&lt;img src="https://i.gyazo.com/830d5a91089c3344c8b406c66ea547b8.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;h2 id="実装">実装&lt;/h2>
&lt;h3 id="始める前に">始める前に&lt;/h3>
&lt;p>わかってる、わかってる。メモリ キャッシュを処理する &lt;a href="https://docs.microsoft.com/en-us/dotnet/api/system.runtime.caching.memorycache?view=netframework-4.8">System.Runtime.Caching&lt;/a> があります。しかし、私はそれを自分で構築することにしました。そのクラスを使用したい場合は、&lt;a href="https://stackoverflow.com/search?q=System.Runtime.Caching">ここ&lt;/a> でハウツーを確認してください。&lt;/p>
&lt;h3 id="キャッシュアイテム">キャッシュアイテム&lt;/h3>
&lt;p>最初のステップは、オブジェクトの値と有効期限を格納するクラスを作成することです。おそらくもっと良い方法があると思いますが、私が考えたのはこれですので、次のとおりです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CacheItem&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Identifier { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">object&lt;/span> Value { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> DateTime ValidUntil { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> CacheItem(&lt;span style="color:#ff7b72">string&lt;/span> identifier, &lt;span style="color:#ff7b72">object&lt;/span> &lt;span style="color:#ff7b72">value&lt;/span>, TimeSpan valid)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Identifier = identifier;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Value = &lt;span style="color:#ff7b72">value&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ValidUntil = DateTime.UtcNow.Add(valid);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="キャッシュリポジトリ">キャッシュリポジトリ&lt;/h3>
&lt;p>次に、オブジェクトを処理してどこかに保存するクラスが必要になります (&lt;code>CacheItem&lt;/code> として)。私はサフィックス &lt;code>Repository&lt;/code> を持つクラス内のすべてのデータ/モデルを処理したいと考えていますが、&lt;em>しかしそうする必要はありません&lt;/em>。そこで、キャッシュ用にデータ/モデルを構築しましょう。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CacheRepository&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; Cache = &lt;span style="color:#ff7b72">new&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> T Set&amp;lt;T&amp;gt;(&lt;span style="color:#ff7b72">string&lt;/span> key, Func&amp;lt;T&amp;gt; lookup, TimeSpan durationMinutes)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> item = &lt;span style="color:#ff7b72">new&lt;/span> Models.Item(key, lookup(), durationMinutes);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> Save&amp;lt;T&amp;gt;(key, item);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> T Save&amp;lt;T&amp;gt;(&lt;span style="color:#ff7b72">string&lt;/span> key, Item item)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Cache[key] = JsonConvert.SerializeObject(item);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> (T)item.Value;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> T Get&amp;lt;T&amp;gt;(&lt;span style="color:#ff7b72">string&lt;/span> key)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> cached = Cache.FirstOrDefault(x =&amp;gt; x.Key == key).Value;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> item = &lt;span style="color:#ff7b72">string&lt;/span>.IsNullOrEmpty(cached) ? &lt;span style="color:#79c0ff">null&lt;/span> : JsonConvert.DeserializeObject&amp;lt;Item&amp;gt;(cached);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> (item == &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ? &lt;span style="color:#ff7b72">default&lt;/span> : (item.ValidUntil &amp;gt; DateTime.UtcNow)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ? JsonConvert.DeserializeObject&amp;lt;T&amp;gt;(JsonConvert.SerializeObject(item.Value)) : &lt;span style="color:#ff7b72">default&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> T GetOrSet&amp;lt;T&amp;gt;(&lt;span style="color:#ff7b72">string&lt;/span> key, Func&amp;lt;T&amp;gt; lookup, TimeSpan durationMinutes)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> cache = Get&amp;lt;T&amp;gt;(key);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> cache == &lt;span style="color:#79c0ff">null&lt;/span> ? Set(key, lookup, durationMinutes) : cache;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これまでのところ、&lt;code>Cache&lt;/code> という名前の静的ディクショナリがあり、そこにすべての項目が保存されます。これはアプリケーションの実行中にのみ持続することに注意してください。そのため、この &lt;em>チュートリアル&lt;/em> タイトルにはメモリ内キャッシュが含まれています。&lt;/p>
&lt;p>&lt;em>&lt;code>CacheRepository&lt;/code> クラスがロードされると、&lt;code>Cache&lt;/code> 項目が初期化されることに注意してください。&lt;/em>&lt;/p>
&lt;p>CacheRepository クラスを呼び出すときに使用できる唯一のメソッドは、3 つのパラメーターを必要とする &lt;code>GetOrSet(string key, Func&amp;lt;T&amp;gt; lookup, TimeSpan durationMinutes)&lt;/code> です。&lt;/p>
&lt;ol>
&lt;li>&lt;code>key&lt;/code>: 保存するオブジェクトの識別子。&lt;/li>
&lt;li>&lt;code>lookup&lt;/code>: キャッシュの有効期限が切れた場合、またはキャッシュが null の場合のコールバック関数。&lt;/li>
&lt;li>&lt;code>durationMinutes&lt;/code>: 現在時刻 (UTC) に追加される期間 (分単位)。&lt;/li>
&lt;/ol>
&lt;h2 id="キャッシュにかかる時間">キャッシュにかかる時間&lt;/h2>
&lt;p>次に、キャッシュ リポジトリを使用して、どこかからデータを取得します。&lt;/p>
&lt;p>これらすべてを理解するために、いくつかのプロパティを含むサンプル オブジェクトを作成し、そのオブジェクトのリストをフェッチして入力するリポジトリを作成しましょう。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">User&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Guid Id { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Name { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Email { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">UserRepository&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;User&amp;gt; Get()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Code to get users&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>User&lt;/code> のリストを埋めるメソッドがあるので、&lt;code>CacheRepository&lt;/code> クラスを利用しましょう。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> UserRepository _userRepository = &lt;span style="color:#ff7b72">new&lt;/span> UserRepository();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> List&amp;lt;User&amp;gt; _user;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;User&amp;gt; User
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">get&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _user = CacheRepository.GetOrSet(&lt;span style="color:#a5d6ff">$&amp;#34;users&amp;#34;&lt;/span>, usersRepo.Get, TimeSpan.FromMinutes(&lt;span style="color:#a5d6ff">10&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> _user;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>同様に、変数 &lt;code>User&lt;/code> にアクセスするたびに、キー &lt;code>users&lt;/code> を持つオブジェクトの値を &lt;code>CacheRepository&lt;/code> に要求します。&lt;/p>
&lt;p>そのキーが存在する場合は、有効期限がチェックされます。これらの条件のいずれかが false の場合、コールバックを使用してオブジェクトの値を (&lt;code>usersRepo.Get&lt;/code> で) 設定し、有効期限を &lt;code>DateTime.UtcNow + TimeSpan.FromMinutes(10)&lt;/code> に設定してキャッシュに保存して返します。&lt;/p></content:encoded><category>.NET</category></item><item><title>Devtools を使用してページのスクリーンショットを撮る</title><link>https://emimontesdeoca.github.io/ja/posts/google-chrome-screenshot-tool/</link><pubDate>Thu, 09 May 2019 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/google-chrome-screenshot-tool/</guid><description>Chrome DevTools の組み込みスクリーンショット コマンドを使用して、ページ全体と領域のスクリーンショットをキャプチャします。</description><content:encoded>&lt;p>Web サイトの現在の状態の完全なスクリーンショット、またはその一部をクライアントやチームメイトに見せる必要がある場合があったため、&lt;a href="https://chrome.google.com/webstore/detail/full-page-screen-capture/fdpohaocaechififmbbbbbknoalclacl?hl=en">全ページ スクリーン キャプチャ&lt;/a> を使用し、他の種類のスクリーンショットには &lt;a href="https://support.microsoft.com/en-us/help/13776/windows-use-snipping-tool-to-capture-screenshots">Windows のスニペット ツール&lt;/a> または &lt;a href="https://www.techsmith.com/store/snagit">Snaggit&lt;/a> を使用してきました。この投稿のようにオンラインでホストしたい場合は、&lt;a href="https://gyazo.com">Gyazo&lt;/a> にも GIF メーカーがあります。&lt;/p>
&lt;p>最近、&lt;strong>Devtools&lt;/strong> に拡張機能を必要とせずにページ全体のスクリーンショットを実行するツールがあることを発見しました。&lt;/p>
&lt;p>これは新しい考えではなく、&lt;a href="https://developers.google.com/web/updates/2017/04/devtools-release-notes">2017 年 4 月の Devtools アップデート&lt;/a> に含まれていました。&lt;em>しかし、私は岩の下に住んでいて、今まで気づかなかったようです&amp;hellip;&lt;/em>&lt;/p>
&lt;h2 id="開発ツール">開発ツール&lt;/h2>
&lt;p>&lt;a href="https://developers.google.com/web/tools/chrome-devtools/?hl=en">公式 Devtools ページ&lt;/a> に記載されているとおり:&lt;/p>
&lt;blockquote>
&lt;p>Chrome DevTools は、Google Chrome ブラウザに直接組み込まれた Web 開発者ツールのセットです。 DevTools は、ページをその場で編集し、問題を迅速に診断するのに役立ち、最終的にはより良い Web サイトをより迅速に構築するのに役立ちます。&lt;/p>&lt;/blockquote>
&lt;h2 id="devtools-でのコマンドの実行">Devtools でのコマンドの実行&lt;/h2>
&lt;p>Devtools を開く方法はすでに知っていますが、&lt;em>この投稿のために&lt;/em>忘れてしまった場合に備えて、キー &lt;code>F12&lt;/code> またはショートカット &lt;code>Ctrl + Shift + I&lt;/code> を使用して開いてください。次に、Devtools 内のメニューで &lt;code>Run command&lt;/code> を開くか、&lt;code>Ctrl + Shift + P&lt;/code> を使用する必要があります。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/8209d7d3132efba1843a3d51e4ad2183">&lt;img src="https://i.gyazo.com/8209d7d3132efba1843a3d51e4ad2183.gif" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;h2 id="スクリーンショット-ツール">スクリーンショット ツール&lt;/h2>
&lt;p>&lt;a href="https://gyazo.com/25ea6c9d258a1117efca5a2d92f715e2">&lt;img src="https://i.gyazo.com/25ea6c9d258a1117efca5a2d92f715e2.gif" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>&lt;code>screenshot&lt;/code> と入力するとコマンドがフィルタリングされ、使用できるスクリーンショット方法は 4 種類あります。&lt;/p>
&lt;ol>
&lt;li>エリアのスクリーンショット&lt;/li>
&lt;li>フルサイズのスクリーンショット&lt;/li>
&lt;li>ノードのスクリーンショット&lt;/li>
&lt;li>スクリーンショットをキャプチャする&lt;/li>
&lt;/ol>
&lt;p>すべてのスクリーンショット方法を確認する前に、devtools が画像の処理を完了した後、&lt;strong>画像は Web ページの名前で自動的にダウンロードされます&lt;/strong>。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/89a4935eb0ddb1a06ae997551fd19677">&lt;img src="https://i.gyazo.com/89a4935eb0ddb1a06ae997551fd19677.gif" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/e2e29c7fd7e90bb8bfd36ef130793394">&lt;img src="https://i.gyazo.com/e2e29c7fd7e90bb8bfd36ef130793394.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;h3 id="エリアのスクリーンショット">エリアのスクリーンショット&lt;/h3>
&lt;p>&lt;code>area screenshot&lt;/code> を使用すると、Web サイトの一部を選択してスクリーンショットを作成できます。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/f508a479893b6e8af9234d6a77404b26">&lt;img src="https://i.gyazo.com/f508a479893b6e8af9234d6a77404b26.gif" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>結果:&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/2b096e1ca2974157e478587a4902c1d2">&lt;img src="https://i.gyazo.com/2b096e1ca2974157e478587a4902c1d2.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;h3 id="フルサイズのスクリーンショット">フルサイズのスクリーンショット&lt;/h3>
&lt;p>&lt;code>full size screenshot&lt;/code> は、Web ページ全体の最初から最後までのスクリーンショットを撮ります。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/e6c30b8eafbef04091bf66c16429017c">&lt;img src="https://i.gyazo.com/e6c30b8eafbef04091bf66c16429017c.gif" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>結果:&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/9519ca32c0029a36432b700268741280">&lt;img src="https://i.gyazo.com/9519ca32c0029a36432b700268741280.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;h3 id="ノードのスクリーンショット">ノードのスクリーンショット&lt;/h3>
&lt;p>&lt;code>node screenshot&lt;/code> を使用すると、inspect 要素で選択したノードのスクリーンショットを作成できます。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/5b936026735964bb789c4bcae02bb792">&lt;img src="https://i.gyazo.com/5b936026735964bb789c4bcae02bb792.gif" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>結果:&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/56089c3d89f9b059dfb018d18572276d">&lt;img src="https://i.gyazo.com/56089c3d89f9b059dfb018d18572276d.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;h3 id="スクリーンショットをキャプチャする">スクリーンショットをキャプチャする&lt;/h3>
&lt;p>&lt;code>capture screenshot&lt;/code> は、スクロールせずに現在のページとブラウザーのサイズのスクリーンショットを撮ります。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/6dc4f654e8f218e44fcc1fb51ce4c9f5">&lt;img src="https://i.gyazo.com/6dc4f654e8f218e44fcc1fb51ce4c9f5.gif" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>結果:&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/3f15d4a004cba6152d1f9606c20540cc">&lt;img src="https://i.gyazo.com/3f15d4a004cba6152d1f9606c20540cc.png" alt="Gyazo からの画像">&lt;/a>&lt;/p></content:encoded></item><item><title>.NET Framework の埋め込みリソースと外部リソースを使用したカスタム言語</title><link>https://emimontesdeoca.github.io/ja/posts/embedded-resources-and-external-resources/</link><pubDate>Mon, 06 May 2019 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/embedded-resources-and-external-resources/</guid><description>.NET Framework の埋め込みおよび外部 .resx リソース ファイルを使用して、多言語サポートを実装します。</description><content:encoded>&lt;h1 id="はじめに">はじめに&lt;/h1>
&lt;p>仕事中に、多くの開発者が対処しなければならない通常の問題を思いつきました。それは、クライアントにサービスを提供している言語とクライアントのカスタム言語です。&lt;/p>
&lt;p>たとえば、基本言語が非公式言語を使用していて、クライアントが高齢者であるため、正式な言語を望んでいるとします。&lt;/p>
&lt;p>また、このソリューションは複数の言語に対応しており、英語とスペイン語を使用することにも注意してください。&lt;/p>
&lt;p>&lt;em>私の母国語は英語ではありません。チュートリアル内の間違いをお詫び申し上げます。間違いを見つけて修正したい場合は、&lt;a href="https://github.com/emimontesdeoca/emimontesdeoca.github.io">このリポジトリ&lt;/a> でプル リクエストを開いていただければ、喜んで承認します。&lt;/em>&lt;/p>
&lt;h1 id="リソース">リソース&lt;/h1>
&lt;p>リソースは拡張子 &lt;code>.resx&lt;/code> を持つ XML ファイルであり、キー/値構造を持ち、次のようになります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;utf-8&amp;#34;?&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;root&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:schema&lt;/span> id=&lt;span style="color:#a5d6ff">&amp;#34;root&amp;#34;&lt;/span> xmlns=&lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span> xmlns:xsd=&lt;span style="color:#a5d6ff">&amp;#34;http://www.w3.org/2001/XMLSchema&amp;#34;&lt;/span> xmlns:msdata=&lt;span style="color:#a5d6ff">&amp;#34;urn:schemas-microsoft-com:xml-msdata&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:import&lt;/span> namespace=&lt;span style="color:#a5d6ff">&amp;#34;http://www.w3.org/XML/1998/namespace&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:element&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;root&amp;#34;&lt;/span> msdata:IsDataSet=&lt;span style="color:#a5d6ff">&amp;#34;true&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:complexType&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:choice&lt;/span> maxOccurs=&lt;span style="color:#a5d6ff">&amp;#34;unbounded&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:element&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;metadata&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:complexType&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:sequence&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:element&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;value&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;xsd:string&amp;#34;&lt;/span> minOccurs=&lt;span style="color:#a5d6ff">&amp;#34;0&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:sequence&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:attribute&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;name&amp;#34;&lt;/span> use=&lt;span style="color:#a5d6ff">&amp;#34;required&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;xsd:string&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:attribute&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;type&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;xsd:string&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:attribute&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;mimetype&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;xsd:string&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:attribute&lt;/span> ref=&lt;span style="color:#a5d6ff">&amp;#34;xml:space&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:complexType&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:element&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:element&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;assembly&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:complexType&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:attribute&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;alias&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;xsd:string&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:attribute&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;name&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;xsd:string&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:complexType&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:element&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:element&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;data&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:complexType&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:sequence&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:element&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;value&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;xsd:string&amp;#34;&lt;/span> minOccurs=&lt;span style="color:#a5d6ff">&amp;#34;0&amp;#34;&lt;/span> msdata:Ordinal=&lt;span style="color:#a5d6ff">&amp;#34;1&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:element&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;comment&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;xsd:string&amp;#34;&lt;/span> minOccurs=&lt;span style="color:#a5d6ff">&amp;#34;0&amp;#34;&lt;/span> msdata:Ordinal=&lt;span style="color:#a5d6ff">&amp;#34;2&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:sequence&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:attribute&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;name&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;xsd:string&amp;#34;&lt;/span> use=&lt;span style="color:#a5d6ff">&amp;#34;required&amp;#34;&lt;/span> msdata:Ordinal=&lt;span style="color:#a5d6ff">&amp;#34;1&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:attribute&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;type&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;xsd:string&amp;#34;&lt;/span> msdata:Ordinal=&lt;span style="color:#a5d6ff">&amp;#34;3&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:attribute&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;mimetype&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;xsd:string&amp;#34;&lt;/span> msdata:Ordinal=&lt;span style="color:#a5d6ff">&amp;#34;4&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:attribute&lt;/span> ref=&lt;span style="color:#a5d6ff">&amp;#34;xml:space&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:complexType&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:element&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:element&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;resheader&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:complexType&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:sequence&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:element&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;value&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;xsd:string&amp;#34;&lt;/span> minOccurs=&lt;span style="color:#a5d6ff">&amp;#34;0&amp;#34;&lt;/span> msdata:Ordinal=&lt;span style="color:#a5d6ff">&amp;#34;1&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:sequence&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;xsd:attribute&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;name&amp;#34;&lt;/span> type=&lt;span style="color:#a5d6ff">&amp;#34;xsd:string&amp;#34;&lt;/span> use=&lt;span style="color:#a5d6ff">&amp;#34;required&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:complexType&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:element&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:choice&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:complexType&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:element&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/xsd:schema&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;resheader&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;resmimetype&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;value&amp;gt;&lt;/span>text/microsoft-resx&lt;span style="color:#7ee787">&amp;lt;/value&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/resheader&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;resheader&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;version&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;value&amp;gt;&lt;/span>2.0&lt;span style="color:#7ee787">&amp;lt;/value&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/resheader&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;resheader&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;reader&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;value&amp;gt;&lt;/span>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089&lt;span style="color:#7ee787">&amp;lt;/value&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/resheader&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;resheader&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;writer&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;value&amp;gt;&lt;/span>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089&lt;span style="color:#7ee787">&amp;lt;/value&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/resheader&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;data&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;Action_cancel&amp;#34;&lt;/span> xml:space=&lt;span style="color:#a5d6ff">&amp;#34;preserve&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;value&amp;gt;&lt;/span>Finish&lt;span style="color:#7ee787">&amp;lt;/value&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/data&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;data&lt;/span> name=&lt;span style="color:#a5d6ff">&amp;#34;Action_greeting&amp;#34;&lt;/span> xml:space=&lt;span style="color:#a5d6ff">&amp;#34;preserve&amp;#34;&lt;/span>&lt;span style="color:#7ee787">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;value&amp;gt;&lt;/span>Hello&lt;span style="color:#7ee787">&amp;lt;/value&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/data&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;/root&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>英語&lt;/strong>と&lt;strong>スペイン語&lt;/strong>という 2 つの異なる言語のリソースを作成するので、ファイルの命名法は &lt;code>resources.ISOLANGUAGECODE.resx&lt;/code> になります。たとえば、スペイン語の場合は &lt;code>resource.es.resx&lt;/code>、英語の場合は &lt;code>resource.resx&lt;/code> となります。後でドイツ語を追加する場合に備えて、ファイルの名前は &lt;code>resource.de.resx&lt;/code> となります。&lt;/p>
&lt;h2 id="埋め込みリソース">埋め込みリソース&lt;/h2>
&lt;p>埋め込みリソースは、プロジェクトがコンパイルされると、&lt;code>dll&lt;/code> 内に追加されます。&lt;/p>
&lt;p>これはリソースが埋め込まれた画像です。ご覧のとおり、ファイル &lt;code>Resource.resx&lt;/code> はコンパイルでは表示されません。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/b696d4ff1129634477c0fe3d570a05e8">&lt;img src="https://i.gyazo.com/b696d4ff1129634477c0fe3d570a05e8.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;h2 id="外部リソース">外部リソース&lt;/h2>
&lt;p>一方、外部リソースまたは埋め込まれていないリソースは、コンパイル後にフォルダーに追加されるリソースです。&lt;/p>
&lt;p>リソース ファイル (&lt;code>.resx&lt;/code>) は &lt;code>Properties&lt;/code> フォルダー内にあります。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/db83dc7bb9e3d3fb521cb321eaa8e74a">&lt;img src="https://i.gyazo.com/db83dc7bb9e3d3fb521cb321eaa8e74a.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;h1 id="プロジェクトのビルド">プロジェクトのビルド&lt;/h1>
&lt;p>.NET Framework のコンソール プロジェクトから始めましょう。&lt;/p>
&lt;h2 id="埋め込みリソースファイルの作成">埋め込みリソースファイルの作成&lt;/h2>
&lt;p>いくつかのキーを含むリソース ファイルを追加し、それが埋め込みリソースであることを確認します。これは、後で外部リソースで更新するために使用します。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/79af1981a3ee3214089791a3511b94ba">&lt;img src="https://i.gyazo.com/79af1981a3ee3214089791a3511b94ba.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;h2 id="外部リソースファイルの作成">外部リソースファイルの作成&lt;/h2>
&lt;p>外部リソースを埋め込みリソースから分離するために、後で簡単にアクセスできるように、名前を付けて外部リソースをフォルダー内に追加します。&lt;/p>
&lt;p>&lt;em>ヒント: プロパティ フォルダー内にフォルダーを追加するには、フォルダーの外に作成して内部に移動します。Visual Studio では作成できません&lt;/em>&lt;/p>
&lt;p>埋め込みリソースに対して行ったのと同じことを行い、ファイルの &lt;code>properties&lt;/code> 内にある &lt;code>Advanced&lt;/code>、&lt;code>Build action&lt;/code> に移動し、それを &lt;code>Content&lt;/code> に変更し、&lt;code>Copy to Output Dictionary&lt;/code> を &lt;code>Copy if newer&lt;/code> に変更します。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/6f7d53c82760b6989d74dc1864bbc2d4">&lt;img src="https://i.gyazo.com/6f7d53c82760b6989d74dc1864bbc2d4.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>これは次のようになります。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/75b2f06b11fa99b01bca7b5ac64b4e35">&lt;img src="https://i.gyazo.com/75b2f06b11fa99b01bca7b5ac64b4e35.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;h2 id="appconfig-へのキーの追加">app.config へのキーの追加&lt;/h2>
&lt;p>私たちはクールな開発者であり、すべてをうまく仕上げることを好み、各クライアントのコードを&lt;strong>変更することはありません&lt;/strong>。そのため、リソース ファイルを検索するフォルダーの名前を持つキーを &lt;code>appconfig&lt;/code> に追加しましょう。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-xml" data-lang="xml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;utf-8&amp;#34; ?&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;configuration&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;startup&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;supportedRuntime&lt;/span> version=&lt;span style="color:#a5d6ff">&amp;#34;v4.0&amp;#34;&lt;/span> sku=&lt;span style="color:#a5d6ff">&amp;#34;.NETFramework,Version=v4.7.2&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/startup&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;appSettings&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;add&lt;/span> key=&lt;span style="color:#a5d6ff">&amp;#34;CustomResources.Folder&amp;#34;&lt;/span> value=&lt;span style="color:#a5d6ff">&amp;#34;John&amp;#34;&lt;/span> &lt;span style="color:#7ee787">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;lt;/appSettings&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#7ee787">&amp;lt;/configuration&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>```したがって、後でリソースを検索するときは、`Properties.Doe` ではなく `Properties.John` を検索します。
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>また、app.config を簡単に変更できるため、アプリケーションがすでにデプロイされている場合でも変更が容易になります。
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>## 埋め込みリソース ファイルへのアクセス
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>.NET Framework の使用は非常に簡単なので、Embedded プロパティにアクセスするには `Properties` オブジェクトを使用するだけです。このオブジェクトにはリソース ファイルがプロパティとして含まれ、そのオブジェクト内には `resx` ファイルにあるすべてのキー/値が含まれます。
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>```csharp
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>static void Main(string[] args)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> var hello = Properties.Resource.Action_greeting;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> var bye = Properties.Resource.Action_cancel;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine($&amp;#34;Action_greeting value: {hello}, Action_cancel value: {bye}&amp;#34;);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.Read();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これを実行すると、次のようなものが表示されるはずです。&lt;/p>
&lt;p>&lt;code>Action_greeting value: Hello, Action_cancel value: Cancel&lt;/code>&lt;/p>
&lt;h2 id="外部リソース-ファイルへのアクセス">外部リソース ファイルへのアクセス&lt;/h2>
&lt;p>この部分はほぼ同じで、&lt;code>Properties&lt;/code> オブジェクトによってリソース ファイルにアクセスできます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> hello = Properties.Resource.Action_greeting;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> bye = Properties.Resource.Action_cancel;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> johnhello = Properties.John.Resource.Action_greeting;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> johnbye = Properties.John.Resource.Action_cancel;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> doehello = Properties.Doe.Resource.Action_greeting;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> doebye = Properties.Doe.Resource.Action_cancel;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="問題">問題&lt;/h1>
&lt;p>このメソッドを使用する主な問題は、すべてのキーがオブジェクト内のプロパティであるため、前に見たようにそれを呼び出す必要があることです。 &lt;code>John&lt;/code> のリソース ファイルのキー &lt;code>Action_greeting&lt;/code> を呼び出したい場合は、次の &lt;code>Properties.John&lt;/code> を使用してから &lt;code>Resource.Action_greeting&lt;/code> を使用する必要があります。&lt;/p>
&lt;p>&lt;strong>まさにそこに問題があります。&lt;/strong>&lt;/p>
&lt;p>それは、多数のクライアント向けにアプリケーションを開発している場合、クライアントごとにリソース ファイルの呼び出し方法を変更するのは悪い考えだからです。&lt;/p>
&lt;p>&lt;em>想像できますか?&lt;/em> クライアントごとにアプリケーションをコンパイルし、 &lt;code>John&lt;/code> を &lt;code>Doe&lt;/code> に変更し、さらに別の値に変更します。 &lt;strong>それは非常識です!&lt;/strong>&lt;/p>
&lt;h1 id="解決策">解決策&lt;/h1>
&lt;p>私たちのチームリーダーは、フォールバック システムのような非常に良い方法を考えました。リソースの基本モデルが必要です。そして、クライアントごとに、そのリソースでファイルを更新するリソース ファイルが必要です。これで、最終的に 1 つのリソースのリストが得られます。&lt;/p>
&lt;p>クライアントがカスタム リソースを望まない場合はベース リソースを使用し、クライアントが必要な場合はクライアントのリソースを使用します。&lt;/p>
&lt;p>これをチェックリストに入れるには、次のことを行う必要があります。&lt;/p>
&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> すべてのキー/値を埋め込みリソースの辞書にマップする方法を見つけます。&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> すべてのキー/値を外部リソースの辞書にマッピングする方法を見つける&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 両方のファイルを混合し、言語ごとに 1 つの辞書を作成します&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 辞書にアクセスして値を返すメソッドを作成します。&lt;/li>
&lt;/ul>
&lt;h2 id="図">図&lt;/h2>
&lt;p>&lt;a href="https://gyazo.com/c7e9ae6c7792c3bace10ff9b3b2b08ee">&lt;img src="https://i.gyazo.com/c7e9ae6c7792c3bace10ff9b3b2b08ee.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;h2 id="コードを書いてみましょう">コードを書いてみましょう&lt;/h2>
&lt;p>まず、リソース ファイルの取得からそれらの混合、値の返しまでのすべてのロジックが含まれる分離クラスを作成しましょう。そのクラスは &lt;code>CustomResources&lt;/code> と呼ばれます。&lt;/p>
&lt;p>これは次のようなものです:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">CustomResources&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; _ResourcesEnglish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; ResourcesEnglish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; _ResourcesSpanish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; ResourcesSpanish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; OverwriteDictionary(Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; currentDictionary, Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; newDictionary, &lt;span style="color:#ff7b72">bool&lt;/span> addIfDoesntExist = &lt;span style="color:#79c0ff">false&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; GetDictionaryFromEmbedded(&lt;span style="color:#ff7b72">string&lt;/span> embedded, &lt;span style="color:#ff7b72">string&lt;/span> cultureInfoCode)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; GetDictionaryFromFile(&lt;span style="color:#ff7b72">string&lt;/span> file)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GetText(&lt;span style="color:#ff7b72">string&lt;/span> key)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GetText(&lt;span style="color:#ff7b72">string&lt;/span> key, &lt;span style="color:#ff7b72">string&lt;/span> language)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>プロパティの遅延読み込みを実装していることに注意してください。これにより、パフォーマンスが向上し、辞書が 1 回読み込まれるようになります。&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;code>GetDictionaryFromEmbedded&lt;/code>: 埋め込みリソースから辞書を返します。&lt;/li>
&lt;li>&lt;code>GetDictionaryFromFile&lt;/code>: 外部リソースから辞書を返します。&lt;/li>
&lt;li>&lt;code>OverwriteDictionary&lt;/code>: 2 つの辞書を混合し、1 つの辞書を返します。&lt;/li>
&lt;li>&lt;code>GetText&lt;/code>: キーを指定して値を返します&lt;/li>
&lt;/ul>
&lt;h2 id="埋め込みリソースから辞書へ">埋め込みリソースから辞書へ&lt;/h2>
&lt;p>XML ファイルからすべてのプロパティを取得し、辞書を返す必要があります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; GetDictionaryFromEmbedded(&lt;span style="color:#ff7b72">string&lt;/span> embedded, &lt;span style="color:#ff7b72">string&lt;/span> cultureInfoCode)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; res = &lt;span style="color:#ff7b72">new&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ResourceManager rm = &lt;span style="color:#ff7b72">new&lt;/span> ResourceManager(embedded, Assembly.GetExecutingAssembly());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> resourceSet = rm.GetResourceSet(&lt;span style="color:#ff7b72">new&lt;/span> CultureInfo(cultureInfoCode), &lt;span style="color:#79c0ff">true&lt;/span>, &lt;span style="color:#79c0ff">true&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> resourceDictionary = resourceSet.Cast&amp;lt;DictionaryEntry&amp;gt;()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .ToDictionary(r =&amp;gt; r.Key.ToString(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> r =&amp;gt; r.Value.ToString());
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res = resourceDictionary;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception e)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> a = e.Message;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Error getting resource file&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> res;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>2 つのこと:- &lt;code>embedded&lt;/code> というパラメーターが必要であることに注意してください。パラメーターはデザイナーに表示されるファイルの名前であり、この場合は &lt;code>resources-demo.Properties.Resource&lt;/code> です。&lt;/p>
&lt;ul>
&lt;li>また、cultreInfoCode というパラメータもあります。これは、選択される言語のコードです。幸いなことに、.NET Framework がその仕事をしてくれます。何もする必要はありません。英語またはスペイン語のいずれかを設定するだけで、&lt;code>resource.es.resx&lt;/code> または &lt;code>resource.resx&lt;/code> のいずれかが選択されます。&lt;/li>
&lt;/ul>
&lt;h2 id="外部リソースから辞書へ">外部リソースから辞書へ&lt;/h2>
&lt;p>ファイルからの取得は少しハック的ですが難しくはありません。実行可能ファイルの現在の場所を取得し、リソース ファイルの場所を連結して、それを解析して辞書に変換する必要があります。&lt;/p>
&lt;p>ただし、&lt;code>ResXResourceReader&lt;/code> にアクセスするには、まず &lt;code>System.Windows.Forms&lt;/code> への参照を追加する必要があります。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/5bdf8353acc57b1d95b72acc14af41cd">&lt;img src="https://i.gyazo.com/5bdf8353acc57b1d95b72acc14af41cd.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>次に &lt;code>GetDictionaryFromFile&lt;/code> メソッドに進みます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; GetDictionaryFromFile(&lt;span style="color:#ff7b72">string&lt;/span> file)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; res = &lt;span style="color:#ff7b72">new&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> currentPath = (System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase) + file).Replace(&lt;span style="color:#a5d6ff">&amp;#34;file:\\&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> (ResXResourceReader resxReader = &lt;span style="color:#ff7b72">new&lt;/span> ResXResourceReader(currentPath))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (DictionaryEntry entry &lt;span style="color:#ff7b72">in&lt;/span> resxReader)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> res.Add((&lt;span style="color:#ff7b72">string&lt;/span>)entry.Key, (&lt;span style="color:#ff7b72">string&lt;/span>)entry.Value);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception e)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> a = e.Message;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> res;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ファイルパラメータには、実行可能ファイルからリソースファイルまでの場所を入力する必要があります。この場合は &lt;code>&amp;quot;\\Properties\\John\\Resource.resx&amp;quot;&lt;/code> です。&lt;/p>
&lt;h2 id="辞書の混合">辞書の混合&lt;/h2>
&lt;p>これでほぼ完了です。まず、&lt;code>app.settings&lt;/code> にアクセスできるように &lt;code>System.Configuration&lt;/code> を参照に追加することを忘れないでください。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; OverwriteDictionary(Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; currentDictionary, Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; newDictionary, &lt;span style="color:#ff7b72">bool&lt;/span> addIfDoesntExist = &lt;span style="color:#79c0ff">false&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> identifier = ConfigurationManager.AppSettings[&lt;span style="color:#a5d6ff">&amp;#34;CustomResources.Folder&amp;#34;&lt;/span>];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (String.IsNullOrEmpty(identifier))
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> currentDictionary;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> item &lt;span style="color:#ff7b72">in&lt;/span> newDictionary)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentDictionary[item.Key] = item.Value;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span>(addIfDoesntExist)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> currentDictionary.Add(item.Key, item.Value);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> currentDictionary;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="辞書からテキストを取得する">辞書からテキストを取得する&lt;/h2>
&lt;p>言語を選択するプライベート メソッドを呼び出すパブリック メソッドを作成します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GetText(&lt;span style="color:#ff7b72">string&lt;/span> key, &lt;span style="color:#ff7b72">string&lt;/span> language)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">switch&lt;/span> (language)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;es&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> ResourceSpanish[key];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">case&lt;/span> &lt;span style="color:#a5d6ff">&amp;#34;en&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">default&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> ResourceEnglish[key];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#a5d6ff">$&amp;#34;No value with key: {key} and language: {language}&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> GetText(&lt;span style="color:#ff7b72">string&lt;/span> key)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">try&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> GetText(key, Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">catch&lt;/span> (Exception)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> &lt;span style="color:#a5d6ff">$&amp;#34;No value with key: {key}&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>さらに言語を追加する場合は、スイッチを変更する必要があることに注意してください。&lt;/strong>&lt;/p>
&lt;h2 id="プロパティゲッターにコードを追加する">プロパティゲッターにコードを追加する&lt;/h2>
&lt;p>これですべてのメソッドが揃ったので、パブリック プロパティのゲッターを変更して値を取得できます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; _ResourcesEnglish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; ResourcesEnglish
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">get&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (_ResourcesEnglish == &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> folderIndentifier = ConfigurationManager.AppSettings[&lt;span style="color:#a5d6ff">&amp;#34;CustomResources.Folder&amp;#34;&lt;/span>]; ;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> baseResources = GetDictionaryFromEmbedded(&lt;span style="color:#a5d6ff">&amp;#34;resources-demo.Properties.Resource&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> customResources = GetDictionaryFromFile(&lt;span style="color:#a5d6ff">$&amp;#34;\\Properties\\{folderIndentifier}\\Resource.resx&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;en&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _ResourcesEnglish = OverwriteDictionary(baseResources, customResources);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> _ResourcesEnglish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; _ResourcesSpanish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> Dictionary&amp;lt;&lt;span style="color:#ff7b72">string&lt;/span>, &lt;span style="color:#ff7b72">string&lt;/span>&amp;gt; ResourcesSpanish
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">get&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (_ResourcesSpanish == &lt;span style="color:#79c0ff">null&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> folderIndentifier = ConfigurationManager.AppSettings[&lt;span style="color:#a5d6ff">&amp;#34;CustomResources.Folder&amp;#34;&lt;/span>]; ;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> baseResources = GetDictionaryFromEmbedded(&lt;span style="color:#a5d6ff">&amp;#34;resources-demo.Properties.Resources&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> customResources = GetDictionaryFromFile(&lt;span style="color:#a5d6ff">$&amp;#34;\\Properties\\{folderIndentifier}\\Resources.es.resx&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;es&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> _ResourcesSpanish = OverwriteDictionary(baseResources, customResources);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> _ResourcesSpanish;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol>
&lt;li>まず、app.settings から識別子を取得します。&lt;/li>
&lt;li>次に、基本リソース、つまり埋め込みリソースを取得します。&lt;/li>
&lt;li>その後、カスタム リソースを取得します。そのためにはフォルダー名 (識別子) が必要です。&lt;/li>
&lt;li>次に、それらを混合して値を返します。&lt;/li>
&lt;/ol>
&lt;p>遅延読み込みなので、これはすべて 1 回で実行されます。&lt;/p>
&lt;h1 id="テスト">テスト&lt;/h1>
&lt;p>コードに関連するすべてが完了したので、テストしましょう。辞書から値を取得するには、値を返すメソッド &lt;code>CustomResources.GetText(string key)&lt;/code> を呼び出す必要があります。&lt;/p>
&lt;h2 id="リソース-ファイル全体を更新する">リソース ファイル全体を更新する&lt;/h2>
&lt;p>このテストは、リソース ファイルのキー/値全体を更新する場合です。図からわかるように、キーは同じですが値が異なります。&lt;/p>
&lt;p>&lt;code>John&lt;/code> をテストします。それを設定するために、app.config を &lt;code>&amp;lt;add key=&amp;quot;CustomResources.Folder&amp;quot; value=&amp;quot;John&amp;quot; /&amp;gt;&lt;/code> に設定します。&lt;/p>
&lt;p>次に、基本リソース ファイル (&lt;code>Properties/Resource.es.resx&lt;/code>) を確認してみましょう。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/26d6cef147ca3cdd93a1d6cc4c60c212">&lt;img src="https://i.gyazo.com/26d6cef147ca3cdd93a1d6cc4c60c212.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>次に、外部リソース ファイル (&lt;code>Properties/John/Resource.es.resx&lt;/code>):&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/fd6582da0d6a427c2b21cc8b1999f9e0">&lt;img src="https://i.gyazo.com/fd6582da0d6a427c2b21cc8b1999f9e0.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>OK、すべての設定が完了しました。コンソール アプリケーションを実行します。プロパティの &lt;code>get&lt;/code> 部分に戻ってすべてを確認しましょう。&lt;/p>
&lt;p>&lt;code>folderIdentifier&lt;/code> には appseting の値があります。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/ac30d3deb7abe29a877b368d9300b990">&lt;img src="https://i.gyazo.com/ac30d3deb7abe29a877b368d9300b990.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>&lt;code>baseResources&lt;/code> には、基本リソース、つまり埋め込まれたリソースの値が含まれます。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/a61410b365b55d4735f2b2622a18b966">&lt;img src="https://i.gyazo.com/a61410b365b55d4735f2b2622a18b966.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>&lt;code>customResources&lt;/code> には、フォルダー &lt;code>John&lt;/code> 内の外部リソースの値があります。&lt;a href="https://gyazo.com/0e93733fbd616b274a69cdffbc16afb1">&lt;img src="https://i.gyazo.com/0e93733fbd616b274a69cdffbc16afb1.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>そして最後に、&lt;code>_ResourcesSpanish&lt;/code> には、基本リソースから外部リソースまで混合された値が含まれます。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/0e25a79023364ea1d5be2109dbf6492c">&lt;img src="https://i.gyazo.com/0e25a79023364ea1d5be2109dbf6492c.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;h2 id="1-つだけ更新しています">1 つだけ更新しています&lt;/h2>
&lt;p>次に、同じものを異なるシナリオでテストしてみましょう。一方のキーを更新し、もう一方のキーを同じにするだけです。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/9c2a05fb976f1f671b13c9cf77220336">&lt;img src="https://i.gyazo.com/9c2a05fb976f1f671b13c9cf77220336.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>ご覧のとおり、ファイルは &lt;code>Action_greeting&lt;/code> については同じ値の意味を持ちますが、&lt;code>Action_cancel&lt;/code> については異なる値を持っているため、&lt;code>Action_cancel&lt;/code> のみを更新する必要があります。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/16938859b05bb1f0fe08751a08b1fcad">&lt;img src="https://i.gyazo.com/16938859b05bb1f0fe08751a08b1fcad.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;h2 id="リソース-ファイルがありません">リソース ファイルがありません&lt;/h2>
&lt;p>外部リソース ファイルを提供しない場合でも、ファイルがあることが期待されているため、失敗すると空の辞書が返され、両方の辞書を混合すると基本の辞書が使用されるため、まったく問題ありません。&lt;/p>
&lt;h2 id="埋め込みリソースにペアがありません">埋め込みリソースにペアがありません&lt;/h2>
&lt;p>外部リソース ファイルにペアがある場合、デフォルトでは最終的な辞書には追加されません。両方の辞書を混合するときにメソッド &lt;code>OverwriteDictionary()&lt;/code> を呼び出し、パラメータ &lt;code>addIfDoesntExist&lt;/code> を &lt;code>true&lt;/code> に設定することで変更できます。&lt;/p>
&lt;h2 id="さまざまな言語">さまざまな言語&lt;/h2>
&lt;p>ご覧のとおり、言語を指定していません。これはすべて、&lt;code>GetText(string key, string language)&lt;/code> を呼び出す関数 &lt;code>GetText(string key)&lt;/code> によって行われており、パラメーター &lt;code>language&lt;/code> には、スレッドの現在の言語を返す &lt;code>TwoLetterISOLanguageName&lt;/code> が代入されています。&lt;/p>
&lt;p>私の場合はスペイン語をデフォルト言語として設定しているため、常にスペイン語で表示されますが、英語を使用してみることもできます。&lt;/p>
&lt;p>少しコーディングして、スペイン語と英語の両方をチェックするものを構築しましょう。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> Main(&lt;span style="color:#ff7b72">string&lt;/span>[] args)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(&amp;#34;en-US&amp;#34;);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> System.Threading.Thread.CurrentThread.CurrentCulture = &lt;span style="color:#ff7b72">new&lt;/span> CultureInfo(&lt;span style="color:#a5d6ff">&amp;#34;en-US&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> WriteText(&lt;span style="color:#a5d6ff">&amp;#34;Action_greeting&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> WriteText(&lt;span style="color:#a5d6ff">&amp;#34;Action_cancel&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(&amp;#34;es-ES&amp;#34;);&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> System.Threading.Thread.CurrentThread.CurrentCulture = &lt;span style="color:#ff7b72">new&lt;/span> CultureInfo(&lt;span style="color:#a5d6ff">&amp;#34;es-ES&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> WriteText(&lt;span style="color:#a5d6ff">&amp;#34;Action_greeting&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> WriteText(&lt;span style="color:#a5d6ff">&amp;#34;Action_cancel&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.Read();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">private&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> &lt;span style="color:#ff7b72">void&lt;/span> WriteText(&lt;span style="color:#ff7b72">string&lt;/span> key) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Console.WriteLine(&lt;span style="color:#a5d6ff">$&amp;#34;[Culture: {System.Threading.Thread.CurrentThread.CurrentCulture}] Key: {key} -&amp;gt; value: {CustomResources.GetText(key)}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これを実行すると、出力は次のようになります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-fallback" data-lang="fallback">&lt;span style="display:flex;">&lt;span>[Culture: en-US] Key: Action_greeting -&amp;gt; value: Hello
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[Culture: en-US] Key: Action_cancel -&amp;gt; value: Finish
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[Culture: es-ES] Key: Action_greeting -&amp;gt; value: Hola
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>[Culture: es-ES] Key: Action_cancel -&amp;gt; value: Terminar
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;a href="https://gyazo.com/de7e5efcc2ff5e82529749b1188706ff">&lt;img src="https://i.gyazo.com/de7e5efcc2ff5e82529749b1188706ff.png" alt="Gyazo からの画像">&lt;/a>&lt;/p>
&lt;p>最初の 2 つの値は英語の &lt;code>resources.resx&lt;/code> からのもので、最後の 2 つの値はスペイン語で、値は &lt;code>resources.es.resx&lt;/code> から取得されます。&lt;/p>
&lt;p>#それだけです&lt;/p>
&lt;p>このチュートリアルでは、言語に埋め込みリソースと外部リソースの両方を混合する方法を見つけました。これはチーム内で抱えていた問題の解決策であり、それ以来問題なく実行されています。&lt;/p>
&lt;p>ソースコードは&lt;a href="https://github.com/emimontesdeoca/resources-demo">ここ&lt;/a>で確認できます。&lt;/p>
&lt;p>ご質問がございましたら、&lt;a href="https://twitter.com/emimontesdeocaa">@emimontesdeocaa&lt;/a> までお気軽にツイートしてください。時間ができたら折り返しご連絡いたします。&lt;/p></content:encoded><category>.NET</category></item><item><title>フローケースの Bot Framework と DirectLine を使用した統合テスト</title><link>https://emimontesdeoca.github.io/ja/posts/integration-test-bot-framework-with-flow-cases/</link><pubDate>Wed, 25 Apr 2018 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/integration-test-bot-framework-with-flow-cases/</guid><description>Bot Framework 統合テストを拡張して、複数ターンの会話フロー シナリオをサポートします。</description><content:encoded>&lt;h2 id="はじめに">はじめに&lt;/h2>
&lt;p>前回のブログ投稿では、単一のケースに対していくつかの統合テストを実行しました。 「単一ケース」とは、ボットに何かを質問した後、ボットが 1 回だけ応答し、その結果を比較するケースです。&lt;/p>
&lt;p>&lt;strong>このガイドでは、認証と API 呼び出しがどのように行われるかについては説明しません。確認したい場合は、単一のケースのガイドを参照してください。&lt;/strong>&lt;/p>
&lt;h3 id="フロー会話についてはどうですか">フロー会話についてはどうですか?&lt;/h3>
&lt;p>現在の方法では、会話にいかなる流れもありません。たとえば、助けを求めたい場合に、さまざまなオプションを含むメニューが表示され、そのうちの 1 つを選択した後に別のメニューが表示される場合です。これにより、2 つ以上の &lt;code>responses&lt;/code> が作成されます。 &lt;strong>つまり、以前に使用した統合テスト ソリューションは機能しないということになります。&lt;/strong>&lt;/p>
&lt;p>以下は、単一ケースの統合テストがどのように機能するかを示す図です。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/8915f2653033c1143947ef59196403f4">&lt;img src="https://i.gyazo.com/8915f2653033c1143947ef59196403f4.png" alt="https://gyazo.com/8915f2653033c1143947ef59196403f4">&lt;/a>&lt;/p>
&lt;p>これは、現在の統合テスト ソリューションをフロー ケースにどのように適応させるかを示す図です。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/ef77fa168d3b5f8b4a116ff38b5edd83">&lt;img src="https://i.gyazo.com/ef77fa168d3b5f8b4a116ff38b5edd83.png" alt="https://gyazo.com/ef77fa168d3b5f8b4a116ff38b5edd83">&lt;/a>&lt;/p>
&lt;p>&lt;strong>次の説明では、Bot Framework の動作に関する基本情報がすべて網羅されているわけではありません。理解できない場合は、公式ドキュメントを確認してください。&lt;/strong>&lt;/p>
&lt;h2 id="ケース例">ケース例&lt;/h2>
&lt;p>次のガイドでは、私が作成したフロー会話を備えたボットを使用します。これは、助けを求め、さまざまなオプションを選択します。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/0a1104cce67c07331a6e0fbd8e19b3e2">&lt;img src="https://i.gyazo.com/0a1104cce67c07331a6e0fbd8e19b3e2.gif" alt="https://gyazo.com/0a1104cce67c07331a6e0fbd8e19b3e2">&lt;/a>&lt;/p>
&lt;h2 id="新しい-json-構造">新しい JSON 構造&lt;/h2>
&lt;p>ボットと通信するときに複数の &lt;code>request&lt;/code> があるため、json 構造を変更して、これから実行するすべての &lt;code>request&lt;/code> を追加する必要があります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;secret&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;direct-line-secret&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;directlineGenerateTokenEndpoint&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;https://directline.botframework.com/v3/directline/tokens/generate&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;directlineConversationEndpoint&amp;#34;&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a5d6ff">&amp;#34;https://directline.botframework.com/v3/directline/conversations/&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;entries&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;PedirAyuda&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;requests&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;message&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;text&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Ayuda&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;from&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;default-user&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;User&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;locale&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;es&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;textFormat&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;plain&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;timestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T08:04:37.195Z&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;channelData&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;clientActivityId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;1523261059363.6264723268323733.0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;entities&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;ClientCapabilities&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;requiresBotState&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;supportsTts&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;supportsListening&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;61hacck8j6jg&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;message&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;text&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Telefono&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;from&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;default-user&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;User&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;locale&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;es&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;textFormat&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;plain&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;timestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T08:04:37.195Z&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;channelData&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;clientActivityId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;1523261059363.6264723268323733.0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;entities&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;ClientCapabilities&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;requiresBotState&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;supportsTts&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;supportsListening&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;61hacck8j6jg&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;message&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;text&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Oficina&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;from&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;default-user&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;User&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;locale&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;es&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;textFormat&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;plain&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;timestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T08:04:37.195Z&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;channelData&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;clientActivityId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;1523261059363.6264723268323733.0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;entities&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;ClientCapabilities&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;requiresBotState&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;supportsTts&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;supportsListening&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;61hacck8j6jg&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;message&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;text&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Tenerife&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;from&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;default-user&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;User&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;locale&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;es&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;textFormat&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;plain&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;timestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T08:04:37.195Z&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;channelData&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;clientActivityId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;1523261059363.6264723268323733.0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;entities&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;ClientCapabilities&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;requiresBotState&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;supportsTts&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;supportsListening&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;61hacck8j6jg&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;response&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;message&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;timestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T08:04:37.901Z&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;localTimestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T09:04:37+01:00&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;serviceUrl&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;http://localhost:50629&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;channelId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;emulator&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;from&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;j98bbdf097a&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Bot&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;conversation&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;eabcie4be8ak&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;recipient&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;default-user&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;locale&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;es&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;text&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;922920252&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;attachments&amp;#34;&lt;/span>: [],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;entities&amp;#34;&lt;/span>: [],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;replyToId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;61hacck8j6jg&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;47me557ikbf7&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;assert&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Request.Text == Response.Text&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ご覧のとおり、重要な変更があり、&lt;code>request&lt;/code> は &lt;code>requests&lt;/code> になりました。つまり、単一の &lt;code>activity&lt;/code> ではなく &lt;code>List&amp;lt;Activity&amp;gt;&lt;/code> が存在することになります。&lt;/p>
&lt;h2 id="新しいオブジェクト">新しいオブジェクト&lt;/h2>
&lt;p>以前は、&lt;code>TestEntry&lt;/code> と &lt;code>TestEntryCollection&lt;/code> という単一のケースに対してオブジェクトを設定していました。フローケースの場合は、新しいオブジェクト &lt;code>TestEntryFlow&lt;/code> および &lt;code>TestEntryFlowCollection&lt;/code> を作成します。&lt;/p>
&lt;h3 id="testentryflow">&lt;code>TestEntryFlow&lt;/code>&lt;/h3>
&lt;p>このオブジェクトはコレクション内にあるすべてのエントリに対応しています。前に述べたように、&lt;code>Requests&lt;/code> オブジェクトが単一の &lt;code>Activity&lt;/code> ではなく &lt;code>List&amp;lt;Activity&amp;gt;&lt;/code> になっていることがわかります。&lt;/p>
&lt;p>ボットに複数回質問するため、会話に送信される複数の &lt;code>activities&lt;/code> が必要です。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">TestEntryFlow&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Entry name&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;name&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Name { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Activity requested by the entry&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;requests&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;Activity&amp;gt; Requests { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Activity response expected by the entry&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;response&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Activity Response { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Assert value in string&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;assert&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Assert { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="testentirescollection">&lt;code>TestEntiresCollection&lt;/code>&lt;/h3>
&lt;p>このオブジェクトには、&lt;code>secret&lt;/code> やエンドポイントなどの &lt;code>DirectLine&lt;/code> の関連情報に加えて、テストする &lt;code>Entries&lt;/code> のリストが含まれます。&lt;/p>
&lt;p>&lt;code>Entries&lt;/code> は &lt;code>TestEntry&lt;/code> ではなく &lt;code>TestEntryFlow&lt;/code> のリストになっていることに注意してください。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">TestEntryFlowCollection&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// DirectLine Secret&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;secret&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Secret { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Endpoint to get the token using the secret for DirectLine&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;directlineGenerateTokenEndpoint&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> DirectLineGenerateTokenEndpoint { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Endpoint for a conversation in DirectLine&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;directlineConversationEndpoint&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> DirectLineConversationEndpoint { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Entries list&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;entries&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;TestEntryFlow&amp;gt; Entries { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="フローケースの-testmethod-を作成する">フローケースの &lt;code>TestMethod&lt;/code> を作成する&lt;/h2>
&lt;h3 id="新しいフロー">新しいフロー&lt;/h3>
&lt;p>まず、もう一度図を見てください（上に投稿したものと同じです）。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/ef77fa168d3b5f8b4a116ff38b5edd83">&lt;img src="https://i.gyazo.com/ef77fa168d3b5f8b4a116ff38b5edd83.png" alt="https://gyazo.com/ef77fa168d3b5f8b4a116ff38b5edd83">&lt;/a>&lt;/p>
&lt;p>ご覧のとおり、テストを行うためのフロー構造はほぼ同じです。&lt;/p>
&lt;ol>
&lt;li>
&lt;p>情報を入手する&lt;/p>
&lt;/li>
&lt;li>
&lt;p>認証する&lt;/p>
&lt;/li>
&lt;li>
&lt;p>会話を作成する&lt;strong>そして、ここで何が変わるのか、今度はすべてのリクエストを会話に複数回送信する必要があります。これを行うには、リクエストごとにループしてボットに送信し、最新の応答を予想される応答と比較する必要があります。&lt;/strong>&lt;/p>
&lt;/li>
&lt;li>
&lt;p>すべてのリクエストを送信する&lt;/p>
&lt;/li>
&lt;li>
&lt;p>すべてのメッセージを取得する&lt;/p>
&lt;/li>
&lt;li>
&lt;p>最新の応答を取得する&lt;/p>
&lt;/li>
&lt;li>
&lt;p>期待される応答と比較する&lt;/p>
&lt;/li>
&lt;li>
&lt;p>結果のアサート&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h3 id="コード">コード&lt;/h3>
&lt;p>まず最初に、ファイルから情報を取得する必要があります。これは、以前に単一のケースで行ったのと同じです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Load entries from file&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> path = System.IO.File.ReadAllText(&lt;span style="color:#a5d6ff">@&amp;#34;C:\dataFlow.json&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Deserialize to object&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> data = JsonConvert.DeserializeObject&amp;lt;TestEntryFlowCollection&amp;gt;(path);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ここで、&lt;code>data.entries&lt;/code> の &lt;code>TestEntryFlow&lt;/code> ごとにループする必要があります。これにより、&lt;code>requests&lt;/code> でループする新しい部分まで、単一のケースで行ったのと同じフローに従うことができます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Arrange with current requested values&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">string&lt;/span> token, newToken, conversationId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Activity latestResponse = &lt;span style="color:#ff7b72">new&lt;/span> Activity();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Act for step&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 1 - Get token using secret from DirectLine in BotFramework panel&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>token = Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(data.Secret, data.DirectLineGenerateTokenEndpoint, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>).token;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 2 - Create a new conversation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> createdConversation = Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(token, data.DirectLineConversationEndpoint, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// This returns a new token and a conversationId&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>newToken = createdConversation.token;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>conversationId = createdConversation.conversationId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 3 - Send an activity to the conversation with new token and conversationId&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">string&lt;/span> directlineConversationActivitiesEndpoint = data.DirectLineConversationEndpoint + conversationId + &lt;span style="color:#a5d6ff">&amp;#34;/activities&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>次のステップは非常に簡単です。&lt;code>entry.requests&lt;/code> をループして、すべての &lt;code>activity&lt;/code> を会話に送信する必要があります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">foreach&lt;/span> (Activity step &lt;span style="color:#ff7b72">in&lt;/span> entry.Requests)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (step.Type == ActivityTypes.Message)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Step&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(newToken, directlineConversationActivitiesEndpoint, JsonConvert.SerializeObject(step));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 4 - Get all activities, we get a List&amp;lt;activity&amp;gt; and a watermark&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> getLastActivity = Utils.downloadString&amp;lt;ActivityResponse&amp;gt;(newToken, directlineConversationActivitiesEndpoint);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 5 - Get the latest activity which is the response we should be expecting&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> latestResponse = getLastActivity.activities[Int32.Parse(getLastActivity.watermark)];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最新のメッセージを取得するには &lt;code>watermark&lt;/code> を使用します。&lt;code>watermark&lt;/code> は、会話情報を要求するときに DirectLine API が返す値です。&lt;/p>
&lt;p>その後、&lt;code>globals&lt;/code> に &lt;code>latestReponse&lt;/code> と &lt;code>expectedResponse&lt;/code> を入力するだけです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Arrange with new values&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> globals = &lt;span style="color:#ff7b72">new&lt;/span> Objects.Globals { Request = entry.Response, Response = latestResponse };
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>そしてこのケースを終了するには、&lt;code>entry&lt;/code> 内の &lt;code>assert&lt;/code> 文字列を評価します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Assert&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Assert.IsTrue(&lt;span style="color:#ff7b72">await&lt;/span> CSharpScript.EvaluateAsync&amp;lt;&lt;span style="color:#ff7b72">bool&lt;/span>&amp;gt;(entry.Assert, globals: globals));
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="最終コード">最終コード&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task ShouldTestFlowCases()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Load entries from file&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> path = System.IO.File.ReadAllText(&lt;span style="color:#a5d6ff">@&amp;#34;C:\dataFlow.json&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Deserialize to object&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> data = JsonConvert.DeserializeObject&amp;lt;TestEntryFlowCollection&amp;gt;(path);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Flow: Arrange -&amp;gt; Act -&amp;gt; arrange -&amp;gt; assert&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (TestEntryFlow entry &lt;span style="color:#ff7b72">in&lt;/span> data.Entries)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Arrange with current requested values&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> token, newToken, conversationId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Activity latestResponse = &lt;span style="color:#ff7b72">new&lt;/span> Activity();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Act for step&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 1 - Get token using secret from DirectLine in BotFramework panel&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> token = Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(data.Secret, data.DirectLineGenerateTokenEndpoint, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>).token;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 2 -Create a new conversation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> createdConversation = Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(token, data.DirectLineConversationEndpoint, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// This returns a new token and a conversationId&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> newToken = createdConversation.token;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> conversationId = createdConversation.conversationId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 3 - Send an activity to the conversation with new token and conversationId&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> directlineConversationActivitiesEndpoint = data.DirectLineConversationEndpoint + conversationId + &lt;span style="color:#a5d6ff">&amp;#34;/activities&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (Activity step &lt;span style="color:#ff7b72">in&lt;/span> entry.Requests)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (step.Type == ActivityTypes.Message)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Step&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(newToken, directlineConversationActivitiesEndpoint, JsonConvert.SerializeObject(step));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 4 - Get all activities, we get a List&amp;lt;activity&amp;gt; and a watermark&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> getLastActivity = Utils.downloadString&amp;lt;ActivityResponse&amp;gt;(newToken, directlineConversationActivitiesEndpoint);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 5 - Get the latest activity which is the response we should be expecting&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> latestResponse = getLastActivity.activities[Int32.Parse(getLastActivity.watermark)];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Arrange with new values&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> globals = &lt;span style="color:#ff7b72">new&lt;/span> Objects.Globals { Request = entry.Response, Response = latestResponse };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Assert&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Assert.IsTrue(&lt;span style="color:#ff7b72">await&lt;/span> CSharpScript.EvaluateAsync&amp;lt;&lt;span style="color:#ff7b72">bool&lt;/span>&amp;gt;(entry.Assert, globals: globals));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> Task.CompletedTask;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="改善点">改善点&lt;/h2>
&lt;p>より良いフローが可能であると信じていますが、この改善は JSON 構造も変更する必要があることを意味します。また、これを実現するには、JSON をさらに埋める必要があります。&lt;/p>
&lt;p>このテストをより良くするには、&lt;code>request&lt;/code> ごとに &lt;code>response&lt;/code> を用意し、メッセージを送信するたびにアサートする必要があります。私たちが現在それを行っている方法は、すべての &lt;code>requests&lt;/code> と &lt;strong>最後の &lt;code>response&lt;/code>&lt;/strong> を保存することです。&lt;/p>
&lt;p>これがどのようになるかを示す図を作成しました。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/dfd4e9f87ff69159f02a0bcc70ae1edc">&lt;img src="https://i.gyazo.com/dfd4e9f87ff69159f02a0bcc70ae1edc.png" alt="https://gyazo.com/dfd4e9f87ff69159f02a0bcc70ae1edc">&lt;/a>&lt;/p>
&lt;p>最終的な応答だけをテストするのではなく、フロー内のほぼすべての動作をテストするため、テストの整合性という点ではこの方法の方が全体的にはるかに優れていると私は強く思います。&lt;/p>
&lt;hr>
&lt;p>&lt;strong>このガイドはこれですべてです。このガイドは単一ケース ガイドの続きであることを覚えておいてください。迷った場合は、より長く、すべてについてより多くの説明が記載されているガイドを確認してください。&lt;/strong>&lt;/p>
&lt;p>すべてのコードは私の github の &lt;a href="https://github.com/emimontesdeoca/integration-test-directline-bot-framework">this&lt;/a> リポジトリに保存されていることに注意してください。&lt;/p>
&lt;p>良い一日を！&lt;/p></content:encoded><category>.NET</category><category>Bot Framework</category></item><item><title>Bot FrameworkとDirectLineを使った連携テスト(1)</title><link>https://emimontesdeoca.github.io/ja/posts/integration-test-bot-framework-1/</link><pubDate>Tue, 24 Apr 2018 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/integration-test-bot-framework-1/</guid><description>DirectLine API と JSON テスト ケースを使用して、Bot Framework チャットボットの統合テストをセットアップします。</description><content:encoded>&lt;p>会社に入社してから短期間で、Bot Framework の結合テストを担当することになりました。&lt;/p>
&lt;p>グループでの私の仕事は、ボットを可能な限り安定させることですが、まず、ボット フレームワークとは何でしょうか?&lt;/p>
&lt;blockquote>
&lt;p>インテリジェントなボットを構築、接続、展開、管理して、Web サイト、アプリ、Cortana、Microsoft Teams、Skype、Slack、Facebook Messenger などでユーザーと自然に対話します。使用した分だけ料金を支払うだけで、完全なボット構築環境をすぐに始められます。&lt;/p>&lt;/blockquote>
&lt;p>Bot Framework の詳細については、&lt;a href="https://dev.botframework.com/">ここ&lt;/a> をご覧ください。&lt;/p>
&lt;p>&lt;strong>次の説明では、Bot Framework の動作に関する基本情報がすべて網羅されているわけではありません。理解できない場合は、公式ドキュメントを確認してください。&lt;/strong>&lt;/p>
&lt;h2 id="なぜ統合テストが必要なのでしょうか">なぜ統合テストが必要なのでしょうか?&lt;/h2>
&lt;p>統合テストが必要なのは、同僚が修正、新機能、さらには新しいバグをプッシュするたびに、コードを運用環境にプッシュする前にこのテストが実行され、テストのいずれかが失敗した場合、コードは運用環境に移行しないためです。つまり、エンド ユーザーにはバグが発生しません。&lt;/p>
&lt;h2 id="ボットでの統合テスト">ボットでの統合テスト?&lt;/h2>
&lt;p>ボットでは、統合テストが非常に重要であると私は信じています。一部のメニューが機能しない、または一部の機能が何も返さないボットは使用できません。&lt;/p>
&lt;p>企業が顧客のためにボットを使用しているのは、人々に自分たちの問題で忙しくしてほしくないからです。ボットがユーザーを助けることができれば、他の従業員は自分の時間をより重要なことに費やすことができるでしょう。&lt;/p>
&lt;h2 id="ソリューションの概要">ソリューションの概要。&lt;/h2>
&lt;p>これを機能させるために、Visual Studio でテスト プロジェクトを使用しました。このプロジェクトでは、API レストに WebClient を使用し、ケースを保存する Json ファイルを使用します。&lt;/p>
&lt;h2 id="json-ファイル">JSON ファイル&lt;/h2>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;secret&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;direct-line-secret&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;directlineGenerateTokenEndpoint&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;https://directline.botframework.com/v3/directline/tokens/generate&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;directlineConversationEndpoint&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;https://directline.botframework.com/v3/directline/conversations/&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;entries&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;DecirHola&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;request&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;message&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;text&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Hola&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;from&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;default-user&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;User&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;locale&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;es&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;textFormat&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;plain&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;timestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T08:04:37.195Z&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;channelData&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;clientActivityId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;1523261059363.6264723268323733.0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;entities&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;ClientCapabilities&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;requiresBotState&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;supportsTts&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;supportsListening&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;61hacck8j6jg&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;response&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;message&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;timestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T08:04:37.901Z&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;localTimestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T09:04:37+01:00&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;serviceUrl&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;http://localhost:50629&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;channelId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;emulator&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;from&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;j98bbdf097a&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Bot&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;conversation&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;eabcie4be8ak&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;recipient&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;default-user&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;locale&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;es&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;text&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;No tengo respuesta para eso.&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;attachments&amp;#34;&lt;/span>: [],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;entities&amp;#34;&lt;/span>: [],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;replyToId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;61hacck8j6jg&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;47me557ikbf7&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;assert&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Request.Text == Response.Text&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>ご覧のとおり、次のものがあります。&lt;/p>
&lt;ul>
&lt;li>ダイレクトラインシークレット -&amp;gt; 公開ボットからのシークレット&lt;/li>
&lt;li>直接トークン生成エンドポイント -&amp;gt; シークレットを使用してトークンを取得するエンドポイント&lt;/li>
&lt;li>直接会話エンドポイント -&amp;gt; 会話を操作するためのエンドポイント&lt;/li>
&lt;li>エントリ -&amp;gt; テストケース
&lt;ul>
&lt;li>リクエスト -&amp;gt; 会話に送信するもの&lt;/li>
&lt;li>応答 -&amp;gt; 取得が期待されるもの&lt;/li>
&lt;li>アサート -&amp;gt; 何を比較しているのか&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h2 id="デシリアライゼーション">デシリアライゼーション&lt;/h2>
&lt;p>json ファイルが完全にフォーマットされているので、それをソリューションにロードする必要があるため、JSON.NET といくつかのクラスを使用します。まず、すべてが含まれるエントリ コレクションがあり、次にコレクションごとにエントリのリストがあります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Object to parse from the file&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">TestEntriesCollection&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// DirectLine Secret&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;secret&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Secret { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Endpoint to get the token using the secret for DirectLine&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;directlineGenerateTokenEndpoint&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> DirectLineGenerateTokenEndpoint { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Endpoint for a conversation in DirectLine&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;directlineConversationEndpoint&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> DirectLineConversationEndpoint { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Entries list&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;entries&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> List&amp;lt;TestEntry&amp;gt; Entries { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これは、テスト ケースの名前、要求、応答、およびアサートを持つオブジェクトです。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">TestEntry&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Entry name&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;name&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Name { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Activity requested by the entry&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;request&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Activity Request { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Activity response expected by the entry&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;response&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Activity Response { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Assert value in string&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [JsonProperty(&amp;#34;assert&amp;#34;)]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">string&lt;/span> Assert { &lt;span style="color:#ff7b72">get&lt;/span>; &lt;span style="color:#ff7b72">set&lt;/span>; }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="テストケースでの-json-からオブジェクトへの解析">テストケースでの json からオブジェクトへの解析&lt;/h2>
&lt;p>解析オブジェクトのクラスがあれば、オブジェクトとして読み取る必要があるため、非常に簡単です。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Load entries from file&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> path = System.IO.File.ReadAllText(&lt;span style="color:#a5d6ff">@&amp;#34;C:\data.json&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// Deserialize to object&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> data = JsonConvert.DeserializeObject&amp;lt;TestEntriesCollection&amp;gt;(path);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>このコレクションを使用すると、それをループして、たとえば foreach を使用して情報を取得できるようになります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (TestEntry entry &lt;span style="color:#ff7b72">in&lt;/span> data.Entries)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ....
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>この部分はこれですべてです。次の部分には DirectLine の認証が含まれます。すべてのコードは &lt;a href="https://github.com/emimontesdeoca/integration-test-directline-bot-framework">this&lt;/a> リポジトリの github に保存されていることに注意してください。&lt;/p></content:encoded><category>.NET</category><category>Bot Framework</category></item><item><title>Bot FrameworkとDirectLineを使った連携テスト(2)</title><link>https://emimontesdeoca.github.io/ja/posts/integration-test-bot-framework-2/</link><pubDate>Tue, 24 Apr 2018 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/integration-test-bot-framework-2/</guid><description>Bot Framework 統合テスト用に DirectLine 認証と API 呼び出しを実装します (パート 2)。</description><content:encoded>&lt;h4 id="このパートではdirectline-承認を作成しボットの応答から値を取得します">このパートでは、DirectLine 承認を作成し、ボットの応答から値を取得します。&lt;/h4>
&lt;p>逆シリアル化が完了したので、今度はコレクションと全体から情報を取得し、承認の取得、API 呼び出しの実行、結果のアサートに使用します。&lt;/p>
&lt;p>&lt;strong>次の説明では、Bot Framework の動作に関する基本情報がすべて網羅されているわけではありません。理解できない場合は、公式ドキュメントを確認してください。&lt;/strong>&lt;/p>
&lt;h2 id="webclient-を使用した-api-呼び出しの実行">WebClient を使用した API 呼び出しの実行&lt;/h2>
&lt;p>API の呼び出しを容易にするために、&lt;code>utils&lt;/code> クラスを作成しました。ここに、数回使用する関数を保存します。このクラスには、POST 用の &lt;code>uploadString&lt;/code> と GET 用の &lt;code>downloadString&lt;/code> が含まれています。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Utils&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Uploads to an URL and gets result&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;typeparam name=&amp;#34;T&amp;#34;&amp;gt;Type of object you are receiving&amp;lt;/typeparam&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param name=&amp;#34;bearer&amp;#34;&amp;gt;Token&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param name=&amp;#34;url&amp;#34;&amp;gt;Url&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param name=&amp;#34;serializedJson&amp;#34;&amp;gt;Serialized JSON to send&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> T uploadString&amp;lt;T&amp;gt;(&lt;span style="color:#ff7b72">string&lt;/span> bearer, &lt;span style="color:#ff7b72">string&lt;/span> url, &lt;span style="color:#ff7b72">string&lt;/span> serializedJson)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> serializedResult = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Webclient&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> client = &lt;span style="color:#ff7b72">new&lt;/span> WebClient())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Add headers&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client.Headers.Add(&lt;span style="color:#a5d6ff">&amp;#34;Content-Type&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;application/json&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client.Headers.Add(&lt;span style="color:#a5d6ff">&amp;#34;Authorization&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">$&amp;#34;Bearer {bearer}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Upload string&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> serializedResult = client.UploadString(url, serializedJson);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Get result and return it as an object&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> JsonConvert.DeserializeObject&amp;lt;T&amp;gt;(serializedResult);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Downloads from URL&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;typeparam name=&amp;#34;T&amp;#34;&amp;gt;Type of object you are receiving&amp;lt;/typeparam&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param name=&amp;#34;bearer&amp;#34;&amp;gt;Token&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;param name=&amp;#34;url&amp;#34;&amp;gt;Url&amp;lt;/param&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">static&lt;/span> T downloadString&amp;lt;T&amp;gt;(&lt;span style="color:#ff7b72">string&lt;/span> bearer, &lt;span style="color:#ff7b72">string&lt;/span> url)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> serializedResult = &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Webclient&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">using&lt;/span> (&lt;span style="color:#ff7b72">var&lt;/span> client = &lt;span style="color:#ff7b72">new&lt;/span> WebClient())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Add headers&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client.Headers.Add(&lt;span style="color:#a5d6ff">&amp;#34;Content-Type&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">&amp;#34;application/json&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client.Headers.Add(&lt;span style="color:#a5d6ff">&amp;#34;Authorization&amp;#34;&lt;/span>, &lt;span style="color:#a5d6ff">$&amp;#34;Bearer {bearer}&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Download string&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> serializedResult = client.DownloadString(url);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Get result and return it as an object&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">return&lt;/span> JsonConvert.DeserializeObject&amp;lt;T&amp;gt;(serializedResult);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="directline-認証">DirectLine 認証&lt;/h2>
&lt;p>公式ドキュメントを読めばその方法がわかり、非常に簡単ですが、関数を使用するとさらに簡単になります。まず最初に、時間切れになった場合に備えて、ケースごとに認証を行っているため、foreach ステートメント内にいることを覚えておいてください。これは、テストが失敗することを意味します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Arrange with current requested values&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">string&lt;/span> token, newToken, conversationId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Act&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 1 - Get token using secret from DirectLine in BotFramework panel&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>token = Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(data.Secret, data.DirectLineGenerateTokenEndpoint, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>).token;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>これで、会話エンドポイントに対する以降のすべての呼び出しを行うために使用されるトークンが取得されました。&lt;/p>
&lt;h2 id="会話を作成しています">会話を作成しています。&lt;/h2>
&lt;p>ボットと会話するには、まず会話を作成する必要があります。この会話は会話 ID を含む新しいトークンを返します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 2 -Create a new conversation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> createdConversation = Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(token, data.DirectLineConversationEndpoint, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-style:italic">// This returns a new token and a conversationId&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>newToken = createdConversation.token;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>conversationId = createdConversation.conversationId;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>また、&lt;code>newToken&lt;/code> と &lt;code>conversationId&lt;/code> も保存します。どちらもユーザーがボットにメッセージを送信するために必要です。&lt;/p>
&lt;h2 id="アクティビティを会話に送信する">アクティビティを会話に送信する&lt;/h2>
&lt;p>&lt;code>conversationId&lt;/code> と &lt;code>conversationEndpoint&lt;/code> を使用して、json ファイルの &lt;code>request&lt;/code> である &lt;code>Activity&lt;/code> を送信する最終エンドポイントを作成できます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 3 - Send an activity to the conversation with new token and conversationId&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">string&lt;/span> directlineConversationActivitiesEndpoint = data.DirectLineConversationEndpoint + conversationId + &lt;span style="color:#a5d6ff">&amp;#34;/activities&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(newToken, directlineConversationActivitiesEndpoint, JsonConvert.SerializeObject(entry.Request));
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="最新のメッセージを取得する">最新のメッセージを取得する&lt;/h2>
&lt;p>メッセージ履歴では、アクティビティを送信した後、ボットはすでに応答しているはずです。そのため、ウォーターマーク付きのすべてのメッセージを取得し、そのウォーターマークを使用して最新のメッセージ/アクティビティをフィルターする必要があります。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 4 - Get all activities, we get a List&amp;lt;activity&amp;gt; and a watermark&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> getLastActivity = Utils.downloadString&amp;lt;ActivityResponse&amp;gt;(newToken, directlineConversationActivitiesEndpoint);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 5 - Get the latest activity which is the response we should be expecting&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> latestResponse = getLastActivity.activities[Int32.Parse(getLastActivity.watermark)];
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>この部分はこれですべてです。次の部分には、json の &lt;code>assert&lt;/code> からテキストを取得し、JavaScript で C# で &lt;code>eval()&lt;/code> を使用するようなコードに変換し、&lt;code>Assert.isTrue()&lt;/code> を使用して最終テスト結果を取得する部分が含まれます。&lt;/p>
&lt;p>すべてのコードは私の github の &lt;a href="https://github.com/emimontesdeoca/integration-test-directline-bot-framework">this&lt;/a> リポジトリに保存されていることに注意してください。&lt;/p></content:encoded><category>.NET</category><category>Bot Framework</category></item><item><title>Bot FrameworkとDirectLineを使った連携テスト(3)</title><link>https://emimontesdeoca.github.io/ja/posts/integration-test-bot-framework-3/</link><pubDate>Tue, 24 Apr 2018 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/posts/integration-test-bot-framework-3/</guid><description>Bot Framework 統合テストで Roslyn CodeAnalysis を使用してボットの応答を評価します (パート 3)。</description><content:encoded>&lt;h4 id="これはガイドの最新の部分ですこの部分ではjson-のアサーションからテキストを評価します">これはガイドの最新の部分です。この部分では、json のアサーションからテキストを評価します。&lt;/h4>
&lt;p>&lt;code>request&lt;/code> を使用し、&lt;code>activity&lt;/code> としてボットに送信したので、&lt;code>response&lt;/code> を取得しました。そして、json 内の &lt;code>assert&lt;/code> (json に保存した応答。これは予想される応答です) とボットから取得した応答を比較する必要があります。&lt;/p>
&lt;p>&lt;strong>次の説明では、Bot Framework の動作に関する基本情報がすべて網羅されているわけではありません。理解できない場合は、公式ドキュメントを確認してください。&lt;/strong>&lt;/p>
&lt;h2 id="microsoftcodeanalysis-をソリューションに追加する">Microsoft.CodeAnalysis をソリューションに追加する&lt;/h2>
&lt;p>まず、CodeAnalysis を NuGet パッケージとして含める必要があります。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/ce97a60ecab998f162f6ac7a2ab2c9a7">&lt;img src="https://i.gyazo.com/ce97a60ecab998f162f6ac7a2ab2c9a7.png" alt="https://gyazo.com/ce97a60ecab998f162f6ac7a2ab2c9a7">&lt;/a>&lt;/p>
&lt;p>インストール後、忘れずにパッケージを &lt;code>.cs&lt;/code> ファイルに追加してください。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">using&lt;/span> &lt;span style="color:#ff7b72">Microsoft.CodeAnalysis.CSharp.Scripting&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="globals-オブジェクトの作成">&lt;code>Globals&lt;/code> オブジェクトの作成&lt;/h2>
&lt;p>評価するには、パラメータを評価者に渡す必要があります。このエバリュエーターには構成ファイルが必要です。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Object to pass parameters to Roslyn compiler&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">class&lt;/span> &lt;span style="color:#f0883e;font-weight:bold">Globals&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// ExpectedResponse&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Activity Request;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// ReceivedResponse&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// &amp;lt;/summary&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">public&lt;/span> Activity Response;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>この部分は非常に重要です。その構成ファイルには、比較するオブジェクトが含まれるため、&lt;strong>期待される応答&lt;/strong>と&lt;strong>受信した応答&lt;/strong>を渡す必要があります。&lt;/p>
&lt;p>その情報を使用して、json ファイル内の &lt;code>assert&lt;/code> を使用し、その内容を評価します。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Arrange with new values&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">var&lt;/span> globals = &lt;span style="color:#ff7b72">new&lt;/span> Objects.Globals { Request = entry.Response, Response = latestResponse };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Assert&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>Assert.IsTrue(&lt;span style="color:#ff7b72">await&lt;/span> CSharpScript.EvaluateAsync&amp;lt;&lt;span style="color:#ff7b72">bool&lt;/span>&amp;gt;(entry.Assert, globals: globals));
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>&lt;code>EvaluateAsync&amp;lt;T&amp;gt;&lt;/code> は T を評価して返します。この例では、評価する &lt;code>string&lt;/code> と、評価対象のデータを持つ &lt;code>globals&lt;/code> を渡します。&lt;/strong>&lt;/p>
&lt;p>エントリ(名前、リクエスト、レスポンス、アサートを持つ)を使用した例でこれを説明してみます。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;DecirHola&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;request&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;message&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;text&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Hola&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;from&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;default-user&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;User&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;locale&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;es&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;textFormat&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;plain&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;timestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T08:04:37.195Z&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;channelData&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;clientActivityId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;1523261059363.6264723268323733.0&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;entities&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;ClientCapabilities&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;requiresBotState&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;supportsTts&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;supportsListening&amp;#34;&lt;/span>: &lt;span style="color:#79c0ff">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;61hacck8j6jg&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;response&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;type&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;message&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;timestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T08:04:37.901Z&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;localTimestamp&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;2018-04-09T09:04:37+01:00&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;serviceUrl&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;http://localhost:50629&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;channelId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;emulator&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;from&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;j98bbdf097a&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Bot&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;conversation&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;eabcie4be8ak&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;recipient&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;default-user&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;locale&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;es&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;text&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;No tengo respuesta para eso.&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;attachments&amp;#34;&lt;/span>: [],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;entities&amp;#34;&lt;/span>: [],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;replyToId&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;61hacck8j6jg&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;id&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;47me557ikbf7&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#7ee787">&amp;#34;assert&amp;#34;&lt;/span>: &lt;span style="color:#a5d6ff">&amp;#34;Request.Text == Response.Text&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>このエントリから重要なことを理解しましょう。まず、アサート &lt;strong>&lt;code>&amp;quot;assert&amp;quot;: &amp;quot;Request.Text == Response.Text&amp;quot;&lt;/code>&lt;/strong> です。これは、&lt;code>Request.Text&lt;/code> と &lt;code>Response.Text&lt;/code> を比較し、値をブール値として返すことを意味します。&lt;/p>
&lt;p>ただし、関数 &lt;code>await CSharpScript.EvaluateAsync&amp;lt;bool&amp;gt;(entry.Assert, globals: globals)&lt;/code> を呼び出すときは、2 つのパラメーターを渡します。&lt;/p>
&lt;ul>
&lt;li>&lt;code>string&lt;/code> 評価する文字列 -&amp;gt; &lt;code>&amp;quot;Request.Text == Response.Text&amp;quot;&lt;/code>&lt;/li>
&lt;li>評価者用の &lt;code>globals&lt;/code> データ -&amp;gt; この場合、&lt;code>Request&lt;/code> と &lt;code>Response&lt;/code> を与える必要があります。リクエストは &lt;strong>期待される応答&lt;/strong>で、応答は &lt;strong>受信された応答&lt;/strong>です。&lt;/li>
&lt;/ul>
&lt;p>エバリュエーターにデータを入力しているので、文字列を使用して評価できるようになり、&lt;code>true&lt;/code> または &lt;code>false&lt;/code> が返されます。&lt;/p>
&lt;h2 id="完了">完了&lt;/h2>
&lt;p>これで完了です。完成した &lt;code>TestMethod&lt;/code> をご覧ください。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-csharp" data-lang="csharp">&lt;span style="display:flex;">&lt;span>[TestMethod]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff7b72">public&lt;/span> &lt;span style="color:#ff7b72">async&lt;/span> Task ShouldTestSingleCases()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Load entries from file&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> path = System.IO.File.ReadAllText(&lt;span style="color:#a5d6ff">@&amp;#34;C:\data.json&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// Deserialize to object&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> data = JsonConvert.DeserializeObject&amp;lt;TestEntriesCollection&amp;gt;(path);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Flow: Arrange -&amp;gt; Act -&amp;gt; arrange -&amp;gt; assert&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">foreach&lt;/span> (TestEntry entry &lt;span style="color:#ff7b72">in&lt;/span> data.Entries)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Arrange with current requested values&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> token, newToken, conversationId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">if&lt;/span> (entry.Request.Type == ActivityTypes.Message)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Act&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 1 - Get token using secret from DirectLine in BotFramework panel&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> token = Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(data.Secret, data.DirectLineGenerateTokenEndpoint, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>).token;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 2 -Create a new conversation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> createdConversation = Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(token, data.DirectLineConversationEndpoint, &lt;span style="color:#a5d6ff">&amp;#34;&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-style:italic">// This returns a new token and a conversationId&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> newToken = createdConversation.token;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> conversationId = createdConversation.conversationId;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 3 - Send an activity to the conversation with new token and conversationId&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">string&lt;/span> directlineConversationActivitiesEndpoint = data.DirectLineConversationEndpoint + conversationId + &lt;span style="color:#a5d6ff">&amp;#34;/activities&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Utils.uploadString&amp;lt;DirectLineAuth&amp;gt;(newToken, directlineConversationActivitiesEndpoint, JsonConvert.SerializeObject(entry.Request));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 4 - Get all activities, we get a List&amp;lt;activity&amp;gt; and a watermark&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> getLastActivity = Utils.downloadString&amp;lt;ActivityResponse&amp;gt;(newToken, directlineConversationActivitiesEndpoint);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// 5 - Get the latest activity which is the response we should be expecting&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> latestResponse = getLastActivity.activities[Int32.Parse(getLastActivity.watermark)];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Arrange with new values&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">var&lt;/span> globals = &lt;span style="color:#ff7b72">new&lt;/span> Objects.Globals { Request = entry.Response, Response = latestResponse };
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#8b949e;font-weight:bold;font-style:italic">/// Assert&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> Assert.IsTrue(&lt;span style="color:#ff7b72">await&lt;/span> CSharpScript.EvaluateAsync&amp;lt;&lt;span style="color:#ff7b72">bool&lt;/span>&amp;gt;(entry.Assert, globals: globals));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff7b72">await&lt;/span> Task.CompletedTask;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="図">図&lt;/h2>
&lt;p>ここで、この点を理解するために私たちがたどったすべてのフローを図に示します。理解できなかったことがあれば、これで理解できると思います。&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/1e3b7c9c2286844062878b4b8ca02d2d">&lt;img src="https://i.gyazo.com/1e3b7c9c2286844062878b4b8ca02d2d.png" alt="https://gyazo.com/1e3b7c9c2286844062878b4b8ca02d2d">&lt;/a>&lt;/p>
&lt;p>これですべてです。これは、ケースが 1 対 1 で、ユーザーが &lt;code>activity&lt;/code> を送信し、ボットが別の単一の &lt;code>activity&lt;/code> を返す単一のケースに対して行われます。&lt;/p>
&lt;p>気に入っていただければ幸いです。&lt;strong>次は、複数の応答があるテスト フロー ケースです。&lt;/strong>&lt;/p>
&lt;p>&lt;a href="https://gyazo.com/d964cfac395ed438a5282e60614863e7">&lt;img src="https://i.gyazo.com/d964cfac395ed438a5282e60614863e7.png" alt="https://gyazo.com/d964cfac395ed438a5282e60614863e7">&lt;/a>&lt;/p>
&lt;p>すべてのコードは私の github の &lt;a href="https://github.com/emimontesdeoca/integration-test-directline-bot-framework">this&lt;/a> リポジトリに保存されていることに注意してください。&lt;/p></content:encoded><category>.NET</category><category>Bot Framework</category><category>NuGet</category></item><item><title>プロフィール</title><link>https://emimontesdeoca.github.io/ja/about/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/about/</guid><description>Emiliano Montesdeocaについて — Microsoft MVP、クラウドソリューションチームリード、コミュニティ推進者。</description><content:encoded>&lt;h2 id="私について">私について&lt;/h2>
&lt;p>私は&lt;strong>エミリアーノ・モンテスデオカ（Emiliano Montesdeoca）&lt;strong>です。ウルグアイ・スペイン出身のソフトウェア開発者であり、&lt;strong>開発者テクノロジー分野のMicrosoft MVP&lt;/strong>、そして&lt;/strong>カナリア諸島テネリフェ&lt;/strong>在住の誇り高き父親です。&lt;/p>
&lt;p>複雑な技術的課題に取り組み、Microsoftテクノロジーを使ってスケーラブルなクラウドソリューションを構築することが大好きです。日々のツールキットは**.NET**、&lt;strong>Azure&lt;/strong>、&lt;strong>Semantic KernelによるAI&lt;/strong>、そして**.NET Aspire**のような最新アーキテクチャを中心に展開しています。&lt;/p>
&lt;h2 id="私の仕事">私の仕事&lt;/h2>
&lt;p>&lt;a href="https://intelequia.com">Intelequia Technologies&lt;/a>の&lt;strong>クラウドソリューションチームリード&lt;/strong>として、クラウドネイティブアプリケーションの設計と提供を主導しています — 戦略的ビジョンと実践的な実行を融合させながら。アーキテクチャとコードの交差点で力を発揮し、チームがエレガントかつ本番環境に対応したソリューションを提供できるよう努めています。&lt;/p>
&lt;h2 id="コミュニティ">コミュニティ&lt;/h2>
&lt;p>国際的な登壇者として頻繁に活動しており、&lt;a href="https://sessionize.com/emimontesdeoca/">2023&lt;/a>、&lt;a href="https://sessionize.com/emimontesdeoca/">2024&lt;/a>、&lt;a href="https://sessionize.com/emimontesdeoca/">2025&lt;/a>に&lt;strong>Sessionize最も活発な登壇者&lt;/strong>として認定されました。世界中のカンファレンスでの講演やブログを通じて、実践的なインサイトを共有しています。&lt;/p>
&lt;p>また、.NETコミュニティのためのリソースである&lt;a href="https://thedotnetblog.com">&lt;strong>The .NET Blog&lt;/strong>&lt;/a>の作成者でもあり、学んだことや構築したものについてこのブログで定期的に執筆しています。&lt;/p>
&lt;p>メンタリングとコミュニティ構築は私の本質的な部分です。私のキャリアを形成してくれたエコシステムへの恩返しは、真剣に取り組んでいることです。&lt;/p>
&lt;h2 id="つながりましょう">つながりましょう&lt;/h2>
&lt;p>イベントでの登壇、プロジェクトへの協力、クラウドアーキテクチャやAIに関する会話など、いつでも歓迎します。お気軽にご連絡ください！&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Email&lt;/strong>: &lt;a href="mailto:emimontesdeoca@outlook.es">emimontesdeoca@outlook.es&lt;/a>&lt;/li>
&lt;li>&lt;strong>GitHub&lt;/strong>: &lt;a href="https://github.com/emimontesdeoca">@emimontesdeoca&lt;/a>&lt;/li>
&lt;li>&lt;strong>X / Twitter&lt;/strong>: &lt;a href="https://twitter.com/emimontesdeocaa">@emimontesdeocaa&lt;/a>&lt;/li>
&lt;li>&lt;strong>LinkedIn&lt;/strong>: &lt;a href="https://www.linkedin.com/in/emimontesdeoca/">emimontesdeoca&lt;/a>&lt;/li>
&lt;li>&lt;strong>Instagram&lt;/strong>: &lt;a href="https://www.instagram.com/emimontesdeoca/">@emimontesdeoca&lt;/a>&lt;/li>
&lt;li>&lt;strong>Sessionize&lt;/strong>: &lt;a href="https://sessionize.com/emimontesdeoca/">emimontesdeoca&lt;/a>&lt;/li>
&lt;/ul></content:encoded></item><item><title>講演</title><link>https://emimontesdeoca.github.io/ja/speaking/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/ja/speaking/</guid><description>Emiliano Montesdeocaのカンファレンス講演とセッション — Microsoft MVPおよび国際スピーカー。</description><content:encoded>&lt;p>私は**.NET**、&lt;strong>Azure&lt;/strong>、&lt;strong>AI&lt;/strong>、&lt;strong>クラウドネイティブ開発&lt;/strong>を中心に、国際的な技術カンファレンスで頻繁に講演しています。2023年、2024年、2025年に&lt;strong>Sessionize最も活発な登壇者&lt;/strong>として表彰され、開発者テクノロジー分野の&lt;strong>Microsoft MVP&lt;/strong>資格を保有しています。&lt;/p>
&lt;p>イベントでの登壇をご希望の方は、&lt;a href="mailto:emimontesdeoca@outlook.es">ご連絡ください&lt;/a>！&lt;/p>
&lt;hr>
&lt;h2 id="2026">2026&lt;/h2>
&lt;h3 id="2026年のblazor">2026年のBlazor&lt;/h3>
&lt;p>2026年のBlazorの現状と、.NET 10とともにどのように進化したかを見ていきます。最新の変更と改善、実際のアプリケーション開発への影響、各シナリオに最適なBlazorホスティングモデルを検討します。&lt;/p>
&lt;h3 id="aiでレベル3サポートを強化azure-functions--openai実践">AIでレベル3サポートを強化：Azure Functions + OpenAI実践&lt;/h3>
&lt;p>Azure Functions、Azure Monitorアラート、Azure OpenAIが連携してレベル3サポートを加速する方法。自動アラートトリアージからインテリジェントなログ分析まで — 実際のサポートワークフローにAIを追加する実践的な解説。&lt;/p>
&lt;h3 id="ダッシュボードからエージェントへオブザーバビリティの次のステップ">ダッシュボードからエージェントへ：オブザーバビリティの次のステップ&lt;/h3>
&lt;p>AIエージェントと本番オブザーバビリティの世界をつなぐハンズオンセッション。Semantic Kernelから、OpenTelemetryデータをクエリし、MCPを介してGrafanaと対話し、実際のシステムの運用と最適化を支援する自律型エージェントまで。&lt;/p>
&lt;p>&lt;strong>予定&lt;/strong>: &lt;a href="https://globalazure.es">Global Azure 2026&lt;/a> — 2026年4月、スペイン・マドリード&lt;/p>
&lt;hr>
&lt;h2 id="2025">2025&lt;/h2>
&lt;h3 id="クリスマスを救うmicrosoftエージェントフレームワーク">クリスマスを救うMicrosoftエージェントフレームワーク&lt;/h3>
&lt;p>Microsoft Agent Frameworkを使って複数のAIエージェントを構築・調整し、完璧なクリスマスプレゼントを見つけます。各エージェントは専門化され — ギフトアイデア生成から価格比較まで — よりスマートなホリデーショッピングのために連携します。&lt;/p>
&lt;h3 id="aiでレベル3サポートを強化azure-functions--semantic-kernel実践">AIでレベル3サポートを強化：Azure Functions + Semantic Kernel実践&lt;/h3>
&lt;p>Azure Functions、Azure Monitor、Semantic Kernelが連携してレベル3サポートをより速く効率的に — 自動アラート検出からインテリジェントなログ分析まで。&lt;/p>
&lt;h3 id="net-10でblazorの何が新しくなった">.NET 10でBlazorの何が新しくなった？&lt;/h3>
&lt;p>パフォーマンスの向上、フォーム、永続状態、より強力なJavaScriptインターオプなど、.NET 10向けBlazorの主要な新機能を探ります。&lt;/p>
&lt;h3 id="csemantic-kernelgeminiによるハイブリッドaiスマートなエンタープライズアプリを作る">C#、Semantic Kernel、GeminiによるハイブリッドAI：スマートなエンタープライズアプリを作る&lt;/h3>
&lt;p>C#でハイブリッドAIワークフローをオーケストレーション — ローカルモデル(Phi-3)とVertex AI経由のGemini Pro/Ultraを組み合わせ、Semantic KernelプラグインでAIエージェントを構築し、適応型アーキテクチャを設計します。&lt;/p>
&lt;h3 id="net-aspire頭痛なしにcloud-nativeアプリを構築">.NET Aspire：頭痛なしにCloud-Nativeアプリを構築&lt;/h3>
&lt;p>スケーラブルで回復力があり、観察可能なクラウドファーストマイクロサービスを構築するハンズオンセッション。.NET Aspireがクラウドの70%のボイラープレートをどのように排除するかを探ります。&lt;/p>
&lt;h3 id="net-aspire混乱なしのcloud-native">.NET Aspire：混乱なしのCloud-Native&lt;/h3>
&lt;p>観察可能性と自動回復力を最初のコミットから組み込んだ、スケーラブルに生まれたアプリ設計の実例。&lt;/p>
&lt;hr>
&lt;h2 id="2024">2024&lt;/h2>
&lt;h3 id="aiがsqlと出会うnetとsemantic-kernelでスマートなピザ屋を作る">AIがSQLと出会う：.NETとSemantic Kernelでスマートなピザ屋を作る&lt;/h3>
&lt;p>コードとデータを理解する自然言語AI対話 — .NETとSemantic Kernelを活用したインテリジェントなピザ屋を構築。&lt;/p>
&lt;h3 id="net-aspire複雑さなしのcloud-nativeアプリケーション">.NET Aspire：複雑さなしのCloud-Nativeアプリケーション&lt;/h3>
&lt;p>頭痛なしにクラウドファーストマイクロサービスを構築 — .NET AspireがCloud-native開発をいかに単純化するかを示す実践的なデモ中心セッション。&lt;/p>
&lt;h3 id="power-platformlow-codeからpro-codeへの旅">Power Platform：Low-codeからPro-codeへの旅&lt;/h3>
&lt;p>同じプラットフォーム内でTypeScriptを使用して、Low-code Power AppsからAdvancedなPro-codeコンポーネントへ移行。&lt;/p>
&lt;h3 id="インテリジェント自動化aiの統合でエンタープライズアプリを変革">インテリジェント自動化：AIの統合でエンタープライズアプリを変革&lt;/h3>
&lt;p>.NETアプリケーションを次のレベルへ — Semantic Kernelと.NET Aspireを使用して伝統的なロジックとAIベースの推論を組み合わせ。&lt;/p>
&lt;h3 id="low-codeからpro-codeへエンタープライズソリューションにおけるai開発アプローチ">Low-CodeからPro-Codeへ：エンタープライズソリューションにおけるAI開発アプローチ&lt;/h3>
&lt;p>エンタープライズソリューションのさまざまなアプローチを探る — AIを含むPower PlatformからSemantic Kernelを使用した従来の開発まで。&lt;/p>
&lt;h3 id="azureで出張費を管理">Azureで出張費を管理&lt;/h3>
&lt;p>Azure AI Vision、Document Intelligence、Semantic KernelとOpenAIを使用して出張費管理を自動化。&lt;/p>
&lt;hr>
&lt;h2 id="バッジと表彰">バッジと表彰&lt;/h2>
&lt;ul>
&lt;li>🏆 &lt;strong>Sessionize Most Active Speaker 2025&lt;/strong>&lt;/li>
&lt;li>🏆 &lt;strong>Sessionize Most Active Speaker 2024&lt;/strong>&lt;/li>
&lt;li>🏆 &lt;strong>Sessionize Most Active Speaker 2023&lt;/strong>&lt;/li>
&lt;li>🏅 &lt;strong>開発者テクノロジー分野 Microsoft MVP&lt;/strong>&lt;/li>
&lt;/ul>
&lt;p>&lt;a href="https://sessionize.com/emimontesdeoca/">Sessionizeですべてのセッションを見る →&lt;/a>&lt;/p></content:encoded></item></channel></rss>