<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Hi Work Notes</title><link>https://www.hiwork.me/</link><description>Recent content on Hi Work Notes</description><generator>Hugo</generator><language>zh-cn</language><lastBuildDate>Tue, 10 Mar 2026 15:55:34 +0800</lastBuildDate><atom:link href="https://www.hiwork.me/index.xml" rel="self" type="application/rss+xml"/><item><title>.NET 10 新增功能概览</title><link>https://www.hiwork.me/posts/dotnet/dotnet-10/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/dotnet/dotnet-10/</guid><description>&lt;h1 id="net-10-新增功能概览"&gt;.NET 10 新增功能概览&lt;/h1&gt;
&lt;h2 id="类库方面的改进"&gt;类库方面的改进&lt;/h2&gt;
&lt;h3 id="用字符串比较数字排序"&gt;用字符串比较数字排序&lt;/h3&gt;
&lt;p&gt;在 .NET 10 中，&lt;code&gt;System.String&lt;/code&gt; 类新增了 &lt;code&gt;CompareAsNumbers&lt;/code&gt; 方法，用于按数字顺序比较字符串。这对于包含数字的字符串排序非常有用，例如文件名或版本号。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;StringComparer numericStringComparer = StringComparer.Create(CultureInfo.CurrentCulture, CompareOptions.NumericOrdering);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Console.WriteLine(numericStringComparer.Equals(&lt;span style="color:#e6db74"&gt;&amp;#34;02&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;2&amp;#34;&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Output: True&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;foreach&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; os &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt;[] { &lt;span style="color:#e6db74"&gt;&amp;#34;Windows 8&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;Windows 10&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;Windows 11&amp;#34;&lt;/span&gt; }.Order(numericStringComparer))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Console.WriteLine(os);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Output:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Windows 8&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Windows 10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Windows 11&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;HashSet&amp;lt;&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;&amp;gt; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt; = &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; HashSet&amp;lt;&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;&amp;gt;(numericStringComparer) { &lt;span style="color:#e6db74"&gt;&amp;#34;007&amp;#34;&lt;/span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Console.WriteLine(&lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;.Contains(&lt;span style="color:#e6db74"&gt;&amp;#34;7&amp;#34;&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Output: True&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="对十六进制字符串转换的-utf-8-支持"&gt;对十六进制字符串转换的 UTF-8 支持&lt;/h3&gt;
&lt;p&gt;.NET 10 在 Convert 类中增加了对十六进制字符串转换操作的 UTF-8 支持。 这些新方法提供了在 UTF-8 字节序列和十六进制表示形式之间转换的有效方法，而无需中间字符串分配：&lt;/p&gt;</description></item><item><title>Blazor JavaScript 互操作性</title><link>https://www.hiwork.me/posts/helloshop/webapp/blazor-javascript/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/webapp/blazor-javascript/</guid><description>&lt;h1 id="blazor-javascript-互操作性"&gt;Blazor JavaScript 互操作性&lt;/h1&gt;
&lt;h2 id="关于-javascript-位置"&gt;关于 JavaScript 位置&lt;/h2&gt;
&lt;h3 id="在--标记中加载脚本"&gt;在 &lt;!-- raw HTML omitted --&gt; 标记中加载脚本&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;head&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;script&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;js/first.js&amp;#34;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style="color:#f92672"&gt;script&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;script&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; window.&lt;span style="color:#a6e22e"&gt;jsMethod&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;methodParameter&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;script&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;head&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="在--标记中加载脚本-1"&gt;在 &lt;!-- raw HTML omitted --&gt; 标记中加载脚本&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-html" data-lang="html"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;&lt;span style="color:#f92672"&gt;body&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;script&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;src&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;js/first.js&amp;#34;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style="color:#f92672"&gt;script&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;&lt;span style="color:#f92672"&gt;script&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; window.&lt;span style="color:#a6e22e"&gt;jsMethod&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;methodParameter&lt;/span&gt;) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/&lt;span style="color:#f92672"&gt;script&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/&lt;span style="color:#f92672"&gt;body&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="组件并置"&gt;组件并置&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;组件：CountComponent.razor
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;组件：CountComponent.razor.cs
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;样式：CountComponent.razor.css
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;脚本：CountComponent.razor.js
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="从net-调用-js"&gt;从.NET 调用 JS&lt;/h3&gt;
&lt;h4 id="在-wwwroot-下创建-appjs-文件"&gt;在 wwwroot 下创建 app.js 文件&lt;/h4&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;helloWorld&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;alert&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#39;Hello World&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;add&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;a&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;b&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;a&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;b&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id="在-apprazor-中引入-js-文件"&gt;在 App.razor 中引入 JS 文件&lt;/h4&gt;
&lt;p&gt;当前 Web 项目脚本引入&lt;/p&gt;</description></item><item><title>Blazor 全球化与本地化</title><link>https://www.hiwork.me/posts/helloshop/webapp/blazor-globalization-localization/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/webapp/blazor-globalization-localization/</guid><description>&lt;h1 id="blazor-全球化与本地化"&gt;Blazor 全球化与本地化&lt;/h1&gt;
&lt;p&gt;Blazor 应用程序可以通过使用 .NET Core 的全球化和本地化功能来支持多种语言和文化。本文将介绍如何在 Blazor 应用程序中实现全球化和本地化。&lt;/p&gt;
&lt;h2 id="blazor-webassembly"&gt;Blazor WebAssembly&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;PropertyGroup&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;BlazorWebAssemblyLoadAllGlobalizationData&amp;gt;&lt;/span&gt;true&lt;span style="color:#f92672"&gt;&amp;lt;/BlazorWebAssemblyLoadAllGlobalizationData&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;/PropertyGroup&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet add package Microsoft.Extensions.Localization
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="blazor-server"&gt;Blazor Server&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.Services.AddCustomLocalization();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app.UseCustomLocalization();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Blazor 表单验证</title><link>https://www.hiwork.me/posts/helloshop/webapp/blazor-validation/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/webapp/blazor-validation/</guid><description>&lt;h1 id="blazor-表单验证"&gt;Blazor 表单验证&lt;/h1&gt;
&lt;h2 id="使用数据注解-dataannotations-验证"&gt;使用数据注解 DataAnnotations 验证&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Employee&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt; [Required(ErrorMessage = &amp;#34;Name is required&amp;#34;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; Name { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@page &lt;span style="color:#e6db74"&gt;&amp;#34;/employee&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@using System.ComponentModel.DataAnnotations
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@using Microsoft.AspNetCore.Components.Forms
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;h3&amp;gt;Employee Form&amp;lt;/h3&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;EditForm Model=&lt;span style="color:#e6db74"&gt;&amp;#34;@employee&amp;#34;&lt;/span&gt; OnValidSubmit=&lt;span style="color:#e6db74"&gt;&amp;#34;@HandleValidSubmit&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;DataAnnotationsValidator /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;ValidationSummary /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;div class=&lt;span style="color:#e6db74"&gt;&amp;#34;form-group&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;label &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;Name&amp;#34;&lt;/span&gt;&amp;gt;Name&amp;lt;/label&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;InputText id=&lt;span style="color:#e6db74"&gt;&amp;#34;Name&amp;#34;&lt;/span&gt; @bind-Value=&lt;span style="color:#e6db74"&gt;&amp;#34;employee.Name&amp;#34;&lt;/span&gt; class=&lt;span style="color:#e6db74"&gt;&amp;#34;form-control&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;ValidationMessage For=&lt;span style="color:#e6db74"&gt;&amp;#34;@(() =&amp;gt; employee.Name)&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/div&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;button type=&lt;span style="color:#e6db74"&gt;&amp;#34;submit&amp;#34;&lt;/span&gt; class=&lt;span style="color:#e6db74"&gt;&amp;#34;btn btn-primary&amp;#34;&lt;/span&gt;&amp;gt;Submit&amp;lt;/button&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/EditForm&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@code {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; Employee employee = &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; Employee();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="使用-fluentvalidation-验证"&gt;使用 FluentValidation 验证&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet add package Blazored.FluentValidation
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;EmployeeValidator&lt;/span&gt; : AbstractValidator&amp;lt;Employee&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; EmployeeValidator()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; RuleFor(x =&amp;gt; x.Name).NotEmpty().WithMessage(&lt;span style="color:#e6db74"&gt;&amp;#34;Name is required&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@page &lt;span style="color:#e6db74"&gt;&amp;#34;/employee&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@using FluentValidation
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@using FluentValidation.Results
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;h3&amp;gt;Employee Form&amp;lt;/h3&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;EditForm Model=&lt;span style="color:#e6db74"&gt;&amp;#34;@employee&amp;#34;&lt;/span&gt; OnValidSubmit=&lt;span style="color:#e6db74"&gt;&amp;#34;@HandleValidSubmit&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;FluentValidationValidator DisableAssemblyScanning=&lt;span style="color:#e6db74"&gt;&amp;#34;@true&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;ValidationSummary /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;div class=&lt;span style="color:#e6db74"&gt;&amp;#34;form-group&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;label &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt;=&lt;span style="color:#e6db74"&gt;&amp;#34;Name&amp;#34;&lt;/span&gt;&amp;gt;Name&amp;lt;/label&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;InputText id=&lt;span style="color:#e6db74"&gt;&amp;#34;Name&amp;#34;&lt;/span&gt; @bind-Value=&lt;span style="color:#e6db74"&gt;&amp;#34;employee.Name&amp;#34;&lt;/span&gt; class=&lt;span style="color:#e6db74"&gt;&amp;#34;form-control&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;ValidationMessage For=&lt;span style="color:#e6db74"&gt;&amp;#34;@(() =&amp;gt; employee.Name)&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/div&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;button type=&lt;span style="color:#e6db74"&gt;&amp;#34;submit&amp;#34;&lt;/span&gt; class=&lt;span style="color:#e6db74"&gt;&amp;#34;btn btn-primary&amp;#34;&lt;/span&gt;&amp;gt;Submit&amp;lt;/button&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/EditForm&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="自定义验证"&gt;自定义验证&lt;/h2&gt;</description></item><item><title>C# 12 中的新增功能</title><link>https://www.hiwork.me/posts/csharp/csharp-12/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/csharp/csharp-12/</guid><description>&lt;h1 id="c-12-中的新增功能"&gt;C# 12 中的新增功能&lt;/h1&gt;
&lt;h2 id="主构造函数"&gt;主构造函数&lt;/h2&gt;
&lt;p&gt;C# 12 中的一个新功能是主构造函数。主构造函数是一个类的构造函数，它在类的声明中声明，而不是在类的主体中声明。主构造函数的参数可以用于初始化类的属性。&lt;/p&gt;
&lt;h3 id="记录类型"&gt;记录类型&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;record&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Address&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; FirstName, &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; LastName);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="类类型"&gt;类类型&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Person&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; firstName, &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; lastName)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;override&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; ToString()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#e6db74"&gt;$&amp;#34;{firstName},{lastName}&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; Person(&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; firstName) : &lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;(firstName, &lt;span style="color:#e6db74"&gt;&amp;#34;hello&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="集合表达式"&gt;集合表达式&lt;/h2&gt;
&lt;p&gt;C# 12 中的另一个新功能是集合表达式。集合表达式是一种新的语法，用于初始化集合。集合表达式使用大括号，其中包含一个或多个元素初始化器。每个元素初始化器都是一个表达式，它可以是一个值，也可以是一个键值对。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Create an array:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt;[] a = [&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;6&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;8&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Create a list:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;List&amp;lt;&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;&amp;gt; b = [&lt;span style="color:#e6db74"&gt;&amp;#34;one&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;two&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;three&amp;#34;&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Create a span&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Span&amp;lt;&lt;span style="color:#66d9ef"&gt;char&lt;/span&gt;&amp;gt; c = [&lt;span style="color:#e6db74"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;c&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;d&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;e&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;f&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;h&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;i&amp;#39;&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Create a jagged 2D array:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt;[][] twoD = [[&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;], [&lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;6&lt;/span&gt;], [&lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;8&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;9&lt;/span&gt;]];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Create a jagged 2D array from variables:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt;[] row0 = [&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt;[] row1 = [&lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;6&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt;[] row2 = [&lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;8&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;9&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt;[][] twoDFromVariables = [row0, row1, row2];
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="数组索引范围"&gt;数组索引范围&lt;/h2&gt;
&lt;p&gt;数组的 0 索引与 sequence[0] 相同。 ^0 索引与 sequence[sequence.Length] 相同。 表达式 sequence[^0] 会引发异常，就像 sequence[sequence.Length] 一样。 对于任何数字 n，索引 ^n 与 sequence.Length - n 相同。&lt;/p&gt;</description></item><item><title>Copyright</title><link>https://www.hiwork.me/posts/visualstudio/copyright/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/visualstudio/copyright/</guid><description>&lt;h2 id="copyright"&gt;Copyright&lt;/h2&gt;
&lt;p&gt;Generally the copyright notice for a project in the .NET Foundation is given as
&amp;ldquo;Copyright (c) .NET Foundation and Contributors. All Rights Reserved&amp;rdquo;.
The copyright notice should be placed in the LICENSE for the project so
the beginning of the LICENSE for an MIT Licensed project would be:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;The MIT License (MIT)

Copyright (c) .NET Foundation and Contributors
All Rights Reserved

Permission is hereby granted... 
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And an Apache 2.0 Licensed project would begin&lt;/p&gt;</description></item><item><title>Frp</title><link>https://www.hiwork.me/posts/docs/frp/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/docs/frp/</guid><description/></item><item><title>Model Binding</title><link>https://www.hiwork.me/posts/helloshop/model-binding/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/model-binding/</guid><description>&lt;h2 id="模型绑定最佳实践"&gt;模型绑定最佳实践&lt;/h2&gt;
&lt;p&gt;实体对象是 EF 中的概念， 每个实体对象对应数据库中的一张表。模型对象是 MVC 中的概念，是 HTTP 请求和响应的数据结构。HTTP 请求中通过 URL 参数、表单、标头、 JSON 数据等方式传递数据，这些数据最终会被绑定为模型对象中，模型经过转换为实体对象后，被持久化到数据库中。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Product&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; Id { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; Name { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;decimal&lt;/span&gt; Price { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ProductModel&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; Name { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;decimal&lt;/span&gt; Price { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ProductController&lt;/span&gt; : Controller
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;readonly&lt;/span&gt; ShopDbContext _context;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; ProductController(ShopDbContext context)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; _context = context;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt; [HttpPost]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; Task&amp;lt;IActionResult&amp;gt; Create(ProductModel model)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (!ModelState.IsValid)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; BadRequest(ModelState);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; product = &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; Product
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Name = model.Name,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Price = model.Price
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; _context.Products.Add(product);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; _context.SaveChangesAsync();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; CreatedAtAction(nameof(Get), &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; { id = product.Id }, product);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;针对不同 HTTP 操作，应该使用不同的模型对象，比如创建模型、更新模型、查询模型、删除模型等，这样的好处是可以更好的区分模型对象的职责，比如创建模型只需要包含创建所需的字段，更新模型只需要包含更新所需的字段，查询模型只需要包含查询所需的字段，删除模型只需要包含删除所需的字段，职责单一，维护性好，还可以针对不同的模型提供不同的验证规则。&lt;/p&gt;</description></item><item><title>Ordering Entities</title><link>https://www.hiwork.me/posts/helloshop/ordering-entities/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/ordering-entities/</guid><description/></item><item><title>PostgreSQL 高可用性和读写分离</title><link>https://www.hiwork.me/posts/helloshop/postgresql-master-slave/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/postgresql-master-slave/</guid><description>&lt;h1 id="postgresql-高可用性和读写分离"&gt;PostgreSQL 高可用性和读写分离&lt;/h1&gt;
&lt;h2 id="基本概念"&gt;基本概念&lt;/h2&gt;
&lt;p&gt;一个操作系统可以安装多个 PostgreSQL 实例，每个实例都有自己的配置文件、数据目录和端口号。每个实例中可以创建多个数据库，每个数据库中可以创建多个表。使用 Docker 可以方便的创建多个 PostgreSQL 实例。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://oss.xcode.me/notes/helloshop/postgresql-host.svg" alt="postgresql-host"&gt;&lt;/p&gt;
&lt;h2 id="安装主从服务器"&gt;安装主从服务器&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker run --name postgres1 -e POSTGRES_PASSWORD&lt;span style="color:#f92672"&gt;=&lt;/span&gt;postgres -d -p 5431:5432 -v &lt;span style="color:#e6db74"&gt;${&lt;/span&gt;pwd&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;/postgres1/data:/var/lib/postgresql/data postgres
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker run --name postgres2 -e POSTGRES_PASSWORD&lt;span style="color:#f92672"&gt;=&lt;/span&gt;postgres -d -p 5432:5432 -v &lt;span style="color:#e6db74"&gt;${&lt;/span&gt;pwd&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;/postgres2/data:/var/lib/postgresql/data postgres
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="逻辑复制"&gt;逻辑复制&lt;/h2&gt;
&lt;p&gt;逻辑复制是根据复制标识（通常是主键）复制数据对象及其更改的一种方法。 我们使用术语逻辑与物理复制相比，逻辑复制使用发布和订阅模型， 其中一个或多个订阅者订阅发布者节点上的一个或多个发布。 订阅者从他们订阅的发布中提取数据， 并可能随后重新发布数据以允许级联复制或更复杂的配置。&lt;/p&gt;
&lt;h3 id="创建主数据库和从数据库"&gt;创建主数据库和从数据库&lt;/h3&gt;
&lt;p&gt;在 postgres1 中创建数据库 mydb1 数据库。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;CREATE&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;DATABASE&lt;/span&gt; mydb1;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在 postgres2 中创建数据库 mydb2 数据库。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;CREATE&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;DATABASE&lt;/span&gt; mydb2;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在 mydb1 和 mydb2 数据库中创建结构相同的表 mytable。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;CREATE&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;TABLE&lt;/span&gt; mytable (id int &lt;span style="color:#66d9ef"&gt;PRIMARY&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;KEY&lt;/span&gt;, name text &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="将服务器日志级别设置为逻辑"&gt;将服务器日志级别设置为逻辑&lt;/h3&gt;
&lt;p&gt;在 postgres1 的 mydb1 数据库中设置服务器日志级别为逻辑。&lt;/p&gt;</description></item><item><title>RabbitMQ Management</title><link>https://www.hiwork.me/posts/mqtt/rabbitmq-management-mqtt/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/mqtt/rabbitmq-management-mqtt/</guid><description>&lt;h1 id="rabbitmq-management"&gt;RabbitMQ Management&lt;/h1&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rabbitmq-plugins enable rabbitmq_management
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;Default User：guest/guest
Add User：test/test
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href="http://192.168.0.202:15672"&gt;http://192.168.0.202:15672&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="http://mqtt.test:15672"&gt;http://mqtt.test:15672&lt;/a&gt;&lt;/p&gt;
&lt;h1 id="rabbitmq-management-mqtt"&gt;RabbitMQ Management MQTT&lt;/h1&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rabbitmq-plugins enable rabbitmq_mqtt
rabbitmq-plugins disable rabbitmq_mqtt

rabbitmq-plugins enable rabbitmq_web_mqtt
rabbitmq-plugins disable rabbitmq_web_mqtt

rabbitmq-plugins enable_feature_flag all
rabbitmq-plugins disable_feature_flag all
&lt;/code&gt;&lt;/pre&gt;&lt;h1 id="rabbitmq-auth-backend-http"&gt;Rabbitmq Auth Backend Http&lt;/h1&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;rabbitmq-plugins enable rabbitmq_auth_backend_http
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;修改 rabbitmq.conf 配置文件，位置 %AppData%\RabbitMQ&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;auth_backends.1 = http
auth_http.http_method = post
auth_http.user_path = http://some-server/api/RabbitMqAuth/User
auth_http.vhost_path = http://some-server/api/RabbitMqAuth/Vhost
auth_http.resource_path = http://some-server/api/RabbitMqAuth/Resource
auth_http.topic_path = http://some-server/api/RabbitMqAuth/Topic
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href="https://github.com/rabbitmq/rabbitmq-server/tree/main/deps/rabbitmq_auth_backend_http"&gt;RabbitMQ server to perform authentication&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Readme</title><link>https://www.hiwork.me/posts/helloshop/readme/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/readme/</guid><description>&lt;p&gt;朋友们，零度新一代基架今日正式开始搭建。我们非常激动地宣布，新一代基架起名为 HelloShop 项目， 这个基架将演示新一代 .NET 技术栈开发架构，之所以起名为 HelloShop 是以为简单的商店应用能够演示出一个系统的所有技术，当然，这个商城系统也可以是其它系统，只要你能够理解这个基架的设计思想，你就能够快速搭建一个系统，以下文档由 AI 生成，可能会有一些语法错误，我们会在后期进行修正。&lt;/p&gt;
&lt;h3 id="业务场景"&gt;业务场景&lt;/h3&gt;
&lt;p&gt;本商店应用旨在演示一个系统的所有技术，帮助开发者以最小的依赖和代码搭建一个系统，将不同的技术置于微服务架构中。开发者可以根据自己的业务需求快速复用代码并搭建一个系统，而不是花费大量时间在一个通用的框架上。&lt;/p&gt;
&lt;p&gt;我们的目标是提供细粒度的微服务架构和最佳实践，让您可以自行组合各种技术。统一的身份认证和授权系统，服务发现，负载均衡，容错，分布式跟踪，分布式缓存，分布式事务，分布式消息，分布式日志，分布式配置，分布式定时任务等等。&lt;/p&gt;
&lt;p&gt;此外，我们还提供了界面，包括基于 Blazor 的 Web 应用和基于 MAUI 的混合应用，可以在不同的平台上运行，包括桌面应用，安卓应用和 IOS 应用。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.hiwork.me/images/helloshop/architecture.svg" alt="技术架构"&gt;{class=&amp;ldquo;img-responsive w-100&amp;rdquo;}&lt;/p&gt;
&lt;h3 id="所含技术"&gt;所含技术&lt;/h3&gt;
&lt;p&gt;Visual Studio 2022、 .NET 8、C# 12.0、Aspire、ASP.NET Core，EF Core、WebApi、gPRC、Blazor、MAUI、PostgreSQL、MonngDB、Redis、SignalR、Identity、Orleans、日志记录、密钥管理、后台定时任务、服务发现、发布订阅、分布式跟踪、健康检查、性能指标探测、配置管理、容器化技术、单元测试和集成测试、基于 Roslyn 的源代码自动生成器和增量生成器。&lt;/p&gt;
&lt;h3 id="仓库结构"&gt;仓库结构&lt;/h3&gt;
&lt;p&gt;在仓库的结构上，我们跟随了微软惯用的风格，目录结构如下：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;assets&lt;/code&gt; 静态资源，包括图片，图标，视频，音频等。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;build&lt;/code&gt; 构建脚本，包括编译脚本，打包脚本，发布脚本等。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;docs&lt;/code&gt; 相关文档，包括设计文档，架构文档，开发文档，部署文档等。`&lt;/p&gt;
&lt;p&gt;&lt;code&gt;samples&lt;/code&gt; 演示示例，包括代码示例，配置示例，数据示例，文档示例等。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;src&lt;/code&gt; 源代码，包括源代码，配置文件，资源文件，脚本文件等。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tests&lt;/code&gt; 测试代码，包括单元测试，集成测试，端到端测试等。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tools&lt;/code&gt; 项目所使用的一些工具。&lt;/p&gt;
&lt;h3 id="代码仓库"&gt;代码仓库&lt;/h3&gt;
&lt;!-- raw HTML omitted --&gt;
&lt;p&gt;国外仓库：&lt;a href="https://github.com/bit365/hello-shop"&gt;https://github.com/bit365/hello-shop&lt;/a&gt;{target=&amp;quot;_blank&amp;quot;}&lt;/p&gt;
&lt;p&gt;国内仓库：&lt;a href="https://github.com/bit365/hello-shop"&gt;https://gitee.com/bit365/hello-shop&lt;/a&gt;{target=&amp;quot;_blank&amp;quot;}&lt;/p&gt;
&lt;h3 id="后续计划"&gt;后续计划&lt;/h3&gt;
&lt;p&gt;我们刚刚还注册了 helloshopnet.com 和 helloshopnet.cn 两个域名用于后期的基架部署，也算是给 HelloShop 安一个家，目前还是正在备案。&lt;/p&gt;
&lt;h3 id="技术探讨"&gt;技术探讨&lt;/h3&gt;
&lt;p&gt;本套基架会有一些配套视频在 &lt;a href="https://www.xcode.me"&gt; www.xcode.me&lt;/a&gt; 上发布。如果你想要获取最新的视频可以关注我们的微信订阅号 zerostack，我们会在公众号上发布最新的动态，如果你觉得这套基架对你有帮助，你可以给我们一个 Star，这是对我们最大的鼓励。&lt;/p&gt;</description></item><item><title>使用 .NET 迁移助手升级项目</title><link>https://www.hiwork.me/posts/dotnet/upgrade-assistant/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/dotnet/upgrade-assistant/</guid><description>&lt;h1 id="使用-net-迁移助手升级项目"&gt;使用 .NET 迁移助手升级项目&lt;/h1&gt;
&lt;h2 id="什么是-net-升级助手"&gt;什么是 .NET 升级助手？&lt;/h2&gt;
&lt;p&gt;.NET 升级助手是一个工具，可帮助您将老项目迁移到最新 .NET 框架的工具，它可以帮助您：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;识别项目中的依赖项和 API 使用情况。&lt;/li&gt;
&lt;li&gt;为项目提供 .NET 目标框架的建议。&lt;/li&gt;
&lt;li&gt;提供升级项目所需的代码更改。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="在-visual-studio-中使用-net-升级助手"&gt;在 Visual Studio 中使用 .NET 升级助手&lt;/h2&gt;
&lt;p&gt;可以使用 Visual Studio 安装 .NET 升级助手扩展。&lt;/p&gt;
&lt;h2 id="在命令行中使用-net-升级助手"&gt;在命令行中使用 .NET 升级助手&lt;/h2&gt;
&lt;p&gt;在命令行中使用 .NET 安装升级助手，需要安装 .NET 升级助手 CLI 工具。&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;dotnet tool install -g upgrade-assistant
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;将工具迁移工具升级到最新版本&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;dotnet tool update -g upgrade-assistant
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在项目所在的文件夹中运行以下命令：&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;upgrade-assistant --apply-updates
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;即可升级项目到最新版本。&lt;/p&gt;</description></item><item><title>使用 Aspire 启动分布式微服务</title><link>https://www.hiwork.me/posts/helloshop/aspire-host/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/aspire-host/</guid><description>&lt;h1 id="使用-aspire-启动分布式微服务"&gt;使用 Aspire 启动分布式微服务&lt;/h1&gt;
&lt;h2 id="准备数据库"&gt;准备数据库&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker run --name postgres -e POSTGRES_PASSWORD&lt;span style="color:#f92672"&gt;=&lt;/span&gt;postgres -e TZ&lt;span style="color:#f92672"&gt;=&lt;/span&gt;Asia/Shanghai -d -p 5432:5432 postgres
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker run --name pgadmin -e PGADMIN_DEFAULT_EMAIL&lt;span style="color:#f92672"&gt;=&lt;/span&gt;test@test.com -e PGADMIN_DEFAULT_PASSWORD&lt;span style="color:#f92672"&gt;=&lt;/span&gt;test -e TZ&lt;span style="color:#f92672"&gt;=&lt;/span&gt;Asia/Shanghai -d -p 5050:80 dpage/pgadmin4
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="初始化-dapr-运行时"&gt;初始化 Dapr 运行时&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dapr init -s --from-dir C:&lt;span style="color:#ae81ff"&gt;\d&lt;/span&gt;apr
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="延长等待-rabbitmq-启动时间"&gt;延长等待 RabbitMQ 启动时间&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; IResourceBuilder&amp;lt;IDaprSidecarResource&amp;gt; WithReference(&lt;span style="color:#66d9ef"&gt;this&lt;/span&gt; IResourceBuilder&amp;lt;IDaprSidecarResource&amp;gt; builder, IResourceBuilder&amp;lt;IResourceWithConnectionString&amp;gt; resourceBuilder, &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; waitInSeconds = &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="修改-mediatr-依赖注入"&gt;修改 MediatR 依赖注入&lt;/h2&gt;
&lt;p&gt;之前使用 options.AddBehavior 方法注册行为，现在使用 options.AddOpenBehavior 方法注册行为。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.Services.AddMediatR(options =&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.AddOpenBehavior(&lt;span style="color:#66d9ef"&gt;typeof&lt;/span&gt;(LoggingBehavior&amp;lt;,&amp;gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.AddOpenBehavior(&lt;span style="color:#66d9ef"&gt;typeof&lt;/span&gt;(ValidatorBehavior&amp;lt;,&amp;gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.AddOpenBehavior(&lt;span style="color:#66d9ef"&gt;typeof&lt;/span&gt;(TransactionBehavior&amp;lt;,&amp;gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="使用分布式事件的微服务需要依赖注入上下文"&gt;使用分布式事件的微服务需要依赖注入上下文&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.Services.AddHttpContextAccessor();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="azure-data-studio-删除数据库"&gt;Azure Data Studio 删除数据库&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;-- 终止所有连接到目标数据库的会话
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt; pg_terminate_backend(pg_stat_activity.pid)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; pg_stat_activity
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;WHERE&lt;/span&gt; pg_stat_activity.datname &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;my_database&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;AND&lt;/span&gt; pid &lt;span style="color:#f92672"&gt;&amp;lt;&amp;gt;&lt;/span&gt; pg_backend_pid();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;-- 删除数据库
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;DROP&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;DATABASE&lt;/span&gt; my_database;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="identityservice-演示数据生成前自动创建数据库"&gt;IdentityService 演示数据生成前自动创建数据库&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; serviceProvider.GetRequiredService&amp;lt;IdentityServiceDbContext&amp;gt;().Database.EnsureCreatedAsync(cancellationToken);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="统一使用-datetimeoffset-类型而不是-datetime-类型"&gt;统一使用 DateTimeOffset 类型而不是 DateTime 类型&lt;/h2&gt;
&lt;p&gt;使用 UTC 时间的优点是可以在不同的时区之间进行转换，而不会丢失时间信息，让程序更好的处理时间，有利于跨时区的应用程序国际化本地化。&lt;/p&gt;</description></item><item><title>使用 Azure Data Studio 管理数据库</title><link>https://www.hiwork.me/posts/helloshop/azure-data-studio/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/azure-data-studio/</guid><description>&lt;h1 id="使用-azure-data-studio-管理数据库"&gt;使用 Azure Data Studio 管理数据库&lt;/h1&gt;
&lt;p&gt;Azure Data Studio 是一个轻量级的跨平台数据库工具，提供了一个现代化的用户界面，可以帮助用户更轻松地管理数据库。&lt;/p&gt;
&lt;h2 id="全部代码开源"&gt;全部代码开源&lt;/h2&gt;
&lt;p&gt;Azure Data Studio 是一个开源项目，全部代码都托管在 GitHub 上，用户可以自由查看源代码，也可以参与到项目中来。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/Microsoft/azuredatastudio"&gt;https://github.com/Microsoft/azuredatastudio&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="可管理的数据库"&gt;可管理的数据库&lt;/h2&gt;
&lt;p&gt;Azure Data Studio 支持多种数据库，包括 SQL Server、PostgreSQL、MySQL 、TimescaleDB、Oracle 等，通过安装相应的扩展，可以支持更多的数据库。&lt;/p&gt;
&lt;h2 id="带有智能感知的查询编辑器"&gt;带有智能感知的查询编辑器&lt;/h2&gt;
&lt;p&gt;丰富的 SQL 编辑器、IntelliSense、关键字完成、代码片段、代码导航和源代码管理集成 (Git)）提供一种基于键盘的新式 SQL 编码体验。&lt;/p&gt;
&lt;h2 id="智能-sql-代码片段"&gt;智能 SQL 代码片段&lt;/h2&gt;
&lt;p&gt;Azure Data Studio 提供了大量的 SQL 代码片段，可以帮助用户快速编写 SQL 代码。&lt;/p&gt;
&lt;h2 id="使用服务器组"&gt;使用服务器组&lt;/h2&gt;
&lt;p&gt;借助服务器组，可以分类管理服务器，方便用户更轻松地管理多个服务器。&lt;/p&gt;
&lt;h2 id="使用集成终端"&gt;使用集成终端&lt;/h2&gt;
&lt;p&gt;用户界面中的“集成终端”窗口中使用常用的命令行工具，例如：sqlcmd 命令。&lt;/p&gt;
&lt;h2 id="使用表设计器"&gt;使用表设计器&lt;/h2&gt;
&lt;p&gt;Azure Data Studio 提供了一个表设计器，可以帮助用户更轻松地设计表。&lt;/p&gt;
&lt;h2 id="备份和还原数据库"&gt;备份和还原数据库&lt;/h2&gt;
&lt;p&gt;Azure Data Studio 提供了备份和还原数据库的功能，可以帮助用户更轻松地备份和还原数据库。&lt;/p&gt;
&lt;h2 id="使用-copilot-插件"&gt;使用 Copilot 插件&lt;/h2&gt;
&lt;p&gt;Copilot 插件可以借助强大的 AI 帮助用户更轻松地编写 SQL 代码，提高编码效率。&lt;/p&gt;
&lt;h2 id="可使用-git-管理-sql-脚本"&gt;可使用 Git 管理 SQL 脚本&lt;/h2&gt;
&lt;p&gt;Azure Data Studio 集成了 Git，可以帮助用户更轻松地管理 SQL 脚本。&lt;/p&gt;</description></item><item><title>使用 CQRS 模式实现订单服务</title><link>https://www.hiwork.me/posts/helloshop/ordering-service/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/ordering-service/</guid><description>&lt;h1 id="使用-cqrs-模式实现订单服务"&gt;使用 CQRS 模式实现订单服务&lt;/h1&gt;
&lt;p&gt;CQRS 是命令和查询责任分离的英文缩写，它是一种将读取操作和更新操作分离的模式，查询返回结果，不改变系统的状态，没有副作用。命令执行更改系统状态，幂等性是指相同的命令多次执行，结果是一样的，不会有副作用。&lt;/p&gt;
&lt;h2 id="为什么要使用-cqrs-模式"&gt;为什么要使用 CQRS 模式&lt;/h2&gt;
&lt;p&gt;在传统的体系结构中，使用同一数据模型查询和更新数据库。 这十分简单，非常适用于基本的 CRUD 操作。&lt;/p&gt;
&lt;p&gt;CQRS 将读取和写入分离到不同的模型，使用命令来更新数据，使用查询来读取数据。读取存储可以是写入存储的只读副本，或者读取和写入存储可以具有完全不同的结构。 使用多个只读副本可以提高查询性能，尤其是在只读副本靠近应用程序实例的分布式方案中。&lt;/p&gt;
&lt;h2 id="使用-mediatr-实现-cqrs-中的-command-模式"&gt;使用 MediatR 实现 CQRS 中的 Command 模式&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://oss.xcode.me/notes/helloshop/ordering-service-flow.svg" alt="ordering-service-flow"&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet add package MediatR
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CreateOrderCommand&lt;/span&gt; : IRequest&amp;lt;&lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; Product { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; Quantity { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CreateOrderCommandHandler1&lt;/span&gt; : IRequestHandler&amp;lt;CreateOrderCommand, &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; Task&amp;lt;&lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt;&amp;gt; Handle(CreateOrderCommand request, CancellationToken cancellationToken)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; Task.FromResult(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CreateOrderCommandHandler2&lt;/span&gt; : IRequestHandler&amp;lt;CreateOrderCommand, &lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; Task&amp;lt;&lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt;&amp;gt; Handle(CreateOrderCommand request, CancellationToken cancellationToken)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; Task.FromResult(&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrdersController&lt;/span&gt; : ControllerBase
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;readonly&lt;/span&gt; IMediator _mediator;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; OrdersController(IMediator mediator)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; _mediator = mediator;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt; [HttpPost]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; Task&amp;lt;IActionResult&amp;gt; CreateOrder(CreateOrderCommand command)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; orderId = &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; _mediator.Send(command);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; Ok(orderId);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.Services.AddMediatR(options =&amp;gt; options.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()));
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="使用-mediator-请求管道处理-cqrs-中的命令"&gt;使用 Mediator 请求管道处理 CQRS 中的命令&lt;/h2&gt;
&lt;p&gt;=&lt;/p&gt;</description></item><item><title>使用 Dapr 实现分布式事件总线</title><link>https://www.hiwork.me/posts/helloshop/distributed-events/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/distributed-events/</guid><description>&lt;h1 id="使用-dapr-实现分布式事件总线"&gt;使用 Dapr 实现分布式事件总线&lt;/h1&gt;
&lt;h2 id="抽象分布式总线"&gt;抽象分布式总线&lt;/h2&gt;
&lt;p&gt;使用设计模式，将分布式总线的使用方式进行抽象，定义了总线的基本功能。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://oss.xcode.me/notes/helloshop/event-bus-abstraction.svg" alt="event-bus-abstraction"&gt;&lt;/p&gt;
&lt;h2 id="基于事件总线的组件"&gt;基于事件总线的组件&lt;/h2&gt;
&lt;p&gt;使用组件化的方式实现分布式事件总线，将总线的实现细节封装在组件中。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://oss.xcode.me/notes/helloshop/event-bus-component.svg" alt="event-bus-component"&gt;&lt;/p&gt;
&lt;p&gt;基于 RabbbitMQ 实现：https://github.com/dotnet/eShop/tree/main/src/EventBusRabbitMQ&lt;/p&gt;
&lt;h2 id="事件总线的-dapr-实现"&gt;事件总线的 Dapr 实现&lt;/h2&gt;
&lt;p&gt;Dapr 是一个开源的分布式应用程序运行时，发布和订阅模块使微服务能够使用事件驱动架构的消息相互通信，Dapr 提供了一个事件总线的实现，使用 Dapr 发布事件，提供 Dapr 订阅终结点，将消息路由到不同的事件处理程序。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://oss.xcode.me/notes/helloshop/event-bus-endpoint.svg" alt="event-bus-endpoint"&gt;&lt;/p&gt;</description></item><item><title>使用 Dpar 发布订阅配置分布式事件</title><link>https://www.hiwork.me/posts/helloshop/distributed-event-dapr/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/distributed-event-dapr/</guid><description>&lt;h1 id="使用-dpar-发布订阅配置分布式事件"&gt;使用 Dpar 发布订阅配置分布式事件&lt;/h1&gt;
&lt;h2 id="dapr-技术架构"&gt;Dapr 技术架构&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://docs.dapr.io/concepts/overview"&gt;https://docs.dapr.io/concepts/overview&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="安装-dapr-cli-脚手架工具"&gt;安装 Dapr CLI 脚手架工具&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://docs.dapr.io/zh-hans/getting-started/install-dapr-cli"&gt;https://docs.dapr.io/zh-hans/getting-started/install-dapr-cli&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="基于容器初始化-dapr-运行时"&gt;基于容器初始化 Dapr 运行时&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://docs.dapr.io/zh-hans/getting-started/install-dapr-selfhost"&gt;https://docs.dapr.io/zh-hans/getting-started/install-dapr-selfhost&lt;/a&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dapr init
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="不用容器离线初始化-dapr-运行时"&gt;不用容器离线初始化 Dapr 运行时&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dapr init -s
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="使用离线包初始化-dapr-运行时"&gt;使用离线包初始化 Dapr 运行时&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://docs.dapr.io/zh-hans/operations/hosting/self-hosted/self-hosted-airgap/"&gt;https://docs.dapr.io/zh-hans/operations/hosting/self-hosted/self-hosted-airgap/&lt;/a&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dapr init --from-dir C:&lt;span style="color:#ae81ff"&gt;\d&lt;/span&gt;aprbundle
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="打开默认初始化目录"&gt;打开默认初始化目录&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;explorer &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;$env&lt;span style="color:#e6db74"&gt;:USERPROFILE\.dapr&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;explorer &lt;span style="color:#e6db74"&gt;&amp;#34;%USERPROFILE%\.dapr&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="使用-dashboard-查看-dapr-运行状态"&gt;使用 Dashboard 查看 Dapr 运行状态&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dapr dashboard
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="发布订阅配置分布式事件"&gt;发布订阅配置分布式事件&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/components/redis-pubsub.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="基于-redis-发布订阅配置"&gt;基于 Redis 发布订阅配置&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;apiVersion&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;dapr.io/v1alpha1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;kind&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Component&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;pubsub&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;type&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;pubsub.redis&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;version&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;v1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;redisHost&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;value&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;localhost:6379&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;redisPassword&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;value&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="基于-rabbitmq-发布订阅配置"&gt;基于 RabbitMQ 发布订阅配置&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;/components/rabbitmq-pubsub.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;apiVersion&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;dapr.io/v1alpha1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;kind&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;Component&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;pubsub&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;type&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;pubsub.rabbitmq&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;version&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;v1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;host&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;value&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;amqp://guest:guestpwd@localhost:5672&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;durable&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;value&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;false&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;deletedWhenUnused&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;value&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;false&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;autoAck&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;value&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;false&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;reconnectWait&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;value&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;0&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f92672"&gt;name&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;concurrency&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;value&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;parallel&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="使用-dapr-命令启动-sidecar-和应用程序"&gt;使用 Dapr 命令启动 Sidecar 和应用程序&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dapr run --app-id myapp --dapr-http-port &lt;span style="color:#ae81ff"&gt;3500&lt;/span&gt; --dapr-grpc-port &lt;span style="color:#ae81ff"&gt;50001&lt;/span&gt; --app-port &lt;span style="color:#ae81ff"&gt;5000&lt;/span&gt; --log-level debug --config ./configuration/config.yaml --components-path ./components dotnet run
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="使用-aspire-命令启动-sidecar-和应用程序"&gt;使用 Aspire 命令启动 Sidecar 和应用程序&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet add package Aspire.Hosting.Dapr
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; pubsub = builder.AddDaprPubSub(&lt;span style="color:#e6db74"&gt;&amp;#34;pubsub&amp;#34;&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; DaprComponentOptions { LocalPath = &lt;span style="color:#e6db74"&gt;&amp;#34;./DaprComponents/&amp;#34;&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; productService = builder.AddProject&amp;lt;Projects.HelloWorld_ProductService&amp;gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;productservice&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .WithReference(identityService)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .WithDaprSidecar()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .WithReference(pubsub);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="使用-rabbitmq-发布订阅配置"&gt;使用 RabbitMQ 发布订阅配置&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;var username &lt;span style="color:#f92672"&gt;=&lt;/span&gt; builder.AddParameter&lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;username&amp;#34;&lt;/span&gt;, secret: true&lt;span style="color:#f92672"&gt;)&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;var password &lt;span style="color:#f92672"&gt;=&lt;/span&gt; builder.AddParameter&lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;password&amp;#34;&lt;/span&gt;, secret: true&lt;span style="color:#f92672"&gt;)&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;var messaging &lt;span style="color:#f92672"&gt;=&lt;/span&gt; builder.AddRabbitMQ&lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;messaging&amp;#34;&lt;/span&gt;, username, password&lt;span style="color:#f92672"&gt;)&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;// Service consumption
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.AddProject&amp;lt;Projects.ExampleProject&amp;gt;&lt;span style="color:#f92672"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .WithReference&lt;span style="color:#f92672"&gt;(&lt;/span&gt;messaging&lt;span style="color:#f92672"&gt;)&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>使用 Github Copilot 自动编码</title><link>https://www.hiwork.me/posts/copilot/github-copilot/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/copilot/github-copilot/</guid><description>&lt;h1 id="使用-github-copilot-自动编码"&gt;使用 Github Copilot 自动编码&lt;/h1&gt;
&lt;h2 id="什么是-github-copilot"&gt;什么是 Github Copilot&lt;/h2&gt;
&lt;p&gt;Github Copilot 是 Github 与 OpenAI 合作推出的一款基于人工智能的代码自动补全工具。它可以根据上下文提示，自动生成代码片段，帮助开发者提高编码效率。&lt;/p&gt;
&lt;h2 id="如何使用-github-copilot"&gt;如何使用 Github Copilot&lt;/h2&gt;
&lt;p&gt;Github Copilot 作为插件的在 Visual Studio Code 和 Visual Studio 中使用。安装插件后，即可在编码过程中使用。&lt;/p&gt;
&lt;h2 id="github-copilot-是收费的吗"&gt;Github Copilot 是收费的吗&lt;/h2&gt;
&lt;p&gt;Github Copilot 是收费的，但是在 2021 年 7 月 1 日至 2022 年 6 月 30 日期间，Github Copilot 将免费提供给所有用户使用。&lt;/p&gt;</description></item><item><title>使用 gRPC 实现购物车服务</title><link>https://www.hiwork.me/posts/helloshop/basket-service-grpc/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/basket-service-grpc/</guid><description>&lt;h1 id="使用-grpc-实现购物车服务"&gt;使用 gRPC 实现购物车服务&lt;/h1&gt;
&lt;h2 id="什么是-grpc-通信框架"&gt;什么是 gRPC 通信框架&lt;/h2&gt;
&lt;p&gt;gRPC 是一个高性能、开源和通用的 RPC 框架，由 Google 开发，基于 HTTP/2 协议，支持多种语言（如 Go、Java、Python、C++、Node.js、Ruby、C#、Objective-C、PHP 和 Dart）。&lt;/p&gt;
&lt;p&gt;gRPC 使用 Protocol Buffers 作为接口定义语言（IDL），Protocol Buffers 是一种轻便高效的结构化数据序列化方法，类似于 XML 或 JSON，但更小、更快、更简单。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://oss.xcode.me/notes/helloshop/basket-service-grpc.svg" alt="basket-service-grpc"&gt;&lt;/p&gt;
&lt;h2 id="aspnet-core-中使用-grpc-实现服务"&gt;ASP.NET Core 中使用 gRPC 实现服务&lt;/h2&gt;
&lt;p&gt;定义一个 gRPC 服务，需要创建一个 gRPC 服务定义文件（.proto 文件），然后使用 gRPC 工具生成服务端和客户端代码。&lt;/p&gt;
&lt;h2 id="带有-openapi-的-grpc-服务"&gt;带有 OpenAPI 的 gRPC 服务&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet add package Microsoft.AspNetCore.Grpc.Swagger
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;IncludeHttpRuleProtos&amp;gt;&lt;/span&gt;true&lt;span style="color:#f92672"&gt;&amp;lt;/IncludeHttpRuleProtos&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; builder = WebApplication.CreateBuilder(args);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.Services.AddGrpc().AddJsonTranscoding();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.Services.AddGrpcSwagger();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.Services.AddSwaggerGen(c =&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; c.SwaggerDoc(&lt;span style="color:#e6db74"&gt;&amp;#34;v1&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; OpenApiInfo { Title = &lt;span style="color:#e6db74"&gt;&amp;#34;gRPC transcoding&amp;#34;&lt;/span&gt;, Version = &lt;span style="color:#e6db74"&gt;&amp;#34;v1&amp;#34;&lt;/span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; app = builder.Build();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app.UseSwagger();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (app.Environment.IsDevelopment())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; app.UseSwaggerUI(c =&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; c.SwaggerEndpoint(&lt;span style="color:#e6db74"&gt;&amp;#34;/swagger/v1/swagger.json&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;My API V1&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app.MapGrpcService&amp;lt;GreeterService&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app.Run();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-proto" data-lang="proto"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// My amazing greeter service.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;service&lt;/span&gt; Greeter {&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Sends a greeting.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;rpc&lt;/span&gt; SayHello (HelloRequest) &lt;span style="color:#66d9ef"&gt;returns&lt;/span&gt; (HelloReply) {&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;option&lt;/span&gt; (google.api.http) &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; get&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;/v1/greeter/{name}&amp;#34;&lt;/span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;message&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;HelloRequest&lt;/span&gt; {&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Name to say hello to.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; name &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;message&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;HelloReply&lt;/span&gt; {&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Hello reply message.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;message&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="单元测试-grpc-服务"&gt;单元测试 gRPC 服务&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet add package moq
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;GreeterServiceTests&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt; [Fact]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; Task SayHello_ReturnsHello()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Arrange&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; mockLogger = &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; Mock&amp;lt;ILogger&amp;lt;GreeterService&amp;gt;&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; service = &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; GreeterService(mockLogger.Object);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; request = &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; HelloRequest { Name = &lt;span style="color:#e6db74"&gt;&amp;#34;Unit Test&amp;#34;&lt;/span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; context = &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; ServerCallContext();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Act&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; reply = &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; service.SayHello(request, context);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Assert&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Assert.Equal(&lt;span style="color:#e6db74"&gt;&amp;#34;Hello Unit Test&amp;#34;&lt;/span&gt;, reply.Message);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="集成测试-grpc-服务"&gt;集成测试 gRPC 服务&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet add package Microsoft.AspNetCore.TestHost
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;GreeterServiceIntegrationTests&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;readonly&lt;/span&gt; TestServer _server;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;readonly&lt;/span&gt; Greeter.GreeterClient _client;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; GreeterServiceIntegrationTests()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; _server = &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; TestServer(&lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; WebHostBuilder()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .ConfigureServices(services =&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; services.AddGrpc();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .Configure(app =&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; app.UseRouting();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; app.UseEndpoints(endpoints =&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; endpoints.MapGrpcService&amp;lt;GreeterService&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; _client = &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; Greeter.GreeterClient(_server.CreateGrpcChannel());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt; [Fact]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; Task SayHello_ReturnsHello()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Arrange&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; request = &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; HelloRequest { Name = &lt;span style="color:#e6db74"&gt;&amp;#34;Integration Test&amp;#34;&lt;/span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Act&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; reply = &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; _client.SayHelloAsync(request);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Assert&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Assert.Equal(&lt;span style="color:#e6db74"&gt;&amp;#34;Hello Integration Test&amp;#34;&lt;/span&gt;, reply.Message);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>使用 HybridCache 混合缓存库</title><link>https://www.hiwork.me/posts/helloshop/hybrid-cache-library/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/hybrid-cache-library/</guid><description>&lt;h1 id="使用-hybridcache-混合缓存库"&gt;使用 HybridCache 混合缓存库&lt;/h1&gt;
&lt;p&gt;混合缓存可以同时使用内存缓存和分布式缓存，它可以提高缓存的命中率，减少对分布式缓存的访问，从而提高性能。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当从缓存中获取数据时，它首先会从内存缓存中获取数据，如果内存缓存中没有数据，它会从分布式缓存中获取数据。&lt;/li&gt;
&lt;li&gt;当向缓存中写入数据时，它会同时向内存缓存和分布式缓存中写入数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Service 1 ----&amp;gt; HybridCache ----&amp;gt; MemoryCache ----&amp;gt; DistributedCache
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;a href="https://learn.microsoft.com/zh-cn/azure/architecture/best-practices/caching"&gt;https://learn.microsoft.com/zh-cn/azure/architecture/best-practices/caching&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="安装程序包"&gt;安装程序包&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet add package Microsoft.Extensions.Caching.Hybrid
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="配置服务"&gt;配置服务&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.Services.AddHybridCache();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="使用服务"&gt;使用服务&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;SomeService&lt;/span&gt;(HybridCache cache)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; HybridCache _cache = cache;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; Task&amp;lt;&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;&amp;gt; GetSomeInfoAsync(&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; name, &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; id, CancellationToken token = &lt;span style="color:#66d9ef"&gt;default&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; _cache.GetOrCreateAsync( &lt;span style="color:#e6db74"&gt;$&amp;#34;{name}-{id}&amp;#34;&lt;/span&gt;,&lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; cancel =&amp;gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; GetDataFromTheSourceAsync(name, id, cancel),cancellationToken: token );
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; Task&amp;lt;&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;&amp;gt; GetDataFromTheSourceAsync(&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; name, &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; id, CancellationToken token)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; someInfo = &lt;span style="color:#e6db74"&gt;$&amp;#34;someinfo-{name}-{id}&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; someInfo;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="按标记移除缓存条目"&gt;按标记移除缓存条目&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;SomeService&lt;/span&gt;(HybridCache cache)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; HybridCache _cache = cache;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; Task&amp;lt;&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;&amp;gt; GetSomeInfoAsync(&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; name, &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; id, CancellationToken token = &lt;span style="color:#66d9ef"&gt;default&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; tags = &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; List&amp;lt;&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;&amp;gt; { &lt;span style="color:#e6db74"&gt;&amp;#34;tag1&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;tag2&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;tag3&amp;#34;&lt;/span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; entryOptions = &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; HybridCacheEntryOptions
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Expiration = TimeSpan.FromMinutes(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; LocalCacheExpiration = TimeSpan.FromMinutes(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; _cache.GetOrCreateAsync(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;$&amp;#34;{name}-{id}&amp;#34;&lt;/span&gt;, &lt;span style="color:#75715e"&gt;// Unique key to the cache entry&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; cancel =&amp;gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; GetDataFromTheSourceAsync(name, id, cancel),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; entryOptions,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tags,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cancellationToken: token
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; );
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; Task&amp;lt;&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;&amp;gt; GetDataFromTheSourceAsync(&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; name, &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; id, CancellationToken token)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; someInfo = &lt;span style="color:#e6db74"&gt;$&amp;#34;someinfo-{name}-{id}&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; someInfo;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; Task RemoveByTagAsync(&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; tag)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; _cache.RemoveByTagAsync(tag);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>使用 JwtBearer 令牌进行身份验证</title><link>https://www.hiwork.me/posts/helloshop/jwt-bearer-authentication/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/jwt-bearer-authentication/</guid><description>&lt;h1 id="使用-jwtbearer-令牌进行身份验证"&gt;使用 JwtBearer 令牌进行身份验证&lt;/h1&gt;
&lt;h2 id="介绍"&gt;介绍&lt;/h2&gt;
&lt;p&gt;JwtBearer 令牌身份验证是一种基于 JSON Web 令牌的身份验证方法, 用于验证用户的身份, 它是一种无状态的身份验证方法, 适用于 Web API 和 Web 应用程序。&lt;/p&gt;
&lt;h2 id="安装-nuget-包"&gt;安装 NuGet 包&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="自建-identity-api-终结点"&gt;自建 Identity Api 终结点&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.Services.AddIdentity&amp;lt;User, Role&amp;gt;(options =&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.SignIn.RequireConfirmedAccount = &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.Password.RequireDigit = &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.Password.RequireLowercase = &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.Password.RequireUppercase = &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.Password.RequireNonAlphanumeric = &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.Password.RequiredLength = &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}).AddEntityFrameworkStores&amp;lt;IdentityServiceDbContext&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="使用-jwtbearer-验证令牌"&gt;使用 JwtBearer 验证令牌&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.Services.AddAuthentication(options =&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.DefaultSignInScheme = CustomJwtBearerDefaults.AuthenticationScheme;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}).AddJwtBearer(options =&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.TokenValidationParameters.ValidateIssuer = &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.TokenValidationParameters.ValidateAudience = &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.TokenValidationParameters.IssuerSigningKey = &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; SymmetricSecurityKey(Encoding.Default.GetBytes(issuerSigningKey));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="自定义身份认证处理程序"&gt;自定义身份认证处理程序&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CustomJwtBearerDefaults&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CustomJwtBearerOptions&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CustomJwtBearerHandler&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CustomJwtBearerExtensions&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="配置令牌生成"&gt;配置令牌生成&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.Services.AddAuthentication().AddJwtBearer().AddCustomJwtBearer(options =&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.IssuerSigningKey = issuerSigningKey;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.SecurityAlgorithm = SecurityAlgorithms.HmacSha256;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="在容器中运行-pgadmin-管理工具"&gt;在容器中运行 pgAdmin 管理工具&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docier pull dpage/pgadmin4
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker run --name pgadmin -e PGADMIN_DEFAULT_EMAIL&lt;span style="color:#f92672"&gt;=&lt;/span&gt;test@test.com -e PGADMIN_DEFAULT_PASSWORD&lt;span style="color:#f92672"&gt;=&lt;/span&gt;test -e TZ&lt;span style="color:#f92672"&gt;=&lt;/span&gt;Asia/Shanghai -d -p 5050:80 dpage/pgadmin4
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>使用 OpenAPI 规范生成 API 文档</title><link>https://www.hiwork.me/posts/helloshop/openapi/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/openapi/</guid><description>&lt;h1 id="使用-openapi-规范生成-api-文档"&gt;使用 OpenAPI 规范生成 API 文档&lt;/h1&gt;
&lt;p&gt;Swagger 是一个与语言无关的规范，用于描述 REST API。 它使计算机和用户无需直接访问源代码即可了解 REST API 的功能，Swagger 项目已于捐赠给 OpenAPI 计划，自此它被称为 OpenAPI 规范。&lt;/p&gt;
&lt;p&gt;Swashbuckle 是一个用于 .NET Core 的开源项目，它是 OpenAPI 工具的集合，用于生成 OpenAPI 规范的文档。 它可以自动生成 API 文档，包括 API 的描述、请求和响应的格式、参数的描述等。&lt;/p&gt;
&lt;h2 id="安装-swashbuckleaspnetcore"&gt;安装 Swashbuckle.AspNetCore&lt;/h2&gt;
&lt;p&gt;在 .NET Core 项目中，可以通过 NuGet 安装 Swashbuckle.AspNetCore 包。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet add package Swashbuckle.AspNetCore
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="配置-swashbuckle"&gt;配置 Swashbuckle&lt;/h2&gt;
&lt;p&gt;在 Startup.cs 文件中，可以通过以下方式配置 Swagger：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.Services.AddEndpointsApiExplorer();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.Services.AddSwaggerGen();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (app.Environment.IsDevelopment())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; app.UseSwagger();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; app.UseSwaggerUI();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>使用 PostgreSQL 数据库存储数据</title><link>https://www.hiwork.me/posts/helloshop/efcore-postgresql-provider/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/efcore-postgresql-provider/</guid><description>&lt;h1 id="使用-postgresql-数据库存储数据"&gt;使用 PostgreSQL 数据库存储数据&lt;/h1&gt;
&lt;h2 id="在-docker-中-启动-postgresql-数据库"&gt;在 Docker 中 启动 PostgreSQL 数据库&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://www.postgresql.org"&gt;https://www.postgresql.org&lt;/a&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker pull postgres
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker run --name postgres -e POSTGRES_PASSWORD&lt;span style="color:#f92672"&gt;=&lt;/span&gt;postgres -e TZ&lt;span style="color:#f92672"&gt;=&lt;/span&gt;Asia/Shanghai -d -p 5432:5432 postgres
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="使用-pgadmin-连接-postgresql-数据库"&gt;使用 PgAdmin 连接 PostgreSQL 数据库&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://www.pgadmin.org"&gt;https://www.pgadmin.org&lt;/a&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;SHOW timezone;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="efcore-使用-postgresql-数据库"&gt;EfCore 使用 PostgreSQL 数据库&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="定义实体类型"&gt;定义实体类型&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;User&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; Id { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; UserName { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; PasswordHash { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; DateTimeOffset CreationTime { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; } = DateTimeOffset.UtcNow;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="配置实体类型"&gt;配置实体类型&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;UserEntityTypeConfiguration&lt;/span&gt; : IEntityTypeConfiguration&amp;lt;User&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; Configure(EntityTypeBuilder&amp;lt;User&amp;gt; builder)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.ToTable(&lt;span style="color:#e6db74"&gt;&amp;#34;Users&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.HasKey(x =&amp;gt; x.Id);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.Property(x =&amp;gt; x.UserName).IsRequired().HasMaxLength(&lt;span style="color:#ae81ff"&gt;50&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.Property(x =&amp;gt; x.PasswordHash).IsRequired().HasMaxLength(&lt;span style="color:#ae81ff"&gt;50&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.Property(x =&amp;gt; x.CreationTime);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="创建-dbcontext-上下文"&gt;创建 DbContext 上下文&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;IdentityServiceDbContext&lt;/span&gt; : DbContext
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; IdentityServiceDbContext(DbContextOptions&amp;lt;IdentityServiceDbContext&amp;gt; options) : &lt;span style="color:#66d9ef"&gt;base&lt;/span&gt;(options)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;protected&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;override&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; OnModelCreating(ModelBuilder builder)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;base&lt;/span&gt;.OnModelCreating(builder);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; AppContext.SetSwitch(&lt;span style="color:#e6db74"&gt;&amp;#34;Npgsql.EnableLegacyTimestampBehavior&amp;#34;&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; AppContext.SetSwitch(&lt;span style="color:#e6db74"&gt;&amp;#34;Npgsql.DisableDateTimeInfinityConversions&amp;#34;&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="数据库连接字符串"&gt;数据库连接字符串&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;ConnectionStrings&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;IdentityDatabase&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;Host=localhost;Port=5432;Database=IdentityService;Username=postgres;Password=postgres&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="注册数据库上下文"&gt;注册数据库上下文&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.Services.AddDbContext&amp;lt;IdentityServiceDbContext&amp;gt;(options =&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.UseNpgsql(builder.Configuration.GetConnectionString(&lt;span style="color:#e6db74"&gt;&amp;#34;IdentityDatabase&amp;#34;&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="迁移数据库"&gt;迁移数据库&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet tool install --global dotnet-ef
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet add package Microsoft.EntityFrameworkCore.Design
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet ef migrations add InitialCreate --output-dir EntityFrameworks/Migrations
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet ef database update
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="postgresql-数据库命名约定"&gt;PostgreSQL 数据库命名约定&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/efcore/EFCore.NamingConventions"&gt;https://github.com/efcore/EFCore.NamingConventions&lt;/a&gt;&lt;/p&gt;</description></item><item><title>使用 TimeProvider 类注入时间</title><link>https://www.hiwork.me/posts/helloshop/time-provider/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/time-provider/</guid><description>&lt;h1 id="使用-timeprovider-类注入时间"&gt;使用 TimeProvider 类注入时间&lt;/h1&gt;
&lt;p&gt;System.TimeProvider 是一种时间抽象，它以 DateTimeOffset 类型的形式提供时间点。 通过使用 TimeProvider，可确保代码可测试且可预测。 TimeProvider 已在 .NET 8 中引入。&lt;/p&gt;
&lt;h2 id="默认实现"&gt;默认实现&lt;/h2&gt;
&lt;p&gt;默认情况下，TimeProvider 使用 DateTimeOffset.UtcNow 作为时间源。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Console.WriteLine(&lt;span style="color:#e6db74"&gt;$&amp;#34;Local: {TimeProvider.System.GetLocalNow()}&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Console.WriteLine(&lt;span style="color:#e6db74"&gt;$&amp;#34;Utc: {TimeProvider.System.GetUtcNow()}&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="自定义实现"&gt;自定义实现&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CustomTimeProvider&lt;/span&gt;: TimeProvider
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;override&lt;/span&gt; DateTimeOffset GetUtcNow() =&amp;gt; DateTimeOffset.UtcNow.AddHours(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.Services.AddSingleton(TimeProvider.System);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.Services.AddSingleton&amp;lt;TimeProvider, CustomTimeProvider&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;MyService&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;readonly&lt;/span&gt; TimeProvider _timeProvider;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; MyService(TimeProvider timeProvider)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; _timeProvider = timeProvider;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; DoSomething()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; now = _timeProvider.Now;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Console.WriteLine(now);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="faketimeprovider-实现"&gt;FakeTimeProvider 实现&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet add package Microsoft.Extensions.TimeProvider.Testing
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FakeTimeProvider fakeTimeProvider = &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;fakeTimeProvider.SetUtcNow(fakeTimeProvider.GetUtcNow().AddHours(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;MyService service = &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; (timeProvider);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;service.DoSomething();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>使用 TimeProvider 获取时间</title><link>https://www.hiwork.me/posts/helloshop/use-time-provider/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/use-time-provider/</guid><description>&lt;h1 id="使用-timeprovider-获取时间"&gt;使用 TimeProvider 获取时间&lt;/h1&gt;
&lt;h2 id="传统方式获取时间"&gt;传统方式获取时间&lt;/h2&gt;
&lt;p&gt;我们通常使用 &lt;code&gt;DateTime.Now&lt;/code&gt; 和 &lt;code&gt;DateTimeOffset.Now&lt;/code&gt; 来获取当前时间，但是这样的代码很难测试，时间依赖于系统时间，很难控制和重现，只能修改系统时间。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; now1 = DateTime.Now;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; nowUtc1 = DateTime.UtcNow;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; now2 = DateTimeOffset.Now;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; nowUtc2 = DateTimeOffset.UtcNow;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="最新的-timeprovider-类型"&gt;最新的 TimeProvider 类型&lt;/h2&gt;
&lt;p&gt;为了解决上述问题，在 .NET 8 中新增了 TimeProvider 类型，可以用来获取时间，并且可以在测试中轻松地控制时间。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; now = TimeProvider.System.GetLocalNow();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; nowUtc = TimeProvider.System.GetUtcNow();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="重写-timeprovider-模拟时间"&gt;重写 TimeProvider 模拟时间&lt;/h2&gt;
&lt;p&gt;我们可以重写 TimeProvider 类型，模拟时间，方便测试。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;MyTimeProvider&lt;/span&gt; : TimeProvider
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;override&lt;/span&gt; DateTimeOffset GetUtcNow() =&amp;gt; DateTimeOffset.UtcNow;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="依赖注入-timeprovider-实现"&gt;依赖注入 TimeProvider 实现&lt;/h2&gt;
&lt;p&gt;默认实现 TimeProvider.System 使用系统时间。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.Services.AddSingleton(TimeProvider.System);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;注入 MyTimeProvider 使用自定义时间。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.Services.AddSingleton&amp;lt;TimeProvider, MyTimeProvider&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="使用-timeprovider-的代码"&gt;使用 TimeProvider 的代码&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;MyService&lt;/span&gt;(TimeProvider timeProvider)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; DoSomething()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; now = timeProvider.GetLocalNow();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; nowUtc = timeProvider.GetUtcNow();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;演示零度框架中使用 TimeProvider 代码。&lt;/p&gt;</description></item><item><title>使用 Visual Studio 2022 中的 .HTTP 文件</title><link>https://www.hiwork.me/posts/visualstudio/use-http-files/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/visualstudio/use-http-files/</guid><description>&lt;h1 id="使用-visual-studio-2022-中的-http-文件"&gt;使用 Visual Studio 2022 中的 .HTTP 文件&lt;/h1&gt;
&lt;h2 id="注释"&gt;注释&lt;/h2&gt;
&lt;p&gt;以 # 或 // 开头的行是注释，当 Visual Studio 发送 HTTP 请求时，将忽略这些行。&lt;/p&gt;
&lt;h2 id="变量"&gt;变量&lt;/h2&gt;
&lt;p&gt;以 @ 开头的行使用语法 @VariableName=Value 定义变量。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-http" data-lang="http"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;@hostname=localhost
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;@port=44320
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;GET https://{{hostname}}:{{port}}/weatherforecast
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="多个请求"&gt;多个请求&lt;/h2&gt;
&lt;p&gt;请求格式为：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-http" data-lang="http"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;[&amp;lt;HTTP-verb&amp;gt;] &amp;lt;url&amp;gt; [&amp;lt;HTTP-version&amp;gt;]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;[&amp;lt;request-headers&amp;gt;]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;[&amp;lt;request-body&amp;gt;]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-http" data-lang="http"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;GET https://localhost:7220/weatherforecast
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;###
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;GET https://localhost:7220/weatherforecast?date=2023-05-11&amp;amp;location=98006
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;###
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;GET&lt;/span&gt; https://localhost:7220/weatherforecast &lt;span style="color:#66d9ef"&gt;HTTP&lt;/span&gt;&lt;span style="color:#f92672"&gt;/&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;###
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="请求标头"&gt;请求标头&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-http" data-lang="http"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;GET https://localhost:7220/weatherforecast
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;Date: Wed, 27 Apr 2023 07:28:00 GMT
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;###
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;GET https://localhost:7220/weatherforecast
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;Cache-Control: max-age=604800
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;Age: 100
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;###
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="请求正文"&gt;请求正文&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-http" data-lang="http"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;POST https://localhost:7220/weatherforecast
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;Content-Type: application/json
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt; &amp;#34;date&amp;#34;: &amp;#34;2023-05-11&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt; &amp;#34;location&amp;#34;: &amp;#34;98006&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#960050;background-color:#1e0010"&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="使用模板创建-http-文件"&gt;使用模板创建 HTTP 文件&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;右键单击 ASP.NET Core 项目&lt;/li&gt;
&lt;li&gt;添加&amp;gt;新建项&lt;/li&gt;
&lt;li&gt;ASP.NET Core&amp;gt;常规&lt;/li&gt;
&lt;li&gt;选择 HTTP 文件，然后选择添加&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="使用终结点资源管理器"&gt;使用终结点资源管理器&lt;/h2&gt;
&lt;p&gt;视图&amp;gt;其他窗口&amp;gt;终结点资源管理&lt;/p&gt;</description></item><item><title>使用分布式锁解决并发问题</title><link>https://www.hiwork.me/posts/helloshop/distributed-lock/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/distributed-lock/</guid><description>&lt;h1 id="使用分布式锁解决并发问题"&gt;使用分布式锁解决并发问题&lt;/h1&gt;
&lt;h2 id="单进程应用程序中的锁"&gt;单进程应用程序中的锁&lt;/h2&gt;
&lt;p&gt;在单机环境下，我们可以使用线程锁来解决并发问题，但是在分布式系统中，线程锁无法解决并发问题，因为分布式系统中的线程锁只能锁住当前进程，无法锁住其他进程。&lt;/p&gt;
&lt;p&gt;在 .NET 中，我们可以使用 &lt;code&gt;lock&lt;/code&gt; 关键字来实现线程锁。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/statements/lock"&gt;https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/statements/lock&lt;/a&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;readonly&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;object&lt;/span&gt; _lock = &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;object&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; DoSomething()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;lock&lt;/span&gt; (_lock)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 业务逻辑&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;C# 13 引入了新的线程同步类型 System.Threading.Lock，它通过作用域管理的方式简化了锁的使用，使代码更加清晰可靠。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;using&lt;/span&gt; System.Threading.Lock;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;readonly&lt;/span&gt; Lock _lock = &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; Lock();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; DoSomething()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;using&lt;/span&gt; (_lock.EnterScope())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// 业务逻辑&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="开源分布式锁的实现"&gt;开源分布式锁的实现&lt;/h2&gt;
&lt;p&gt;DistributedLock 是一个 .NET 库，它基于各种底层技术提供强大且易于使用的分布式互斥锁、读写器锁和信号量。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/madelson/DistributedLock"&gt;https://github.com/madelson/DistributedLock&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="零度框架中的分布式锁"&gt;零度框架中的分布式锁&lt;/h2&gt;
&lt;p&gt;零度框架中提供分布式锁的基本抽象，并基于 Dapr 实现了分布式锁。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://docs.dapr.io/developing-applications/building-blocks/distributed-lock/distributed-lock-api-overview"&gt;https://docs.dapr.io/developing-applications/building-blocks/distributed-lock/distributed-lock-api-overview&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="redis-分布式锁原理"&gt;Redis 分布式锁原理&lt;/h2&gt;
&lt;p&gt;Redis 分布式锁的实现原理是通过 SETNX 命令，SETNX 是 Redis 的一个原子性操作，它会在键不存在时设置键的值，如果键已经存在，则不做任何操作。&lt;/p&gt;</description></item><item><title>使用缓存提高系统性能</title><link>https://www.hiwork.me/posts/helloshop/distributed-cache/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/distributed-cache/</guid><description>&lt;h1 id="使用缓存提高系统性能"&gt;使用缓存提高系统性能&lt;/h1&gt;
&lt;p&gt;缓存是一种常见的技术，目标是提高系统的性能和伸缩性。 为缓存数据，它会暂时会经常访问的数据复制到位置靠近应用程序的快速存储。 如果这种快速数据存储比原始源更靠近应用程序，则缓存可以通过更快速提供数据，大幅改善客户端应用程序的响应时间。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://oss.xcode.me/notes/helloshop/cache-aside.png" alt="cache-aside"&gt;&lt;/p&gt;
&lt;h2 id="缓存类型"&gt;缓存类型&lt;/h2&gt;
&lt;h3 id="私有专用缓存"&gt;私有专用缓存&lt;/h3&gt;
&lt;p&gt;专用缓存是指缓存服务运行在应用程序的同一台服务器上，它是应用程序的一部分，通常是一个库或模块。专用缓存通常是一个内存缓存，如 &lt;code&gt;MemoryCache&lt;/code&gt; 或 &lt;code&gt;ConcurrentDictionary&lt;/code&gt;，它是一个轻量级的缓存，适用于小型应用程序。专用缓存的优点是简单易用，缺点是无法跨服务器共享数据，不适用于大型应用程序，使用 IMemoryCache 接口可以在 .NET 中使用专用缓存。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://oss.xcode.me/notes/helloshop/private-cache.png" alt="privatecache"&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;services.AddMemoryCache();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;MyService&lt;/span&gt;(IMemoryCache cache) : IService
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; Task Test()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cache.Set(&lt;span style="color:#e6db74"&gt;&amp;#34;key&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;value&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;value&lt;/span&gt; = cache.Get(&lt;span style="color:#e6db74"&gt;&amp;#34;key&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="分布式共享缓存"&gt;分布式共享缓存&lt;/h3&gt;
&lt;p&gt;分布式缓存是指缓存服务运行在多台服务器上，它是一个独立的服务，可以跨服务器共享数据。分布式缓存通常是一个内存缓存，如 Redis、Memcached、Garnet 等，它是一个高性能、低延迟的缓存，适用于大型应用程序。分布式缓存的优点是高性能、低延迟，支持大规模数据存储和访问，缺点是复杂度高，需要额外的服务器资源，使用 IDistributedCache 接口可以在 .NET 中使用分布式缓存。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://oss.xcode.me/notes/helloshop/shared-cache.png" alt="cache-shared"&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;services.AddMemoryDistributedCache();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;MyService&lt;/span&gt;(IDistributedCache cache) : IService
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; Task Test()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; cache.SetStringAsync(&lt;span style="color:#e6db74"&gt;&amp;#34;key&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;value&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;value&lt;/span&gt; = &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; cache.GetStringAsync(&lt;span style="color:#e6db74"&gt;&amp;#34;key&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="使用-redis-分布式缓存"&gt;使用 Redis 分布式缓存&lt;/h2&gt;
&lt;p&gt;Redis 是一个高性能、开源的分布式缓存系统，它是一个内存数据库，支持多种数据结构，如字符串、列表、哈希表、集合等，Redis 的设计目标是提供一个高性能、低延迟的分布式缓存系统，以支持大规模的数据存储和访问，同时提供高可用性和可扩展性。Redis 采用 RESP 协议，使用 C 语言编写，支持多种语言，如 C、C++、C#、Java、Python、Node.js 等，可以在 .NET 中使用 StackExchange.Redis 开源库对缓存进行操作。&lt;/p&gt;</description></item><item><title>全球化与本地化</title><link>https://www.hiwork.me/posts/helloshop/localization/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/localization/</guid><description>&lt;h1 id="全球化与本地化"&gt;全球化与本地化&lt;/h1&gt;
&lt;p&gt;全球化是指 Web 应用程序能够适应不同的文化和地区，而不需要修改代码。本地化是指 Web 应用程序能够根据用户的文化和地区显示不同的内容。全球化和本地化是两个不同的概念，但是经常一起使用。&lt;/p&gt;
&lt;p&gt;实现多语言的方法有很多种，可以使用资源文件、数据库、配置文件等方式，本文主要介绍使用资源文件的方式实现多语言，也是微软官方推荐的方式。其它的比如 PO 文件、JSON 文件等也可以实现多语言，但是不如资源文件方便.&lt;/p&gt;
&lt;h2 id="创建资源文件"&gt;创建资源文件&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Welcome.en-US.resx
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Welcome.zh-CN.resx
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="添加本地化服务和资源定位"&gt;添加本地化服务和资源定位&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; IServiceCollection AddCustomLocalization(&lt;span style="color:#66d9ef"&gt;this&lt;/span&gt; IServiceCollection services)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; services.AddLocalization(options =&amp;gt; options.ResourcesPath = &lt;span style="color:#e6db74"&gt;&amp;#34;Resources&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; services;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="使用本地化服务"&gt;使用本地化服务&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;HelloWorldController&lt;/span&gt;(IStringLocalizerFactory stringLocalizerFactory) : ControllerBase
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt; [HttpGet]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; IActionResult Get()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; location = Assembly.GetExecutingAssembly().FullName;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ArgumentException.ThrowIfNullOrWhiteSpace(location);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; localizer = stringLocalizerFactory.Create(&lt;span style="color:#e6db74"&gt;&amp;#34;Welcome&amp;#34;&lt;/span&gt;, location);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; Ok(localizer[&lt;span style="color:#e6db74"&gt;&amp;#34;HelloWorld&amp;#34;&lt;/span&gt;].Value);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="使用-http-请求设置语言"&gt;使用 HTTP 请求设置语言&lt;/h2&gt;
&lt;p&gt;UseRequestLocalization 中间件从请求中获取语言设置，然后设置当前线程的语言，以便在后续的请求中使用，这样就可以实现全局的本地化，而不需要在每个控制器中设置本地化。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; IApplicationBuilder UseCustomLocalization(&lt;span style="color:#66d9ef"&gt;this&lt;/span&gt; IApplicationBuilder app)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; supportedCultures = &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt;[] { &lt;span style="color:#e6db74"&gt;&amp;#34;zh-CN&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;en-US&amp;#34;&lt;/span&gt; };
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; localizationOptions = &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; RequestLocalizationOptions().SetDefaultCulture(supportedCultures.First())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .AddSupportedCultures(supportedCultures)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .AddSupportedUICultures(supportedCultures);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; app.UseRequestLocalization(localizationOptions);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; app;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;使用下面的方式从 Header 、 QueryString 或者 Cookie 中获取语言设置：&lt;/p&gt;</description></item><item><title>关于 Aspire 应用宿主的编排</title><link>https://www.hiwork.me/posts/helloshop/aspire-host-overview/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/aspire-host-overview/</guid><description>&lt;h1 id="关于-aspire-应用宿主的编排"&gt;关于 Aspire 应用宿主的编排&lt;/h1&gt;
&lt;h2 id="通过-add-方法添加应用组件"&gt;通过 Add 方法添加应用组件&lt;/h2&gt;
&lt;p&gt;通过 AddProject、AddContainer、AddExecutable、AddParameter，AddConnectionString 等方法，可以将应用宿主编排为一个完整的应用。&lt;/p&gt;
&lt;h2 id="配置显式资源启动"&gt;配置显式资源启动&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.AddProject&amp;lt;Projects.MyApp&amp;gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;dbmigration&amp;#34;&lt;/span&gt;).WithExplicitStart();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;使用 WithExplicitStart 方法，可以配置显式资源启动，显式资源启动的资源需要手动启动。&lt;/p&gt;
&lt;h2 id="资源的引用通过-withreference-方法"&gt;资源的引用通过 WithReference 方法&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.AddProject&amp;lt;Projects.MyApp&amp;gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;myapp&amp;#34;&lt;/span&gt;).WithReference(postgresdb);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;被引用资源可以是一个终结点，连接字符串，或者其他资源，所引用的资源会通过环境变量的方式传递给引用资源。&lt;/p&gt;
&lt;h2 id="等待资源启动和完成"&gt;等待资源启动和完成&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.AddProject&amp;lt;Projects.MyApp&amp;gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;myapp&amp;#34;&lt;/span&gt;).WithWaitFor(postgresdb);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.AddProject&amp;lt;Projects.MyApp&amp;gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;myapp&amp;#34;&lt;/span&gt;).WithWaitForCompletion(&lt;span style="color:#e6db74"&gt;&amp;#34;dbmigration&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="自定义容器的启动参数"&gt;自定义容器的启动参数&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; cache = builder.AddRedis(&lt;span style="color:#e6db74"&gt;&amp;#34;cache&amp;#34;&lt;/span&gt;).WithImageTag(&lt;span style="color:#e6db74"&gt;&amp;#34;latest&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="容器资源生存期"&gt;容器资源生存期&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; cache1 = builder.AddRedis(&lt;span style="color:#e6db74"&gt;&amp;#34;cache1&amp;#34;&lt;/span&gt;).WithLifetime(ContainerLifetime.Persistent);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; cache2 = builder.AddRedis(&lt;span style="color:#e6db74"&gt;&amp;#34;cache1&amp;#34;&lt;/span&gt;).WithLifetime(ContainerLifetime.Session);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Persistent 表示容器资源的生存期为持久，Session 表示容器资源的生存期为会话。&lt;/p&gt;
&lt;h2 id="外部参数"&gt;外部参数&lt;/h2&gt;
&lt;p&gt;在 Host 项目的 appsettings.json 配置文件中添加如下节点。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;Parameters&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;myvalue&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;local-value&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; value1 = builder.AddParameter(&lt;span style="color:#e6db74"&gt;&amp;#34;myvalue&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; value2 = builder.AddParameter(&lt;span style="color:#e6db74"&gt;&amp;#34;myvalue&amp;#34;&lt;/span&gt;, secret: &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.AddProject&amp;lt;Projects.ApiService&amp;gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;api&amp;#34;&lt;/span&gt;).WithEnvironment(&lt;span style="color:#e6db74"&gt;&amp;#34;EXAMPLE_VALUE&amp;#34;&lt;/span&gt;, value1);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="外部连接字符串"&gt;外部连接字符串&lt;/h2&gt;
&lt;p&gt;在 Host 项目的 appsettings.json 配置文件中添加如下节点。&lt;/p&gt;</description></item><item><title>关于 EditorConfig 文件</title><link>https://www.hiwork.me/posts/visualstudio/editorconfig/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/visualstudio/editorconfig/</guid><description>&lt;h1 id="关于-editorconfig-文件"&gt;关于 EditorConfig 文件&lt;/h1&gt;
&lt;p&gt;EditorConfig 有助于为跨不同编辑器和 IDE 处理同一项目的多个开发人员保持一致的编码风格。&lt;/p&gt;
&lt;p&gt;EditorConfig 项目由用于定义编码样式的文件格式和文本编辑器插件集合组成，这些插件使编辑器能够读取文件格式并遵循定义的样式。EditorConfig 文件易于读取，并且与版本控制系统配合得很好。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://editorconfig.org"&gt;https://editorconfig.org&lt;/a&gt;&lt;/p&gt;
&lt;h1 id="visual-studio-中的-editorconfig-支持"&gt;Visual Studio 中的 EditorConfig 支持&lt;/h1&gt;
&lt;p&gt;Visual Studio 2019 及更高版本支持 EditorConfig 文件，可以在 Visual Studio 中使用 EditorConfig 文件来定义和维护代码样式设置。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/dotnet/aspnetcore/blob/main/.editorconfig"&gt;https://github.com/dotnet/aspnetcore/blob/main/.editorconfig&lt;/a&gt;&lt;/p&gt;
&lt;h1 id="版权说明模板"&gt;版权说明模板&lt;/h1&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;// Licensed to the .NET Foundation under one or more agreements.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;// The .NET Foundation licenses this file to you under the MIT license.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the &amp;lt;&amp;lt;LICENSE_TYPE&amp;gt;&amp;gt; license.
// See the LICENSE file in the project root for more information.
&lt;/code&gt;&lt;/pre&gt;&lt;h1 id="使用-editorconfig-添加文件头版权信息"&gt;使用 EditorConfig 添加文件头版权信息&lt;/h1&gt;
&lt;p&gt;在 Visual Studio 中使用 EditorConfig 文件添加文件头版权信息。&lt;/p&gt;</description></item><item><title>分页排序和多条件查询</title><link>https://www.hiwork.me/posts/helloshop/paging/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/paging/</guid><description>&lt;h1 id="分页排序和多条件查询"&gt;分页排序和多条件查询&lt;/h1&gt;
&lt;h2 id="分页参数"&gt;分页参数&lt;/h2&gt;
&lt;p&gt;请求应使用 GET 方法&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;http://localhost:8080/api/products?keyword&lt;span style="color:#f92672"&gt;=&lt;/span&gt;test&amp;amp;pagenumber&lt;span style="color:#f92672"&gt;=&lt;/span&gt;1&amp;amp;pagesize&lt;span style="color:#f92672"&gt;=&lt;/span&gt;5&amp;amp;orderby&lt;span style="color:#f92672"&gt;=&lt;/span&gt;id desc,price asc
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;响应返回如下&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;totalCount&amp;#34;&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;100&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;items&amp;#34;&lt;/span&gt;: [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;test&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;price&amp;#34;&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;100&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;name&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;test&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;price&amp;#34;&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;200&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="分页请求模型"&gt;分页请求模型&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;PagedAndSortedRequest&lt;/span&gt; : PagedRequest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string?&lt;/span&gt; OrderBy { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;init&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="分页响应模型"&gt;分页响应模型&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;PagedResponse&lt;/span&gt;&amp;lt;T&amp;gt;(IReadOnlyList&amp;lt;T&amp;gt; items, &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; totalCount)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; IReadOnlyList&amp;lt;T&amp;gt; Items { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;init&lt;/span&gt;; } = items;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; TotalCount { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;init&lt;/span&gt;; } = totalCount;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="扩展-iqueryable-以便通过属性名称排序"&gt;扩展 IQueryable 以便通过属性名称排序&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;QueryableOrderByExtensions.cs
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;将字符串条件转化为排序表达式&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;IOrderedQueryable&amp;lt;TSource&amp;gt; OrderBy(IQueryable&amp;lt;TSource&amp;gt; source, &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; propertyName)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="扩展-iqueryable-以便搜索和排序"&gt;扩展 IQueryable 以便搜索和排序&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;QueryableExtensions.cs
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;扩展分页和排序方法&lt;/p&gt;</description></item><item><title>创建代码仓库</title><link>https://www.hiwork.me/posts/helloshop/git-repositories/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/git-repositories/</guid><description>&lt;h1 id="创建代码仓库"&gt;创建代码仓库&lt;/h1&gt;
&lt;h2 id="仓库命名规范"&gt;仓库命名规范&lt;/h2&gt;
&lt;p&gt;在 Github 上创建一个名为 HelloShop 的代码仓库，关于代码仓库的命名规范使用，使用 SnakeCaseLower 命名法。&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Naming policy&lt;/th&gt;
 &lt;th&gt;Original&lt;/th&gt;
 &lt;th&gt;Converted&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;PascalCase&lt;/td&gt;
 &lt;td&gt;HelloShop&lt;/td&gt;
 &lt;td&gt;HelloShop&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;CamelCase&lt;/td&gt;
 &lt;td&gt;HelloShop&lt;/td&gt;
 &lt;td&gt;helloShop&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;SnakeCaseLower&lt;/td&gt;
 &lt;td&gt;HelloShop&lt;/td&gt;
 &lt;td&gt;hello_shop&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;SnakeCaseUpper&lt;/td&gt;
 &lt;td&gt;HelloShop&lt;/td&gt;
 &lt;td&gt;HELLO_SHOP&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;KebabCaseLower&lt;/td&gt;
 &lt;td&gt;HelloShop&lt;/td&gt;
 &lt;td&gt;hello-shop&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;KebabCaseUpper&lt;/td&gt;
 &lt;td&gt;HelloShop&lt;/td&gt;
 &lt;td&gt;HELLO-SHOP&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="仓库文件夹结构"&gt;仓库文件夹结构&lt;/h2&gt;
&lt;p&gt;在仓库中创建一个名为 src 的文件夹，用于存放源代码。&lt;/p&gt;
&lt;h2 id="说明文件"&gt;说明文件&lt;/h2&gt;
&lt;p&gt;在仓库中创建一个名为 README.md 的文件，用于存放仓库的说明文档，当然如果可能每个文件夹都应该有一个说明文档。&lt;/p&gt;
&lt;h2 id="忽略文件"&gt;忽略文件&lt;/h2&gt;
&lt;p&gt;gitignore 文件的作用是指定不需要提交到代码仓库的文件，例如编译后的文件、日志文件等。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/github/gitignore/blob/main/VisualStudio.gitignore"&gt;https://github.com/github/gitignore/blob/main/VisualStudio.gitignore&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="属性文件"&gt;属性文件&lt;/h2&gt;
&lt;p&gt;gitattributes 文件的作用是指定文件的属性，例如文件的换行符、文件的编码等。&lt;/p&gt;
&lt;h2 id="目录结构"&gt;目录结构&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;assets&lt;/code&gt; 静态资源，包括图片，图标，视频，音频等。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;build&lt;/code&gt; 构建脚本，包括编译脚本，打包脚本，发布脚本等。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;docs&lt;/code&gt; 相关文档，包括设计文档，架构文档，开发文档，部署文档等。`&lt;/p&gt;
&lt;p&gt;&lt;code&gt;samples&lt;/code&gt; 演示示例，包括代码示例，配置示例，数据示例，文档示例等。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;src&lt;/code&gt; 源代码，包括源代码，配置文件，资源文件，脚本文件等。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tests&lt;/code&gt; 测试代码，包括单元测试，集成测试，端到端测试等。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;tools&lt;/code&gt; 项目所使用的一些工具。&lt;/p&gt;
&lt;h2 id="仓库分支"&gt;仓库分支&lt;/h2&gt;
&lt;p&gt;仓库分支用于管理仓库的版本，例如 master，develop，release，hotfix, feature 等。&lt;/p&gt;
&lt;h2 id="仓库标签"&gt;仓库标签&lt;/h2&gt;
&lt;p&gt;仓库标签用于标记仓库的版本，例如 v1.0.0，v1.0.1，v1.1.0，v2.0.0 等。&lt;/p&gt;</description></item><item><title>制作动态流程图和架构图</title><link>https://www.hiwork.me/posts/helloshop/drawio/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/drawio/</guid><description>&lt;p&gt;&lt;img src="https://oss.xcode.me/notes/helloshop/2024-04-09-07-17-04.png" alt="2024-04-09-07-17-04"&gt;&lt;/p&gt;
&lt;h1 id="制作动态流程图和架构图"&gt;制作动态流程图和架构图&lt;/h1&gt;
&lt;h2 id="微软-visio-工具"&gt;微软 Visio 工具&lt;/h2&gt;
&lt;p&gt;Visio 是微软的画图工具，非常强大，但是收费，而且只能在 Windows 系统上使用，不支持 Mac 和 Linux 系统，虽然微软推出了 Visio Online，但是功能还是比较弱，而且收费。&lt;/p&gt;
&lt;h2 id="开源-drawio-工具"&gt;开源 Drawio 工具&lt;/h2&gt;
&lt;p&gt;Drawio 是一个开源的画图工具，支持在线和桌面两种方式，支持多种格式，支持多种平台，支持多种语言，支持多种图形，支持多种导出格式，支持多种插件，支持多种集成方式，也支持在 Visual Studio Code 中使用插件的方式画图。&lt;/p&gt;
&lt;h2 id="矢量图和位图"&gt;矢量图和位图&lt;/h2&gt;
&lt;p&gt;矢量图是由数学公式描述的图形，可以无限放大，不会失真，位图是由像素点描述的图形，放大会失真。&lt;/p&gt;
&lt;h2 id="画图入口"&gt;画图入口&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://www.drawio.com"&gt;Drawio 官网&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.drawio.com"&gt;在线画图&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="容器和容器大小缩放"&gt;容器和容器大小缩放&lt;/h2&gt;
&lt;p&gt;设置图形 Container 属性为 true，可以将多个图形组合成一个图形，然后可以对这个图形进行缩放，设置 Collapsed 属性为 true，可以将图形折叠起来，只显示标题。设置 Resizable 属性为 false，可以禁止容器子图形缩放。&lt;/p&gt;
&lt;h2 id="浮动链接和固定链接"&gt;浮动链接和固定链接&lt;/h2&gt;
&lt;p&gt;浮动链接是指链接的位置随着图形的移动而移动，固定链接是指链接的位置不随着图形的移动而移动，可以添加自定义连接点。&lt;/p&gt;
&lt;h2 id="拖动连接线文字"&gt;拖动连接线文字&lt;/h2&gt;
&lt;p&gt;设置连接线文字可以拖动，可以将连接线文字拖动到连接线的任意位置，可以自定难以连接点的位置。&lt;/p&gt;
&lt;h2 id="连接线的样式和反转"&gt;连接线的样式和反转&lt;/h2&gt;
&lt;p&gt;可以设置连接线的样式，可以设置连接线的反转，可以设置连接线的弯曲程度。&lt;/p&gt;
&lt;h2 id="图形和连接线文字位置"&gt;图形和连接线文字位置&lt;/h2&gt;
&lt;p&gt;设置文字位置，可以将文字放在图形或连接线左上角，设置文字位置为中心，可以将文字放在图形的中心。&lt;/p&gt;
&lt;h2 id="连接线添加航点"&gt;连接线添加航点&lt;/h2&gt;
&lt;p&gt;可以在连接线上添加航点，可以在连接线上添加箭头，可以在连接线上添加标签。&lt;/p&gt;
&lt;h2 id="任何目标可设置链接"&gt;任何目标可设置链接&lt;/h2&gt;
&lt;p&gt;可以设置任何对象的超级链接，可以设置连接线的超级链接，可以设置图形的超级链接。&lt;/p&gt;
&lt;h2 id="替换图形"&gt;替换图形&lt;/h2&gt;
&lt;p&gt;可以将新图形拖动到现有图形上替换图形，替换后，图形的属性和连接线都会保留。&lt;/p&gt;
&lt;h2 id="对齐方式和分布方式"&gt;对齐方式和分布方式&lt;/h2&gt;
&lt;p&gt;可以设置图形的对齐方式，可以设置图形的分布方式。&lt;/p&gt;
&lt;h2 id="带着样式画图"&gt;带着样式画图&lt;/h2&gt;
&lt;p&gt;可以带着样式画图，可以将样式应用到图形上，也可以清除默认样式。&lt;/p&gt;
&lt;h2 id="嵌入图像和在线图像"&gt;嵌入图像和在线图像&lt;/h2&gt;
&lt;p&gt;理解几何图形，图形库图形，嵌入图像，在线图像的区别。&lt;/p&gt;
&lt;h2 id="嵌入-svg-图像修改样式"&gt;嵌入 SVG 图像修改样式&lt;/h2&gt;
&lt;p&gt;使用阿里图标库 iconfont 找图，可以下载 SVG 图像，然后嵌入到 Drawio 中，可以修改 SVG 图像的样式，但需要配置规则，如下代码。&lt;/p&gt;</description></item><item><title>在 Aspire 中集成 PostgreSQL 数据库</title><link>https://www.hiwork.me/posts/helloshop/aspire-postgresql/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/aspire-postgresql/</guid><description>&lt;h1 id="在-aspire-中集成-postgresql-数据库"&gt;在 Aspire 中集成 PostgreSQL 数据库&lt;/h1&gt;
&lt;h2 id="在-host-项目中添加-postgresql-数据库"&gt;在 Host 项目中添加 PostgreSQL 数据库&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet add package Aspire.Hosting.PostgreSQL
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; builder = DistributedApplication.CreateBuilder(args);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; postgres = builder.AddPostgres(&lt;span style="color:#e6db74"&gt;&amp;#34;postgres&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; postgresdb = postgres.AddDatabase(&lt;span style="color:#e6db74"&gt;&amp;#34;postgresdb&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; exampleProject = builder.AddProject&amp;lt;Projects.ExampleProject&amp;gt;().WithReference(postgresdb);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="添加-postgresql-pgadmin-资源"&gt;添加 PostgreSQL pgAdmin 资源&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; postgres = builder.AddPostgres(&lt;span style="color:#e6db74"&gt;&amp;#34;postgres&amp;#34;&lt;/span&gt;).WithPgAdmin();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="添加-postgresql-pgweb-资源"&gt;添加 PostgreSQL pgWeb 资源&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; postgres = builder.AddPostgres(&lt;span style="color:#e6db74"&gt;&amp;#34;postgres&amp;#34;&lt;/span&gt;).WithPgWeb();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="在最终的应用中引用-postgresql-数据库"&gt;在最终的应用中引用 PostgreSQL 数据库&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet add package Aspire.Npgsql.EntityFrameworkCore.PostgreSQL
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.AddNpgsqlDbContext&amp;lt;IdentityServiceDbContext&amp;gt;(connectionName: DbConstants.ConnectionStringName, configureDbContextOptions: options =&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; NpgsqlDbContextOptionsBuilder(options).MigrationsHistoryTable(DbConstants.MigrationsHistoryTableName);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>在项目中添加文件头注释</title><link>https://www.hiwork.me/posts/helloshop/copyight/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/copyight/</guid><description>&lt;h1 id="在项目中添加文件头注释"&gt;在项目中添加文件头注释&lt;/h1&gt;
&lt;h2 id="文件头注释"&gt;文件头注释&lt;/h2&gt;
&lt;p&gt;文件头注释是一种用于说明文件版权和许可的注释。它通常包括版权声明、许可证信息、作者信息和其他相关信息。文件头注释是一种很好的实践，可以帮助开发人员了解文件的版权和许可信息。&lt;/p&gt;
&lt;h2 id="版权申明规范"&gt;版权申明规范&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// Copyright (c) HelloShop Corporation. All rights reserved.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// See the license file in the project root for more information.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;namespace&lt;/span&gt; HelloShop.AppHost
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;internal&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Class1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="关于-editorconfig-配置文件"&gt;关于 EditorConfig 配置文件&lt;/h2&gt;
&lt;p&gt;为跨不同编辑器和 IDE 处理同一项目的多个开发人员保持一致的编码风格。&lt;/p&gt;
&lt;p&gt;Visual Studio 2019 及更高版本支持 EditorConfig 文件，可以在 Visual Studio 中使用 EditorConfig 文件来定义和维护代码样式设置。&lt;/p&gt;
&lt;h2 id="版权说明模板"&gt;版权说明模板&lt;/h2&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;file_header_template = Copyright (c) HelloShop Corporation. All rights reserved.\nSee the license file in the project root for more information.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;对于文件名，可以使用 &lt;code&gt;{fileName}&lt;/code&gt; 作为占位符。&lt;/p&gt;</description></item><item><title>基于策略和资源的授权机制</title><link>https://www.hiwork.me/posts/helloshop/authorization/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/authorization/</guid><description>&lt;h1 id="基于策略和资源的授权机制"&gt;基于策略和资源的授权机制&lt;/h1&gt;
&lt;p&gt;身份认证完成后 HttpContext.User 中包含了用户的身份信息，但是用户的身份信息并不包含用户的权限信息。用户的权限信息需要通过授权系统来获取。&lt;/p&gt;
&lt;h2 id="aspnet-core-中的授权系统"&gt;ASP.NET Core 中的授权系统&lt;/h2&gt;
&lt;p&gt;ASP.NET Core 中的授权系统是基于策略的授权系统，可以通过声明式的方式来定义授权策略。授权策略可以基于角色，也可以基于资源，也可以基于其他的条件。授权策略可以通过声明式的方式来定义，也可以通过代码的方式来定义。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://oss.xcode.me/notes/helloshop/authorization.svg" alt="authorization"&gt;&lt;/p&gt;
&lt;h2 id="使用-iauthorizationservice-接口"&gt;使用 IAuthorizationService 接口&lt;/h2&gt;
&lt;p&gt;IAuthorizationService 接口是 ASP.NET Core 中的授权服务接口，可以通过该接口来进行授权操作。IAuthorizationService 接口提供了多个授权方法，可以通过这些方法来进行授权操作。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;interface&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;IAuthorizationService&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Task&amp;lt;AuthorizationResult&amp;gt; AuthorizeAsync(ClaimsPrincipal user, &lt;span style="color:#66d9ef"&gt;object&lt;/span&gt; resource, IEnumerable&amp;lt;IAuthorizationRequirement&amp;gt; requirements);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Task&amp;lt;AuthorizationResult&amp;gt; AuthorizeAsync(ClaimsPrincipal user, &lt;span style="color:#66d9ef"&gt;object&lt;/span&gt; resource, &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; policyName);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Task&amp;lt;AuthorizationResult&amp;gt; AuthorizeAsync(ClaimsPrincipal user, &lt;span style="color:#66d9ef"&gt;object&lt;/span&gt; resource, AuthorizationPolicy policy);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;AuthorizationResult 是授权结果，包含了授权的结果和失败的原因。&lt;/p&gt;
&lt;h2 id="实现授权策略提供者"&gt;实现授权策略提供者&lt;/h2&gt;
&lt;p&gt;使用 OperationAuthorizationRequirement 表示授权条件，将上一期视频中的权限定义转成授权条件提供给系统，一个策略中本可以有多个条件，但为了简单这里的策略中只加一个条件。&lt;/p&gt;
&lt;p&gt;需要注意的是，多个策略是 OR 的关系，多个条件是 AND 的关系。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CustomAuthorizationPolicyProvider&lt;/span&gt;(IOptions&amp;lt;AuthorizationOptions&amp;gt; options, IPermissionDefinitionManager permissionDefinitionManager) : DefaultAuthorizationPolicyProvider(options)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="实现资源表示"&gt;实现资源表示&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;interface&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;IAuthorizationResource&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; ResourceType =&amp;gt; GetType().Name;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; ResourceId =&amp;gt; GetType().GetProperty(&lt;span style="color:#e6db74"&gt;&amp;#34;Id&amp;#34;&lt;/span&gt;)?.GetValue(&lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;)?.ToString() ?? &lt;span style="color:#66d9ef"&gt;throw&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; NotImplementedException();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="设计一个表示资源的记录类"&gt;设计一个表示资源的记录类&lt;/h2&gt;
&lt;p&gt;一般来说，实体类型就是一种资源，可以直接从 IAuthorizationResource 接口实现，但为了方便表示资源，可以设计一个表示资源的记录类。&lt;/p&gt;</description></item><item><title>基于资源授权的最佳实践</title><link>https://www.hiwork.me/posts/helloshop/resource-based-authorization/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/resource-based-authorization/</guid><description>&lt;h1 id="基于资源授权的最佳实践"&gt;基于资源授权的最佳实践&lt;/h1&gt;
&lt;h2 id="在-identity-service-中提供权限检查接口"&gt;在 Identity Service 中提供权限检查接口&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;[HttpHead]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; Task&amp;lt;IActionResult&amp;gt; CheckPermission(&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; permissionName, &lt;span style="color:#66d9ef"&gt;string?&lt;/span&gt; resourceType = &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;string?&lt;/span&gt; resourceId = &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; permissionChecker.IsGrantedAsync(permissionName, resourceType, resourceId))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; Ok();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; Forbid();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="远程权限检查器"&gt;远程权限检查器&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Dictionary&amp;lt;&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;string?&lt;/span&gt;&amp;gt; parameters = &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt; [nameof(name)]&lt;/span&gt; = name,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt; [nameof(resourceType)]&lt;/span&gt; = resourceType,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt; [nameof(resourceId)]&lt;/span&gt; = resourceId
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;};
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; queryString = QueryHelpers.AddQueryString(&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;.Empty, parameters);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;HttpRequestMessage request = &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt;(HttpMethod.Head, queryString);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;using&lt;/span&gt; HttpResponseMessage response = &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; httpClient.SendAsync(request);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; response.IsSuccessStatusCode;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="重新实现权限处理程序"&gt;重新实现权限处理程序&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;PermissionRequirementHandler&lt;/span&gt;(IPermissionChecker permissionChecker) : AuthorizationHandler&amp;lt;OperationAuthorizationRequirement&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;protected&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;override&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; Task HandleRequirementAsync(AuthorizationHandlerContext context, OperationAuthorizationRequirement requirement)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (context.Resource &lt;span style="color:#66d9ef"&gt;is&lt;/span&gt; IAuthorizationResource resource)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; permissionChecker.IsGrantedAsync(context.User, requirement.Name, resource.ResourceType, resource.ResourceId))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; context.Succeed(requirement);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; context.Fail();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; permissionChecker.IsGrantedAsync(context.User, requirement.Name))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; context.Succeed(requirement);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; context.Fail();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="取消缓存以便测试"&gt;取消缓存以便测试&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; distributedCache.SetObjectAsync(cacheKey, &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; PermissionGrantCacheItem(isGranted), &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; DistributedCacheEntryOptions
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; AbsoluteExpiration = DateTimeOffset.Now
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="可读性重构"&gt;可读性重构&lt;/h2&gt;
&lt;p&gt;重新生成演示数据，并将授权中的 Name 改为 PermissionName，具有更强的可读性。&lt;/p&gt;</description></item><item><title>多租户应用程序设计</title><link>https://www.hiwork.me/posts/helloshop/multi-tenancy/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/multi-tenancy/</guid><description>&lt;h1 id="多租户应用程序设计"&gt;多租户应用程序设计&lt;/h1&gt;
&lt;h2 id="什么是多租户应用程序"&gt;什么是多租户应用程序&lt;/h2&gt;
&lt;p&gt;多租户应用程序是一种软件架构设计，允许单个实例的软件服务多个客户，每个客户被称为一个租户，租户之间的数据自动隔离的，租户之间的数据不会相互影响。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://oss.xcode.me/notes/helloshop/multi-tenancy.svg" alt="multi-tenancy"&gt;&lt;/p&gt;
&lt;p&gt;零度编程官网搜索「多租户」查看多租户设计视频教程。&lt;/p&gt;
&lt;h2 id="单表字段隔离租户数据"&gt;单表字段隔离租户数据&lt;/h2&gt;
&lt;p&gt;单表多租户设计是指在一个表中存储多个租户的数据，通过在表中增加一个租户 TenantId 字段来区分不同租户的数据。&lt;/p&gt;
&lt;h3 id="多租户数据查询"&gt;多租户数据查询&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;FieldIsolationServiceDbContext&lt;/span&gt;(DbContextOptions&amp;lt;FieldIsolationServiceDbContext&amp;gt; options, ICurrentTenant currentTenant) : DbContext(options)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;protected&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;override&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; OnModelCreating(ModelBuilder modelBuilder)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; modelBuilder.Entity&amp;lt;Product&amp;gt;().Property(p =&amp;gt; p.Name).HasMaxLength(&lt;span style="color:#ae81ff"&gt;32&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;foreach&lt;/span&gt; (IMutableEntityType entityType &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt; modelBuilder.Model.GetEntityTypes())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (entityType.ClrType.IsAssignableTo(&lt;span style="color:#66d9ef"&gt;typeof&lt;/span&gt;(IMultiTenant)))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; modelBuilder.Entity(entityType.ClrType).AddQueryFilter&amp;lt;IMultiTenant&amp;gt;(e =&amp;gt; e.TenantId == currentTenant.TenantId);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;base&lt;/span&gt;.OnModelCreating(modelBuilder);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;protected&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;override&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; OnConfiguring(DbContextOptionsBuilder optionsBuilder)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; optionsBuilder.AddInterceptors(&lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; TenantSaveChangesInterceptor(currentTenant));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;base&lt;/span&gt;.OnConfiguring(optionsBuilder);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="保存数据时自动设置租户编号"&gt;保存数据时自动设置租户编号&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; MultiTenancyTracking(DbContext dbContext)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; IEnumerable&amp;lt;EntityEntry&amp;lt;IMultiTenant&amp;gt;&amp;gt; multiTenancyEntries = dbContext.ChangeTracker.Entries&amp;lt;IMultiTenant&amp;gt;().Where(entry =&amp;gt; entry.State == EntityState.Added || entry.State == EntityState.Modified);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; multiTenancyEntries?.ToList().ForEach(entityEntry =&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; entityEntry.Entity.TenantId ??= currentTenant.TenantId;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="多数据库隔离租户数据"&gt;多数据库隔离租户数据&lt;/h2&gt;
&lt;p&gt;多数据库多租户设计是指为每个租户创建一个独立的数据库，通过数据库连接字符串来区分不同租户的数据。&lt;/p&gt;</description></item><item><title>如何优雅使用 Git 版本控制</title><link>https://www.hiwork.me/posts/visualstudio/git-with-visual-studio/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/visualstudio/git-with-visual-studio/</guid><description>&lt;h1 id="如何优雅使用-git-版本控制"&gt;如何优雅使用 Git 版本控制&lt;/h1&gt;
&lt;h2 id="git-简介"&gt;Git 简介&lt;/h2&gt;
&lt;p&gt;Git 是一个开源的分布式版本控制系统，用于敏捷高效地处理任何或小或大的项目。&lt;/p&gt;
&lt;h2 id="克隆存储库"&gt;克隆存储库&lt;/h2&gt;
&lt;p&gt;将远程存储库克隆到本地，在本地进行修改，然后提交到远程存储库。&lt;/p&gt;
&lt;h2 id="创建存储库"&gt;创建存储库&lt;/h2&gt;
&lt;p&gt;在本地创建存储库，然后将本地存储库推送到远程存储库。关于代码存储库的命名规范使用，使用 KebabCaseLower 命名法。&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Naming policy&lt;/th&gt;
 &lt;th&gt;Original&lt;/th&gt;
 &lt;th&gt;Converted&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;PascalCase&lt;/td&gt;
 &lt;td&gt;HelloShop&lt;/td&gt;
 &lt;td&gt;HelloShop&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;CamelCase&lt;/td&gt;
 &lt;td&gt;HelloShop&lt;/td&gt;
 &lt;td&gt;helloShop&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;SnakeCaseLower&lt;/td&gt;
 &lt;td&gt;HelloShop&lt;/td&gt;
 &lt;td&gt;hello_shop&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;SnakeCaseUpper&lt;/td&gt;
 &lt;td&gt;HelloShop&lt;/td&gt;
 &lt;td&gt;HELLO_SHOP&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;KebabCaseLower&lt;/td&gt;
 &lt;td&gt;HelloShop&lt;/td&gt;
 &lt;td&gt;hello-shop&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;KebabCaseUpper&lt;/td&gt;
 &lt;td&gt;HelloShop&lt;/td&gt;
 &lt;td&gt;HELLO-SHOP&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="本地仓库设置"&gt;本地仓库设置&lt;/h2&gt;
&lt;p&gt;Git 设置包括全局设置和存储库设置，可设置多个远程存储库。&lt;/p&gt;
&lt;h3 id="说明文件"&gt;说明文件&lt;/h3&gt;
&lt;p&gt;在存储库中创建一个名为 README.md 的文件，用于存放存储库的说明文档，当然如果可能每个文件夹都应该有一个说明文档。&lt;/p&gt;
&lt;h3 id="忽略文件"&gt;忽略文件&lt;/h3&gt;
&lt;p&gt;gitignore 文件的作用是指定不需要提交到代码存储库的文件，例如编译后的文件、日志文件等。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/github/gitignore/blob/main/VisualStudio.gitignore"&gt;https://github.com/github/gitignore/blob/main/VisualStudio.gitignore&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="常规操作"&gt;常规操作&lt;/h2&gt;
&lt;p&gt;提取、拉取、提交、推送、同步、回滚、标签、冲突、暂存（行）、拉取。&lt;/p&gt;
&lt;h2 id="分支管理"&gt;分支管理&lt;/h2&gt;
&lt;p&gt;存储库分支用于管理存储库的版本，例如 master，develop，release，hotfix, feature 等。主要操作包括：创建分支，切换分支，合并分支，删除分支。&lt;/p&gt;
&lt;h2 id="存储库管理"&gt;存储库管理&lt;/h2&gt;
&lt;p&gt;浏览存储库、管理存储库、使用多个存储库、解决合并冲突。&lt;/p&gt;</description></item><item><title>学习并使用 PostgreSQL 数据库</title><link>https://www.hiwork.me/posts/database/postgre-sql/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/database/postgre-sql/</guid><description>&lt;h1 id="学习并使用-postgresql-数据库"&gt;学习并使用 PostgreSQL 数据库&lt;/h1&gt;
&lt;h2 id="为什么说-postgresql-是最先进的关系型数据库"&gt;为什么说 PostgreSQL 是最先进的关系型数据库？&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;功能更强大：PostgreSQL具有更多高级功能，例如复杂查询、触发器和多版本并发控制等，这使得它更适合处理复杂的数据操作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据一致性更高：PostgreSQL使用可靠的多版本并发控制系统，能够在高并发场景下保证数据的一致性和完整性。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可扩展性更好：PostgreSQL支持更好的水平和垂直扩展，以满足各种规模的应用需求。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安全性：PostgreSQL提供了访问控制和数据加密等安全特性，保护数据免受恶意攻击。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;备份和恢复：PostgreSQL具有强大的备份和恢复功能，能够恢复各种故障情况下的数据。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;免费和开源：PostgreSQL是一种免费和开源的数据库系统，用户可以自由地使用、修改和分发其源代码。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="安装-postgresql-和-pgadmin"&gt;安装 PostgreSQL 和 pgAdmin&lt;/h2&gt;
&lt;p&gt;安装 PostgreSQL 和 pgAdmin 的方法有很多种，这里介绍一种比较简单的方法，学习使用 pgAdmin 管理 PostgreSQL 数据库，远程连接 PostgreSQL 数据库。&lt;/p&gt;
&lt;h2 id="在-visual-studio-code-中连接-postgresql-数据库"&gt;在 Visual Studio Code 中连接 PostgreSQL 数据库&lt;/h2&gt;
&lt;p&gt;在 Visual Studio Code 中连接 PostgreSQL 数据库，可以使用微软开发的 PostgreSQL 插件，也可以使用第三方开发的 PostgreSQL 插件。&lt;/p&gt;
&lt;h2 id="在-visual-studio-code-中使用-ai-助手编写-sql-语句"&gt;在 Visual Studio Code 中使用 AI 助手编写 SQL 语句&lt;/h2&gt;
&lt;p&gt;Github Copilot 是一款由 OpenAI 开发的人工智能编程助手，它可以根据上下文提示程序员编写代码，目前支持 12 种编程语言，可以轻松实现 SQL 智能生成等。&lt;/p&gt;
&lt;h2 id="将-sql-server-数据库迁移到-postgresql"&gt;将 SQL Server 数据库迁移到 PostgreSQL&lt;/h2&gt;
&lt;p&gt;SQL Server 导入导出向导可以将 SQL Server 数据库迁移到 PostgreSQL 数据库，参见微软文档：&lt;/p&gt;</description></item><item><title>实现 CQRS 中的 Query 模式</title><link>https://www.hiwork.me/posts/helloshop/ordering-service-cqrs-query/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/ordering-service-cqrs-query/</guid><description>&lt;h1 id="实现-cqrs-中的-query-模式"&gt;实现 CQRS 中的 Query 模式&lt;/h1&gt;
&lt;p&gt;在 CQRS 模式的全程是 Command Query Responsibility Segregation，即命令查询职责分离。在 CQRS 模式中，命令和查询是分开实现的，命令负责写操作，查询负责读操作，这样可以更好地实现单一职责原则，同时也可以更好地实现性能优化。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://learn.microsoft.com/zh-cn/azure/architecture/patterns/cqrs"&gt;https://learn.microsoft.com/zh-cn/azure/architecture/patterns/cqrs&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.xcode.me/Training/UnitNote/576"&gt;https://www.xcode.me/Training/UnitNote/576&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="定义订单查询接口"&gt;定义订单查询接口&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;interface&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;IOrderQueries&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Task&amp;lt;OrderDetails&amp;gt; GetOrderAsync(&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; id);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="实现订单查询接口"&gt;实现订单查询接口&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderQueries&lt;/span&gt; : IOrderQueries
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;readonly&lt;/span&gt; OrderingContext _context;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; OrderQueries(OrderingContext context)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; _context = context;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dbContext.Database.SetConnectionString(&lt;span style="color:#e6db74"&gt;&amp;#34;db2_connection_string&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; Task&amp;lt;OrderDetails&amp;gt; GetOrderAsync(&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; id)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// do something&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="注册订单查询接口"&gt;注册订单查询接口&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;services.AddScoped&amp;lt;IOrderQueries, OrderQueries&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="使用订单查询接口"&gt;使用订单查询接口&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OrderController&lt;/span&gt; : ControllerBase
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;readonly&lt;/span&gt; IOrderQueries _orderQueries;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; OrderController(IOrderQueries orderQueries)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; _orderQueries = orderQueries;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt; [HttpGet(&amp;#34;{id}&amp;#34;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; Task&amp;lt;IActionResult&amp;gt; GetOrderAsync(&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; id)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; order = &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; _orderQueries.GetOrderAsync(id);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; Ok(order);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>实现产品管理微服务</title><link>https://www.hiwork.me/posts/helloshop/product-service/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/product-service/</guid><description>&lt;h1 id="实现产品管理微服务"&gt;实现产品管理微服务&lt;/h1&gt;
&lt;h2 id="创建实体"&gt;创建实体&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;HelloWorld.ProductService.Entities.Products
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Product &amp;amp; CatalogBrand
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="efcore-使用-postgresql-数据库"&gt;EfCore 使用 PostgreSQL 数据库&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="创建-dbcontext-上下文"&gt;创建 DbContext 上下文&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;namespace&lt;/span&gt; HelloWorld.ProductService.EntityFrameworks
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ProductServiceDbContext&lt;/span&gt;(DbContextOptions&amp;lt;ProductServiceDbContext&amp;gt; options) : DbContext(options)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;protected&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;override&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; OnModelCreating(ModelBuilder builder)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;base&lt;/span&gt;.OnModelCreating(builder);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="创建实体配置类"&gt;创建实体配置类&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;namespace&lt;/span&gt; HelloWorld.ProductService.EntityFrameworks.EntityConfigurations.Products
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ProductEntityTypeConfiguration&lt;/span&gt; : IEntityTypeConfiguration&amp;lt;Product&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; Configure(EntityTypeBuilder&amp;lt;Product&amp;gt; builder)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.ToTable(&lt;span style="color:#e6db74"&gt;&amp;#34;Products&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.Property(x =&amp;gt; x.Name).HasMaxLength(&lt;span style="color:#ae81ff"&gt;32&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.HasOne(x =&amp;gt; x.Brand).WithMany();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="数据库连接字符串"&gt;数据库连接字符串&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;ConnectionStrings&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;ProductDatabase&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;Host=localhost;Port=5432;Database=ProductService;Username=postgres;Password=postgres&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="注册数据库上下文"&gt;注册数据库上下文&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.Services.AddDbContext&amp;lt;ProductServiceDbContext&amp;gt;(options =&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.UseNpgsql(builder.Configuration.GetConnectionString(DbConstants.ConnectionStringName));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="迁移数据库"&gt;迁移数据库&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet tool install --global dotnet-ef
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet add package Microsoft.EntityFrameworkCore.Design
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet ef migrations add InitialCreate --output-dir EntityFrameworks/Migrations
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet ef database update
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="创建模型"&gt;创建模型&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;namespace&lt;/span&gt; HelloWorld.ProductService.Models.Products
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ProductCreateRequest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; required &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; Name { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;init&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string?&lt;/span&gt; Description { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;init&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;decimal&lt;/span&gt; Price { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;init&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; BrandId { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;init&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string?&lt;/span&gt; ImageUrl { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;init&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="创建自动映射"&gt;创建自动映射&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;namespace&lt;/span&gt; HelloWorld.ProductService.AutoMapper
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ProductsMapConfiguration&lt;/span&gt; : Profile
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; ProductsMapConfiguration()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CreateMap&amp;lt;ProductCreateRequest, Product&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CreateMap&amp;lt;ProductUpdateRequest, Product&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CreateMap&amp;lt;Product, ProductListItem&amp;gt;().AfterMap((src, dest) =&amp;gt; dest.BrandName = src.Brand.Name);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CreateMap&amp;lt;Product, ProductDetailsResponse&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CreateMap&amp;lt;BrandCreateRequest, Brand&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CreateMap&amp;lt;BrandUpdateRequest, Brand&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CreateMap&amp;lt;Brand, BrandDetailsResponse&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CreateMap&amp;lt;Brand, BrandListItem&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="创建验证器"&gt;创建验证器&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;namespace&lt;/span&gt; HelloWorld.ProductService.Validations.Products
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;BrandUpdateRequestValidator&lt;/span&gt; : AbstractValidator&amp;lt;BrandUpdateRequest&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; BrandUpdateRequestValidator()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; RuleFor(x =&amp;gt; x.Id).GreaterThan(&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; RuleFor(x =&amp;gt; x.Name).NotNull().NotEmpty().Length(&lt;span style="color:#ae81ff"&gt;8&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;32&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="创建控制器"&gt;创建控制器&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;namespace&lt;/span&gt; HelloWorld.ProductService.Controllers
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt; [Route(&amp;#34;api/[controller]&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;)]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt; [ApiController]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ProductsController&lt;/span&gt; : ControllerBase
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="定义权限"&gt;定义权限&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;namespace&lt;/span&gt; HelloWorld.ProductService.PermissionProviders
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CatalogPermissions&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CatalogPermissionDefiitionProvider&lt;/span&gt; : IPermissionDefinitionProvider
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="权限本地化"&gt;权限本地化&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;CatalogPermissionDefinitionProvider.en-US.resx
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;CatalogPermissionDefinitionProvider.zh-CN.resx
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="实体属性本地化"&gt;实体属性本地化&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;namespace&lt;/span&gt; HelloWorld.ProductService.Entities.Products
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Product&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt; [Display(Name = &amp;#34;ProductName&amp;#34;)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; Name { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="重复性工作"&gt;重复性工作&lt;/h2&gt;
&lt;p&gt;重复性工作可以通过代码生成工具来减少，例如 Visual Studio 插件， T4 模板， Roslyn 等。&lt;/p&gt;</description></item><item><title>实现权限访问控制列表</title><link>https://www.hiwork.me/posts/helloshop/access-control-list/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/access-control-list/</guid><description>&lt;h1 id="实现权限访问控制列表"&gt;实现权限访问控制列表&lt;/h1&gt;
&lt;h2 id="授权三要素"&gt;授权三要素&lt;/h2&gt;
&lt;p&gt;主体、资源、操作。 例如：用户（主体）对文件（资源）的读写（操作）。主体、资源、操作三者之间的关系称为授权关系，基于这种关系，我们可以将授权关系抽象为一个三元组（主体，资源，操作），这个三元组就是授权的基本单位。&lt;/p&gt;
&lt;h2 id="基于角色的访问控制"&gt;基于角色的访问控制&lt;/h2&gt;
&lt;p&gt;基于角色的访问控制（Role-Based Access Control，简称 RBAC）是一种访问控制模型，通过角色来控制用户对资源的访问权限。角色是一组权限的集合，用户通过分配角色来获取相应的权限。基于角色的访问控制模型简单易用。还有其它的访问控制模型，如基于属性的访问控制（Attribute-Based Access Control，简称 ABAC）。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://oss.xcode.me/notes/helloshop/role-based-access-control.svg" alt="role-based-access-control"&gt;&lt;/p&gt;
&lt;h2 id="权限访问控制列表"&gt;权限访问控制列表&lt;/h2&gt;
&lt;p&gt;权限访问控制列表（Access Control List，简称 ACL）是一种权限控制模型，用于控制用户对资源的访问权限。ACL 通过为每个资源定义一个访问控制列表，来控制用户对资源的操作权限。&lt;/p&gt;
&lt;h2 id="acl-存储设计"&gt;ACL 存储设计&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;PermissionGranted&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; Id { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; RoleId { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; required &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; PermissionName { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string?&lt;/span&gt; ResourceType { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string?&lt;/span&gt; ResourceId { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="在-dbcontext-中配置-acl-实体"&gt;在 DbContext 中配置 ACL 实体&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; Configure(EntityTypeBuilder&amp;lt;PermissionGranted&amp;gt; builder)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.ToTable(&lt;span style="color:#e6db74"&gt;&amp;#34;PermissionGranted&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.Property(x =&amp;gt; x.Id);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.Property(x =&amp;gt; x.PermissionName).HasMaxLength(&lt;span style="color:#ae81ff"&gt;64&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.Property(x =&amp;gt; x.ResourceType).HasMaxLength(&lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.Property(x =&amp;gt; x.ResourceId).HasMaxLength(&lt;span style="color:#ae81ff"&gt;32&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.HasOne&amp;lt;Role&amp;gt;().WithMany().HasForeignKey(x =&amp;gt; x.RoleId).IsRequired();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.HasIndex(x =&amp;gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; { x.RoleId, x.PermissionName, x.ResourceType, x.ResourceId }).IsUnique();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="权限-acl-存储设计"&gt;权限 ACL 存储设计&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;PermissionGranted&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; Id { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; RoleId { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; required &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; PermissionName { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string?&lt;/span&gt; ResourceType { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string?&lt;/span&gt; ResourceId { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="在-dbcontext-中配置-acl-实体-1"&gt;在 DbContext 中配置 ACL 实体&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; Configure(EntityTypeBuilder&amp;lt;PermissionGranted&amp;gt; builder)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.ToTable(&lt;span style="color:#e6db74"&gt;&amp;#34;PermissionGranted&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.Property(x =&amp;gt; x.Id);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.Property(x =&amp;gt; x.PermissionName).HasMaxLength(&lt;span style="color:#ae81ff"&gt;64&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.Property(x =&amp;gt; x.ResourceType).HasMaxLength(&lt;span style="color:#ae81ff"&gt;16&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.Property(x =&amp;gt; x.ResourceId).HasMaxLength(&lt;span style="color:#ae81ff"&gt;32&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.HasOne&amp;lt;Role&amp;gt;().WithMany().HasForeignKey(x =&amp;gt; x.RoleId).IsRequired();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.HasIndex(x =&amp;gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; { x.RoleId, x.PermissionName, x.ResourceType, x.ResourceId }).IsUnique();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="设计一个权限检查器"&gt;设计一个权限检查器&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;interface&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;IPermissionChecker&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Task&amp;lt;&lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt;&amp;gt; IsGrantedAsync(&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; name, &lt;span style="color:#66d9ef"&gt;string?&lt;/span&gt; resourceType = &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;string?&lt;/span&gt; resourceId = &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Task&amp;lt;&lt;span style="color:#66d9ef"&gt;bool&lt;/span&gt;&amp;gt; IsGrantedAsync(ClaimsPrincipal claimsPrincipal, &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; name, &lt;span style="color:#66d9ef"&gt;string?&lt;/span&gt; resourceType = &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;string?&lt;/span&gt; resourceId = &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="实现抽象的权限检查器"&gt;实现抽象的权限检查器&lt;/h2&gt;
&lt;p&gt;遍历每个角色的权限，如果有一个角色拥有该权限，则返回 true，表示授权通过。&lt;/p&gt;</description></item><item><title>开发 Blazor Hybrid 应用</title><link>https://www.hiwork.me/posts/helloshop/webapp/blazor-app/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/webapp/blazor-app/</guid><description>&lt;h1 id="开发-blazor-hybrid-应用"&gt;开发 Blazor Hybrid 应用&lt;/h1&gt;
&lt;h2 id="组件化开发"&gt;组件化开发&lt;/h2&gt;
&lt;p&gt;组件化开发允许开发者将 UI 组件封装为独立的 Blazor 组件，并在应用程序中复用。这种方式可以提高代码的可维护性和可重用性， 类似于 React 和 Vue 等现代前端框架。&lt;/p&gt;
&lt;p&gt;组件 = HTML + CSS + JS&lt;/p&gt;
&lt;p&gt;Blazor 组件 = Razor + CSS + C#&lt;/p&gt;
&lt;p&gt;&lt;img src="https://oss.xcode.me/notes/helloshop/webapp/blazor-component.svg" alt="blazor-component"&gt;&lt;/p&gt;
&lt;h2 id="maui-介绍"&gt;MAUI 介绍&lt;/h2&gt;
&lt;p&gt;MAUI 是 .NET 6 中引入的跨平台 UI 框架，允许开发者使用 C# 和 XAML 创建原生应用程序。它是 Xamarin.Forms 的演进，支持 Android、iOS、macOS 和 Windows 平台。&lt;/p&gt;
&lt;h2 id="blazor-介绍"&gt;Blazor 介绍&lt;/h2&gt;
&lt;p&gt;Blazor 是一个用于构建交互式 Web 应用程序的框架，允许开发者使用 C# 和 Razor 语法在客户端和服务器端共享代码。Blazor 支持 WebAssembly 和服务器端渲染两种模式，使得开发者可以根据需要选择合适的模式来构建应用程序。&lt;/p&gt;
&lt;h2 id="maui--blazor"&gt;MAUI + Blazor&lt;/h2&gt;
&lt;p&gt;Blazor Hybrid 应用程序结合了 MAUI 和 Blazor 的优势，允许开发者在 MAUI 应用程序中嵌入 Blazor 组件。这样，开发者可以使用 Blazor 的组件化开发方式，同时享受 MAUI 提供的原生 UI 和跨平台支持。&lt;/p&gt;</description></item><item><title>数据库对象命名约定</title><link>https://www.hiwork.me/posts/helloshop/efcore-naming-conventions/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/efcore-naming-conventions/</guid><description>&lt;h1 id="数据库对象命名约定"&gt;数据库对象命名约定&lt;/h1&gt;
&lt;p&gt;不同数据库对象的命名约定不同，EF Core 为了适配不同数据库，提供了一些命名约定的配置选项。&lt;/p&gt;
&lt;h2 id="常见的命名方法"&gt;常见的命名方法&lt;/h2&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Naming policy&lt;/th&gt;
 &lt;th&gt;Original&lt;/th&gt;
 &lt;th&gt;Converted&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;PascalCase&lt;/td&gt;
 &lt;td&gt;HelloShop&lt;/td&gt;
 &lt;td&gt;HelloShop&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;CamelCase&lt;/td&gt;
 &lt;td&gt;HelloShop&lt;/td&gt;
 &lt;td&gt;helloShop&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;SnakeCaseLower&lt;/td&gt;
 &lt;td&gt;HelloShop&lt;/td&gt;
 &lt;td&gt;hello_shop&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;SnakeCaseUpper&lt;/td&gt;
 &lt;td&gt;HelloShop&lt;/td&gt;
 &lt;td&gt;HELLO_SHOP&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;KebabCaseLower&lt;/td&gt;
 &lt;td&gt;HelloShop&lt;/td&gt;
 &lt;td&gt;hello-shop&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;KebabCaseUpper&lt;/td&gt;
 &lt;td&gt;HelloShop&lt;/td&gt;
 &lt;td&gt;HELLO-SHOP&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;可使用 &lt;a href="https://github.com/Humanizr/Humanizer"&gt;Humanizr&lt;/a&gt; 库将字符串转换为不同的命名风格。&lt;/p&gt;
&lt;h2 id="表名使用单数还是复数"&gt;表名使用单数还是复数&lt;/h2&gt;
&lt;p&gt;EF Core 默认情况下，表名使用单数形式，可以通过约定器或者手动指定复数表名，社区对单数和复数表名的争论一直存在，没有统一的标准，但零度推荐使用单数形式。&lt;/p&gt;
&lt;h2 id="手动指定表名和列名"&gt;手动指定表名和列名&lt;/h2&gt;
&lt;h3 id="表名"&gt;表名&lt;/h3&gt;
&lt;p&gt;默认情况下，EF Core 使用实体类型的名称作为表名。可以通过重写 &lt;code&gt;OnModelCreating&lt;/code&gt; 方法来修改表名：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;protected&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;override&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; OnModelCreating(ModelBuilder modelBuilder)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; modelBuilder.Entity&amp;lt;Blog&amp;gt;().ToTable(&lt;span style="color:#e6db74"&gt;&amp;#34;blogs&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="列名"&gt;列名&lt;/h3&gt;
&lt;p&gt;默认情况下，EF Core 使用属性名作为列名。可以通过 &lt;code&gt;HasColumnName&lt;/code&gt; 方法来修改列名：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;protected&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;override&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; OnModelCreating(ModelBuilder modelBuilder)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; modelBuilder.Entity&amp;lt;Blog&amp;gt;().Property(b =&amp;gt; b.Url).HasColumnName(&lt;span style="color:#e6db74"&gt;&amp;#34;blog_url&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="postgresql-命名约定"&gt;PostgreSQL 命名约定&lt;/h2&gt;
&lt;p&gt;PostgreSQL 的命名约定采用小写蛇形命名法，即单词之间使用下划线分隔，如 &lt;code&gt;blog_url&lt;/code&gt;，表名使用单数形式。&lt;/p&gt;
&lt;h2 id="使用-efcorenamingconventions-库"&gt;使用 EFCore.NamingConventions 库&lt;/h2&gt;
&lt;p&gt;EFCore.NamingConventions 库提供了一些命名约定的配置选项，可以方便地配置表名、列名等。&lt;/p&gt;</description></item><item><title>更新 .NET 8.0 到 .NET 9.0</title><link>https://www.hiwork.me/posts/helloshop/upgrading-from-net-9/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/upgrading-from-net-9/</guid><description>&lt;h1 id="更新-net-80-到-net-90"&gt;更新 .NET 8.0 到 .NET 9.0&lt;/h1&gt;
&lt;h2 id="更新-visual-studio-最新预览版"&gt;更新 Visual Studio 最新预览版&lt;/h2&gt;
&lt;p&gt;下载并安装 Visual Studio 预览版，安装 .NET 9.0 SDK。&lt;/p&gt;
&lt;h2 id="更新项目文件"&gt;更新项目文件&lt;/h2&gt;
&lt;p&gt;打开项目文件，将 &lt;code&gt;&amp;lt;TargetFramework&amp;gt;net8.0&amp;lt;/TargetFramework&amp;gt;&lt;/code&gt; 更新为 &lt;code&gt;&amp;lt;TargetFramework&amp;gt;net9.0&amp;lt;/TargetFramework&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="更新-nuget-包"&gt;更新 NuGet 包&lt;/h2&gt;
&lt;p&gt;打开 NuGet 包管理器，更新所有 NuGet 包到最新版本。&lt;/p&gt;
&lt;h2 id="更新-aspire-和--maui-工作负载"&gt;更新 Aspire 和 MAUI 工作负载&lt;/h2&gt;
&lt;p&gt;打开 Aspire 工作负载，更新所有 Aspire 包到最新版本。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet workload install aspire android ios maccatalyst maui-android maui-desktop maui-mobile maui-windows
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="若要将-tab-自动补全添加到适用于-net-cli-的-powershell"&gt;若要将 Tab 自动补全添加到适用于 .NET CLI 的 PowerShell&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://learn.microsoft.com/zh-cn/dotnet/core/tools/enable-tab-autocomplete"&gt;https://learn.microsoft.com/zh-cn/dotnet/core/tools/enable-tab-autocomplete&lt;/a&gt;&lt;/p&gt;</description></item><item><title>模型自动验证机制</title><link>https://www.hiwork.me/posts/helloshop/model-validations/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/model-validations/</guid><description>&lt;h1 id="模型自动验证机制"&gt;模型自动验证机制&lt;/h1&gt;
&lt;p&gt;模型验证是 ASP.NET Core MVC 中的一个重要特性，它可以帮助我们验证用户输入的数据是否符合预期。&lt;/p&gt;
&lt;h2 id="基于数据注解的验证"&gt;基于数据注解的验证&lt;/h2&gt;
&lt;p&gt;参考微软 &lt;a href="https://learn.microsoft.com/zh-cn/aspnet/core/mvc/models/validation?view=aspnetcore-5.0#built-in-attributes-1"&gt;数据注解&lt;/a&gt; 文档。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;User&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt; [Required]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt; [StringLength(32)]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; Name { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt; [Required]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt; [EmailAddress]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; Email { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="基于链式调用的验证"&gt;基于链式调用的验证&lt;/h2&gt;
&lt;p&gt;FluentValidation 是一个.NET库，用于构建类型安全的验证规则。它的设计目标是提供一个简单、清晰的API，同时还能够支持复杂的验证规则。&lt;/p&gt;
&lt;p&gt;参考 &lt;a href="https://fluentvalidation.net/"&gt;FluentValidation&lt;/a&gt; 官方文档。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet add package FluentValidation.AspNetCore
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="实现验证器"&gt;实现验证器&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;UserValidator&lt;/span&gt; : AbstractValidator&amp;lt;User&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; UserValidator()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; RuleFor(x =&amp;gt; x.Name).NotEmpty().MaximumLength(&lt;span style="color:#ae81ff"&gt;32&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; RuleFor(x =&amp;gt; x.Email).NotEmpty().EmailAddress();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="自动依赖注入"&gt;自动依赖注入&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;services.AddValidatorsFromAssembly(assembly).AddFluentValidationAutoValidation();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ValidatorOptions.Global.LanguageManager = &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; CustomFluentValidationLanguageManager();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="自定义验证错误消息"&gt;自定义验证错误消息&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;UserValidator&lt;/span&gt; : AbstractValidator&amp;lt;User&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; UserValidator()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; RuleFor(x =&amp;gt; x.Name).NotEmpty().MaximumLength(&lt;span style="color:#ae81ff"&gt;32&lt;/span&gt;).WithMessage(&lt;span style="color:#e6db74"&gt;&amp;#34;Name is required and must be less than 32 characters.&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; RuleFor(x =&amp;gt; x.Email).NotEmpty().EmailAddress().WithMessage(&lt;span style="color:#e6db74"&gt;&amp;#34;Email is required and must be a valid email address.&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="自定义验证逻辑"&gt;自定义验证逻辑&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;UserCreateRequestValidator&lt;/span&gt; : AbstractValidator&amp;lt;UserCreateRequest&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; UserCreateRequestValidator(IdentityDbContext context)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; RuleFor(m =&amp;gt; m.UserName).NotNull().NotEmpty().Length(&lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;).Matches(&lt;span style="color:#e6db74"&gt;&amp;#34;^[a-zA-Z]+$&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; RuleFor(m =&amp;gt; m.PhoneNumber).NotNull().NotEmpty().Length(&lt;span style="color:#ae81ff"&gt;11&lt;/span&gt;).Matches(&lt;span style="color:#e6db74"&gt;@&amp;#34;^1\d{10}$&amp;#34;&lt;/span&gt;).Must((model, phoneNumber) =&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; !context.Users.Any(e =&amp;gt; e.PhoneNumber == phoneNumber);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; RuleFor(m =&amp;gt; m.Password).NotNull().NotEmpty().Length(&lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; RuleFor(m =&amp;gt; m.Email).EmailAddress().Length(&lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;50&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="通用错误消息"&gt;通用错误消息&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CustomFluentValidationLanguageManager&lt;/span&gt; : FluentValidation.Resources.LanguageManager
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; CustomFluentValidationLanguageManager()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; AddTranslation(&lt;span style="color:#e6db74"&gt;&amp;#34;en&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;NotNullValidator&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;The {PropertyName} field is required.&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; AddTranslation(&lt;span style="color:#e6db74"&gt;&amp;#34;en&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;MaximumLengthValidator&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;The {PropertyName} field must be less than {MaxLength} characters.&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; AddTranslation(&lt;span style="color:#e6db74"&gt;&amp;#34;en&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;EmailAddressValidator&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;The {PropertyName} field must be a valid email address.&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>用于生成初始数据的提供程序</title><link>https://www.hiwork.me/posts/helloshop/data-seeding/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/data-seeding/</guid><description>&lt;h1 id="用于生成初始数据的提供程序"&gt;用于生成初始数据的提供程序&lt;/h1&gt;
&lt;p&gt;数据播种是使用一组初始数据填充数据库的过程，通常在数据库首次创建时执行，这些数据通常是静态的，不会随时间变化，数据播种通常用于填充一些基本数据，例如用户、角色、权限、商品、分类等。&lt;/p&gt;
&lt;h2 id="通用接口设计"&gt;通用接口设计&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;interface&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;IDataSeedingProvider&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Task SeedingAsync(IServiceProvider serviceProvider);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="实现具体接口"&gt;实现具体接口&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;DataSeedingProvider&lt;/span&gt;(UserManager&amp;lt;User&amp;gt; userManager) : IDataSeedingProvider
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; Task SeedingAsync(IServiceProvider ServiceProvider)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; user = &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; userManager.FindByNameAsync(&lt;span style="color:#e6db74"&gt;&amp;#34;admin&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (user == &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; userManager.CreateAsync(&lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; User
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; UserName = &lt;span style="color:#e6db74"&gt;&amp;#34;admin&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Email = &lt;span style="color:#e6db74"&gt;&amp;#34;admin@test.com&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }, &lt;span style="color:#e6db74"&gt;&amp;#34;admin&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="在容器中注册"&gt;在容器中注册&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.Services.AddTransient&amp;lt;IDataSeedingProvider, DataSeedingProvider&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="自动执行扩展"&gt;自动执行扩展&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;DataSeedingExtensions&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; IApplicationBuilder UseDataSeedingProviders(&lt;span style="color:#66d9ef"&gt;this&lt;/span&gt; IApplicationBuilder app)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;using&lt;/span&gt; var serviceScope = app.ApplicationServices.CreateScope();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; dataSeedingProviders = serviceScope.ServiceProvider.GetServices&amp;lt;IDataSeedingProvider&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;foreach&lt;/span&gt; (IDataSeedingProvider dataSeedingProvider &lt;span style="color:#66d9ef"&gt;in&lt;/span&gt; dataSeedingProviders)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dataSeedingProvider.SeedingAsync(serviceScope.ServiceProvider).Wait();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; app;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="自动注册实现"&gt;自动注册实现&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;DataSeedingExtensions&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; IServiceCollection AddDataSeedingProviders(&lt;span style="color:#66d9ef"&gt;this&lt;/span&gt; IServiceCollection services, Assembly? assembly = &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; assembly ??= Assembly.GetCallingAssembly();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; dataSeedProviders = assembly.ExportedTypes.Where(t =&amp;gt; t.IsAssignableTo(&lt;span style="color:#66d9ef"&gt;typeof&lt;/span&gt;(IDataSeedingProvider)) &amp;amp;&amp;amp; t.IsClass);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dataSeedProviders.ToList().ForEach(t =&amp;gt; services.AddTransient(&lt;span style="color:#66d9ef"&gt;typeof&lt;/span&gt;(IDataSeedingProvider), t));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; services;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="执行顺序问题"&gt;执行顺序问题&lt;/h2&gt;
&lt;p&gt;自动注册的提供程序的执行顺序是不确定的，如果需要确定执行顺序，可以设计 Order 属性，然后在 UseDataSeedingProviders 方法中按照 Order 属性排序执行。&lt;/p&gt;</description></item><item><title>用户界面和设计语言</title><link>https://www.hiwork.me/posts/helloshop/design/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/design/</guid><description>&lt;h1 id="用户界面和设计语言"&gt;用户界面和设计语言&lt;/h1&gt;
&lt;h2 id="用户体验"&gt;用户体验&lt;/h2&gt;
&lt;p&gt;用户体验（User Experience，简称 UX）是用户在使用产品时的感受，是一个综合性的概念，包括用户对产品的感知、情感、态度、情绪等。用户体验设计（User Experience Design，简称 UXD）是一种以用户为中心的设计方法，目的是提高用户满意度，提高用户对产品的认知和使用。&lt;/p&gt;
&lt;p&gt;颜色、字体、布局、交互等因素都会影响用户体验，因此在设计用户界面时需要考虑用户体验，提供简洁、直观、易用的界面，以提高用户满意度。&lt;/p&gt;
&lt;h2 id="扁平化和拟物化"&gt;扁平化和拟物化&lt;/h2&gt;
&lt;p&gt;扁平化设计（Flat Design）是一种简洁、直观的设计风格，强调简单、明了的界面设计，去除了繁琐的装饰和冗余的元素，使用户更容易理解和操作。拟物化设计（Skeuomorphism）是一种仿真真实物体的设计风格，强调真实感和细节，使用户更容易理解和操作。&lt;/p&gt;
&lt;p&gt;搜索 Flat vs Skeuomorphism 了解更多信息。&lt;/p&gt;
&lt;h2 id="常见的设计语言"&gt;常见的设计语言&lt;/h2&gt;
&lt;p&gt;Google Material Design、Microsoft Fluent Design System、Alibaba Ant Design &amp;hellip;&lt;/p&gt;
&lt;h2 id="material-design"&gt;Material Design&lt;/h2&gt;
&lt;h3 id="设计语言"&gt;设计语言&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://material.io/design"&gt;https://material.io/design&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="组件库"&gt;组件库&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://github.com/MudBlazor/MudBlazor"&gt;https://github.com/MudBlazor/MudBlazor&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/MudBlazor/"&gt;https://github.com/MudBlazor/&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="fluent-design-system"&gt;Fluent Design System&lt;/h2&gt;
&lt;h3 id="设计语言-1"&gt;设计语言&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://www.microsoft.com/design/fluent"&gt;https://www.microsoft.com/design/fluent&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="组件库-1"&gt;组件库&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://github.com/microsoft/fast-blazor"&gt;https://github.com/microsoft/fast-blazor&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="ant-design"&gt;Ant Design&lt;/h2&gt;
&lt;h3 id="设计语言-2"&gt;设计语言&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://ant.design"&gt;https://ant.design&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="组件库-2"&gt;组件库&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://github.com/ant-design-blazor/ant-design-blazor"&gt;https://github.com/ant-design-blazor/ant-design-blazor&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="商业组件库"&gt;商业组件库&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/radzenhq/radzen-blazor"&gt;https://github.com/radzenhq/radzen-blazor&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="原型工具"&gt;原型工具&lt;/h2&gt;
&lt;p&gt;MasterGo 和 Figma 是两个常用的原型工具，可以用来设计和制作用户界面原型，提供了丰富的组件库和交互效果，可以帮助设计师快速制作用户界面原型。&lt;/p&gt;
&lt;h2 id="设计令牌"&gt;设计令牌&lt;/h2&gt;
&lt;p&gt;设计令牌（Design Tokens）是一种用来定义设计系统的方法，包括颜色、字体、间距、阴影等设计元素，可以帮助设计师和开发者快速创建一致的设计风格，提高设计效率。&lt;/p&gt;
&lt;h2 id="选择组件"&gt;选择组件&lt;/h2&gt;
&lt;p&gt;风格统一、易用性、可定制性、性能优化、文档完善、社区活跃、维护更新、商业授权等因素都会影响组件库的选择。&lt;/p&gt;
&lt;h2 id="互补色"&gt;互补色&lt;/h2&gt;
&lt;p&gt;互补色是指在色彩环中相对位置相反的两种颜色，当两种互补色放在一起时，会产生强烈的对比效果，使得两种颜色更加突出。互补色的组合通常会产生视觉冲击，因此在设计中需要谨慎使用，浏览器开发者工具监测对比度，以确保用户体验。&lt;/p&gt;
&lt;h2 id="应用案例"&gt;应用案例&lt;/h2&gt;
&lt;p&gt;Aspire 的仪表盘设计是基于 Fluent Design 的，采用了扁平化设计风格，简洁、直观、易用，提高了用户体验。&lt;/p&gt;</description></item><item><title>网关和服务发现</title><link>https://www.hiwork.me/posts/helloshop/yarp-reverse-proxy/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/yarp-reverse-proxy/</guid><description>&lt;h1 id="网关和服务发现"&gt;网关和服务发现&lt;/h1&gt;
&lt;h2 id="构建和设计-api-网关"&gt;构建和设计 API 网关&lt;/h2&gt;
&lt;p&gt;API 网关是一个服务器，它是客户端和后端服务之间的中介。它接收来自客户端的请求，然后将这些请求转发到后端服务。API 网关还可以执行其他任务，例如身份验证、监视、负载平衡、缓存、请求分析和日志记录。&lt;/p&gt;
&lt;h2 id="bff-模式聚合多个服务"&gt;BFF 模式聚合多个服务&lt;/h2&gt;
&lt;p&gt;API 网关可以聚合多个服务，基于 BFF（后端用于前端）模式，当客户端请求数据时，API 网关可以调用多个服务来获取数据，然后将数据聚合到一个响应中返回给客户端，以减少客户端与后端服务之间的通信次数，从而提高性能。&lt;/p&gt;
&lt;h2 id="常见的-api-网关"&gt;常见的 API 网关&lt;/h2&gt;
&lt;p&gt;Nginx、Envoy、Ocelot、Yarp、Zuul、Kong、Traefik、Tyk、Ambassador、Gloo、AWS API Gateway、Azure API Management、Google Cloud Endpoints、Apigee、MuleSoft Anypoint API Gateway、WSO2 API Manager、TIBCO Mashery、3scale API Management、Akana、CA API Gateway、Dell Boomi、DreamFactory、IBM API Connect、Mashape、Mashery、MuleSoft Anypoint Platform、OpenLegacy、Oracle API Platform Cloud Service、Repre。&lt;/p&gt;
&lt;h2 id="基于net-的开源-api-网关"&gt;基于.NET 的开源 API 网关&lt;/h2&gt;
&lt;p&gt;Ocelot 和 Yarp 是一个基于.NET 的开源 API 网关，它可以用于构建和设计微服务架构中的 API 网关。Yarp 提供了很多功能，例如路由、负载平衡、认证、授权、限流、熔断、重试、缓存、请求转发、请求重写、请求日志、响应缓存、响应压缩、响应缓存、响应重写、响应日志、监视、跟踪、跨域、安全、性能、可扩展性、可配置性、可管理性、可监视性、可测试性、可部署性、可维护性、可扩展性、可升级。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/microsoft/reverse-proxy"&gt;Github 开源仓库&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://microsoft.github.io/reverse-proxy"&gt;官方参考文档&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="使用-yarp-构建和设计-api-网关"&gt;使用 Yarp 构建和设计 API 网关&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet add package Yarp.ReverseProxy
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="使用配置文件配置-yarp"&gt;使用配置文件配置 Yarp&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;ReverseProxy&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;Routes&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;testserviceRoute&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;ClusterId&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;testServiceCluster&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;Match&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;Path&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;testservice/{**remainder}&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;Transforms&amp;#34;&lt;/span&gt;: [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; { &lt;span style="color:#f92672"&gt;&amp;#34;PathPattern&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;{**remainder}&amp;#34;&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;Clusters&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;testServiceCluster&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;Destinations&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;testServiceCluster/destination1&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;Address&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;http://identityservice&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="net-中的服务发现"&gt;.NET 中的服务发现&lt;/h2&gt;
&lt;p&gt;服务发现是一种用于发现和定位服务的机制，它可以帮助客户端找到服务的位置和地址。服务发现可以用于构建和设计微服务架构中的 API 网关，以便客户端可以通过 API 网关访问后端服务。&lt;/p&gt;</description></item><item><title>聚合 API 网关</title><link>https://www.hiwork.me/posts/helloshop/gateway-aggregation/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/gateway-aggregation/</guid><description>&lt;h1 id="聚合-api-网关"&gt;聚合 API 网关&lt;/h1&gt;
&lt;p&gt;使用网关可将多个单独请求聚合成一个请求。 当客户端必须向不同的后端系统发出多个调用来执行某项操作时，此模式非常有用。&lt;/p&gt;
&lt;h2 id="上下文和问题"&gt;上下文和问题&lt;/h2&gt;
&lt;p&gt;在某些情况下，客户端需要向多个后端系统发出多个请求。 例如，客户端可能需要从多个服务中检索数据，然后将这些数据聚合到一个响应中。 在这种情况下，客户端必须发出多个请求，这可能会导致性能问题。 此外，客户端还必须处理多个响应，这可能会导致复杂性问题。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://oss.xcode.me/notes/helloshop/gateway-aggregation-problem.svg" alt="gateway-aggregation-problem"&gt;&lt;/p&gt;
&lt;h2 id="解决方案"&gt;解决方案&lt;/h2&gt;
&lt;p&gt;使用网关减少客户端与服务之间的通信频率。 网关会接收客户端请求，将请求分派到不同的后端系统，然后聚合结果并将其返回给请求客户端。此模式可以减少应用程序向后端服务发出的请求数，并通过高延迟网络改进应用程序的性能。&lt;/p&gt;
&lt;p&gt;&lt;img src="https://oss.xcode.me/notes/helloshop/gateway-aggregation.svg" alt="gateway-aggregation"&gt;&lt;/p&gt;
&lt;h2 id="权限聚合"&gt;权限聚合&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;interface&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;IPermissionService&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Task&amp;lt;IReadOnlyList&amp;lt;PermissionGroupDefinitionResponse&amp;gt;&amp;gt; GetAllPermissionDefinitionsAsync(CancellationToken cancellationToken=&lt;span style="color:#66d9ef"&gt;default&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="重构用户和角色控制器"&gt;重构用户和角色控制器&lt;/h2&gt;
&lt;p&gt;修改 UsersController 从 IdentityServiceDbContext 更改为 UserManager&lt;!-- raw HTML omitted --&gt;&lt;/p&gt;
&lt;p&gt;实现 RolesController 使用 RoleManager&lt;!-- raw HTML omitted --&gt;&lt;/p&gt;
&lt;p&gt;UserCreateRequestValidator 从 IdentityServiceDbContext 更改为 UserManager&lt;!-- raw HTML omitted --&gt;&lt;/p&gt;
&lt;p&gt;权限定义端点返回的 Json 中 name 属性更改为 groupName&lt;/p&gt;
&lt;h2 id="远程权限检查-bug-修复"&gt;远程权限检查 BUG 修复&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; Task&amp;lt;IActionResult&amp;gt; CheckPermission(&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; roleId, &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt; permissionName, &lt;span style="color:#66d9ef"&gt;string?&lt;/span&gt; resourceType = &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;string?&lt;/span&gt; resourceId = &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="注意事项"&gt;注意事项&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;网关不应在后端服务之间造成服务耦合。&lt;/p&gt;</description></item><item><title>聚合 OpenApi 文档</title><link>https://www.hiwork.me/posts/helloshop/open-api-aggregate/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/open-api-aggregate/</guid><description>&lt;h1 id="聚合-openapi-文档"&gt;聚合 OpenApi 文档&lt;/h1&gt;
&lt;p&gt;每个微服务都有自己的 OpenApi 文档，但是在实际开发中，我们更希望能够将所有微服务的 OpenApi 文档聚合到一起，以便于查看和调试，微服务都是基于 Aspire 框架开发的，所以我们可以使用 Aspire 框架提供的服务发现功能来自动聚合所有微服务的 OpenApi 文档。&lt;/p&gt;
&lt;h2 id="使用-aspire-服务发现自动配置-openapi-文档"&gt;使用 Aspire 服务发现自动配置 OpenApi 文档&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OpenApiConfigureOptions&lt;/span&gt;() : IConfigureOptions&amp;lt;SwaggerUIOptions&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.Services.AddTransient&amp;lt;IConfigureOptions&amp;lt;SwaggerUIOptions&amp;gt;, OpenApiConfigureOptions&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="定制-openapi-文档样式"&gt;定制 OpenApi 文档样式&lt;/h2&gt;
&lt;p&gt;在 Resource/OpenApi 文件夹下创建 &lt;code&gt;Custom.css&lt;/code&gt; 文件。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-css" data-lang="css"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.&lt;span style="color:#a6e22e"&gt;swagger-ui&lt;/span&gt; .&lt;span style="color:#a6e22e"&gt;topbar-wrapper&lt;/span&gt; &lt;span style="color:#f92672"&gt;img&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;content&lt;/span&gt;: url(&lt;span style="color:#e6db74"&gt;&amp;#39;https://test.com/logo.svg&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;.&lt;span style="color:#a6e22e"&gt;swagger-ui&lt;/span&gt; .&lt;span style="color:#a6e22e"&gt;topbar-wrapper&lt;/span&gt; .&lt;span style="color:#a6e22e"&gt;link&lt;/span&gt;::&lt;span style="color:#a6e22e"&gt;after&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;margin-left&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;0.5&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;rem&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;content&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;HelloWorld&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="关联-css-文件到-openapi-文档"&gt;关联 CSS 文件到 OpenApi 文档&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;OpenApiExtensions&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; IServiceCollection AddOpenApi(&lt;span style="color:#66d9ef"&gt;this&lt;/span&gt; IServiceCollection)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; services.Configure&amp;lt;SwaggerUIOptions&amp;gt;(options =&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.DocumentTitle = Assembly.GetExecutingAssembly().GetName().Name;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.InjectStylesheet(&lt;span style="color:#e6db74"&gt;&amp;#34;/ServiceDefaults/Resources/OpenApi/Custom.css&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; IApplicationBuilder UseOpenApi(&lt;span style="color:#66d9ef"&gt;this&lt;/span&gt; IApplicationBuilder app)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Configure the HTTP request pipeline.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; app.UseSwagger(apiConfigureOptions)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; app.Map(&lt;span style="color:#e6db74"&gt;&amp;#34;/ServiceDefaults&amp;#34;&lt;/span&gt;, appBuilder =&amp;gt; appBuilder.UseStaticFiles(&lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; StaticFileOptions
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; FileProvider = &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; EmbeddedFileProvider(Assembly.GetExecutingAssembly())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; app;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>自动映射实体和模型</title><link>https://www.hiwork.me/posts/helloshop/model-mapper/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/model-mapper/</guid><description>&lt;h1 id="自动映射实体和模型"&gt;自动映射实体和模型&lt;/h1&gt;
&lt;h2 id="使用-automapper-自动映射实体和模型"&gt;使用 AutoMapper 自动映射实体和模型&lt;/h2&gt;
&lt;p&gt;AutoMapper 是一个对象映射工具，可以自动映射实体对象和模型对象，减少手动映射的工作量，提高开发效率，除此之外 Mapster 也是一个高性能的对象映射工具，支持源生成代码，性能更好。AutoMapper 比 Mapster 更加流行，更加成熟，更加稳定，更加易用，更加灵活，更加强大，更加全面，更加受欢迎。AutoMapper 是 .NET Foundation 的一部分，是一个开源项目，是一个非常优秀的对象映射工具。&lt;/p&gt;
&lt;h2 id="安装-nuget-包"&gt;安装 NuGet 包&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet add package AutoMapper
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="在依赖注入中注册-automapper"&gt;在依赖注入中注册 AutoMapper&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly());
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="创建映射配置文件"&gt;创建映射配置文件&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;MappingProfile&lt;/span&gt; : Profile
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; MappingProfile()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CreateMap&amp;lt;User, UserDto&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CreateMap&amp;lt;UserDto, User&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="使用-automapper-映射实体和模型"&gt;使用 AutoMapper 映射实体和模型&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;UserService&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;readonly&lt;/span&gt; IMapper _mapper;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; UserService(IMapper mapper)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; _mapper = mapper;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; UserDto GetUser(&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; id)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; user = _dbContext.Users.Find(id);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; _mapper.Map&amp;lt;UserDto&amp;gt;(user);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; UpdateUser(UserDto userDto)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; user = _mapper.Map&amp;lt;User&amp;gt;(userDto);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; _dbContext.Users.Update(user);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; _dbContext.SaveChanges();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="分页模式"&gt;分页模式&lt;/h2&gt;
&lt;p&gt;常见的分页模式有两种，一种是基于页码和页大小的分页模式，一种是基于游标和页大小的分页模式。&lt;/p&gt;</description></item><item><title>自动迁移和数据库初始化</title><link>https://www.hiwork.me/posts/helloshop/migration-dataseeding/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/migration-dataseeding/</guid><description>&lt;h1 id="自动迁移和数据库初始化"&gt;自动迁移和数据库初始化&lt;/h1&gt;
&lt;h2 id="生成迁移脚本"&gt;生成迁移脚本&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet ef migrations add InitialCreate --output-dir Infrastructure/Migrations
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="手动将最新迁移脚本应用到数据库"&gt;手动将最新迁移脚本应用到数据库&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet ef database update
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="使用代码创建数据库"&gt;使用代码创建数据库&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; dbContext.Database.EnsureCreatedAsync();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;EnsureCreatedAsync&lt;/code&gt; 会创建数据库（如果不存在），并会创建所有表，但不会应用迁移脚本，适用于开发环境。&lt;/p&gt;
&lt;h2 id="使用代码将最新迁移脚本应用到数据库"&gt;使用代码将最新迁移脚本应用到数据库&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; dbContext.Database.MigrateAsync();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;MigrateAsync&lt;/code&gt; 会应用迁移脚本，如果数据库不存在，会先创建数据库，适用于生产环境。&lt;/p&gt;
&lt;h2 id="注意事项"&gt;注意事项&lt;/h2&gt;
&lt;p&gt;如果先前已经使用 &lt;code&gt;EnsureCreatedAsync&lt;/code&gt; 创建了数据库，再使用 &lt;code&gt;MigrateAsync&lt;/code&gt; 会抛出异常，因为数据库和所有表已经存在，因为迁移中有创建表的操作脚本。&lt;/p&gt;
&lt;p&gt;如果数据库不存在，使用 &lt;code&gt;MigrateAsync&lt;/code&gt; 会先创建数据库，再创建迁移历史表，然后再应用迁移脚本，但这个过程中会打印一些错误警告信息，虽然可以忽略，但不够优雅。&lt;/p&gt;
&lt;h2 id="优雅的做法"&gt;优雅的做法&lt;/h2&gt;
&lt;p&gt;这里使用到一些 EF Core 的内部服务，注意他们在实现细节上的区别。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;private&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; ValueTask RunMigrationAsync(TDbContext dbContext, CancellationToken cancellationToken)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; strategy = dbContext.Database.CreateExecutionStrategy();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; dbCreator = dbContext.GetService&amp;lt;IRelationalDatabaseCreator&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; historyRepository = dbContext.GetService&amp;lt;IHistoryRepository&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; strategy.ExecuteAsync(&lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; () =&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (!&lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; dbCreator.ExistsAsync(cancellationToken))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; dbCreator.CreateAsync(cancellationToken);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; historyRepository.CreateIfNotExistsAsync();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; dbContext.Database.MigrateAsync(cancellationToken);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;1、使用 &lt;code&gt;dbCreator.ExistsAsync&lt;/code&gt; 先检查数据库是否存在，不存在则仅仅创建数据库（它不会创建表），这个时候得到一个空的数据库。&lt;/p&gt;</description></item><item><title>订单微服务架构概述</title><link>https://www.hiwork.me/posts/helloshop/ordering-architecture/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/ordering-architecture/</guid><description>&lt;h1 id="订单微服务架构概述"&gt;订单微服务架构概述&lt;/h1&gt;
&lt;p&gt;订单部分使用了很多技术点，这包括 CQRS 模式、MediatR 本地命令分发、本地事件处理、分布式事件的抽象和实现、发件箱模式、分布式锁等技术。&lt;/p&gt;
&lt;h2 id="订单服务架构图"&gt;订单服务架构图&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://oss.xcode.me/notes/helloshop/ordering-architecture.svg" alt="系统基本架构"&gt;&lt;/p&gt;
&lt;h2 id="微服务通信"&gt;微服务通信&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://oss.xcode.me/notes/helloshop/ordering-sequence.svg" alt="微服务通信"&gt;&lt;/p&gt;
&lt;h2 id="客户端部分"&gt;客户端部分&lt;/h2&gt;
&lt;p&gt;客户端部分主要包括了订单的创建、支付、取消、查询等功能。客户端通过 API Gateway 调用订单微服务，订单微服务通过 MediatR 处理命令，然后通过事件总线发布事件，事件总线将事件分发到事件处理器，事件处理器处理事件，然后将事件持久化到数据库。&lt;/p&gt;
&lt;h2 id="订单-webapi-部分"&gt;订单 WebApi 部分&lt;/h2&gt;
&lt;p&gt;使用 WebApi 作为订单微服务的入口，接收客户端的请求，然后通过 MediatR 处理命令，然后通过事件总线发布事件。&lt;/p&gt;
&lt;h2 id="queries-部分"&gt;Queries 部分&lt;/h2&gt;
&lt;p&gt;使用从库来处理查询，从库是一个独立的数据库，用于处理查询，从库的数据是通过流式复制从主库同步过来的，从库的数据是只读的，从库的数据是最终一致的。&lt;/p&gt;
&lt;h2 id="commands-部分"&gt;Commands 部分&lt;/h2&gt;
&lt;p&gt;使用命令模式处理订单的创建、支付、取消等命令，命令模式是一个行为设计模式，命令数据最终会被持久化到主数据库。&lt;/p&gt;
&lt;h2 id="本地事件"&gt;本地事件&lt;/h2&gt;
&lt;p&gt;本地事件是指在微服务内部的事件，本地事件是通过 MediatR 发布的，只能在当前微服务内部使用。&lt;/p&gt;
&lt;h2 id="分布式事件"&gt;分布式事件&lt;/h2&gt;
&lt;p&gt;分布式事件是指在微服务之间的事件，分布式事件是通过事件总线发布的，可以在多个微服务之间使用，事件的发布和处理是异步的，使用 Dapr 的 Pub/Sub 组件实现事件的发布和订阅，可使用很多种消息队列来实现。&lt;/p&gt;
&lt;h2 id="使用分布式锁"&gt;使用分布式锁&lt;/h2&gt;
&lt;p&gt;使用分布式锁来处理并发问题，分布式锁是通过 Dapr 组件实现的，分布式锁是一个分布式系统中的同步工具，用于解决并发问题，目前 Dapr 的分布式锁是基于 Redis 实现的。&lt;/p&gt;</description></item><item><title>设计权限定义和权限提供者</title><link>https://www.hiwork.me/posts/helloshop/permission-definition/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/permission-definition/</guid><description>&lt;h1 id="设计权限定义和权限提供者"&gt;设计权限定义和权限提供者&lt;/h1&gt;
&lt;h2 id="权限定义"&gt;权限定义&lt;/h2&gt;
&lt;p&gt;每个微服务是自治的，权限定义也是每个微服务的职责，各自定义自己的权限，权限管理器会自动合并权限定义，权限定义上下文是微服务内的权限定义和全局权限定义的合集，权限提供者会根据权限定义上下文提供权限数据，权限分组是为了方便权限定义的管理，权限分组是权限定义的一部分，权限提供者自动注入到容器，提供权限查询端点。&lt;/p&gt;
&lt;h2 id="架构设计"&gt;架构设计&lt;/h2&gt;
&lt;p&gt;&lt;img src="https://oss.xcode.me/notes/helloshop/permission-definition.svg" alt="权限定义"&gt;&lt;/p&gt;
&lt;h2 id="权限扩展方法"&gt;权限扩展方法&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;IServiceCollection AddPermissionDefinitions(&lt;span style="color:#66d9ef"&gt;this&lt;/span&gt; IServiceCollection services)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;EndpointRouteBuilder MapPermissionDefinitions(&lt;span style="color:#66d9ef"&gt;this&lt;/span&gt; IEndpointRouteBuilder endpoints, &lt;span style="color:#66d9ef"&gt;params&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;[] tags)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>负载测试和压力测试</title><link>https://www.hiwork.me/posts/helloshop/load-tests/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/load-tests/</guid><description>&lt;h1 id="负载测试和压力测试"&gt;负载测试和压力测试&lt;/h1&gt;
&lt;h2 id="负载测试"&gt;负载测试&lt;/h2&gt;
&lt;p&gt;测试应用是否可以在特定情况下处理指定的用户负载，同时仍满足响应目标，应用在正常状态下运行。&lt;/p&gt;
&lt;h2 id="压力测试"&gt;压力测试&lt;/h2&gt;
&lt;p&gt;在极端条件下（通常为长时间）运行时测试应用的稳定性，测试会对应用施加高用户负载（峰值或逐渐增加的负载）或限制应用的计算资源。压力测试可确定压力下的应用是否能够从故障中恢复，并正常返回到预期的行为，在压力下，应用在异常高的压力下运行。&lt;/p&gt;
&lt;h2 id="测试工具"&gt;测试工具&lt;/h2&gt;
&lt;p&gt;云端测试 和 Apache JMeter 是两种流行的负载测试工具。负载测试和压力测试应在发布和生产模式下完成，而不是在调试和开发模式下进行。&lt;/p&gt;
&lt;h2 id="使用-apache-jmeter-工具"&gt;使用 Apache JMeter 工具&lt;/h2&gt;
&lt;p&gt;Apache JMeter 是一个开源的 Java 应用程序，用于执行负载测试、功能测试和性能测试。JMeter 可以模拟多种类型的负载，包括负载测试、压力测试、功能测试、基准测试等。&lt;/p&gt;
&lt;p&gt;提供强大的图形化界面，可以通过图形化界面创建测试计划，然后执行测试计划。JMeter 支持多种协议，包括 HTTP、HTTPS、FTP、JMS、SOAP、TCP 和 LDAP，还可以通过插件支持其他协议。&lt;/p&gt;
&lt;p&gt;&lt;a href="https://jmeter.apache.org/usermanual/build-web-test-plan.html"&gt;https://jmeter.apache.org/usermanual/build-web-test-plan.html&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="基准测试"&gt;基准测试&lt;/h2&gt;
&lt;p&gt;基准测试是一种测试方法，用于确定应用程序的性能基准，以便在应用程序的生命周期中进行性能优化。使用 &lt;code&gt;BenchmarkDotNet&lt;/code&gt; 可以在 .NET 应用程序中进行基准测试。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet add package BenchmarkDotNet
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Md5VsSha256&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt; [Benchmark]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;byte&lt;/span&gt;[] Sha256() =&amp;gt; sha256.ComputeHash(data);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt; [Benchmark]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;byte&lt;/span&gt;[] Md5() =&amp;gt; md5.ComputeHash(data);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Program&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;static&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; Main(&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;[] args)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; BenchmarkRunner.Run&amp;lt;Md5VsSha256&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>身份认证系统</title><link>https://www.hiwork.me/posts/helloshop/identity-api-endpoints/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/identity-api-endpoints/</guid><description>&lt;h1 id="身份认证系统"&gt;身份认证系统&lt;/h1&gt;
&lt;h2 id="aspnet-core-identity-体系结构"&gt;ASP.NET Core Identity 体系结构&lt;/h2&gt;
&lt;p&gt;ASP.NET Core Identity 是一个成熟的身份认证系统，它提供了用户管理、角色管理、声明管理、密码管理、登录管理、外部登录管理、双重身份认证、电子邮件确认、电话号码确认、安全令牌管理、用户锁定、用户解锁、用户注销、用户删除、用户恢复等功能。&lt;/p&gt;
&lt;h2 id="identity-体系结构说明"&gt;Identity 体系结构说明&lt;/h2&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;实体类型&lt;/th&gt;
 &lt;th&gt;说明&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;User&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;表示用户。&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;Role&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;表示角色。&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;UserClaim&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;表示用户拥有的声明。&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;UserToken&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;表示用户的身份验证令牌。&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;UserLogin&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;将用户与登录名相关联。&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;RoleClaim&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;表示向角色内的所有用户授予的声明。&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;UserRole&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;关联用户和角色的联接实体。&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="使用-efcore-存储-identity-数据"&gt;使用 EfCore 存储 Identity 数据&lt;/h2&gt;
&lt;h3 id="定义实体类型"&gt;定义实体类型&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;User&lt;/span&gt; : IdentityUser&amp;lt;&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; DateTimeOffset CreationTime { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; } = DateTimeOffset.UtcNow;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Role&lt;/span&gt; : IdentityRole&amp;lt;&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; DateTimeOffset CreationTime { &lt;span style="color:#66d9ef"&gt;get&lt;/span&gt;; &lt;span style="color:#66d9ef"&gt;set&lt;/span&gt;; } = DateTimeOffset.UtcNow;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="配置实体类型"&gt;配置实体类型&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;UserEntityTypeConfiguration&lt;/span&gt; : IEntityTypeConfiguration&amp;lt;User&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; Configure(EntityTypeBuilder&amp;lt;User&amp;gt; builder)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.ToTable(&lt;span style="color:#e6db74"&gt;&amp;#34;Users&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.HasKey(x =&amp;gt; x.Id);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.Property(x =&amp;gt; x.CreationTime);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="复用-identity-数据库上下文"&gt;复用 Identity 数据库上下文&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet &lt;span style="color:#66d9ef"&gt;add&lt;/span&gt; package Microsoft.AspNetCore.Identity.EntityFrameworkCore
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;IdentityServiceDbContext&lt;/span&gt;(DbContextOptions&amp;lt;IdentityServiceDbContext&amp;gt; options) : IdentityDbContext&amp;lt;User, Role, &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt;(options)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;protected&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;override&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; OnModelCreating(ModelBuilder builder)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;base&lt;/span&gt;.OnModelCreating(builder);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; AppContext.SetSwitch(&lt;span style="color:#e6db74"&gt;&amp;#34;Npgsql.EnableLegacyTimestampBehavior&amp;#34;&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; AppContext.SetSwitch(&lt;span style="color:#e6db74"&gt;&amp;#34;Npgsql.DisableDateTimeInfinityConversions&amp;#34;&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="使用-identity-api-终结点"&gt;使用 Identity API 终结点&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;builder.Services.AddIdentityApiEndpoints&amp;lt;User&amp;gt;(options =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.Password.RequireDigit = &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.Password.RequireLowercase = &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.Password.RequireUppercase = &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.Password.RequireNonAlphanumeric = &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.Password.RequiredLength = &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options.SignIn.RequireConfirmedAccount = &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}).AddEntityFrameworkStores&amp;lt;IdentityServiceDbContext&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app.MapGroup(&lt;span style="color:#e6db74"&gt;&amp;#34;identity&amp;#34;&lt;/span&gt;).MapIdentityApi&amp;lt;User&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="操作-identity-数据"&gt;操作 Identity 数据&lt;/h2&gt;
&lt;p&gt;UserManager、RoleManager、SignInManager、PasswordValidator 等服务都可以用来操作 Identity 数据，没有特殊情况不建议使用 DbContext 直接操作 Identity 数据。&lt;/p&gt;</description></item><item><title>零度框架中的测试</title><link>https://www.hiwork.me/posts/helloshop/dot-net-core-testing/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/dot-net-core-testing/</guid><description>&lt;h1 id="零度框架中的测试"&gt;零度框架中的测试&lt;/h1&gt;
&lt;p&gt;使用自动测试是确保应用程序代码按作者期望执行操作的一种绝佳方式。 零度框架中提供单元测试、集成测试和负载测试，.NET 平台上的测试框架有 &lt;code&gt;xUnit&lt;/code&gt;、&lt;code&gt;NUnit&lt;/code&gt; 和 &lt;code&gt;MSTest&lt;/code&gt;，用的最多的是 &lt;code&gt;xUnit&lt;/code&gt;，无论使用任何一种测试框架，都可以通过命令行或者 IDE 来运行测试。&lt;/p&gt;
&lt;h2 id="单元测试"&gt;单元测试&lt;/h2&gt;
&lt;p&gt;单元测试是一种试验单个软件组件或方法（也称为“工作单元”）的测试。 单元测试仅应测试开发人员控件内的代码。 它们不测试基础结构问题。 基础结构问题包括与数据库、文件系统和网络资源的交互，零度框架中的单元测试使用 &lt;code&gt;xUnit&lt;/code&gt; 框架，&lt;/p&gt;
&lt;p&gt;项目命名规范： &lt;code&gt;&amp;lt;ProjectName&amp;gt;.UnitTests&lt;/code&gt;。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-csharp" data-lang="csharp"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Calculator&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; Add(&lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; a, &lt;span style="color:#66d9ef"&gt;int&lt;/span&gt; b)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; a + b;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;CalculatorTests&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt; [Fact]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;public&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;void&lt;/span&gt; Add_WhenCalled_ReturnsTheSumOfArguments()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Arrange&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; calculator = &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; Calculator();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Act&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;var&lt;/span&gt; result = calculator.Add(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// Assert&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Assert.Equal(&lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;, result);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="单元测试最佳做法"&gt;单元测试最佳做法&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://learn.microsoft.com/zh-cn/dotnet/core/testing/unit-testing-best-practices"&gt;https://learn.microsoft.com/zh-cn/dotnet/core/testing/unit-testing-best-practices&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="集成测试"&gt;集成测试&lt;/h2&gt;
&lt;p&gt;集成测试也称为功能测试，与单元测试的不同之处在于，它试验两个或更多软件组件一同工作集成能力。这些测试在更广泛范围的受测系统上运行，而单元测试则侧重于单个组件函数，通常，集成测试会包括对基础结构问题的测试，例如数据库、文件系统和网络资源的交互。&lt;/p&gt;
&lt;p&gt;项目命名规范： &lt;code&gt;&amp;lt;ProjectName&amp;gt;.FunctionalTests&lt;/code&gt;。&lt;/p&gt;</description></item><item><title>零度框架升级到 Aspire 13.0 版本</title><link>https://www.hiwork.me/posts/helloshop/dotnet-aspire-13-0/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/dotnet-aspire-13-0/</guid><description>&lt;h1 id="零度框架升级到-aspire-130-版本"&gt;零度框架升级到 Aspire 13.0 版本&lt;/h1&gt;
&lt;h2 id="简化解决方案文件格式"&gt;简化解决方案文件格式&lt;/h2&gt;
&lt;p&gt;从 sln 到 slnx，新的 slnx 格式更易于阅读和维护，减少了冗余信息，使得项目结构更加清晰。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet sln migrate
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;同时修改 slnf 中引用的 sln 文件为 slnx 文件。&lt;/p&gt;
&lt;h2 id="将项目目标框架升级到-net-10-版本"&gt;将项目目标框架升级到 .NET 10 版本&lt;/h2&gt;
&lt;p&gt;在项目文件中将目标框架从 net9.0 修改为 net10.0：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;TargetFramework&amp;gt;&lt;/span&gt;net10.0&lt;span style="color:#f92672"&gt;&amp;lt;/TargetFramework&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="将-aspire-升级到-1300-版本"&gt;将 Aspire 升级到 13.0.0 版本&lt;/h2&gt;
&lt;p&gt;在项目文件中将 Aspire 包的版本号更新为 13.0.0&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-diff" data-lang="diff"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;- &amp;lt;Project Sdk=&amp;#34;Microsoft.NET.Sdk&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;+ &amp;lt;Project Sdk=&amp;#34;Aspire.AppHost.Sdk/13.0.0&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;- &amp;lt;Sdk Name=&amp;#34;Aspire.AppHost.Sdk&amp;#34; Version=&amp;#34;9.5.2&amp;#34; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;PropertyGroup&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;OutputType&amp;gt;Exe&amp;lt;/OutputType&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;- &amp;lt;TargetFramework&amp;gt;net9.0&amp;lt;/TargetFramework&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;+ &amp;lt;TargetFramework&amp;gt;net10.0&amp;lt;/TargetFramework&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;ImplicitUsings&amp;gt;enable&amp;lt;/ImplicitUsings&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;Nullable&amp;gt;enable&amp;lt;/Nullable&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;- &amp;lt;IsAspireHost&amp;gt;true&amp;lt;/IsAspireHost&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;UserSecretsId&amp;gt;0afc20a6-cd99-4bf7-aae1-1359b0d45189&amp;lt;/UserSecretsId&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/PropertyGroup&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;ItemGroup&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;- &amp;lt;PackageReference Include=&amp;#34;Aspire.Hosting.AppHost&amp;#34; Version=&amp;#34;9.5.2&amp;#34; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;+ &amp;lt;!-- Aspire.Hosting.AppHost is now included automatically by the SDK --&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/ItemGroup&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/Project&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="将中央包-directorypackagesprops-中的包更到最新"&gt;将中央包 Directory.Packages.props 中的包更到最新&lt;/h2&gt;
&lt;p&gt;列出所有需要更新的包&lt;/p&gt;</description></item><item><title>零度框架升级到 Aspire 9.0 版本</title><link>https://www.hiwork.me/posts/helloshop/upgrade-to-aspire-9/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/upgrade-to-aspire-9/</guid><description>&lt;h1 id="零度框架升级到-aspire-90-版本"&gt;零度框架升级到 Aspire 9.0 版本&lt;/h1&gt;
&lt;h2 id="升级-visual-studio-开发工具"&gt;升级 Visual Studio 开发工具&lt;/h2&gt;
&lt;p&gt;使用 Visual Studio Installer 升级，升级到 v17.12 后，自动安装 .NET 9.0 SDK 和 Aspire 9.0 工作负载。&lt;/p&gt;
&lt;h2 id="非-visual-studio-开发环境"&gt;非 Visual Studio 开发环境&lt;/h2&gt;
&lt;h3 id="安装-net-core-sdk-90-版本"&gt;安装 .NET Core SDK 9.0 版本&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://dotnet.microsoft.com/zh-cn/download"&gt;安装最新 .NET SDK 9.0 版&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;查看 .NET SDK 版本：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet --version
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet --list-sdks
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="升级-aspire-工作负载"&gt;升级 Aspire 工作负载&lt;/h3&gt;
&lt;p&gt;在项目目录下执行以下命令：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet workload uninstall aspire
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet workload install aspire
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet workload list
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet workload update
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="升级项目文件"&gt;升级项目文件&lt;/h2&gt;
&lt;p&gt;Host 项目添加新节点：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;Sdk&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Name=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;Aspire.AppHost.Sdk&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Version=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;9.0.0&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;所有项目文件的目标框架改为 net9.0：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;TargetFramework&amp;gt;&lt;/span&gt;net9.0&lt;span style="color:#f92672"&gt;&amp;lt;/TargetFramework&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;安装 EF Core 基础包&lt;/p&gt;</description></item><item><title>零度框架升级到 Aspire 9.3 版本</title><link>https://www.hiwork.me/posts/helloshop/dotnet-aspire-9-3/</link><pubDate>Tue, 10 Mar 2026 15:55:34 +0800</pubDate><guid>https://www.hiwork.me/posts/helloshop/dotnet-aspire-9-3/</guid><description>&lt;h1 id="零度框架升级到-aspire-93-版本"&gt;零度框架升级到 Aspire 9.3 版本&lt;/h1&gt;
&lt;h2 id="从-host-项目删除-isaspirehost-属性"&gt;从 Host 项目删除 IsAspireHost 属性&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;IsAspireHost&amp;gt;&lt;/span&gt;true&lt;span style="color:#f92672"&gt;&amp;lt;/IsAspireHost&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="工作负载升级"&gt;工作负载升级&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet workload update
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="升级-visual-studio-到最新版"&gt;升级 Visual Studio 到最新版&lt;/h2&gt;
&lt;p&gt;通过 Visual Studio Installer 升级到最新版本。&lt;/p&gt;
&lt;h2 id="可使用-ai-升级项目"&gt;可使用 AI 升级项目&lt;/h2&gt;
&lt;p&gt;GitHub Copilot 应用现代化 - 适用于 .NET 的升级是一个功能强大的 Visual Studio 扩展，可与你配合使用，将项目升级到较新版本的 .NET、升级依赖项并应用代码修复。&lt;/p&gt;
&lt;p&gt;GitHub Copilot 应用程序现代化作为 Visual Studio 扩展分发，是一个交互式升级过程。&lt;/p&gt;
&lt;p&gt;GitHub Copilot app modernization - upgrade for .NET&lt;/p&gt;
&lt;h2 id="更新项目模板"&gt;更新项目模板&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet new update
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="使用升级助手将项目升级到最新"&gt;使用升级助手将项目升级到最新&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet tool install -g UpgradeAssistant
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dotnet upgrade-assistant upgrade
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;也可以使用 Visual Studio 的升级助手。&lt;/p&gt;
&lt;h2 id="关于-cpm-中央包管理的升级"&gt;关于 CPM 中央包管理的升级&lt;/h2&gt;
&lt;p&gt;CPM 中央包管理的升级是一个重要的更新，允许您在项目中使用中央包版本管理，升级 CPM 目前需要手动更新。&lt;/p&gt;</description></item><item><title>My First Post</title><link>https://www.hiwork.me/me/my-first-post/</link><pubDate>Tue, 10 Mar 2026 12:52:12 +0800</pubDate><guid>https://www.hiwork.me/me/my-first-post/</guid><description>&lt;p&gt;This is &lt;strong&gt;bold&lt;/strong&gt; text, and this is &lt;em&gt;emphasized&lt;/em&gt; text.&lt;/p&gt;
&lt;p&gt;Visit the &lt;a href="https://gohugo.io"&gt;Hugo&lt;/a&gt; website!&lt;/p&gt;
&lt;figure class="mermaid"&gt;
 sequenceDiagram
 participant Alice
 participant Bob
 Alice-&amp;gt;&amp;gt;John: Hello John, how are you?
 loop Healthcheck
 John-&amp;gt;&amp;gt;John: Fight against hypochondria
 end
 Note right of John: Rational thoughts &amp;lt;br/&amp;gt;prevail!
 John--&amp;gt;&amp;gt;Alice: Great!
 John-&amp;gt;&amp;gt;Bob: How about you?
 Bob--&amp;gt;&amp;gt;John: Jolly good!
&lt;/figure&gt;</description></item><item><title>My test me First Post</title><link>https://www.hiwork.me/test/my-first-post/</link><pubDate>Tue, 10 Mar 2026 12:52:12 +0800</pubDate><guid>https://www.hiwork.me/test/my-first-post/</guid><description>&lt;p&gt;This is &lt;strong&gt;bold&lt;/strong&gt; text, and this is &lt;em&gt;emphasized&lt;/em&gt; text.&lt;/p&gt;
&lt;p&gt;Visit the &lt;a href="https://gohugo.io"&gt;Hugo&lt;/a&gt; website!&lt;/p&gt;</description></item></channel></rss>