<?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/zh/</link><description>Microsoft MVP in Developer Technologies. Cloud Solutions Team Lead. Speaker, blogger, and community advocate.</description><language>zh</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/zh/index.xml" rel="self" type="application/rss+xml"/><item><title>Blazor 从零开始：第 3 章 —— 可扩展的组件设计</title><link>https://emimontesdeoca.github.io/zh/posts/blazor-from-scratch-chapter-3/</link><pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/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>当每个页面都变成巨大 &lt;code>.razor&lt;/code> 文件时，Blazor 项目会很快失控。组件化可以避免这一点：更一致、更可复用、边界更清晰。&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="第一步从职责单一的组件开始">第一步：从职责单一的组件开始&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="第二步用参数显式表达行为">第二步：用参数显式表达行为&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="第三步用-renderfragment-做布局组合">第三步：用 &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="第四步优先组合而不是巨型页面">第四步：优先组合，而不是巨型页面&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="第五步保持标记与逻辑的平衡">第五步：保持标记与逻辑的平衡&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>这样既能保持标记可读，也能让 C# 逻辑更清晰。&lt;/p>
&lt;hr>
&lt;h2 id="第六步实用的目录结构">第六步：实用的目录结构&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>做出一个数百行的“上帝组件”&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/zh/posts/blazor-from-scratch-chapter-2/</link><pubDate>Thu, 14 May 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/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/zh/posts/blazor-from-scratch-chapter-1/</link><pubDate>Fri, 08 May 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/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> 这听起来很简单，但答案有几个层面——尤其是因为多年来&amp;quot;Blazor&amp;quot;已经有了略微不同的含义。读完这篇文章，你将了解 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> 的刻意融合——Razor 是 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>全栈 Web 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 是一个&lt;strong>面向 .NET 的基于组件的 UI 框架&lt;/strong>。你使用组件构建 UI——可以保存状态、响应事件并组合成更大结构的 C# 和 HTML 标记的自包含单元。如果你使用过 React 或 Vue，这个思维模型会感觉熟悉。关键区别在于，你写的是 C# 而不是 JavaScript。&lt;/p>
&lt;p>Blazor 中的组件写在 &lt;code>.razor&lt;/code> 文件中。&lt;code>.razor&lt;/code> 文件使用 Razor 语法混合 HTML 标记和 C# 代码，你可能已经从 ASP.NET MVC 视图中了解过这种语法。一个简单的组件看起来像这样：&lt;/p>
&lt;div class="highlight">&lt;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>这里存在很多混淆，所以让我们精确一些。今天的&amp;quot;Blazor&amp;quot;指的是一系列渲染模式，而不是单一的部署模型。理解差异很重要，因为它影响性能、基础设施要求以及你的组件可以和不能做什么。&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（社区版免费）、带有 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/zh/posts/blazor-from-scratch-intro/</link><pubDate>Mon, 04 May 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/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>你是 .NET 开发者，听说过 Blazor，但从未找到合适的时机或起点深入研究。&lt;/li>
&lt;li>你尝试过 Blazor，让它运行起来了，但感觉在猜测&lt;em>为什么&lt;/em>事情会这样运作。&lt;/li>
&lt;li>你来自 JavaScript/React/Angular 的世界，想了解微软对现代前端的答案是什么样的。&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、流式渲染、交互式 Server、交互式 WebAssembly 和 Auto 模式现在都在同一个框架下共存。这是一个真正有趣且能力强大的框架，但复杂度的提升让入门体验变得令人困惑。&lt;/p>
&lt;p>我想构建一个能从你所在之处出发、系统地引导你走完全程的资源。不是官方文档的替代——官方文档很好，你应该去读——而是一个解释&lt;em>是什么&lt;/em>背后的&lt;em>为什么&lt;/em>的伙伴。&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/zh/posts/csharp-union-types/</link><pubDate>Wed, 01 Apr 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/csharp-union-types/</guid><description>深入探讨即将推出的 C# 歧视联合类型 — 它们是什么、它们如何工作以及为什么它们会改变您对域进行建模的方式。</description><content:encoded>&lt;p>如果您已经编写 C# 相当长的时间，那么在尝试对可能是“多种事物之一”的事物进行建模时，您很可能会遇到困难。也许您需要一个方法来返回成功值或错误。也许您正在构建一个处理信用卡、银行转账和数字钱包的支付系统——每个系统都具有完全不同的数据。或者也许您只是看着 F# 或 Rust 并想，“为什么我不能在 C# 中使用它？”&lt;/p>
&lt;p>等待即将结束。 &lt;strong>受歧视的工会即将进入 C#。&lt;/strong>&lt;/p>
&lt;p>这一直是多年来最受欢迎的语言功能之一，社区讨论可以追溯到 2017 年或更早。 C# 语言设计团队一直致力于一项提案，该提案引入了 &lt;code>union&lt;/code> 关键字，用于定义具有详尽模式匹配的封闭类型层次结构。在这篇文章中，我想向您介绍什么是受歧视的联合，为什么它们如此重要，到目前为止我们如何伪造它们，以及提议的语法实际上是什么样子 - 每个都有真实的代码示例。&lt;/p>
&lt;p>在我们深入讨论之前，请注意：在撰写本文时，联合类型功能仍处于提案和预览阶段。我在此描述的语法和行为基于最新公开的设计文档以及 C# 语言设计团队的讨论。在最终版本发布之前情况可能会发生变化。我会明确哪些内容已被确认，哪些内容仍在讨论中。&lt;/p>
&lt;h2 id="什么是受歧视工会">什么是受歧视工会？&lt;/h2>
&lt;p>从本质上讲，可区分联合（有时称为“标记联合”或“总和类型”）是一种可以保存一组固定可能值之一的类型，其中每个变体可以携带不同的数据。 “可区分”部分意味着运行时始终知道它是哪个变体 - 有一个标签可以识别它。&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> [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 通过其泛型类型参数在编译时提供详尽的检查，这非常棒。但它依赖于位置匹配（第一类型、第二类型等），通用签名很快变得笨拙，并且它不与语言的模式匹配集成。这是一个聪明的黑客，但它仍然是一个黑客。&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">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> 属性。编译器无法帮助您，最终您会遇到运行时错误和到处都是空检查。这是类型建模的“字符串类型”方法，并且无法扩展。&lt;/p>
&lt;p>所有这些方法都有一个基本问题：**它们是在对抗语言而不是使用它。**编译器无法推理封闭的可能性集，因此您失去了可区分联合的最有价值的属性 - 详尽的检查。&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> 只能是这三者之一。&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:#f85149">请注意&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:#ff7b72;font-weight:bold">`&lt;/span>switch&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:#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:#a5d6ff">7&lt;/span>.&lt;span style="color:#a5d6ff">0&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>&lt;span style="color:#f85149">以来，模式匹配在&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>C&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:#8b949e;font-style:italic">### 详尽的 Switch 表达式
&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:#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>现代应用程序开发中最常见的模式之一是表示在不使用控制流异常的情况下可以成功或失败的操作。异常应该是异常的——比如网络故障或内存不足的情况。验证错误或“未找到”结果是预期结果，而不是异常。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-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>将此与当前方法进行比较，在当前方法中，您将拥有一个接口或抽象类，其中五个不同的实现分布在五个文件中，并且顶部可能带有访问者模式。联合方法将数据定义和操作保持在一起，可读且经过彻底检查。&lt;/p>
&lt;h3 id="状态机">状态机&lt;/h3>
&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-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>TcpClient&lt;/code>，因为该变体上不存在它。类型系统强制执行状态机的不变量。&lt;/p>
&lt;h2 id="序列化和互操作注意事项联合类型立即出现的实际问题之一是如何序列化它们如果您正在构建-api-或存储数据则需要-json-序列化才能正常工作">序列化和互操作注意事项联合类型立即出现的实际问题之一是：如何序列化它们？如果您正在构建 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>值得注意的是，与其他 .NET 语言的互操作应该很顺利，因为联合将在底层编译为标准 IL 类层次结构。使用 C# 联合的 F# 代码会将其视为标准类型层次结构，反之亦然。&lt;/p>
&lt;h2 id="如何尝试">如何尝试&lt;/h2>
&lt;p>截至撰写本文时，联合类型已作为最新 .NET SDK 预览版中的预览功能提供。要试验建议的语法，您需要：&lt;/p>
&lt;p>1.安装最新的.NET预览版SDK
2. 在项目文件中启用预览语言版本：&lt;/p>
&lt;div class="highlight">&lt;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;/p>
&lt;p>&lt;strong>C# 开发人员可以接触到 F# 的想法。&lt;/strong> 许多 C# 开发人员从远处欣赏 F# 的类型系统，但无法在其组织中采用 F#。联合类型为 C# 带来了 F# 最强大的功能之一，这对整个 .NET 生态系统来说是一个胜利。&lt;/p>
&lt;p>**更少的运行时错误。**单独的详尽检查就可以防止整个类别的错误。每次您向联合添加新变体时，编译器都会引导您到达代码库中需要更新的每个位置。不再有被遗忘的 &lt;code>switch&lt;/code> 情况，默认分支中不再有 &lt;code>NotImplementedException&lt;/code> 仅在生产中出现。&lt;/p>
&lt;h2 id="结论">结论&lt;/h2>
&lt;p>近十年来，受歧视的工会一直位居 C# 社区愿望清单的首位，这是有充分理由的。它们解决了类型系统中的一个根本缺陷——对数据进行建模的能力，这些数据可以是具有编译器强制安全性的“多种事物之一”。&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>密切关注预览版本，尝试该功能，并向语言设计团队提供反馈。这是其中一项功能，一旦您使用过它，您就会想知道如果没有它您将如何生活。&lt;/p></content:encoded><category>.NET</category><category>C#</category></item><item><title>使用语义内核在 C# 中构建 RAG 系统</title><link>https://emimontesdeoca.github.io/zh/posts/rag-csharp-semantic-kernel/</link><pubDate>Wed, 18 Mar 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/rag-csharp-semantic-kernel/</guid><description>使用语义内核、嵌入和向量搜索在 C# 中实现检索增强生成。</description><content:encoded>&lt;h2 id="简介">简介&lt;/h2>
&lt;p>如果您尝试使用法学硕士来回答有关您自己的数据（公司文档、产品规格、内部知识库）的问题，您可能会注意到它要么产生幻觉，要么只是说“我没有这方面的信息”。这是因为模型只知道它接受的训练内容。&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>我们需要两种模型：一种用于聊天完成（回答问题），另一种用于生成嵌入（将文本转换为向量）。&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>重叠很重要——它确保块之间边界的上下文不会丢失。如果相关句子被分成两个块，则重叠意味着它将完全出现在至少其中一个块中。&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> &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:#a5d6ff">500&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:#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> &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>&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>&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> Semantic Kernel &lt;span style="color:#f85149">使其在&lt;/span> C&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>](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>- [&lt;span style="color:#f85149">使用&lt;/span> Azure AI &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/zh/posts/agent-framework-workflows/</link><pubDate>Tue, 10 Feb 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/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>让我们用三个代理构建一个内容创建工作流程：研究员、作家和编辑。&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>然后，您可以将所有研究摘要提供给单个作家代理，以生成一篇有凝聚力的帖子。&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>当您链接多个代理时，错误处理变得至关重要。您不希望某个失败的步骤导致整个工作流程悄无声息地崩溃：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-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> - 每个代理都应该做好一件事。不要试图做瑞士军刀特工。&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-3.5 开发和测试您的工作流程逻辑，然后再切换到 GPT-4o 进行生产。&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 代理框架文档&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/zh/posts/blazor-component-lifecycle/</link><pubDate>Thu, 15 Jan 2026 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/blazor-component-lifecycle/</guid><description>了解从初始化到处置的每个 Blazor 组件生命周期方法以及何时使用每个方法。</description><content:encoded>&lt;p>我已经使用 Blazor 一段时间了，老实说，生命周期方法一开始让我感到困惑。 &lt;code>OnInitialized&lt;/code> vs &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="设置参数异步">设置参数异步&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>一次&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>一件让我困惑的事情：在 Blazor Server 中，&lt;code>OnInitializedAsync&lt;/code> 在预渲染期间被调用&lt;strong>两次&lt;/strong>。第一次是在服务器端预渲染期间，第二次是在建立 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> 参数告诉您这是否是初始渲染。这是 JS Interop 调用的地方，因为此时 DOM 元素已存在：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-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>我经常使用 &lt;code>firstRender&lt;/code> 来避免每次重新渲染时重新初始化 JavaScript 库。如果您要设置事件侦听器、图表库或任何直接接触 DOM 的内容，那么这就是它所在的位置。&lt;/p>
&lt;h1 id="应该渲染">应该渲染&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>我用它来优化处理大型列表的组件。您无需在每个项目更改时重新渲染，而是批量更新并在最后渲染一次。&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>希望你喜欢这篇文章！请随时通过任何社交媒体与我联系：&lt;strong>@emimontesdeoca&lt;/strong>。&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>微软的代理框架拯救圣诞节</title><link>https://emimontesdeoca.github.io/zh/posts/agent-framework-christmas-presents/</link><pubDate>Tue, 16 Dec 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/agent-framework-christmas-presents/</guid><description>使用 Microsoft 的 Agent Framework 和 .NET 构建多代理圣诞礼物购物系统。</description><content:encoded>&lt;h2 id="简介">简介&lt;/h2>
&lt;p>寻找完美的圣诞礼物可能会带来压力。在集思广益的礼物创意、比较各商店的价格以及确保所有物品准时到达之间，假日购物很快就会变得势不可挡。如果我们可以将这些任务委托给协同工作的专门人工智能代理会怎么样？在这篇文章中，我们将探讨如何使用 Microsoft 的代理框架构建一个多代理系统，其中每个代理专门从事特定任务，从生成礼物创意到比较价格，所有这些都通过工作流程进行协调。&lt;/p>
&lt;h2 id="2025-年科技节日日历">2025 年科技节日日历&lt;/h2>
&lt;p>&amp;lt;p对齐=“中心”&amp;gt;
&lt;img src="https://sessionize.com/image/49aa-1140o400o3-sdJUGhdR3FCmm1KuPRM3D3.png"/>&lt;/p>
&lt;/p>
&lt;p>该项目是我在 &lt;strong>2025 年节日科技日历&lt;/strong> 会议的一部分，这是一场在节日期间庆祝科技的精彩社区活动。您可以在 &lt;a href="https://sessionize.com/festive-tech-calendar-2025/">Sessionize&lt;/a> 上找到有关该活动的更多信息。&lt;/p>
&lt;h2 id="什么是微软的代理框架">什么是微软的代理框架？&lt;/h2>
&lt;p>Agent Framework 是 Microsoft 用于构建、编排和部署 AI 代理和多代理系统的解决方案。它为创建可以顺序、并发或通过切换模式工作的代理提供了灵活的基础。该框架支持 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>.NET 9&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>我们的圣诞礼物搜寻器将由三个专业代理组成：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>礼品创意代理&lt;/strong> - 根据收件人的个人资料生成创意礼品建议&lt;/li>
&lt;li>&lt;strong>价格比较代理&lt;/strong> - 查找不同商店的最优惠价格&lt;/li>
&lt;li>&lt;strong>Summary Agent&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>现在到了有趣的部分，将我们的代理连接到顺序工作流程中。 Agent 框架提供了 &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>如果我们想同时为多人寻找礼物怎么办？ Agent框架支持并发执行，非常适合这种场景：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-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-基础添加真实的-web-搜索">使用 Bing 基础添加真实的 Web 搜索&lt;/h2>
&lt;p>到目前为止，我们的代理根据人工智能模型的知识生成响应。但是，如果我们想在网络上搜索实际产品价格和库存情况该怎么办？这就是 &lt;strong>Grounding with 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 门户&lt;/a> 中创建 &lt;strong>Grounding with Bing Search&lt;/strong> 资源。确保将其创建在与您的 AI 项目相同的资源组中。&lt;/p>
&lt;h3 id="使用-bing-搜索设置基础">使用 Bing 搜索设置基础&lt;/h3>
&lt;p>Grounding with Bing Search 的优点在于它直接与 Azure AI Agent 集成。代理根据用户的查询决定何时使用搜索工具，搜索网络，并使用结果生成接地响应。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-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="创建具有真实搜索的价格比较代理">创建具有真实搜索的价格比较代理&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">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 搜索扎根的一个重要方面是回复中包含带有源网站链接的引用。以下是提取和显示它们的方法：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-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>Grounding with Bing Search&lt;/strong> 从实时网络中查找真实的产品列表、当前价格和可用交易。代理根据查询自动决定何时进行搜索，并返回带有正确引用的接地响应。&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>让它变得更加强大的是通过工具添加现实世界功能的能力。通过将 &lt;strong>Grounding 与 Microsoft Foundry 的 Bing Search&lt;/strong> 集成，我们的价格比较代理实际上可以在网络上搜索当前价格、交易和库存情况，将简单的 AI 聊天机器人转变为具有正确引用的真正有用的购物助手。&lt;/p>
&lt;p>该框架对顺序、并发和切换模式的支持意味着您可以设计符合您确切需求的代理系统。无论是寻找礼物、计划旅行还是任何其他多步骤任务，代理框架都提供了实现这些目标的构建块。这个假期，让人工智能代理来处理研究，而您则专注于包装礼物并享受与家人共度的时光！&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>构建人工智能驱动的 RSS 提要聚合器</title><link>https://emimontesdeoca.github.io/zh/posts/ai-agent-socials/</link><pubDate>Fri, 12 Dec 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/ai-agent-socials/</guid><description>使用 Semantic Kernel 和 Azure OpenAI 自动执行 RSS 源监控和社交媒体后期生成。</description><content:encoded>&lt;p>作为一名 Microsoft MVP 和技术爱好者，我经常发现自己淹没在 Microsoft 开发博客上发布的令人惊叹的内容的海洋中。从 .NET 公告到 Visual Studio 更新，从 Azure 创新到语义内核深入研究 - Microsoft 生态系统中总是会发生一些令人兴奋的新事物。&lt;/p>
&lt;p>问题？ &lt;strong>跟上这一切几乎是不可能的。&lt;/strong>&lt;/p>
&lt;p>我想掌握最新公告并与我的网络分享它们，但手动检查七个不同的 RSS 源、阅读文章、制作引人入胜的社交媒体帖子以及跟踪我已经分享的内容本身就成为了一项全职工作。每天早上，我都会打开多个浏览器选项卡，浏览数十篇文章，尝试记住我已经分享过哪些文章，然后花宝贵的时间撰写引起我注意的文章。&lt;/p>
&lt;p>所以我做了任何开发人员都会做的事情 – &lt;strong>我将其自动化。&lt;/strong>&lt;/p>
&lt;p>在本综合指南中，我将向您介绍如何构建一个基于 AI 的 RSS 提要聚合器，该聚合器可监视多个 Microsoft DevBlogs RSS 提要的新内容、使用 Azure OpenAI 和 Semantic Kernel 分析文章并生成引人入胜的帖子、为每篇分析的文章创建详细的 Markdown 文档、通过 Telegram 发送通知以便我可以查看和共享内容、跟踪所有内容以避免重复帖子，并通过 GitHub Actions 自动运行。&lt;/p>
&lt;p>让我们深入探讨该解决方案的各个方面。&lt;/p>
&lt;h2 id="这个项目背后的故事">这个项目背后的故事&lt;/h2>
&lt;h3 id="生活在信息超载中">生活在信息超载中&lt;/h3>
&lt;p>让我为您描绘一下我在构建这个工具之前的典型早晨。我会醒来，喝杯咖啡，打开笔记本电脑，查看微软开发者生态系统中的新内容。首先，我会导航到主要的 DevBlogs 站点，查看是否有任何重大公告。然后我会专门查看 .NET 博客，因为那是我的主要技术堆栈。之后，我会跳转到语义内核博客，因为人工智能变得越来越重要。 Visual Studio 博客排在第二位，因为 IDE 更新会显着影响我的日常工作流程。然后是有关 CI/CD 和 GitHub 相关新闻的 DevOps 博客，接着是有关云基础设施更新的 All Things Azure 博客，最后是有关数据库创新的 Azure SQL 博客。&lt;/p>
&lt;p>有七个不同的提要需要检查。这些博客每周都会发布多篇文章，有时在 .NET Conf 或 Build 等重大公告期间每天会发布多篇文章。这可能需要跟踪、阅读和分享数十篇文章。事情是这样的——作为一个重视与社区分享知识的人，我不想只阅读这些文章。我想在 LinkedIn 上与我的网络分享最有价值的内容，帮助其他开发人员也了解最新情况。但制作一篇好的 LinkedIn 帖子需要时间。你需要彻底阅读这篇文章，理解要点，思考为什么它对你的读者很重要，写一个引人入胜的钩子，并很好地格式化所有内容。将其乘以每周几篇文章，您就会看到工作时间。&lt;/p>
&lt;h3 id="我真正想要的是什么">我真正想要的是什么&lt;/h3>
&lt;p>在处理这个问题几个月后，我坐下来思考理想的解决方案是什么样的。首先，我不想再错过重要的公告。系统应在新文章发布后立即自动捕获它们。我还希望通过让人工智能帮助制作引人入胜的帖子来节省内容创建的时间——不是完全取代我的声音，而是给我一个可以定制的坚实的起点。&lt;/p>
&lt;p>一致性是另一个重要因素。我想定期分享内容，而不必记住每天手动执行。跟踪方面也至关重要 - 我需要一种方法来了解我已经分享的内容，以避免发布重复内容并惹恼我的关注者。最后，我想对我处理过的所有内容进行永久记录，保持井然有序，这样我就可以回顾并查看我涵盖了哪些主题。&lt;/p>
&lt;h3 id="解决方案已成型">解决方案已成型&lt;/h3>
&lt;p>我设想的解决方案将使用 GitHub Actions 按计划运行，完全免提。它会自动获取所有七个提要，而无需我打开单个浏览器选项卡。人工智能组件实际上会阅读并理解内容，然后以对我的观众有用的方式进行总结。我不必从头开始撰写帖子，而是创建可随时分享的社交媒体内容，我可以根据需要进行调整。所有内容都会发送到我的 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，因为它是轻量级的，并且通过 C# Dev Kit 扩展提供了出色的 C# 支持。但如果您更喜欢完整的 Visual Studio，那么它也可以完美工作。&lt;/p>
&lt;h3 id="您需要的服务和帐户除了本地工具之外您还需要具有一些服务的帐户最重要的是-azure-openai它为我们的-ai-分析提供支持这是一种即用即付的服务但该用例的成本很低我们所说的是每篇分析文章的成本如果没有-azure-帐户可以注册免费试用版其中包含一些入门积分">您需要的服务和帐户除了本地工具之外，您还需要具有一些服务的帐户。最重要的是 Azure OpenAI，它为我们的 AI 分析提供支持。这是一种即用即付的服务，但该用例的成本很低——我们所说的是每篇分析文章的成本。如果没有 Azure 帐户，可以注册免费试用版，其中包含一些入门积分。&lt;/h3>
&lt;p>对于通知，我们将使用 Telegram 机器人。 Telegram 的伟大之处在于他们的机器人 API 完全免费使用。您可以根据需要创建任意数量的机器人并发送无限的消息。我将在本指南的后面部分引导您完成设置过程。&lt;/p>
&lt;p>最后，您需要一个 GitHub 帐户来托管代码并运行 GitHub Actions。免费套餐对于这个项目来说已经足够了。 GitHub 在私有存储库上每月为您提供 2,000 分钟的 Actions 运行时间，在公共存储库上提供无限分钟的运行时间。&lt;/p>
&lt;h3 id="让这一切成为可能的图书馆">让这一切成为可能的图书馆&lt;/h3>
&lt;p>我们的项目依赖于三个主要的 NuGet 包，每个包都有特定的用途。&lt;/p>
&lt;p>第一个是 HtmlAgilityPack，它是 .NET 中 HTML 解析的黄金标准。当我们从博客中获取文章时，我们会获取页面的完整 HTML，包括导航菜单、页脚、广告以及各种我们不关心的元素。 HtmlAgilityPack 让我们能够解析该 HTML 并仅提取我们需要的文章内容。&lt;/p>
&lt;p>第二个包是Microsoft.SemanticKernel，它是微软用于将AI模型集成到应用程序中的SDK。将其视为 .NET 代码和 GPT-4 等大型语言模型之间的桥梁。它处理 API 调用、令牌管理和响应解析的所有复杂性，让您专注于希望 AI 实际执行的操作。&lt;/p>
&lt;p>第三个包是 System.ServiceModel.Syndicate，它提供了对解析 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 凭证。然后它会从所有七个 Microsoft DevBlogs 源中获取 RSS 源。在处理这些提要时，它会对文章进行重复数据删除，以处理同一篇文章出现在多个提要中的情况。它会根据我们的跟踪文件检查每篇文章，看看我们是否已经处理了它。对于新文章，它将交给人工智能分析器进行处理。ArticleAnalyzer 类是 AI 魔法发生的地方。该组件接收一篇文章并用它做一些事情。首先，它从文章的 URL 中获取完整的 HTML 内容。然后，它从该 HTML 中提取干净的文本，删除我们不需要的所有导航元素、脚本和样式。一旦获得干净的文本，它就会通过语义内核将其发送到 Azure OpenAI，并带有精心设计的提示。人工智能会分析文章并返回结构化响应，其中包括摘要、关键主题、相关性解释，以及最重要的即用型 LinkedIn 帖子。分析器解析此响应并返回包含所有这些信息的 ArticleAnalysis 对象。&lt;/p>
&lt;p>MarkdownGenerator 类采用该 ArticleAnalysis 对象并创建它的永久记录。它生成一个格式良好的 Markdown 文件，其中包括所有文章元数据、人工智能的分析和生成的帖子。这些文件存储在 generated-posts 目录中，为您提供已处理的所有内容的可搜索存档。&lt;/p>
&lt;p>最后，Telegram 集成将生成的帖子内容发送到您的手机。这是你作为人类可以审查人工智能的工作并决定是否分享它的时刻。机器人会向您发送一条包含帖子内容的消息，您可以将其直接复制到 LinkedIn 或先进行修改。&lt;/p>
&lt;h3 id="数据流">数据流&lt;/h3>
&lt;p>让我向您介绍在 .NET 博客上发布新文章时会发生什么情况。当 GitHub Actions 按计划触发我们的应用程序时（假设每六个小时），工作流程就开始了。应用程序唤醒并开始获取所有七个 RSS 源。每个提要都会返回一个 XML 文档，其中包含该博客的最新文章。&lt;/p>
&lt;p>当我们解析每个提要时，我们提取单个文章并将它们存储在一个列表中。但这里有一个棘手的部分 - 主要的开发博客提要通常包含也出现在各个类别提要中的文章。因此，有关“.NET 10”的文章可能会同时出现在主提要和特定于 .NET 的提要中。我们通过跟踪 HashSet 中的 URL 来处理这个问题，这会自动防止重复。&lt;/p>
&lt;p>一旦我们获得了重复数据删除的文章列表，我们就会将其过滤为最近的文章——通常是在最后一天左右发布的文章。我们不想处理在之前的运行中已经处理过的旧文章。然后我们根据跟踪文件检查每一篇最近的文章。如果我们已经处理并发布了一篇文章，我们会跳过它。&lt;/p>
&lt;p>对于每一篇新文章，我们都会启动人工智能分析流程。分析器获取完整文章 HTML，对其进行清理，然后根据我们的提示将其发送到 GPT-4。人工智能会阅读这篇文章并生成全面的分析以及 LinkedIn 帖子。我们将此分析保存到 Markdown 文件中以供记录。分析完成后，我们格式化消息并通过 Telegram 发送。该消息包括生成的帖子内容以及附加的 URL 和主题标签。在我的手机上，我会收到一条通知，查看该帖子，如果我喜欢它，只需轻按几下即可将其复制并在 LinkedIn 上分享。&lt;/p>
&lt;p>最后，我们更新跟踪文件以将本文标记为已处理，因此我们不会在以后的运行中再次处理它。如果创建或修改了任何文件，GitHub Actions 会将这些更改提交回存储库，使所有内容保持同步。&lt;/p>
&lt;h2 id="从头开始设置项目">从头开始设置项目&lt;/h2>
&lt;h3 id="创建解决方案结构">创建解决方案结构&lt;/h3>
&lt;p>让我们开始构建吧。打开终端并导航到要创建项目的位置。我喜欢将我的项目组织在“开发”文件夹中，但您可以将其放在对您有意义的任何位置。&lt;/p>
&lt;p>首先，我们将创建一个新的解决方案文件。在 .NET 中，解决方案是可以容纳多个项目的容器。尽管我们目前只有一个项目，但从解决方案开始可以更轻松地在以后需要时添加更多项目。运行命令 &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>创建项目后，我们需要添加我之前提到的三个 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，我们正在构建一个可执行文件（OutputType 是 Exe），目标是 .NET 9.0，并使用现代 C# 功能，例如隐式使用和可为 null 的引用类型。 ItemGroup 部分列出了我们的三个包依赖项及其确切版本。&lt;/p>
&lt;h2 id="深入研究-rss-源">深入研究 RSS 源&lt;/h2>
&lt;h3 id="rss-到底是什么">RSS 到底是什么？&lt;/h3>
&lt;p>在我们开始编写代码来获取提要之前，让我们确保我们了解我们正在使用的内容。 RSS 代表“真正简单的聚合”，它是一种用于分发内容更新的标准化 XML 格式。这个想法很简单：您不需要用户访问您的网站来查看是否有新内容，而是发布一个列出您最近内容的机器可读文件。然后，应用程序可以定期轮询该文件以发现新文章。&lt;/p>
&lt;p>RSS 自 20 世纪 90 年代末和 2000 年代初就已出现。您可能认为它是过时的技术，但实际上它仍然被广泛使用——尤其是博客、新闻网站和播客。 RSS 的美妙之处在于它的简单性。它只是具有已定义结构的 XML，任何应用程序都可以解析它。&lt;/p>
&lt;h3 id="开发博客提要的结构当您从-microsoft-devblogs-获取-rss-源时您会得到一个遵循特定结构的-xml-文档在顶层有一个-rss-元素其中包含单个频道元素该频道代表博客本身并包含博客标题url-和描述等元数据">开发博客提要的结构当您从 Microsoft DevBlogs 获取 RSS 源时，您会得到一个遵循特定结构的 XML 文档。在顶层，有一个 rss 元素，其中包含单个频道元素。该频道代表博客本身，并包含博客标题、URL 和描述等元数据。&lt;/h3>
&lt;p>在频道内，您会发现多个项目元素，每个元素代表一篇单独的博客文章。每一项都包含标题（文章的标题）、链接（可以在其中阅读全文的 URL）、pubDate（文章发布时间）、dc:creator 元素（作者姓名）、一个或多个类别元素（文章的标签）和说明（通常是文章的摘要或摘录）。&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.Synmination 包的伟大之处在于它为我们解析了所有这些。我们不必手动导航 XML 节点或担心不同的 RSS 版本。我们只需加载提要并返回强类型对象。&lt;/p>
&lt;h3 id="我们监控的七个提要">我们监控的七个提要&lt;/h3>
&lt;p>在我的实现中，我监视七个不同的 Microsoft DevBlogs 源。 devblogs.microsoft.com/feed 上的主要 DevBlogs feed 使我们能够全面了解 Microsoft 在其所有开发人员博客中发布的所有内容。 devblogs.microsoft.com/dotnet/feed 上的 .NET 特定源专门关注 .NET 版本、功能和最佳实践。 devblogs.microsoft.com/semantic-kernel/feed 上的语义内核源涵盖了人工智能编排和集成——随着人工智能成为现代发展的核心，它变得越来越重要。&lt;/p>
&lt;p>devblogs.microsoft.com/visualstudio/feed 上的 Visual Studio 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>让我来看看这段代码的作用。我们首先创建一个 HttpClient，它是 .NET 用于发出 HTTP 请求的内置类。我们设置 User-Agent 标头是因为某些服务器会阻止无法识别自身身份的请求。即使服务器不需要它，也最好设置它。然后，我们向 feed URL 发出 GET 请求并接收字符串形式的响应。该字符串包含 RSS 源的原始 XML。&lt;/p>
&lt;p>为了解析此 XML，我们创建一个 StringReader 来包装我们的响应字符串，然后配置一些 XmlReaderSettings。 DtdProcessing 设置很重要 - RSS 提要有时包含需要处理的 DTD（文档类型定义）声明。 MaxCharactersFromEntities 设置是一种安全措施，通过限制可以发生的实体扩展量来防止 XML 炸弹攻击。&lt;/p>
&lt;p>最后，我们使用这些设置创建一个 XmlReader，并使用 SyndicateFeed.Load 将 XML 解析为强类型 SyndicateFeed 对象。这使我们能够通过良好的 C# 属性而不是原始 XML 导航来访问提要的元数据及其所有项目。&lt;/p>
&lt;h3 id="通过错误处理获取多个提要">通过错误处理获取多个提要&lt;/h3>
&lt;p>在现实世界中，网络请求会失败。服务器宕机、连接超时、XML 格式可能错误。我们需要优雅地处理这些案件。以下是我们如何获取所有 feed，同时对故障具有弹性：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-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>我们在这里维护两个集合。 allArticles 列表将包含我们找到的所有文章，以及它们来自哪个提要。 sawUrls HashSet 跟踪我们已经看过的文章 URL，帮助我们避免重复。&lt;/p>
&lt;p>我们循环遍历每个 feed URL 并将 fetch 操作包装在 try-catch 块中。如果获取特定提要失败（可能是服务器暂时关闭），我们会记录一条警告并继续处理下一个提要。这样，一个提要出现问题就不会妨碍我们处理其他提要。&lt;/p>
&lt;p>对于每个成功获取的提要，我们迭代其项目。我们从项目的 Links 集合中提取文章 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 feed 中。&lt;/p>
&lt;p>如果我们天真地处理每个提要中的每一篇文章，我们最终会多次分析和发布同一篇文章。这会浪费对 Azure OpenAI 的 API 调用，用重复的通知向我们的 Telegram 发送垃圾邮件，如果我们发布重复的通知，还可能会惹恼我们的关注者。解决方案是基于 URL 的重复数据删除。每篇文章都有一个唯一的 URL，因此我们可以将其用作标识符。 HashSet 数据结构非常适合此目的，因为它提供 O(1) 查找时间并自动防止重复。当我们尝试添加集合中已有的 URL 时，Add 方法只会返回 false，让我们知道应该跳过该文章。&lt;/p>
&lt;h3 id="使用-markdown-持久化状态">使用 Markdown 持久化状态&lt;/h3>
&lt;p>重复数据删除可以处理单次运行中的重复项，但是跨运行时又如何呢？当我们的应用程序每六个小时运行一次时，我们需要记住我们已经处理过哪些文章，这样我们就不会再次处理它们。&lt;/p>
&lt;p>我选择将此状态存储在名为 posts-articles.md 的 markdown 文件中。为什么要降价？有几个原因。首先，它是人类可读的。我可以打开文件并立即查看我共享了哪些文章。其次，它是版本控制的。由于该文件位于我们的 Git 存储库中，因此我拥有文章处理时间的完整历史记录。第三，它作为文档。任何查看存储库的人都可以看到应用程序做了什么。&lt;/p>
&lt;p>该文件的格式很简单。它有一个标题、一个显示应用程序上次运行时间的时间戳，以及一个 Markdown 链接格式的文章列表：&lt;/p>
&lt;div class="highlight">&lt;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>对于文件中的每一行，我们使用正则表达式从 Markdown 链接格式中提取 URL。正则表达式 &lt;code>\(([^)]+)\)&lt;/code> 匹配括号内的任何内容，这是 Markdown 链接存储其 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>此函数创建一个 Markdown 格式的条目，其中文章标题作为链接，后跟时间戳，显示我们发布该文章的时间以及最初发布的时间。如果该文件尚不存在，我们首先使用标头创建它。&lt;/p>
&lt;h2 id="ai-分析引擎">AI 分析引擎&lt;/h2>
&lt;h3 id="理解语义内核现在我们进入应用程序中最令人兴奋的部分人工智能分析-semantic-kernel-是-microsoft-的开源-sdk用于将大型语言模型集成到应用程序中它不仅仅是-api-调用的包装器它提供了一个框架用于构建具有插件规划器和内存等功能的复杂人工智能应用程序">理解语义内核现在我们进入应用程序中最令人兴奋的部分——人工智能分析。 Semantic Kernel 是 Microsoft 的开源 SDK，用于将大型语言模型集成到应用程序中。它不仅仅是 API 调用的包装器。它提供了一个框架，用于构建具有插件、规划器和内存等功能的复杂人工智能应用程序。&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>）。这些是从环境变量传入的，确保我们的凭据安全。&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"> 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>让我在这里解释一下设计决策。我们首先给人工智能一个明确的角色：“你是一名专业的技术内容分析师和 LinkedIn 内容创建者。”这使模型能够以适当的风格和声音做出反应。&lt;/p>
&lt;p>我们提供 AI 所需的所有上下文：文章标题、作者、URL、RSS 提要中的标签以及完整的文章内容。我们提供的背景越多，分析就会越好。&lt;/p>
&lt;p>然后我们准确指定我们想要返回的内容。我要求提供四件事：摘要、关键主题、相关性解释和 LinkedIn 帖子。具体来说，对于 LinkedIn 帖子，我给出了关于什么是好帖子的详细说明 - 它应该有一个钩子，突出价值，包括号召性用语，适当使用表情符号，并保持专业的语气。&lt;/p>
&lt;p>负面指示同样重要。我明确告诉 AI 不要在帖子中包含主题标签或 URL。为什么？因为我是单独添加这些的，如果人工智能包含它们，我就会有重复的。这种明确的指示可以防止常见错误。&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:#a5d6ff">8000&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> ChatHistory &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> 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>我们还存储原始的、未解析的响应。这对于调试很有用——如果解析出现问题，我们可以查看人工智能实际返回的内容。&lt;/p>
&lt;h2 id="从-html-中提取内容">从 HTML 中提取内容&lt;/h2>
&lt;h3 id="为什么我们需要清理-html">为什么我们需要清理 HTML&lt;/h3>
&lt;p>当我们从博客中获取文章时，我们会获得该页面的完整 HTML。这不仅仅包括文章内容，还有导航菜单、页眉、页脚、侧边栏、相关文章小部件、评论部分、用于分析和跟踪的脚本、样式表以及各种其他元素。&lt;/p>
&lt;p>如果我们将所有这些发送给人工智能，就会发生一些不好的事情。人工智能将不得不处理大量不相关的文本，浪费标记并可能使分析变得混乱。导航和页脚文本可能会包含在摘要中。脚本和 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 中，该文档将其解析为树结构。然后我们使用 XPath 选择所有要删除的节点。 XPath 表达式 &lt;code>//script|//style|//nav|//footer|//header&lt;/code> 选择所有脚本元素（我们不需要的 JavaScript 代码）、样式元素（我们不需要的 CSS）、导航元素（导航菜单）、页脚元素和页眉元素。&lt;/p>
&lt;p>删除这些节点后，我们得到了 InnerText 属性，该属性在剥离 HTML 标签的同时提取所有文本内容。这为我们提供了文章的纯文本。最后，我们清理空白。 HTML 通常有大量额外的空白用于格式化目的——多个空格、制表符、换行符。我们使用正则表达式将任何空白字符序列替换为单个空格，然后修剪结果。&lt;/p>
&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">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="为什么生成-markdown-文件">为什么生成 Markdown 文件&lt;/h3>
&lt;p>每次我们分析一篇文章时，我们都会生成一个详细的 Markdown 文件来记录该分析。这有几个目的。&lt;/p>
&lt;p>首先，它创建一个可搜索的档案。随着时间的推移，您将建立一个分析文章的集合。您可以搜索这些文件以查找特定主题的过去内容。&lt;/p>
&lt;p>其次，它提供透明度。您可以准确地看到人工智能为每篇文章生成的内容，包括完整的分析和 LinkedIn 帖子。&lt;/p>
&lt;p>第三，它对于调试很有用。如果帖子出现问题，您可以查看 markdown 文件以了解发生了什么。&lt;/p>
&lt;h3 id="markdown-生成器类">Markdown 生成器类&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(analysis.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 对象并生成格式良好的 Markdown 文档。&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="设置-telegram-通知">设置 Telegram 通知&lt;/h2>
&lt;h3 id="为什么我选择-telegram">为什么我选择 Telegram&lt;/h3>
&lt;p>对于通知组件，我考虑了几种选择——电子邮件、短信、Slack、Discord 和 Telegram。我最终选择 Telegram 有几个原因。&lt;/p>
&lt;p>该 API 完全免费，对于合理使用没有速率限制。许多通知服务对您可以免费发送的消息数量有限制，但 Telegram 并不限制向个人用户发送机器人消息。&lt;/p>
&lt;p>机器人 API 非常简单。它只是带有 JSON 负载的 HTTP 请求。没有复杂的身份验证流程，基本功能不需要 Webhook。Telegram 可以在任何地方使用——在我的手机上、在我的桌面上、在我的网络浏览器上。无论我身在何处，我都可以收到通知并立即回复。&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，以便机器人知道向何处发送消息，请通过搜索新机器人并按“开始”来开始与新机器人的对话。然后在浏览器中或使用curl访问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。我们向 sendMessage 端点发出一个 POST 请求，其中包含一个 JSON 正文，其中包含聊天 ID（发送位置）、消息文本（发送内容）以及可选的解析模式（用于格式化）。&lt;/p>
&lt;p>将 parse_mode 设置为“HTML”可以让我们在消息中使用基本的 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>。这可以使通知更具可读性，尽管对于我们当前的用例，我们发送纯文本。&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>优雅降级的概念很重要。我们不希望应用程序仅仅因为未配置一项可选功能而崩溃：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-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>如果启用了人工智能，我们将创建分析器和降价生成器。如果没有，我们将它们留空并在处理过程中跳过与人工智能相关的步骤。即使没有人工智能增强，该应用程序仍然通过获取源和发送基本通知来提供价值。&lt;/p>
&lt;h2 id="使用-github-actions-实现自动化">使用 GitHub Actions 实现自动化&lt;/h2>
&lt;h3 id="为什么选择-github-actions">为什么选择 GitHub Actions&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 分钟”。因此，工作流在午夜、上午 6 点、中午和下午 6 点（UTC）运行。 workflow_dispatch 触发器允许从 GitHub UI 手动运行，这对于测试很有用。&lt;/p>
&lt;p>该作业在 ubuntu-latest（Linux 虚拟机）上运行。我们检查我们的存储库、设置 .NET 9、恢复 NuGet 包并构建项目。&lt;/p>
&lt;p>运行应用程序步骤是奇迹发生的地方。我们使用 ${{ Secrets.SECRET_NAME }} 语法将机密作为环境变量传递。这些秘密安全地存储在 GitHub 中，并且永远不会在日志中公开。&lt;/p>
&lt;p>最后，我们将所有更改提交回存储库。我们使用机器人身份配置 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>回顾我们所涵盖的所有内容，我们构建了一个全面的、由人工智能驱动的 RSS 提要聚合器，可以自动执行过去繁琐的手动流程。该应用程序自动监视七个 Microsoft DevBlogs 源，在每一篇新文章发布后立即捕获它。它处理重复数据删除的复杂性，识别同一篇文章何时出现在多个提要中。由 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 解决了从网页中提取干净文本的混乱问题。 System.ServiceModel.Syndicate 使 RSS 解析变得轻而易举。 Telegram Bot API 为我们提供了免费、可靠的通知。 GitHub Actions 将这一切与自动化、预定的执行结合在一起。&lt;/p>
&lt;h3 id="考虑成本">考虑成本&lt;/h3>
&lt;p>您可能会问的一个问题是：运行该程序需要多少钱？答案是：一点也不多。&lt;/p>
&lt;p>Telegram 完全免费 - 通过机器人发送消息无需付费。&lt;/p>
&lt;p>GitHub Actions 对公共存储库免费。对于私有存储库，您每月可以使用 2,000 分钟的免费套餐，这对于我们的用例来说绰绰有余。&lt;/p>
&lt;p>Azure OpenAI 是唯一付费组件，而且成本极低。使用 GPT-4o 分析一篇典型的博客文章的成本约为 1 到 3 美分。即使您每月处理数十篇文章，您所看到的人工智能成本也不足一美元。&lt;/p>
&lt;h3 id="接下来你可以去哪里">接下来你可以去哪里&lt;/h3>
&lt;p>虽然这个解决方案非常适合我的需求，但您可以通过多种方式扩展它。您可以添加对多个社交平台的支持 - 除了 LinkedIn 之外，还可以发布到 Twitter/X、Mastodon 或 Bluesky。您可以实施情绪分析来跟踪一段时间内文章的基调并发现趋势。您可以为不同的提要允许不同的提示模板，为不同的主题生成不同风格的帖子。您可以构建一个 Web 仪表板来查看和管理帖子，而不是使用 Telegram。您可以跟踪已发布内容的参与度指标，以了解哪些主题最能引起受众的共鸣。&lt;/p>
&lt;h3 id="最后的想法我最喜欢这个项目的是它体现了我坚信的哲学自动化应该处理繁琐的部分同时将创造性和决策部分留给人类该系统完成了所有繁琐的工作获取解析分析生成但我仍然在分享之前检查所有内容人工智能生成的帖子是我可以定制和个性化的起点">最后的想法我最喜欢这个项目的是，它体现了我坚信的哲学：自动化应该处理繁琐的部分，同时将创造性和决策部分留给人类。该系统完成了所有繁琐的工作——获取、解析、分析、生成——但我仍然在分享之前检查所有内容。人工智能生成的帖子是我可以定制和个性化的起点。&lt;/h3>
&lt;p>通过结合 .NET、Semantic Kernel 和 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>使用微软代理框架构建多代理人工智能系统</title><link>https://emimontesdeoca.github.io/zh/posts/microsoft-agent-framework-multi-agent/</link><pubDate>Mon, 01 Dec 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/microsoft-agent-framework-multi-agent/</guid><description>使用 Microsoft 的 .NET 代理框架构建、编排和部署多代理 AI 系统的实用指南。</description><content:encoded>&lt;h2 id="简介">简介&lt;/h2>
&lt;p>我们已经进入了多智能体人工智能系统的时代。该行业不再是单一的单一人工智能处理所有事情，而是转向协作解决复杂问题的专业代理——就像组织良好的专家团队一样。一名代理人进行研究，另一名代理人进行分析，第三名代理人进行写作，一名协调员让每个人都步入正轨。&lt;/p>
&lt;p>如果您使用过大型语言模型，您可能已经达到了单个提示功能的上限。上下文窗口被填满，指令变得混乱，质量下降。多代理架构通过将复杂的任务分解为集中的职责来解决这个问题，其中每个代理都是一件事的专家。&lt;/p>
&lt;p>Microsoft 的代理框架是更广泛的语义内核生态系统的一部分，它为 .NET 开发人员提供了用于构建此类系统的一流工具包。在这篇文章中，我们将从零开始构建一个完全工作的多代理管道，涵盖核心概念、编排模式和入门所需的实用代码。&lt;/p>
&lt;h2 id="什么是微软的代理框架">什么是微软的代理框架？&lt;/h2>
&lt;p>Agent Framework 是 Microsoft 在 .NET 中构建、编排和部署 AI 代理和多代理系统的解决方案。它与语义内核并存，并与之深度集成，语义内核自 2023 年以来一直是微软用于 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="聊天完成代理最直接的代理类型它包装聊天完成端点azure-openaiopenai-等并维护对话它在调用之间是无状态的您提供历史记录它就会做出响应这使得它变得轻量且易于推理">聊天完成代理最直接的代理类型。它包装聊天完成端点（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，该 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>该框架支持四种主要的编排模式，每种模式适合不同的问题。&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>多个代理同时处理同一输入。您将工作展开，然后汇总结果。非常适合获得不同的观点——比如让三个审阅者查看同一个拉取请求。&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>我们将创建三个代理：作家、审阅者和编辑。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-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> 需要两个关键配置：&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框架支持三种主要的扩展机制。&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>让我们将所有内容与实际场景结合起来。想象一下，您正在构建一个内部工具，可以自动为文档团队进行内容审查。该管道有四个阶段：&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="可观察性代理框架与-opentelemetry-集成这意味着您可以跟踪每个代理交互工具调用和令牌使用情况这对于调试多代理工作流程至关重要因为在这种情况下哪个代理导致问题并不总是很明显">可观察性代理框架与 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>多智能体人工智能系统代表了我们构建智能应用程序方式的根本转变。我们可以将问题分解为专门的角色并让代理进行协作，而不是纠结于单一提示来处理所有事情。&lt;/p>
&lt;p>Microsoft 的代理框架使这对于.NET 开发人员来说非常实用。抽象是干净的——代理、群聊、选择和终止策略——而且它们自然组成。结合语义内核的插件生态系统和 Azure 的模型托管，您将拥有用于构建生产级多代理系统的完整堆栈。&lt;/p>
&lt;p>该框架仍在不断发展（许多包都处于预览状态），但核心模式很扎实，方向也很明确。如果您正在 .NET 中构建人工智能驱动的应用程序，那么现在是开始尝试多代理架构的时候了。&lt;/p>
&lt;p>从小事做起——构建两个代理来协作完成一个简单的任务。一旦您看到单代理方法的质量改进，您就会希望将所有内容分解为代理团队。&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/zh/posts/blazor-markup-string-raw-html/</link><pubDate>Sat, 22 Nov 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/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;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>，而不是 &lt;strong>Hello&lt;/strong> &lt;em>world&lt;/em>。 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;h1 id="何时使用它">何时使用它&lt;/h1>
&lt;p>我使用 &lt;code>MarkupString&lt;/code> 来：&lt;/p>
&lt;ul>
&lt;li>CMS 内容或 markdown 已转换为 HTML 服务器端&lt;/li>
&lt;li>富文本编辑器输出&lt;/li>
&lt;li>电子邮件模板预览&lt;/li>
&lt;li>来自可信来源的任何预构建 HTML&lt;/li>
&lt;/ul>
&lt;p>对于来自用户输入的任何内容，始终首先进行清理。安全总比后悔好。&lt;/p>
&lt;p>希望你喜欢这篇文章！请随时通过任何社交媒体与我联系：&lt;strong>@emimontesdeoca&lt;/strong>。&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">标记字符串结构&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/zh/posts/whats-new-ef-core-9/</link><pubDate>Tue, 18 Nov 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/whats-new-ef-core-9/</guid><description>全面了解 Entity Framework Core 9 中最有影响力的功能 — 从 LINQ 改进和批量操作到 JSON 列和 AOT 编译支持。</description><content:encoded>&lt;p>Entity Framework Core 9 于 2024 年 11 月与 .NET 9 一起发布，在花了大量时间在多个项目中使用它之后，我可以说它是一段时间内最有意义的版本之一。不是因为它重新发明了轮子，而是因为它完善了 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）是标准期限支持 (STS)，提供 18 个月的支持，而偶数版本（如 .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 直接在 SQL 中处理更广泛的 &lt;code>GroupBy&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> 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 改进之一是能够将原始值集合直接传递到查询中：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-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 版本中最令人兴奋的功能之一，而 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 中的摩擦点之一是更新 JSON 列内的单个属性会导致整个 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>在受支持的提供程序上，这可以生成更有针对性的 JSON 修改，而不是重写整个 blob。&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>[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> 的两个 &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 为其带来了一流的支持。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-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>&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>更多 LINQ 操作现在转换为 Cosmos DB 的 SQL 方言，包括对 &lt;code>Contains&lt;/code>、&lt;code>Any&lt;/code>、嵌套数组操作和数学函数的更好支持。以前退回到客户端评估的查询现在由服务器端处理。&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>这会生成一个可以在 CI/CD 管道中运行的二进制文件，而无需在部署目标上安装 .NET SDK。&lt;/p>
&lt;h2 id="性能基准以下是我自己测试的一些粗略基准这些来自一个包含大约-200-个实体的项目针对-sql-server-2022-运行并使用-benchmarkdotnet-进行测量您的数字会有所不同但相对改进应该相似">性能基准以下是我自己测试的一些粗略基准。这些来自一个包含大约 200 个实体的项目，针对 SQL Server 2022 运行，并使用 BenchmarkDotNet 进行测量。您的数字会有所不同，但相对改进应该相似。&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>场景&lt;/th>
&lt;th>EF Core 8&lt;/th>
&lt;th>EF Core 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>批量更新（10k 行）&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>编译模型的改进是最显着的，但全面的稳定改进加起来 - 特别是在每秒执行数千个查询的高吞吐量场景中。&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>**3.如果您正在使用编译模型，请重新生成它们。格式已更改，因此旧的编译模型将无法与 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>如果我必须选择对我的项目影响最大的三个功能：&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 5 以来，EF Core 团队一直走在坚实的轨道上，版本 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/zh/posts/getting-started-semantic-kernel/</link><pubDate>Sun, 05 Oct 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/getting-started-semantic-kernel/</guid><description>了解如何使用 Microsoft 的语义内核在 C# 中构建人工智能驱动的应用程序 - 从插件和规划器到内存和函数调用。</description><content:encoded>&lt;p>如果您一直在构建 .NET 应用程序并关注 AI 领域的发展，您可能想知道：&lt;em>将大型语言模型集成到我的 C# 项目中而不会将我的代码库变成意大利面条的最佳方法是什么？&lt;/em> 这正是 Microsoft 的语义内核解决的问题，在去年用它构建生产应用程序之后，我可以告诉您它已成为我的开发人员工具包中最重要的工具之一。&lt;/p>
&lt;p>在这篇文章中，我将引导您完成开始使用语义内核所需的一切 - 从理解核心概念到构建现实世界的人工智能助手。无论您是刚刚涉足 AI 开发，还是正在寻找一种结构化方法来编排现有 .NET 应用程序中的 LLM 调用，本指南都能满足您的需求。&lt;/p>
&lt;h2 id="什么是语义内核">什么是语义内核？&lt;/h2>
&lt;p>Semantic Kernel (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;/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;/ul>
&lt;p>&amp;hellip;你会发现自己在重新发明轮子。 Semantic Kernel 为您提供开箱即用的所有功能，具有一流的 .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> 是语义内核中的中心对象。它是协调器——将你的人工智能服务、插件和配置联系在一起的东西。您创建一个，在其上注册您的服务和插件，然后使用它来执行提示或调用函数。如果您熟悉 ASP.NET Core 中的依赖注入，那么内核会感觉非常熟悉——它本质上是一个具有 AI 超能力的服务容器。&lt;/p>
&lt;h3 id="插件和功能">插件和功能&lt;/h3>
&lt;p>&lt;strong>插件&lt;/strong>是内核可以调用的相关&lt;strong>函数&lt;/strong>的集合。函数有两种类型：&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> 和一个以友好方式汇总天气数据的提示函数。### 人工智能连接器&lt;/p>
&lt;p>连接器是语义内核与人工智能服务对话的方式。最常见的是：&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>如果您直接使用 OpenAI 而不是 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-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>让我们从最简单的示例开始 - 创建一个内核，将其连接到人工智能服务，并向其提出问题。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-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>提示模板允许您使用 Handlebars 样式语法通过变量参数化提示：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-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>插件是 Semantic Kernel 弥合 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> 属性。这些很重要——人工智能会读取描述来了解何时以及如何调用您的函数。良好的描述可以区分有效使用你的工具的人工智能和混乱的人工智能。&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>内核自动执行那些函数
4.结果回传给AI&lt;/li>
&lt;li>AI 使用函数结果组成自然语言响应这个循环可以在一次调用中发生多次——人工智能可能会按顺序调用多个函数来收集它需要的所有信息。您还可以使用 &lt;code>FunctionChoiceBehavior.Required()&lt;/code> 强制 AI 调用至少一个函数，或提供允许使用的特定函数列表。&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>人工智能应用程序中最强大的模式之一是&lt;strong>检索增强生成（RAG）&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">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 管道公开为内核函数，人工智能可以自动决定何时需要搜索您的知识库 - 保持编排干净并让模型做它最擅长的事情。&lt;/p>
&lt;h2 id="策划者和代理人">策划者和代理人&lt;/h2>
&lt;p>随着您的人工智能应用程序变得越来越复杂，您将需要人工智能&lt;strong>规划和执行多步骤任务&lt;/strong>。这就是语义内核代理框架的用武之地。&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>让我们用一个实际的例子将所有内容联系在一起——一个人工智能助手，可以通过阅读文件和回答问题来帮助开发人员理解代码库。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-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>生成 Markdown 格式的文档&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>监控token使用情况。&lt;/strong> 每一个提示、每一个功能描述、每一条聊天记录都会消耗token。使用过滤器来记录和跟踪使用情况：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-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>**保持函数描述准确。**模糊的描述会导致AI错误地调用函数。通过询问以下问题来测试您的描述：“如果我只阅读描述，我能准确地知道何时以及如何使用此功能吗？”&lt;/p>
&lt;h2 id="结论">结论&lt;/h2>
&lt;p>语义内核是从根本上改变您构建应用程序的想法的库之一。它不仅仅是一个 API 包装器，它还是一个编排框架，可让您以可维护、可测试和生产就绪的方式将 AI 功能与传统代码组合在一起。&lt;/p>
&lt;p>我最喜欢它的是它尊重 .NET 生态系统。它使用您已经知道的模式——依赖注入、属性、异步/等待、接口——并将它们扩展到人工智能世界。您不必学习全新的范式；您只需将人工智能添加为工具包中的另一项功能即可。&lt;/p>
&lt;p>如果您正在构建 .NET 应用程序并且尚未探索语义内核，那么现在正是时候。 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/zh/posts/blazor-inherit-components/</link><pubDate>Thu, 04 Sep 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/blazor-inherit-components/</guid><description>使用 ComponentBase 和共享基类通过继承来扩展和重用 Blazor 组件。</description><content:encoded>&lt;p>我正在构建一个具有一堆表单页面的项目，每个表单页面都有相同的加载状态逻辑、相同的错误处理和相同的 toast 通知。复制粘贴所有这些感觉都是错误的，因此我研究了 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> 指令是关键。它告诉 Blazor 使用我们的基类而不是默认的 &lt;code>ComponentBase&lt;/code>。现在，我们在继承它的每个页面中免费获得 &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 模式。但不要过度使用深层继承层次结构 - 如果您发现自己的深度超过两层，那么组合可能会更好（就像上一篇文章中的 &lt;code>LoadingComponent&lt;/code> 方法）。&lt;/p>
&lt;p>我通常将其保留为每个“类型”页面的一个基类：&lt;code>PageBase&lt;/code> 用于常规页面，&lt;code>FormPageBase&lt;/code> 用于表单，仅此而已。&lt;/p>
&lt;p>希望你喜欢这篇文章！请随时通过任何社交媒体与我联系：&lt;strong>@emimontesdeoca&lt;/strong>。&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/zh/posts/dotnet-aspire-cloud-native/</link><pubDate>Wed, 20 Aug 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/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 的核心为您提供四件事：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>AppHost&lt;/strong> — 定义整个分布式应用程序拓扑的项目。存在哪些服务、它们依赖什么以及它们如何连接。&lt;/li>
&lt;li>&lt;strong>服务默认值&lt;/strong> — 一个共享项目，可为您的所有服务一次性配置健康检查、弹性策略和 OpenTelemetry 等横切关注点。&lt;/li>
&lt;li>&lt;strong>组件&lt;/strong> — NuGet 包，提供与 Redis、PostgreSQL、RabbitMQ、Azure 存储等支持服务的标准化集成。&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>这会生成一个包含四个项目的解决方案：&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>第一次经历让我着迷。不到一分钟的时间，您就拥有了一个完全精心编排的分布式应用程序，并且具有可观察性。&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> &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> 中，一行就完成了这一切：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-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>单个 &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>&lt;strong>针对所有传出 HTTP 调用的弹性策略&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="redis">Redis&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>这会将您的 &lt;code>DbContext&lt;/code> 注册为与 AppHost 中定义的 &lt;code>catalogdb&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>&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>&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> 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>&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 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>&lt;/span>&lt;span style="display:flex;">&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> ID &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> 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>最直接的部署路径是 Azure 容器应用程序 (ACA)，它具有一流的 Aspire 支持。您可以直接使用 Azure 开发人员 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="库伯内特斯">库伯内特斯&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 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 和容器编排并不是什么新鲜事。但因为它使它们成为“默认”。它需要您通常必须做出的数百个小决定，使用合理的默认值来实现它们，并允许您在需要时覆盖它们。在我看来，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/zh/posts/blazor-authentication-authorization/</link><pubDate>Sat, 12 Jul 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/blazor-authentication-authorization/</guid><description>在 Blazor 应用程序中实现身份验证和授权的实用指南 - 从 ASP.NET Identity 到 OAuth、基于角色的访问和保护组件。</description><content:encoded>&lt;p>如果您使用过 ASP.NET MVC 或 Razor Pages，您可能对身份验证的工作原理有一个心理模型：中间件拦截请求，检查 cookie 或令牌，填充 &lt;code>HttpContext.User&lt;/code>，然后您就可以开始比赛了。 Blazor 以微妙但重要的方式改变了这种心理模型 - 如果您不尽早理解这些差异，您最终会调试感觉不可能的身份验证问题。&lt;/p>
&lt;p>在这篇文章中，我想介绍 Blazor 中身份验证和授权的实际工作原理，涵盖服务器和 WebAssembly 托管模型。我们将从基础知识一直讲到自定义提供程序、外部 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 使用的相同身份模型。组件不直接与 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> 在这里承担双重职责：它在呈现匹配的页面组件之前检查用户是否经过身份验证和授权，并在未经过身份验证和授权时提供后备 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-身份集成">ASP.NET 身份集成&lt;/h2>
&lt;p>对于大多数项目，您不需要从头开始构建身份验证。 ASP.NET Identity 为您提供开箱即用的用户管理、密码哈希、双因素身份验证和帐户确认。使用 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>，您需要运行迁移来创建身份表：&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-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>，因此您可以直接在标记中检查声明、角色和用户身份。&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>要记住一件事：&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，您需要 &lt;code>AspNet.Security.OAuth.GitHub&lt;/code> NuGet 包，因为它不包含在默认 ASP.NET 库中。&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 攻击。对于安全性更高的应用程序，请考虑仅将令牌保留在内存中并使用刷新令牌，或者采用后端换前端 (BFF) 模式，其中服务器管理令牌而客户端使用仅 HTTP 的 cookie。&lt;/p>
&lt;h2 id="blazor-服务器与-webassembly安全注意事项">Blazor 服务器与 WebAssembly：安全注意事项&lt;/h2>
&lt;p>托管模型从根本上改变您的安全状况。&lt;/p>
&lt;p>&lt;strong>Blazor 服务器&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>身份验证仅在客户端上强制执行以实现用户体验；真正的执行必须发生在 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-服务器组件中使用-httpcontexthttpcontext-在初始-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>AuthorizeRouteView&lt;/code> 而不是普通的 &lt;code>RouteView&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 Identity 和内置模板开始。它们处理用户管理、密码散列和令牌生成的繁重工作。随着您的需求的增长，分层策略和基于声明的授权。在用户需要时添加外部 OAuth 提供商。&lt;/p>
&lt;p>托管模型很重要：Blazor Server 为您提供更传统的安全态势，其中代码保留在服务器上，而 WebAssembly 则推动您采用 API 优先的思维，其中客户端在设计上不受信任。两者本质上都不是更安全——它们只是有不同的威胁模型。&lt;/p>
&lt;p>无论您选择哪种方法，请记住黄金法则：**用户界面中的授权是为了用户体验，服务器上的授权是为了安全。**始终强制执行两者。&lt;/p></content:encoded><category>Blazor</category><category>.NET</category><category>Security</category><category>Web Development</category></item><item><title>Blazor 中的独立 JavaScript 与并置的 JS 文件</title><link>https://emimontesdeoca.github.io/zh/posts/blazor-isolated-js/</link><pubDate>Wed, 18 Jun 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/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>我们在 &lt;code>OnAfterRenderAsync&lt;/code> 中加载模块，因为 JS Interop 在服务器端预渲染期间不可用&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>之前，我习惯将所有内容转储到一个 &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>.razor.js&lt;/code> 结尾，而不仅仅是 &lt;code>.js&lt;/code>。&lt;/p>
&lt;p>希望你喜欢这篇文章！请随时通过任何社交媒体与我联系：&lt;strong>@emimontesdeoca&lt;/strong>。&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">ASP.NET Core Blazor JavaScript 与并置 JS&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/zh/posts/blazor-interactivity-dotnet-9-10/</link><pubDate>Sun, 15 Jun 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/blazor-interactivity-dotnet-9-10/</guid><description>深入探讨 Blazor 的渲染模式、流式 SSR、增强型导航以及 .NET 9 和 .NET 10 中的新交互功能。</description><content:encoded>&lt;p>如果您在过去几年中一直使用 Blazor 构建 Web 应用程序，您就会知道该框架已经取得了“长”的进步。最初是在 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 的核心是渲染模式的概念。您应该了解四种模式：&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时您可以选择交互服务器模式这会在浏览器和服务器之间建立-signalr-连接并通过该连接进行-ui-更新">2. 交互服务器当您需要实时交互（处理按钮点击、处理用户输入、动态更新 UI）时，您可以选择交互服务器模式。这会在浏览器和服务器之间建立 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>如果您希望在不维护服务器连接的情况下进行交互，则交互式 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>这是务实的中间立场，也是我最喜欢的功能之一。 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 是听起来很简单但在感知性能方面产生巨大差异的功能之一。其想法是：服务器不会在发送任何 HTML 之前等待所有数据加载，而是立即发送页面 shell，然后在数据可用时&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>用户“立即”看到带有加载指示器的页面，然后内容就会填充。不需要 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 中微妙但强大的改进之一是增强的导航。默认情况下，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 中。
4.更新浏览器的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>新模型最强大的方面之一是您可以在单个应用程序中混合渲染模式。您的产品列表页面可以是静态 SSR，您的购物车可以是交互式服务器，您的产品配置器可以是交互式 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>&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>&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:#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:#f85149">，则子级不能是&lt;/span>&lt;span style="color:#6e7681"> &lt;/span>Interactive&lt;span style="color:#6e7681"> &lt;/span>WebAssembly&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:#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>&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:#f85149">您将需要一个&lt;/span>&lt;span style="color:#6e7681"> &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:#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:#f85149">，则需要显式处理状态持久性&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:#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:#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>&lt;span style="color:#ff7b72;font-weight:bold">-&lt;/span>&lt;span style="color:#6e7681"> &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="blazor-脚本作为静态-web-资产">Blazor 脚本作为静态 Web 资产&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>当您确实需要交互性时，交互式自动通常是最佳默认选择。它为您提供快速的初始加载（服务器渲染）和最终的客户端执行（WebAssembly）。用户可以两全其美，而您只需编写一次代码。&lt;/p>
&lt;h3 id="为特定情况预留交互服务器">为特定情况预留交互服务器&lt;/h3>
&lt;p>在以下情况下使用交互式服务器：&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/zh/posts/minimal-apis-dotnet/</link><pubDate>Thu, 10 Apr 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/minimal-apis-dotnet/</guid><description>使用 .NET Minimal API 构建干净、快速的 HTTP API 的综合指南 - 从路由处理程序和参数绑定到过滤器和 OpenAPI 集成。</description><content:encoded>&lt;p>如果您已经使用 ASP.NET Core 构建 API 一段时间，您可能非常熟悉基于控制器的方法：创建一个控制器类，用属性装饰它，通过构造函数注入您的服务，并连接您的路由。它有效，而且效果很好——但有时感觉就像你在写很多仪式，相当于“接受这个请求，做一些事情，返回一个响应”。&lt;/p>
&lt;p>这正是 Minimal API 想要解决的问题。 .NET 6 中引入的最小 API 允许您使用很少的样板文件定义 HTTP 端点。没有控制器，没有属性，没有启动类杂耍——只是以干净、函数式的方式直接路由到处理程序映射。&lt;/p>
&lt;p>在这篇文章中，我想引导您了解有关最小 API 的所有信息：从第一个端点一直到组织大型应用程序、处理身份验证、验证、OpenAPI 文档和性能注意事项。让我们开始吧。&lt;/p>
&lt;h2 id="为什么要使用最少的-api">为什么要使用最少的 API？&lt;/h2>
&lt;p>在我们开始之前，让我们先谈谈&lt;em>为什么&lt;/em>您会选择最小 API 而不是传统的基于控制器的方法。&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>好消息是，这不是一个非此即彼的决定。您可以在同一项目中混合使用控制器和 Minimal 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="lambda-表达式">Lambda 表达式&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>&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 的原因之一是参数绑定的直观性。该框架根据上下文和类型确定从哪里提取值。&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>可空类型成为可选参数。默认值完全按照您的预期工作。&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 无法提供开箱即用的功能，其中一个领域是模型验证。没有自动 &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 的最佳功能之一。将它们视为中间件，但范围仅限于特定端点而不是整个管道。它们是在 .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>保护最小 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 感觉非常优雅的小事情之一。&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 自动发现并注册所有模块。它是原始最小 API 方法和完整控制器之间的一个很好的中间立场。&lt;/p>
&lt;h2 id="typedresults-和响应类型">TypedResults 和响应类型&lt;/h2>
&lt;p>从 .NET 7 开始，您可以使用 &lt;code>TypedResults&lt;/code> 而不是 &lt;code>Results&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 的卖点之一是性能，值得理解“为什么”它们更快。&lt;/p>
&lt;p>**减少启动开销。**控制器严重依赖反射来发现端点、绑定模型和应用过滤器。最小 API 使用源生成器（从 .NET 7 开始）在编译时生成绑定代码。这意味着启动时的工作量更少，每个请求的内存分配也更少。&lt;/p>
&lt;p>&lt;strong>没有 MVC 管道。&lt;/strong> 基于控制器的 API 经历完整的 MVC 管道：操作选择、模型绑定、操作过滤器、结果执行。最小 API 会跳过所有这些，直接从路由到处理程序。&lt;/p>
&lt;p>&lt;strong>RequestDelegate 编译。&lt;/strong> 框架将您的 lambda 表达式编译为优化的 &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> .NET &lt;span style="color:#f85149">版本的发布，控制器和&lt;/span> Minimal 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 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> .NET &lt;span style="color:#a5d6ff">6&lt;/span> &lt;span style="color:#f85149">中引入以来，最小&lt;/span> API &lt;span style="color:#f85149">已经取得了长足的进步。最初的“&lt;/span>hello world&lt;span style="color:#f85149">”演示功能已经成熟为生产&lt;/span> API &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> API&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/zh/posts/blazor-isolated-css/</link><pubDate>Wed, 12 Mar 2025 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/blazor-isolated-css/</guid><description>使用 CSS 隔离将 CSS 样式范围限定到各个 Blazor 组件，以避免全局样式冲突。</description><content:encoded>&lt;p>使用 Blazor 时总是困扰我的一件事是很容易意外地破坏组件之间的样式。您在一个组件中添加一个类，突然之间完全不同的页面上的其他内容看起来消失了。事实证明，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>需要记住的一件事是，默认情况下，独立 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;h1 id="何时使用它">何时使用它&lt;/h1>
&lt;p>我现在几乎每个组件都使用 CSS 隔离。它使事情保持干净和可预测。我唯一不使用它的时候是真正的全局样式，例如重置、版式或主题变量——这些样式位于共享 CSS 文件中。&lt;/p>
&lt;p>希望你喜欢这篇文章！请随时通过任何社交媒体与我联系：&lt;strong>@emimontesdeoca&lt;/strong>。&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/zh/posts/keeping-christmas-spending-with-semantic-kernel/</link><pubDate>Sat, 28 Dec 2024 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/keeping-christmas-spending-with-semantic-kernel/</guid><description>使用 Semantic Kernel、Azure OpenAI 和 Blazor 分析收据并跟踪圣诞节支出。</description><content:encoded>&lt;p>﻿## 简介&lt;/p>
&lt;p>随着假期的临近，管理开支可能成为一项挑战，尤其是在购物和礼品购买热潮的情况下。在这篇博文中，我们将探讨如何利用人工智能使用 .NET 技术来帮助跟踪您的圣诞节支出。通过利用语义内核和人工智能的力量分析收据，我们可以有效地提取关键详细信息，例如商店名称、日期、商品列表和总额。该解决方案可让您轻松监控和管理您的圣诞节支出，确保您控制在预算范围内，而无需手动查看收据。&lt;/p>
&lt;h2 id="2024-年人工智能adviento-adviento-西班牙语日历">2024 年人工智能Adviento Adviento 西班牙语日历&lt;/h2>
&lt;p>&amp;lt;p对齐=“中心”&amp;gt;
&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>
&lt;p>这个项目的灵感来自于我参加 &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>对于这个项目，我们将使用 &lt;strong>Azure OpenAI&lt;/strong>，该服务允许我们利用强大的 AI 模型（例如 GPT-4）来处理和分析图像。该过程涉及多个步骤，从设置后端 API 服务到与 Blazor 前端集成以进行图像上传。我们还将使用**.NET Aspire**，这是一个有助于无缝连接一切的组件。&lt;/p>
&lt;h2 id="先决条件">先决条件&lt;/h2>
&lt;p>在我们深入研究代码之前，请确保您满足以下先决条件：&lt;/p>
&lt;ul>
&lt;li>.NET 9&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>&amp;lt;p对齐=“中心”&amp;gt;
&lt;img src="https://imgur.com/gAnGLhM.png">&lt;/p>
&lt;/p>
&lt;p>但让我们一步一步来创造东西吧！&lt;/p>
&lt;h2 id="步骤-0模型">步骤 0：模型&lt;/h2>
&lt;p>收据扫描仪应用程序的核心依赖于几个关键模型，这些模型促进前端、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>人工智能服务是我们收据分析系统的核心。它负责与 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>&amp;lt;p对齐=“中心”&amp;gt;
&lt;img src="https://imgur.com/q3EpCSy.png"/>&lt;/p>
&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>在此步骤中，我们定义一个简单的 API 端点来接受收据图像，将其转发给 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">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>&amp;lt;p对齐=“中心”&amp;gt;
&lt;img src="https://imgur.com/u9mQrpq.png"/>&lt;/p>
&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对齐&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">&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对齐&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">&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:#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>
&lt;/span>&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>&amp;lt;p对齐=“中心”&amp;gt;
&lt;img src="https://imgur.com/5NS416X.png">&lt;/p>
&lt;/p>
&lt;h3 id="2-自动指标收集">2. &lt;strong>自动指标收集&lt;/strong>&lt;/h3>
&lt;p>.NET Aspire 还自动跟踪和报告重要的应用程序指标，例如响应时间、请求计数和错误率。这可以帮助您了解应用程序的执行情况并快速检测任何瓶颈或问题。&lt;/p>
&lt;p>&amp;lt;p对齐=“中心”&amp;gt;
&lt;img src="https://imgur.com/SGawOY3.png">&lt;/p>
&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>&amp;lt;p对齐=“中心”&amp;gt;
&lt;img src="https://imgur.com/OSHhWVb.png">&lt;/p>
&lt;/p>
&lt;h3 id="结论人工智能不再只是一个流行词或我们在科幻电影中看到的东西它正在积极解决当今的现实世界问题就像我们在这个项目中解决的问题一样从收据中提取结构化数据在-azure-openainet-aspire-和-blazor-的帮助下我们可以自动化原本耗时且容易出错的手动任务-ai-不仅仅像-chatgpt-那样聊天或响应提示它可以解释图像提取有价值的信息并在几秒钟内为我们提供可操作的见解">结论人工智能不再只是一个流行词或我们在科幻电影中看到的东西。它正在积极解决当今的现实世界问题，就像我们在这个项目中解决的问题一样——从收据中提取结构化数据。在 &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> 与日志记录和指标无缝集成，我们创建了一个功能强大且可扩展的解决方案。人工智能在简化业务流程、自动化繁琐任务和提高准确性方面的潜力是巨大的，这只是其应用的一个例子。&lt;/p>
&lt;p>这篇文章是 &lt;strong>Calendario de Adviento de Inteligencia Artificial 2024 en Español&lt;/strong> 的一部分，该活动展示了现实世界的人工智能应用，并向西班牙语技术社区介绍最新趋势。如果您想更深入地了解人工智能及其可能性，那么本次活动是一个很好的起点。&lt;/p>
&lt;p>人工智能正在改变我们的工作方式，这个项目只是对可能性的一瞥。人工智能的真正力量在于它解决实际问题的能力——无论是处理收据、分析图像还是预测趋势。我们只是触及了表面。&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/zh/posts/monitoring-prices-containers/</link><pubDate>Thu, 12 Dec 2024 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/monitoring-prices-containers/</guid><description>使用 Docker 容器、Blazor 和 .NET API 后端自动监控圣诞礼物价格。</description><content:encoded>&lt;p>圣诞节即将来临，随之而来的是为亲人寻找完美礼物的快乐任务。如果您和我一样，您可能喜欢买到划算的商品，但在节日期间浏览热门商品价格飞涨的感觉就像现实生活中的雪橇之旅一样 — 令人兴奋，但有点不知所措！今年，我决定充分利用我的技术技能并使流程自动化，而不是让自己因每日价格检查而发疯。&lt;/p>
&lt;p>&amp;lt;p对齐=“中心”&amp;gt;
&lt;img src="https://imgur.com/7K7QC08.png" />&lt;/p>
&lt;/p>
&lt;p>作为一名软件开发人员和热衷于利用技术解决日常问题的人，我找到了一种自动化检查价格过程的方法。我决定创建一个可以为我做这件事的系统，而不是每天手动访问多个在线商店来比较价格。这不仅节省了时间，还能确保我获得最优惠的价格，而无需每天检查的压力。&lt;/p>
&lt;h2 id="为什么要自动化价格监控">为什么要自动化价格监控？&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>：拥抱节日的奉献精神——给自己一份高效的礼物。&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;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-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>使用单个 &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;p>####系统架构图&lt;/p>
&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="步骤-4url-管理刷新和自动定期刷新在此演示中我们仅将其存储在内存中但在实际应用程序中您将使用数据库对于这个有趣的项目让我们坚持使用内存存储">步骤 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-更新-blazor-组件以进行-url-管理和刷新">4.2 更新 Blazor 组件以进行 URL 管理和刷新&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> 后，除了允许手动刷新和 URL 管理之外，Blazor 应用现在还应该每分钟自动刷新产品价格。&lt;/p>
&lt;h2 id="结论">结论&lt;/h2>
&lt;p>建立这个价格监控系统真是太欢乐了！它不仅让我摆脱了日常价格检查的压力，而且还展示了现代网络技术的魔力。&lt;/p>
&lt;h1 id="2024-年节日科技日历">2024 年节日科技日历&lt;/h1>
&lt;p>&amp;lt;p对齐=“中心”&amp;gt;
&lt;img src="https://festivetechcalendar.com/assets/images/Heading.png" />&lt;/p>
&lt;/p>
&lt;p>我创建这篇文章是 &lt;strong>2024 年节日科技日历&lt;/strong> 活动的一部分，该活动汇集了科技爱好者、创新者和数字梦想家，分享知识并庆祝节日精神与技术奇迹的融合。这一举措不仅是为了学习和联系，也是为了回馈。&lt;/p>
&lt;p>&lt;strong>2024 年节日科技日历&lt;/strong> 今年将支持 Beatson 癌症慈善机构。 Beatson 癌症慈善机构致力于支持癌症患者、他们的家人以及照顾他们的医疗保健专业人员。有关他们令人难以置信的工作的更多信息，请访问 &lt;a href="https://www.beatsoncancercharity.org/">https://www.beatsoncancercharity.org/&lt;/a>。&lt;/p>
&lt;p>请访问节庆科技日历网站 &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/zh/posts/custom-validationattribute-blazor/</link><pubDate>Fri, 29 Mar 2024 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/custom-validationattribute-blazor/</guid><description>使用数据注释创建可重用的自定义 ValidationAttribute 类，用于 Blazor 表单验证。</description><content:encoded>&lt;p>﻿# 定制一切&lt;/p>
&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;&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>老实说，对我来说，很明显我们应该至少将验证这些表单的逻辑转移到自定义验证属性中，它使我们可以自由地将登录代码存储在单个位置，并且我们可以稍后将其用于 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/zh/posts/custom-exception-handler-api/</link><pubDate>Sun, 01 Oct 2023 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/custom-exception-handler-api/</guid><description>构建自定义异常处理中间件以从 .NET API 返回干净的错误响应。</description><content:encoded>&lt;p>﻿例外是不好的，我们知道吗？但如果我们必须处理它们怎么办？&lt;/p>
&lt;p>当我们遇到异常时会发生什么，例如，在 API 上，它会显示一条堆栈消息，其中包含我们可能希望从用户获得的响应中删除的大量信息。&lt;/p>
&lt;p>对于演示，我创建了一个 dotnet 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>CustomException&lt;/code> 而不是 &lt;code>Exception&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>CustomException&lt;/code> 而不是 &lt;code>Exception&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>微软为我们提供了一种在抛出异常后处理异常的方法，您可以在[此处](&lt;a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.filters.exceptionfilterattribute?view=aspnetcore-7.0">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>context.Result&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/zh/posts/custom-iservicecollection-services/</link><pubDate>Mon, 04 Sep 2023 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/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>static class&lt;/code>，它有一个名为 &lt;code>AddServices&lt;/code> 的 &lt;code>static&lt;/code> 方法，该方法返回 &lt;code>IServiceCollection&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/zh/posts/api-di-attributes/</link><pubDate>Tue, 22 Aug 2023 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/api-di-attributes/</guid><description>使用 TypeFilterAttribute 而不是 ActionAttribute 在 .NET API 操作筛选器中启用依赖项注入。</description><content:encoded>&lt;p>﻿依赖注入可能是目前 .NET 上最好的功能之一。在任何可能的情况下，您都不可能不使用它，所以如果您像我一样，您非常希望将它添加到您所做的所有实现中。&lt;/p>
&lt;p>过滤器，根据微软官方的&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，有些东西必须运行每个请求，或者几乎所有请求，所以，理想情况下我们想要做的是使用它加上&amp;hellip;&amp;hellip;依赖注入！&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/zh/posts/swagger-libraries-documentation/</link><pubDate>Fri, 17 Feb 2023 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/swagger-libraries-documentation/</guid><description>使 Swagger 能够显示来自外部 .NET 类库中定义的模型的 XML 文档。</description><content:encoded>&lt;p>使用多个库来拆分代码以应对未来的情况是一件好事。就我个人而言，我喜欢这样做，如果我可以拆分我的逻辑，以便可以在其他项目或项目的一部分中重用它。&lt;/p>
&lt;p>至少对我来说，必须拆分数据模型，包括 &lt;code>Entity Framework&lt;/code>，以便可以在 &lt;code>Console application&lt;/code>、&lt;code>Blazor application&lt;/code> 或 &lt;code>API&lt;/code> 中引用和使用它。&lt;/p>
&lt;p>让我们看一下我如何做事情的示例，这是一个项目的屏幕截图，该项目具有一些应用程序和一个包含所有模型的共享库。&lt;/p>
&lt;img src="https://imgur.com/gdg2nn3.png">
&lt;p>这是我从 &lt;code>API&lt;/code> 项目中移出的类的示例，其中每个属性和类都有 &lt;code>summary&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/zh/posts/cleanup-local-branches/</link><pubDate>Mon, 30 Jan 2023 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/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 中的 Identity 路由</title><link>https://emimontesdeoca.github.io/zh/posts/identity-url-change/</link><pubDate>Mon, 23 Jan 2023 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/identity-url-change/</guid><description>通过搭建 Identity 页面自定义默认 ASP.NET Core Identity 登录和注册 URL。</description><content:encoded>&lt;p>﻿甚至想知道微软如何命名如此罕见的东西？我一直认为他们并没有真正做得那么好，但是，事实就是如此！&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;p>&amp;lt;imgalign=“中心”src=“https://i.imgur.com/2W8Oou9.png&amp;quot;&amp;gt;&lt;/p>
&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>为了更新这些页面，微软隐藏了它们，但您可以快速搭建它们并进行您想要的更改！&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> 两次并单击 &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>这些新文件是 ASP.NET 在您选择添加身份时添加到您的项目中的登录和注册页面！&lt;/p>
&lt;h2 id="更新网址">更新网址&lt;/h2>
&lt;p>您可能已经注意到，这些文件是 razor 文件，因为它们的扩展名是 &lt;code>cshtml&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-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="就是这样我希望您学会了如何更新此网址主要是因为在某些项目中当您以某种方式执行网址时然后身份看起来不同这很糟糕哈哈">就是这样我希望您学会了如何更新此网址，主要是因为在某些项目中，当您以某种方式执行网址时，然后身份看起来不同，这很糟糕，哈哈！&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/zh/posts/custom-attributes-net-6-core-api/</link><pubDate>Fri, 09 Dec 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/custom-attributes-net-6-core-api/</guid><description>创建自定义 ActionFilterAttribute 类以验证 .NET 6 Core API 中的请求标头。</description><content:encoded>&lt;p>自定义属性确实是一件很好用的东西，我最近开始使用它们，因为它们允许我创建其中的单个属性并在控制器、类或方法本身上重用它们。&lt;/p>
&lt;p>当您想要执行一些安全操作（例如检查标头或检查您确实需要的参数的值）时，它们确实很有帮助。&lt;/p>
&lt;p>就我而言，我们将在 .NET Core API 项目中使用它，我们将在其中检查所有请求是否包含特定标头。&lt;/p>
&lt;h1 id="标头检查属性">标头检查属性&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>我们有 2 个函数：&lt;code>GetWeatherForecastWithCheck&lt;/code> 和 &lt;code>GetWeatherForecastWithoutCheck&lt;/code>，其中一个会失败，其他不会，但让我们在 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;h1 id="邮递员">邮递员&lt;/h1>
&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;h1 id="就是这样">就是这样&lt;/h1>
&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>）。您还可以在博客标题上找到我的大部分社交活动。&lt;/p>
&lt;p>希望你喜欢这篇文章！呀！&lt;/p></content:encoded><category>.NET</category><category>API</category></item><item><title>在 Blazor 中处理加载组件</title><link>https://emimontesdeoca.github.io/zh/posts/loading-component-blazor/</link><pubDate>Tue, 19 Jul 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/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="加载组件代码">加载组件代码&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>）。您还可以在博客标题上找到我的大部分社交活动。&lt;/p>
&lt;p>希望你喜欢这篇文章！&lt;/p></content:encoded><category>.NET</category><category>Blazor</category></item><item><title>从证书中获取到期日期</title><link>https://emimontesdeoca.github.io/zh/posts/expiration-date-certificate/</link><pubDate>Fri, 27 May 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/expiration-date-certificate/</guid><description>使用 C# HttpClient 和 X509Certificate2 以编程方式检索 SSL 证书到期日期。</description><content:encoded>&lt;p>有一些网络应用程序可以帮助您检查我们使用的域中的证书的当前状态，我们有其中一些。&lt;/p>
&lt;p>我创建了一个每天运行一次的 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/zh/posts/focus-element-blazor/</link><pubDate>Thu, 05 May 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/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="blazor-组件">Blazor 组件&lt;/h2>
&lt;p>初始化脚本后，我们需要找到一个要关注的元素，因此在任何组件中，我们需要将该元素引用到一个对象。&lt;/p>
&lt;p>因此，在我们的 blazor 组件中，让我们添加一个带有 &lt;code> @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 数据库项目生成 *.dacpac 文件</title><link>https://emimontesdeoca.github.io/zh/posts/generate-dacpacs-github-actions/</link><pubDate>Mon, 18 Apr 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/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>）。您还可以在博客标题上找到我的大部分社交活动。&lt;/p>
&lt;p>希望你喜欢这篇文章！呀！&lt;/p></content:encoded><category>CI/CD</category></item><item><title>在 Blazor 中使用 Javascript Interop 切换主题</title><link>https://emimontesdeoca.github.io/zh/posts/blazor-toggle-darkmode/</link><pubDate>Fri, 01 Apr 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/blazor-toggle-darkmode/</guid><description>使用 JavaScript 互操作和 CSS 数据属性在 Blazor 中实现浅色和深色主题切换。</description><content:encoded>&lt;p>对于像我这样每次打开网页时都会出现闪回的人，我想出了一个超级简单的解决方案，介绍如何使用 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>。&lt;/p>
&lt;h2 id="创建-javascript-文件来处理逻辑">创建 Javascript 文件来处理逻辑&lt;/h2>
&lt;p>现在我们有了更新标识符的函数，现在让我们创建一个包含运行该逻辑的函数的 Javscript 文件。我们的文件将被称为 &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="将-javascript-文件添加到-blazor">将 Javascript 文件添加到 Blazor&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>p&lt;/code> 和 &lt;code>app-text&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>）。您还可以在博客标题上找到我的大部分社交活动。&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 存储</title><link>https://emimontesdeoca.github.io/zh/posts/uploading-files-az-blob-blazor/</link><pubDate>Wed, 23 Mar 2022 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/uploading-files-az-blob-blazor/</guid><description>使用本机 HTML5 文件输入将文件从 Blazor 应用上传到 Azure Blob 存储。</description><content:encoded>&lt;p>Azure Blob 存储服务是 Azure 生态系统上最常用的服务之一，它允许您将大量文件上传到云，同时价格超级便宜。它还具有直观的网络，您可以通过直接链接访问或下载它们。&lt;/p>
&lt;p>总的来说，这是一项相当不错的服务，我一直在工作和个人项目中使用它，老实说，最好的事情是如何让它工作的简单性。&lt;/p>
&lt;p>我使用 Azure Blob 存储的大多数情况都是在后端，直接上传操作结果或其他内容。所以我实际上没有从网站等上传任何类型的文件。不幸！&lt;/p>
&lt;p>由于我是 Blazor 粉丝，因此我为您带来了一种使用 HTML5 的本机文件输入将文件上传到 Azure Blob 存储的方法。&lt;/p>
&lt;h1 id="先决条件">先决条件&lt;/h1>
&lt;ul>
&lt;li>Azure 帐户（如果您没有，请使用一堆 $$$ 前往[此处]（又名.ms/免费））。&lt;/li>
&lt;li>一个 IDE（VS、代码，任何都可以）&lt;/li>
&lt;li>.NET Core 3.0或更高版本&lt;/li>
&lt;/ul>
&lt;h1 id="创建-azure-blob-存储">创建 Azure Blob 存储&lt;/h1>
&lt;p>转到 Azure 门户，让我们继续创建存储帐户。&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>Acccess keys&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-server-项目">创建一个很酷的 Blazor Server 项目&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>BlobFile&lt;/code> 的类，在其中存储 &lt;code>Name&lt;/code> 和 &lt;code>Data&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="修改用户界面">修改用户界面&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-存储">上传到 Azure Blob 存储&lt;/h2>
&lt;p>现在我们已经完成了大部分 UI，现在让我们安装将处理 Azure Blob 存储 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-stoare">上传到 Azure Blob Stoare&lt;/h3>
&lt;p>现在让我们执行实际上传文件的逻辑，通常我们会使用连接字符串和 app.config 中的密钥，但为了本教程，我们使用输入并在那里提供数据。&lt;/p>
&lt;p>首先，我们为 Azure Blob 存储类添加 &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 存储，转到 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>）。您还可以在博客标题上找到我的大部分社交活动。&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/zh/posts/override-method-pimcore-core-bundles/</link><pubDate>Thu, 10 Dec 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/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="找到核心控制器中的action">找到核心控制器中的action&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>可能它会找到多个，所以您必须看一下并决定哪一个是那个，在我们的例子中，我们正在使用资产，所以很明显我们想要使用 &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>所以最后 ti 看起来像这样&lt;/p>
&lt;div class="highlight">&lt;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="默认控制器php">默认控制器.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/zh/posts/configure-apple-keyboard-windows/</link><pubDate>Wed, 11 Nov 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/configure-apple-keyboard-windows/</guid><description>安装驱动程序并配置 Apple Magic Keyboard 2 以在 Windows 10 上正常工作。</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>就在昨天，我买了一台 Apple Magic Keyboard 2，即使我有大约 5 个机械键盘，因为我想尝试一下，而且它是无线的。&lt;/p>
&lt;p>我的主要操作系统是 Windows 10，我喜欢它，并且不想更改它，因此考虑到这一点，我知道有必要做一些事情才能使键盘完美工作。我了解苹果的运作方式以及他们如何将自己的设备保留在其生态系统中。&lt;/p>
&lt;h2 id="问题">问题&lt;/h2>
&lt;p>如果您配对键盘，您会认识到以下几点：&lt;/p>
&lt;ul>
&lt;li>功能键不起作用&lt;/li>
&lt;li>有些键映射错误（这发生在我的西班牙语版本中）&lt;/li>
&lt;/ul>
&lt;h2 id="文档">文档&lt;/h2>
&lt;p>为了使其工作，我必须从网上阅读大量内容，但这两个链接帮助我使其工作：&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 上使用 f-1 - f12 键而不按 FN？&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="安装苹果键盘驱动程序">安装苹果键盘驱动程序&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。但是，您需要 2.x 版本，因为准将脚本与 3.x 版本不兼容。
*（选项）默认情况下，安装程序不会将 python.exe 添加到您的 PATH 中。如果需要，您需要启用此选项。 （见右侧截图）&lt;/li>
&lt;li>如果您已经有其他版本的 Python，您可能不想启用此选项。
3.下载brigadier（一个可以帮助您下​​载最新Boot Camp版本的Python脚本）。&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>请右键单击以下链接并使用“链接另存为&amp;hellip;”保存文件。 &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脚本保存为“brigadier.txt”，请运行以下命令：
&lt;ul>
&lt;li>如果 Python 版本 2.x 在您的路径中： 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>运行管理 powershell 并执行 &lt;code>BootCamp.msi&lt;/code>，它将安装一些东西，但我们需要使用 &lt;code>AppleKeyboardMagic2&lt;/code> 文件夹的内容更新驱动程序
11.启动设备管理器（&lt;code>devmgmt.msc&lt;/code>）
12.展开&lt;code>Human Interface Devices&lt;/code>节点
13.寻找&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>您应该看到蓝牙键盘现在被检测为 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-键这意味着您需要按-fn--f5-才能实际按下-f5-按钮">更新 FN 键行为如果您正确安装了所有内容，您会注意到默认情况下启用了 FN 键，这意味着您需要按 &lt;code>fn&lt;/code> + &lt;code>F5&lt;/code> 才能实际按下 &lt;code>F5&lt;/code> 按钮。&lt;/h3>
&lt;p>为了解决这个问题，我找到了一个解决方案，在文档部分中指出，该解决方案通过更改注册表中的某些条目来起作用。&lt;/p>
&lt;p>1.打开注册表
2. 前往&lt;code>HKEY_CURRENT_USER\SOFTWARE\Apple Inc.\Apple Keyboard Support&lt;/code>
3. 创建或更新 &lt;code>OSXFnBehavior&lt;/code> 并将其设置为 &lt;code>0&lt;/code>
4. 重新启动计算机&lt;/p>
&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/zh/posts/adding-compiled-dll-to-nuget/</link><pubDate>Thu, 01 Oct 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/adding-compiled-dll-to-nuget/</guid><description>通过在 nuspec 文件中包含已编译的库引用来修复 NuGet 包中丢失的 DLL。</description><content:encoded>&lt;p>就在前几天，我在缺失的库上发现了一个关于 &lt;code>NullReferenceException&lt;/code> 的错误，该库应该存在，因为它来自我们上传的 nuget 包。&lt;/p>
&lt;p>基本上，我编译了一个引用了几个项目的库类。在构建阶段，它会将 &lt;code>dll&lt;/code> 文件添加到 &lt;code>bin&lt;/code> 文件夹中，但是当 &lt;code>packaging&lt;/code> 将其作为 nuget 包并将其安装在另一个解决方案中时，我引用并应复制到 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/zh/posts/bash-straight-to-wsl-machine-with-terminal/</link><pubDate>Tue, 22 Sep 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/bash-straight-to-wsl-machine-with-terminal/</guid><description>配置 Windows 终端以通过 .bashrc 直接在 Linux 主目录中打开 WSL 会话。</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>颜色&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>请记住，您必须对所有 WSL 安装执行此操作，因为我们正在更新单个 WSL 安装的 &lt;code>bashrc&lt;/code> 文件。&lt;/p></content:encoded></item><item><title>使用 ExpandoObject 生成动态对象</title><link>https://emimontesdeoca.github.io/zh/posts/dynamic-object-generation-expando-object/</link><pubDate>Fri, 06 Mar 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/dynamic-object-generation-expando-object/</guid><description>使用 ExpandoObject 动态创建具有运行时定义的属性的对象，以实现灵活的数据导出。</description><content:encoded>&lt;p>我一直需要将具有自己定义的列的 Excel 文件转换为具有动态列的新文件，并且我对如何在不出现严重性能问题的情况下正确执行此操作感到有点困惑。&lt;/p>
&lt;p>我提出了一个教程，您可以在&lt;a href="https://www.oreilly.com/content/building-c-objects-dynamically/">此处&lt;/a>找到它，该教程使用.NET &lt;a href="https://docs.microsoft.com/en-us/dotnet/api/system.dynamic.expandoobject?view=netframework-4.8">ExpandoObject&lt;/a>，它几乎可以让您创建一个对象并添加动态成员。&lt;/p>
&lt;h2 id="扩展对象">扩展对象&lt;/h2>
&lt;p>微软的定义是：&lt;/p>
&lt;blockquote>
&lt;p>表示一个对象，其成员可以在运行时动态添加和删除。&lt;/p>&lt;/blockquote>
&lt;p>它有一些注释：&lt;/p>
&lt;blockquote>
&lt;p>ExpandoObject 类使您能够在运行时添加和删除其实例的成员，以及设置和获取这些成员的值。此类支持动态绑定，这使您能够使用标准语法（如sampleObject.sampleMember），而不是更复杂的语法（如sampleObject.GetAttribute(&amp;ldquo;sampleMember&amp;rdquo;)）。&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>ColumnExcelItem&lt;/code> 表，该表将用于生成 &lt;code>ExcelItem&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>在本例中，我们只使用了一个案例，但假设您希望为某些用户提供不同的报告。假设 &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>这是学习 &lt;code>dynamic&lt;/code> 在 .NET 中如何工作的一种很酷的方法，当您需要为不同的用户提供不同的角色或模板，并且对投入时间硬编码内容不感兴趣时，这确实是一个很好的解决方案。&lt;/p></content:encoded><category>.NET</category></item><item><title>使用 TravisCI 持续交付 NuGet 包</title><link>https://emimontesdeoca.github.io/zh/posts/automated-nuget-deployment-travis-ci/</link><pubDate>Tue, 21 Jan 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/automated-nuget-deployment-travis-ci/</guid><description>使用 Travis CI 持续交付管道自动化 NuGet 包编译、测试和发布。</description><content:encoded>&lt;p>﻿最近我做了一个&lt;a href="https://emimontesdeoca.github.io/2020/ci-dotnet-core-and-travis-ci/">教程&lt;/a>，介绍如何使用 Travis CI 作为工具来自动测试您刚刚推送或尝试推送到 &lt;code>master&lt;/code> 分支的代码。对其进行编译和测试，并最终向您报告更改的状态。&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> feed 将其公开，这样每个人都可以在他们的库中轻松操作！&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-core-cli">.NET Core 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 Gallery 是所有包作者和使用者使用的中央包存储库。&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> 并创建一个新密钥。&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>因此，让我们在存储库的根目录中创建一个名为 &lt;code>scripts&lt;/code> 的文件夹，其中包含三个 &lt;code>bash&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;p>1.添加不同的&lt;code>stages&lt;/code>
2. 每个&lt;code>stage&lt;/code>取决于分支
3. 如果您的构建是在 master 上，则意味着必须更新包，因此我们将把包推送到 NuGet feeds
4. 如果您的构建是拉取请求，我们仍然会检查构建是否可以编译并测试，但我们不会发布它。&lt;/p>
&lt;h2 id="阶段">阶段&lt;/h2>
&lt;p>从他们的文档中：&lt;/p>
&lt;blockquote>
&lt;p>您可以通过在构建配置（您的 .travis.yml 文件）中指定条件来过滤和拒绝构建、阶段和作业。&lt;/p>&lt;/blockquote>
&lt;p>您可以在&lt;a href="https://docs.travis-ci.com/user/conditional-builds-stages-jobs">其文档页面&lt;/a>中找到有关 TravisCI &lt;code>stages&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">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>正如您所看到的，我们有三个不同的阶段 &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 feed。&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 仪表板中排队，正如您所看到的，我们有 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> 并再次检查构建，看看我们现在有三个作业而不是两个。&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>。&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;h1 id="就是这样">就是这样&lt;/h1>
&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;a href="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/zh/posts/ci-dotnet-core-and-travis-ci/</link><pubDate>Tue, 14 Jan 2020 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/ci-dotnet-core-and-travis-ci/</guid><description>使用 Travis CI 进行自动化构建和测试，为 .NET Core 项目设置持续集成。</description><content:encoded>&lt;p>上周末，我决定要正确启动我一直在不同存储库中执行的 scraper-checker-downloader 项目。&lt;/p>
&lt;p>在开始另一个项目之后，这必须很酷，&lt;strong>就像真正的酷&lt;/strong>，使用 CI/CD、拉取请求、文档、自述文件中的徽章，我所看到的一切都很酷，而且确实是最佳实践。&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;h2 id="这是什么">这是什么？&lt;/h2>
&lt;p>摘自 Martin Fowler 的 &lt;a href="https://martinfowler.com/articles/continuousIntegration.html">博客&lt;/a>，这是我读过的最好的解释：&lt;/p>
&lt;blockquote>
&lt;p>持续集成是一种软件开发实践，团队成员经常集成他们的工作，通常每个人至少每天集成 - 导致每天多次集成。
每个集成都通过自动构建（包括测试）进行验证，以尽快检测集成错误。&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>为了使这项工作成功，您需要三件简单的事情：&lt;/p>
&lt;p>1.Visual Studio 2019最新版本
2.Github账号
3. Travis-CI 帐户链接到您的 Github 帐户&lt;/p>
&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;p>&amp;lt;imgalign=“中心”src=“https://i.gyazo.com/0657eb2bdeb3c331b9e4585d7deed5ef.png&amp;quot;&amp;gt;&lt;/p>
&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-核心版本创建项目后请转到项目属性并将-target-framework-更改为-net-standard-21以使其与-net-core-30-中构建的项目兼容">NET 核心版本创建项目后，请转到项目属性并将 &lt;code>Target framework&lt;/code> 更改为 &lt;code>.NET Standard 2.1&lt;/code>，以使其与 &lt;code>.NET Core 3.0&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="命令行界面">命令行界面&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-核心版本">NET 核心版本&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-核心版本-1">NET 核心版本&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>我们将把测试分成两个不同的文件：&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;p>&amp;lt;imgalign=&amp;ldquo;center&amp;quot;src=&amp;ldquo;&lt;a href="https://i.gyazo.com/ffecc23a14d796af9a46dbb390c0d072.png%22/%3E">https://i.gyazo.com/ffecc23a14d796af9a46dbb390c0d072.png"/>&lt;/a> /&amp;gt;&lt;/p>
&lt;p>现在我们可以运行测试了，因此请转到 Visual Studio 中的 &lt;code>Test Explorer&lt;/code> 并运行它们！&lt;/p>
&lt;img align="center" src="https://i.gyazo.com/17d7ac9b2f1e7fb2666a68fc87882eed.png" />
&lt;h1 id="特拉维斯-ci">特拉维斯 CI&lt;/h1>
&lt;p>如果您现在还不知道，TravisCI 是一个托管的持续集成和部署系统。&lt;/p>
&lt;p>这里我们必须执行一些步骤，但首先我们将链接我们的 Github 存储库以供 TravisCI 代理监听，以便构建和测试我们的项目。&lt;/p>
&lt;h2 id="启用存储库">启用存储库&lt;/h2>
&lt;p>为此，请登录 Travis CI 页面并转到您的存储库，然后通过单击存储库名称旁边的滑块来过滤您创建的项目并启用它。&lt;/p>
&lt;p>&amp;lt;imgalign=“中心”src=“https://i.gyazo.com/28b366dddd3f5caa9100ca6b6d200764.png&amp;rdquo;&amp;gt;&lt;/p>
&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>mono&lt;/code> 因为 &lt;code>.NET Core 3.0&lt;/code> 将在 Linux 中本机运行。&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="上传到master">上传到master&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;p>&amp;lt;imgalign=“中心”src=“https://i.gyazo.com/977ea42a90adccf0736464b6603867a5.png&amp;rdquo;&amp;gt;”
&lt;br/>
&lt;br/>
&amp;lt;imgalign=“中心”src=“https://i.gyazo.com/52a5f9356df5436c862b7df6fe66a9f4.png&amp;quot;&amp;gt;”&lt;/p>
&lt;h2 id="完成">完成&lt;/h2>
&lt;p>&amp;lt;imgalign=“中心”src=“https://i.gyazo.com/c3d3521925a3e20bcf55bf5f6a2a711d.png&amp;quot;&amp;gt;”
&lt;br/>
&lt;br/>
&amp;lt;imgalign=“中心”src=“https://i.gyazo.com/8c749c2ce44837a39fc5cd3e8838a798.png&amp;quot;&amp;gt;”&lt;/p>
&lt;h1 id="让我们打破它">让我们打破它&lt;/h1>
&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;p>&amp;lt;imgalign=“中心”src=“https://i.gyazo.com/a57d0a0f8c07cc1ab6e4f55a8466cbbd.png&amp;quot;&amp;gt;&lt;/p>
&lt;p>现在提交此更改并将其推送到 master，然后等待 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;p>&amp;lt;imgalign=“中心”src=“https://i.gyazo.com/397befe9b7f5a32b6e97511733296b00.png&amp;quot;&amp;gt;”&lt;/p>
&lt;p>在日志的最后我们可以看到错误本身：&lt;/p>
&lt;p>&amp;lt;imgalign=“中心”src=“https://i.gyazo.com/e5cbf32c5dd7a08bf6628d81edff3130.png&amp;quot;&amp;gt;”&lt;/p>
&lt;h2 id="让我们再次修复它">让我们再次修复它！&lt;/h2>
&lt;p>现在恢复我们所做的并将代码推送到 master，并检查新构建的状态。&lt;/p>
&lt;p>测试成功通过：&lt;/p>
&lt;p>&amp;lt;imgalign=“中心”src=“https://i.gyazo.com/02deb8fcb4fca618ff2d79f1c27c6df5.png&amp;quot;&amp;gt;”&lt;/p>
&lt;p>并且构建也成功了：&lt;/p>
&lt;p>&amp;lt;imgalign=“中心”src=“https://i.gyazo.com/38a227f0634c6040c6608f8c51f36cd3.png&amp;quot;&amp;gt;”&lt;/p>
&lt;h1 id="结论">结论&lt;/h1>
&lt;p>&lt;strong>真的很强大&lt;/strong>，CI和CD很早以前就已经存在了，但现在让它在每个项目中运行都非常简单，不管它有多小或多简单。从我的角度来看，每个人至少应该为他们的每个项目设置 CI，因为这是一个很好的实践，它最终会节省你调试和查找错误的时间，如果你设置了正确的 &lt;code>tests&lt;/code> 和 CI，这些错误就不会发生。&lt;/p>
&lt;h1 id="就是这样">就是这样&lt;/h1>
&lt;p>这就是如何创建一个 .NET Core 3.0 解决方案，该解决方案在使用 TravisCI 的每个构建上进行持续集成并将代码存储在 Github 中。&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/zh/posts/my-take-on-in-memory-cache/</link><pubDate>Tue, 03 Sep 2019 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/my-take-on-in-memory-cache/</guid><description>使用 C# 和泛型构建具有过期支持的自定义内存缓存实现。</description><content:encoded>&lt;p>我一直在研究一些处理大量数据的东西。在这样做的过程中，我意识到其中很大一部分永远不会改变，或者至少在一段固定的时间内不会改变。&lt;/p>
&lt;p>所以我认为创建个人缓存存储库会很有用，当然这不是什么新鲜事，几周前我在 StackOverflow 的 &lt;a href="https://nickcraver.com/blog/2019/08/06/stack-overflow-how-we-do-app-caching/#in-memory--redis-cache">post&lt;/a> 中读到了由 &lt;a href="https://nickcraver.com/">Nick Craver&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 类时唯一可用的方法是 &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>：缓存过期或为空时的回调函数。&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>CacheRepository&lt;/code> 询问具有键 &lt;code>users&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/zh/posts/google-chrome-screenshot-tool/</link><pubDate>Thu, 09 May 2019 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/google-chrome-screenshot-tool/</guid><description>使用 Chrome DevTools 内置屏幕截图命令捕获全页和区域屏幕截图。</description><content:encoded>&lt;p>有时，我需要向客户或团队成员展示网站当前状态的完整屏幕截图，甚至是网站的一部分，因此我一直在使用&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>这不是新想法，它已包含在 2017 年 4 月的 &lt;a href="https://developers.google.com/web/updates/2017/04/devtools-release-notes">Devtools 更新&lt;/a> 中，&lt;em>但看起来我住在岩石下，直到现在才发现&amp;hellip;&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 可以帮助您即时编辑页面并快速诊断问题，最终帮助您更快地构建更好的网站。&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;p>1.区域截图
2.全尺寸截图
3.节点截图
4. 截图&lt;/p>
&lt;p>在检查所有截图方法之前，devtools处理完图像后，&lt;strong>会自动以网页名称下载&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> 将让您选择网站的一部分并进行屏幕截图。&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> 将从头到尾截取整个网页的屏幕截图。&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> 将让您制作检查元素中选定节点的屏幕截图。&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/zh/posts/embedded-resources-and-external-resources/</link><pubDate>Mon, 06 May 2019 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/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">this repo&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>，因此文件的命名法将为：&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>提示：要在 Properties 文件夹内添加文件夹，请在外部创建并将其移至内部，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.John` 而不是 `Properties.Doe`。
&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 相当简单，因此为了访问嵌入式属性，我们只需使用 `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>我们的团队领导想到了一个非常好的方法，就像后备系统一样，我们必须有一个资源的基本模型，然后对于每个客户端都有一个资源文件，它将用他们的资源更新文件，最后我们得到一个资源列表。&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"> 混合两个文件并为每种语言提供一个字典&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>请注意，我们正在实现属性的延迟加载，这有助于提高性能并使其加载字典一次。&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>：混合两个字典并返回一个字典。&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>两件事：- 请注意，它需要一个名为 &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>System.Windows.Forms&lt;/code> 的引用，才能访问 &lt;code>ResXResourceReader&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>System.Configuration&lt;/code> 添加到引用中，以便您可以访问 &lt;code>app.settings&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="添加代码到属性-getter">添加代码到属性 getter&lt;/h2>
&lt;p>由于我们现在拥有所有方法，因此我们可以修改公共属性的 getter 来获取值。&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-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>所有这些都将在延迟加载后完成一次。&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>一切设置完毕后，我们运行控制台应用程序，让我们停止到属性的 &lt;code>get&lt;/code> 部分并检查所有内容&lt;/p>
&lt;p>&lt;code>folderIdentifier&lt;/code> 具有应用程序设置的值：&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="只更新一个">只更新一个&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>前两个值来自 &lt;code>resources.resx&lt;/code>，均为英语，最后两个值均来自西班牙语，并且这些值是从 &lt;code>resources.es.resx&lt;/code> 检索的。&lt;/p>
&lt;h1 id="就是这样">就是这样&lt;/h1>
&lt;p>在本教程中，我们找到了一种混合嵌入式和外部语言资源的方法，这是我们团队中遇到的问题的解决方案，从那时起它就一直运行没有任何问题。&lt;/p>
&lt;p>您可以在[此处](&lt;a href="https://github.com/emimontesdeoca/resources-demo">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/zh/posts/integration-test-bot-framework-with-flow-cases/</link><pubDate>Wed, 25 Apr 2018 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/integration-test-bot-framework-with-flow-cases/</guid><description>扩展 Bot Framework 集成测试以支持多轮对话流场景。</description><content:encoded>&lt;h2 id="简介">简介&lt;/h2>
&lt;p>在之前的博文中，我们针对单例做了一些集成测试。 “单一案例”是指在向机器人询问某件事后，他只会回复一次，然后我们比较结果的情况。&lt;/p>
&lt;p>&lt;strong>本指南不会解释身份验证和 API 调用是如何完成的，如果您想查看，请查看单例指南。&lt;/strong>&lt;/p>
&lt;h3 id="心流对话怎么样">心流对话怎么样？&lt;/h3>
&lt;p>使用当前的方式，我们在对话中没有任何类型的流程，例如，如果您想寻求帮助，然后会显示一个包含不同选项的菜单，然后在选择其中一个选项后，会显示另一个菜单。这将产生 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>List&amp;lt;Activity&amp;gt;&lt;/code> 而不是单个 &lt;code>activity&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>List&amp;lt;Activity&amp;gt;&lt;/code>，而不是我之前提到的单个 &lt;code>Activity&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>DirectLine&lt;/code> 的相关信息，例如 &lt;code>secret&lt;/code> 和端点，以及我们将测试的 &lt;code>Entries&lt;/code> 列表。&lt;/p>
&lt;p>请注意，&lt;code>Entries&lt;/code> 现在是 &lt;code>TestEntryFlow&lt;/code> 的列表，而不是 &lt;code>TestEntry&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>latestReponse&lt;/code> 和 &lt;code>expectedResponse&lt;/code> 填充 &lt;code>globals&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/zh/posts/integration-test-bot-framework-1/</link><pubDate>Tue, 24 Apr 2018 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/integration-test-bot-framework-1/</guid><description>使用 DirectLine API 和 JSON 测试用例为 Bot Framework 聊天机器人设置集成测试。</description><content:encoded>&lt;p>自从我在一家公司工作一段时间后，我就被分配从事Bot Framework的集成测试工作。&lt;/p>
&lt;p>我在小组中的工作是使机器人尽可能稳定，但首先，什么是 Bot Framework？&lt;/p>
&lt;blockquote>
&lt;p>构建、连接、部署和管理智能机器人，以便在网站、应用程序、Cortana、Microsoft Teams、Skype、Slack、Facebook Messenger 等上与用户自然交互。快速开始使用完整的机器人构建环境，同时只需为您使用的内容付费。&lt;/p>&lt;/blockquote>
&lt;p>您可以在&lt;a href="https://dev.botframework.com/">此处&lt;/a>找到有关 Bot Framework 的更多信息。&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 中使用了一个测试项目，该项目将使用 WebClient 作为 API Rest 和一个 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>Directline 令牌生成端点 -&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 的授权，请记住所有代码都存储在我的 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集成测试（2）</title><link>https://emimontesdeoca.github.io/zh/posts/integration-test-bot-framework-2/</link><pubDate>Tue, 24 Apr 2018 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/posts/integration-test-bot-framework-2/</guid><description>实施 DirectLine 授权和 API 调用以进行 Bot Framework 集成测试（第 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>，我们可以创建最终端点来发送 &lt;code>Activity&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-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/zh/posts/integration-test-bot-framework-3/</link><pubDate>Tue, 24 Apr 2018 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/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;code>EvaluateAsync&amp;lt;T&amp;gt;&lt;/code> 评估并返回 T，在我们的例子中，我们传递 &lt;code>string&lt;/code> 进行评估，并传递 &lt;code>globals&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-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/zh/about/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/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>微软开发者技术 MVP&lt;/strong>，住在&lt;strong>西班牙加那利群岛特内里费&lt;/strong>的自豪父亲。&lt;/p>
&lt;p>我喜欢解决复杂的技术挑战，并使用微软技术构建可扩展的云解决方案。我的日常工具围绕着 &lt;strong>.NET&lt;/strong>、&lt;strong>Azure&lt;/strong>、&lt;strong>基于 Semantic Kernel 的 AI&lt;/strong> 以及 &lt;strong>.NET Aspire&lt;/strong> 等现代架构。&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>我也是 &lt;a href="https://thedotnetblog.com">&lt;strong>The .NET Blog&lt;/strong>&lt;/a> 的创始人——这是 .NET 社区的资源平台——我也定期在这个博客上写下我学到和构建的内容。&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/zh/speaking/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://emimontesdeoca.github.io/zh/speaking/</guid><description>Emiliano Montesdeoca 的会议演讲和分享 — Microsoft MVP 和国际演讲者。</description><content:encoded>&lt;p>我是一名经常在国际技术大会上演讲的讲师，专注于 &lt;strong>.NET&lt;/strong>、&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-强化三级支持azure-functions--openai-实战">用 AI 强化三级支持：Azure Functions + OpenAI 实战&lt;/h3>
&lt;p>Azure Functions、Azure Monitor 告警和 Azure OpenAI 如何协同工作以加速三级支持。从自动告警分类到智能日志分析——将 AI 融入真实支持工作流的实践解析。&lt;/p>
&lt;h3 id="从仪表板到代理可观察性的下一步">从仪表板到代理：可观察性的下一步&lt;/h3>
&lt;p>一场将 AI 代理世界与生产可观测性连接起来的实践课。从 Semantic Kernel 到通过 MCP 与 Grafana 交互、查询 OpenTelemetry 数据并帮助运营和优化真实系统的自主代理。&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="用微软代理框架拯救圣诞节">用微软代理框架拯救圣诞节&lt;/h3>
&lt;p>使用 Microsoft Agent Framework 构建和协调多个 AI 代理以找到完美的圣诞礼物。每个代理各有专长——从礼物想法生成到价格比较——共同协作实现更聪明的节日购物。&lt;/p>
&lt;h3 id="用-ai-强化三级支持azure-functions--semantic-kernel-实战">用 AI 强化三级支持：Azure Functions + Semantic Kernel 实战&lt;/h3>
&lt;p>Azure Functions、Azure Monitor 和 Semantic Kernel 协同工作，使三级支持更快、更高效——从自动告警检测到智能日志分析。&lt;/p>
&lt;h3 id="net-10-中-blazor-有哪些新变化">.NET 10 中 Blazor 有哪些新变化？&lt;/h3>
&lt;p>探索 .NET 10 中 Blazor 的关键新特性，包括性能改进、表单、持久状态和更强大的 JavaScript 互操作性。&lt;/p>
&lt;h3 id="使用-csemantic-kernel-和-gemini-的混合-ai构建更智能的企业应用">使用 C#、Semantic Kernel 和 Gemini 的混合 AI：构建更智能的企业应用&lt;/h3>
&lt;p>在 C# 中编排混合 AI 工作流——将本地模型 (Phi-3) 与通过 Vertex AI 的 Gemini Pro/Ultra 融合，使用 Semantic Kernel 插件构建认知代理，并设计自适应架构。&lt;/p>
&lt;h3 id="net-aspire无痛构建云原生应用">.NET Aspire：无痛构建云原生应用&lt;/h3>
&lt;p>实践课，构建天生可扩展、弹性和可观测的云优先微服务。探索 .NET Aspire 如何消除 70% 的云样板代码。&lt;/p>
&lt;h3 id="net-aspire无混乱的云原生">.NET Aspire：无混乱的云原生&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无复杂性的云原生应用">.NET Aspire：无复杂性的云原生应用&lt;/h3>
&lt;p>无痛构建云优先微服务——一场实践性的演示驱动分享，展示 .NET Aspire 如何简化云原生开发。&lt;/p>
&lt;h3 id="power-platform从低代码到专业代码的旅程">Power Platform：从低代码到专业代码的旅程&lt;/h3>
&lt;p>在同一平台内使用 TypeScript 从低代码 Power Apps 过渡到高级专业代码组件。&lt;/p>
&lt;h3 id="智能自动化用集成-ai-转型企业应用">智能自动化：用集成 AI 转型企业应用&lt;/h3>
&lt;p>将 .NET 应用推向新高度——使用 Semantic Kernel 和 .NET Aspire 将传统逻辑与基于 AI 的推理结合。&lt;/p>
&lt;h3 id="从低代码到专业代码企业解决方案中的-ai-开发方法">从低代码到专业代码：企业解决方案中的 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 最活跃演讲者 2025&lt;/strong>&lt;/li>
&lt;li>🏆 &lt;strong>Sessionize 最活跃演讲者 2024&lt;/strong>&lt;/li>
&lt;li>🏆 &lt;strong>Sessionize 最活跃演讲者 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>