<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>笔记 &#8211; 有意与无意之间</title>
	<atom:link href="https://zhangxihai.cn/archives/category/note/feed" rel="self" type="application/rss+xml" />
	<link>https://zhangxihai.cn</link>
	<description>千淘万漉虽辛苦 吹尽狂沙始到金 - 生命不息 编程不止</description>
	<lastBuildDate>Sun, 07 Dec 2025 05:35:52 +0000</lastBuildDate>
	<language>zh-CN</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.0.11</generator>
	<item>
		<title>Dapr: 1 &#8211; 认识Dapr</title>
		<link>https://zhangxihai.cn/archives/386</link>
					<comments>https://zhangxihai.cn/archives/386#respond</comments>
		
		<dc:creator><![CDATA[胖爷]]></dc:creator>
		<pubDate>Mon, 17 Nov 2025 03:31:22 +0000</pubDate>
				<category><![CDATA[笔记]]></category>
		<guid isPermaLink="false">https://zhangxihai.cn/?p=386</guid>

					<description><![CDATA[1 Dapr是什么？ Dapr(Distributed Application Runtime), 最早是2019由微软一个国人工程师开发的。 Dapr不是框架，也不是库，它是一个分布式运行时，主要目的是让开发者更容易地构建云原生、事件驱动、以及可扩展的分布式应用，而不用深入了解复杂的底层基础设施。 这意味着你在开发元原生应用时，可以与底层基础设施轻松解耦，...]]></description>
										<content:encoded><![CDATA[<h3>1 Dapr是什么？</h3>
<p>Dapr(Distributed Application Runtime), 最早是2019由微软一个国人工程师开发的。</p>
<p>Dapr不是框架，也不是库，它是一个分布式运行时，主要目的是让开发者更容易地构建云原生、事件驱动、以及可扩展的分布式应用，而不用深入了解复杂的底层基础设施。</p>
<p>这意味着你在开发元原生应用时，可以与底层基础设施轻松解耦，可以专注于业务逻辑，而不是分布式系统的复杂性。</p>
<p>目前稳定版本是v1.16.2.</p>
<p>Dapr跨语言支持，官方团队提供：C#, Java, Go, Python, Node.js, Rust 等 SDK。</p>
<h3>2 Dapr能做什么？</h3>
<p>Dapr提供了一组称为<code>building blocks（构建块）</code>的抽象API，每个构建块抽象了一类常见的分布式系统功能。</p>
<p>通过标准化接口，Dapr允许你轻松切换底层实现（从Memcache到Redis, 从Kafka到RabbitMQ）。</p>
<p>常见构建块：</p>
<table>
<thead>
<tr>
<th>构建块</th>
<th>功能</th>
<th>示例适配器</th>
</tr>
</thead>
<tbody>
<tr>
<td>Service Invocation</td>
<td>服务调用(RPC调用)</td>
<td>Http/gRPC调用，负载均衡</td>
</tr>
<tr>
<td>StateManagement</td>
<td>状态管理</td>
<td>Redis、PostgreSQL、Cosmos DB</td>
</tr>
<tr>
<td>Pub/Sub</td>
<td>发布订阅</td>
<td>Kafka、Azure、Azure Service Bus</td>
</tr>
<tr>
<td>Bindings</td>
<td>外部系统绑定</td>
<td>Cron、消息队列、文件系统</td>
</tr>
<tr>
<td>Actors</td>
<td>虚拟Actor模型</td>
<td>类似Oleans的Actor管理</td>
</tr>
<tr>
<td>Secrets</td>
<td>秘钥管理</td>
<td>Vault、AWS Secrect Mananger</td>
</tr>
<tr>
<td>Observability</td>
<td>可观测性</td>
<td>OpenTelemetry、Zipkin、Prometheus</td>
</tr>
<tr>
<td>workflow</td>
<td>工作流编排</td>
<td>用户长事务或Saga场景</td>
</tr>
</tbody>
</table>
]]></content:encoded>
					
					<wfw:commentRss>https://zhangxihai.cn/archives/386/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>人工智能数学: 1-指数函数、幂函数与对数函数</title>
		<link>https://zhangxihai.cn/archives/400</link>
					<comments>https://zhangxihai.cn/archives/400#respond</comments>
		
		<dc:creator><![CDATA[胖爷]]></dc:creator>
		<pubDate>Wed, 07 Aug 2024 04:28:32 +0000</pubDate>
				<category><![CDATA[笔记]]></category>
		<guid isPermaLink="false">https://zhangxihai.cn/?p=400</guid>

					<description><![CDATA[指数与对数是一对Cp(Couple), 他们是一对互逆的函数，相互间有着严格的关系。 1 指数函数 1.1 什么是指数 Exponents /ɛksˈpəʊnənts/ n.指数；幂；(观点、理论的)拥护者，鼓吹者，倡导者；(某种活动的)能手，大师。exponent的复数。 指数是一个关于“疯狂、贪婪、爆炸”的故事。 任何人碰到2x2x2x2x2这样的算术式...]]></description>
										<content:encoded><![CDATA[<p>指数与对数是一对Cp(Couple), 他们是一对互逆的函数，相互间有着严格的关系。</p>
<h2>1 指数函数</h2>
<h3>1.1 什么是指数</h3>
<p>Exponents /ɛksˈpəʊnənts/ n.指数；幂；(观点、理论的)拥护者，鼓吹者，倡导者；(某种活动的)能手，大师。exponent的复数。</p>
<p>指数是一个关于“疯狂、贪婪、爆炸”的故事。</p>
<p>任何人碰到2x2x2x2x2这样的算术式都会头疼无比，瞬间懒病上身，所以数据学发明了$2^5 = 32$。</p>
<p>2就是底数， 5是指数。</p>
<p>从游戏角度来看，2就是你游戏中的主角，5就是你开了5倍挂！</p>
<p><strong>折纸传说</strong></p>
<p>如果一张0.1毫米厚的纸张，对折再对折，假设可以无限对折：</p>
<ul>
<li>对折10次，就是一本笔记本厚度；</li>
<li>对折23次，1公里；</li>
<li>对折42次，厚度可到月球。</li>
</ul>
<p><strong>丧尸传说</strong><br />
一个丧尸咬了2个人，转换成两个丧尸再给咬了2个人：<br />
<img src="https://zhangxihai.cn/wp-content/uploads/2025/12/001.png" alt="" /></p>
<p>那只要33轮，全地球的人就转换成丧尸了。</p>
<h3>1.2 严格定义</h3>
<p>设<span class="katex-eq" data-katex-display="false">a &gt; 0</span>且<span class="katex-eq" data-katex-display="false">a \neq 1</span> 为固定的实数，那么称作<br />
<span class="katex-eq" data-katex-display="false">y = a^x \quad (x \in \mathbb{R})</span></p>
<ul>
<li>定义域：<span class="katex-eq" data-katex-display="false">\mathbb{R}</span>（全体实数）</li>
<li>值域：<span class="katex-eq" data-katex-display="false">(0, +\infty)</span></li>
<li>它是单射（一对一）且满射（到正实数）的严格单调（<span class="katex-eq" data-katex-display="false">a &gt; 1</span> 时单调递增，<span class="katex-eq" data-katex-display="false"></span>0 < a < 1[/katex] 时单调递减）。</li>
</ul>
<h4>1.3 几何意义</h4>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2025/12/002.jpg" alt="" /></p>
<p>指数是努力结果的展示， </p>
]]></content:encoded>
					
					<wfw:commentRss>https://zhangxihai.cn/archives/400/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>ASP.NET Core MVC视图、局部视图及布局</title>
		<link>https://zhangxihai.cn/archives/322</link>
					<comments>https://zhangxihai.cn/archives/322#respond</comments>
		
		<dc:creator><![CDATA[胖爷]]></dc:creator>
		<pubDate>Fri, 29 Dec 2023 04:30:30 +0000</pubDate>
				<category><![CDATA[笔记]]></category>
		<category><![CDATA[编码]]></category>
		<guid isPermaLink="false">https://zhangxihai.cn/?p=322</guid>

					<description><![CDATA[翻译文章，原文链接 在MVC模式中，视图处理应用程序的数据展示和用户交互。它们还有助于通过将用户界面标记与应用程序的其他部分分离，建立MVC应用程序中的关注点分离（SoC）。 视图是一个包含嵌入式Razor标记的HTML模板。它具有.cshtml扩展名，并基于C#。Razor标记与HTML标记交互，产生然后发送给客户端的网页。 通常，每个控制器操作方法都有相...]]></description>
										<content:encoded><![CDATA[<p>翻译文章，<a href="https://code-maze.com/views-partial-views-and-layouts-in-asp-net-core-mvc/">原文链接</a></p>
<p>在MVC模式中，视图处理应用程序的数据展示和用户交互。它们还有助于通过将用户界面标记与应用程序的其他部分分离，建立MVC应用程序中的关注点分离（SoC）。</p>
<p>视图是一个包含嵌入式Razor标记的HTML模板。它具有<code>.cshtml</code>扩展名，并基于C#。Razor标记与HTML标记交互，产生然后发送给客户端的网页。</p>
<p>通常，每个控制器操作方法都有相应的单独视图文件，视图文件被分组到以各个控制器命名的文件夹中。视图存储在应用程序根目录下的<code>Views</code>文件夹中。</p>
<p>让我们看看我们在上一篇文章中创建的<code>BookStore</code>应用程序：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/a1.jpg" alt="" /></p>
<p><code>BooksController</code>的视图位于<code>Views</code>文件夹内的<code>Book</code>s文件夹中。<code>Books</code>文件夹包含<code>Create</code>、<code>Delete</code>、<code>Details</code>、<code>Edit</code>和<code>Index</code>方法的视图。当用户请求其中一个动作时，BooksController中的动作方法使用适当的视图构建网页并将其返回给用户。</p>
<p>在这篇文章中，我们将重用我们在上一部分中创建的模型和控制器，但会做一些小的更改。但是，我们将从头开始创建视图。</p>
<p>我们强烈建议访问本系列的完整导航：<a href="https://code-maze.com/asp-net-core-mvc-series/">ASP.NET Core MVC系列</a>。</p>
<p>要下载本文的源代码，请访问：<a href="https://github.com/CodeMazeBlog/aspnetcore-mvc-series">视图、局部视图和布局源代码</a>。</p>
<h3>定义 Model &amp; Controller</h3>
<p>让我们使用已有的model;</p>
<pre><code>public class Book
{
    public int Id { get; set; }
    [Display(Name = &quot;Book Title&quot;)]
    [Required]
    public string Title { get; set; }
    public string Genre { get; set; }
    [DataType(DataType.Currency)]
    [Range(1, 100)]
    public decimal Price { get; set; }
    [Display(Name = &quot;Publish Date&quot;)]
    [DataType(DataType.Date)]
    public DateTime PublishDate { get; set; }
}</code></pre>
<p>为了简化，我们将从头开始实现<code>index</code>、<code>views</code>和<code>edit</code>。这将涵盖我们在创建视图时遇到的所有常见情况。</p>
<p>现在，让我们通过移除我们不使用的操作方法来稍微修改现有的控制器：</p>
<pre><code>public class BooksController : Controller
{
    private readonly BookStoreWithDataContext _context;
    public BooksController(BookStoreWithDataContext context)
    {
        _context = context;
    }
    // GET: Books
    public async Task&lt;IActionResult&gt; Index()
    {
        return View(await _context.Book.ToListAsync());
    }
    // GET: Books/Details/5
    public async Task&lt;IActionResult&gt; Details(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }
        var book = await _context.Book
            .FirstOrDefaultAsync(m =&gt; m.Id == id);
        if (book == null)
        {
            return NotFound();
        }
        return View(book);
    }
    // GET: Books/Edit/5
    public async Task&lt;IActionResult&gt; Edit(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }
        var book = await _context.Book.FindAsync(id);
        if (book == null)
        {
            return NotFound();
        }
        return View(book);
    }
    // POST: Books/Edit/5
    // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
    // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task&lt;IActionResult&gt; Edit(int id, [Bind(&quot;Id,Title,Genre,Price,PublishDate&quot;)] Book book)
    {
        if (id != book.Id)
        {
            return NotFound();
        }
        if (ModelState.IsValid)
        {
            try
            {
                _context.Update(book);
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!BookExists(book.Id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }
            return RedirectToAction(nameof(Index));
        }
        return View(book);
    }
    private bool BookExists(int id)
    {
        return _context.Book.Any(e =&gt; e.Id == id);
    }
}</code></pre>
<p>我们现在已经准备好了模型和控制器。下一步是创建视图。</p>
<h3>使用Razor标记创建视图</h3>
<p>特定于控制器的视图将被放置在<code>Views/[ControllerName]</code>文件夹中。在多个控制器之间共享的视图则放置在<code>Views/Shared</code>文件夹中。</p>
<p>为了创建一个视图，让我们添加一个新文件，并以其关联的控制器操作的名称命名，文件扩展名为<code>.cshtml</code>。</p>
<p>例如，要创建与<code>BooksController</code>中的<code>Index</code>操作对应的视图，我们需要在<code>Views/Books</code>文件夹中创建一个<code>Index.cshtml</code>文件。这样，我们就有了一个索引页面的视图。</p>
<p>在本系列的第一部分中，我们使用HTML助手方法创建了视图。在这篇文章中，我们将使用标签助手(tag helpers)来创建视图的不同方法。</p>
<p>标签助手提供了一个HTML友好的开发体验。在大多数情况下，使用标签助手的Razor标记看起来像标准HTML。标签助手减少了Razor视图中HTML和C#之间的明确转换。</p>
<p>在许多情况下，标签助手提供了特定HTML助手的替代方法，但重要的是要理解标签助手无法替代HTML助手，因为一些HTML助手没有对应的标签助手。因此，在某些情况下，我们仍然必须使用HTML助手。</p>
<h4>index 视图</h4>
<p>现在让我们为index页面创建视图：</p>
<pre><code>@model IEnumerable&lt;WorkingWithViews.Models.Book&gt;
@{
    ViewData[&quot;Title&quot;] = &quot;Index&quot;;
    Book firstBook = Model.ToList().FirstOrDefault();
}
&lt;h1&gt;Index&lt;/h1&gt;
&lt;p&gt;
    &lt;a asp-action=&quot;Create&quot;&gt;Create New&lt;/a&gt;
&lt;/p&gt;
&lt;table class=&quot;table&quot;&gt;
    &lt;thead&gt;
        &lt;tr&gt;
            &lt;th&gt;
                &lt;label asp-for=&quot;@firstBook.Id&quot;&gt;&lt;/label&gt;
            &lt;/th&gt;
            &lt;th&gt;
                &lt;label asp-for=&quot;@firstBook.Title&quot;&gt;&lt;/label&gt;
            &lt;/th&gt;
            &lt;th&gt;
                &lt;label asp-for=&quot;@firstBook.Genre&quot;&gt;&lt;/label&gt;
            &lt;/th&gt;
            &lt;th&gt;
                &lt;label asp-for=&quot;@firstBook.Price&quot;&gt;&lt;/label&gt;
            &lt;/th&gt;
            &lt;th&gt;
                &lt;label asp-for=&quot;@firstBook.PublishDate&quot;&gt;&lt;/label&gt;
            &lt;/th&gt;
            &lt;th&gt;&lt;/th&gt;
        &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
        @foreach (var item in Model)
        {
            &lt;tr&gt;
                &lt;td&gt;
                    &lt;label&gt;@item.Id&lt;/label&gt;
                &lt;/td&gt;
                &lt;td&gt;                    
                    &lt;label&gt;@item.Title&lt;/label&gt;
                &lt;/td&gt;
                &lt;td&gt;                    
                    &lt;label&gt;@item.Genre&lt;/label&gt;
                &lt;/td&gt;
                &lt;td&gt;                    
                    &lt;label&gt;@item.Price&lt;/label&gt;
                &lt;/td&gt;
                &lt;td&gt;                    
                    &lt;label&gt;@item.PublishDate&lt;/label&gt;
                &lt;/td&gt;
                &lt;td&gt;
                    &lt;a asp-action=&quot;Edit&quot; asp-route-id=&quot;@item.Id&quot;&gt;Edit&lt;/a&gt;
                    &lt;a asp-action=&quot;Details&quot; asp-route-id=&quot;@item.Id&quot;&gt;Details&lt;/a&gt;
                    &lt;a asp-action=&quot;Delete&quot; asp-route-id=&quot;@item.Id&quot;&gt;Delete&lt;/a&gt;
                &lt;/td&gt;
            &lt;/tr&gt;
        }
    &lt;/tbody&gt;
&lt;/table&gt;</code></pre>
<p>在这里，我们使用的是强类型模型，且模型的类型为<code>IEnumerable&lt;Book&gt;</code>。</p>
<p>首先，我们声明一个变量并将列表中的第一本书分配给它。我们这样做是为了获取属性名称，并将它们分配给表头：</p>
<pre><code>Book firstBook = Model.ToList().FirstOrDefault();</code></pre>
<p><code>asp-for</code> 属性将指定的模型属性名称提取到渲染的HTML中。那么，让我们看看如何渲染一个带有Title属性名称的标签：</p>
<pre><code> &lt;label asp-for=&quot;@firstBook.Title&quot;&gt;&lt;/label&gt;</code></pre>
<p>棒极了。</p>
<p>现在我们需要渲染books集合中的所有项目。为此，我们使用foreach循环来帮助我们渲染一个HTML表格。我们可以渲染带有属性值的标签：</p>
<pre><code>&lt;label&gt;@item.Title&lt;/label&gt;</code></pre>
<p>为了创建动作链接，我们可以使用 <code>asp-action</code> 属性，而为了传递参数，我们可以使用 <code>asp-route-{parametername}</code> 格式。因此，在这种情况下，对于id参数，我们使用 <code>asp-route-id</code>。</p>
<pre><code>&lt;a asp-action=&quot;Edit&quot; asp-route-id=&quot;@item.Id&quot;&gt;Edit&lt;/a&gt;</code></pre>
<h4>Details view</h4>
<p>现在，让我们创建detail视图。</p>
<pre><code>@model WorkingWithViews.Models.Book
@{
    ViewData[&quot;Title&quot;] = &quot;Details&quot;;
}
&lt;h1&gt;Details&lt;/h1&gt;
&lt;div&gt;
    &lt;h4&gt;Book&lt;/h4&gt;
    &lt;hr /&gt;
    &lt;dl class=&quot;row&quot;&gt;
        &lt;dt class=&quot;col-sm-2&quot;&gt;
            &lt;label asp-for=&quot;Title&quot;&gt;&lt;/label&gt;
        &lt;/dt&gt;
        &lt;dd class=&quot;col-sm-10&quot;&gt;
            @Model.Title
        &lt;/dd&gt;
        &lt;dt class=&quot;col-sm-2&quot;&gt;
            &lt;label asp-for=&quot;Genre&quot;&gt;&lt;/label&gt;            
        &lt;/dt&gt;
        &lt;dd class=&quot;col-sm-10&quot;&gt;            
            @Model.Genre
        &lt;/dd&gt;
        &lt;dt class=&quot;col-sm-2&quot;&gt;
            &lt;label asp-for=&quot;Price&quot;&gt;&lt;/label&gt;            
        &lt;/dt&gt;
        &lt;dd class=&quot;col-sm-10&quot;&gt;            
            @Model.Price.ToString(&quot;C&quot;)
        &lt;/dd&gt;
        &lt;dt class=&quot;col-sm-2&quot;&gt;
            &lt;label asp-for=&quot;PublishDate&quot;&gt;&lt;/label&gt;            
        &lt;/dt&gt;
        &lt;dd class=&quot;col-sm-10&quot;&gt;            
            @Model.PublishDate.ToShortDateString()
        &lt;/dd&gt;
    &lt;/dl&gt;
&lt;/div&gt;
&lt;div&gt;    
    &lt;a asp-action=&quot;Edit&quot; asp-route-id=&quot;@Model.Id&quot;&gt;Edit&lt;/a&gt;|
    &lt;a asp-action=&quot;Index&quot;&gt;Back to List&lt;/a&gt;
&lt;/div&gt;</code></pre>
<p>我们已经创建了类似于<code>index</code>视图的内容。但模型的类型是<code>Book</code>。为了检索<code>index</code>名称，我们可以使用<code>asp-for</code>属性：</p>
<pre><code>&lt;label asp-for=&quot;Title&quot;&gt;&lt;/label&gt;</code></pre>
<p>为了显示属性值，我们可以使用 <code>@Model</code> 指令访问模型属性：</p>
<pre><code>&lt;dd class=&quot;col-sm-10&quot;&gt;
    @Model.Title
&lt;/dd&gt;</code></pre>
<h4>Edit view</h4>
<p>一旦我们完成了<code>detail</code>视图的制作，我们就可以继续创建<code>edit</code>视图：</p>
<pre><code>@model WorkingWithViews.Models.Book
@{
    ViewData[&quot;Title&quot;] = &quot;Edit&quot;;
}
&lt;h1&gt;Edit&lt;/h1&gt;
&lt;h4&gt;Book&lt;/h4&gt;
&lt;hr /&gt;
&lt;div class=&quot;row&quot;&gt;
    &lt;div class=&quot;col-md-4&quot;&gt;
        &lt;form asp-action=&quot;Edit&quot;&gt;
            &lt;div asp-validation-summary=&quot;ModelOnly&quot; class=&quot;text-danger&quot;&gt;&lt;/div&gt;
            &lt;input type=&quot;hidden&quot; asp-for=&quot;Id&quot; /&gt;
            &lt;div class=&quot;form-group&quot;&gt;
                &lt;label asp-for=&quot;Title&quot; class=&quot;control-label&quot;&gt;&lt;/label&gt;
                &lt;input asp-for=&quot;Title&quot; class=&quot;form-control&quot; /&gt;
                &lt;span asp-validation-for=&quot;Title&quot; class=&quot;text-danger&quot;&gt;&lt;/span&gt;
            &lt;/div&gt;
            &lt;div class=&quot;form-group&quot;&gt;
                &lt;label asp-for=&quot;Genre&quot; class=&quot;control-label&quot;&gt;&lt;/label&gt;
                &lt;input asp-for=&quot;Genre&quot; class=&quot;form-control&quot; /&gt;
                &lt;span asp-validation-for=&quot;Genre&quot; class=&quot;text-danger&quot;&gt;&lt;/span&gt;
            &lt;/div&gt;
            &lt;div class=&quot;form-group&quot;&gt;
                &lt;label asp-for=&quot;Price&quot; class=&quot;control-label&quot;&gt;&lt;/label&gt;
                &lt;input asp-for=&quot;Price&quot; class=&quot;form-control&quot; /&gt;
                &lt;span asp-validation-for=&quot;Price&quot; class=&quot;text-danger&quot;&gt;&lt;/span&gt;
            &lt;/div&gt;
            &lt;div class=&quot;form-group&quot;&gt;
                &lt;label asp-for=&quot;PublishDate&quot; class=&quot;control-label&quot;&gt;&lt;/label&gt;
                &lt;input asp-for=&quot;PublishDate&quot; class=&quot;form-control&quot; /&gt;
                &lt;span asp-validation-for=&quot;PublishDate&quot; class=&quot;text-danger&quot;&gt;&lt;/span&gt;
            &lt;/div&gt;
            &lt;div class=&quot;form-group&quot;&gt;
                &lt;input type=&quot;submit&quot; value=&quot;Save&quot; class=&quot;btn btn-primary&quot; /&gt;
            &lt;/div&gt;
        &lt;/form&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
    &lt;a asp-action=&quot;Index&quot;&gt;Back to List&lt;/a&gt;
&lt;/div&gt;</code></pre>
<p>对于<code>edit</code>视图，模型也是Book类型。</p>
<p><code>asp-validation-summary</code> 标签助手用于显示验证摘要：</p>
<pre><code>&lt;div asp-validation-summary=&quot;ModelOnly&quot; class=&quot;text-danger&quot;&gt;&lt;/div&gt;</code></pre>
<p><code>ValidationSummary.ModelOnly</code> 仅显示适用于模型级别的验证消息。<code>ValidationSummary.All</code> 将显示属性级和模型级的验证消息。</p>
<p>对于每个属性，我们创建了一个标签用于显示属性名称，一个输入字段用于编辑值，以及一个 span 元素用于显示特定于该属性的验证消息：</p>
<pre><code>&lt;label asp-for=&quot;Title&quot; class=&quot;control-label&quot;&gt;&lt;/label&gt;
&lt;input asp-for=&quot;Title&quot; class=&quot;form-control&quot; /&gt;
&lt;span asp-validation-for=&quot;Title&quot; class=&quot;text-danger&quot;&gt;&lt;/span&gt;</code></pre>
<p>我们在底部有一个按钮用于提交表单：</p>
<pre><code>&lt;input type=&quot;submit&quot; value=&quot;Save&quot; class=&quot;btn btn-primary&quot; /&gt;</code></pre>
<p>当我们尝试在不提供有效值的情况下保存页面时，将根据我们为<code>asp-validation-summary</code>提供的设置显示验证错误。所以，让我们看看这在实际操作中是如何进行的。</p>
<p>当我们将值设置为<code>ModelOnly</code>时：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/a2.jpg" alt="" /></p>
<p>如果设置值为All：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/a3.jpg" alt="" /></p>
<p>就是这样。我们已经为<code>index</code>、<code>detail</code>和<code>edit</code>页面创建了视图。</p>
<h3>部分视图的概念</h3>
<p>部分视图是一个Razor标记文件（<code>.cshtml</code>），它在另一个视图的渲染输出中渲染HTML输出。</p>
<p>部分视图在两种情况下非常有用。第一种情况是当我们想要将大型标记文件分解成更小的组件时。如果我们的标记文件很大、复杂并由几个逻辑部分组成，我们应该将每个部分分解为一个部分视图。然后，标记文件中的代码将会更易于管理，因为标记将只包含整个页面结构和对部分视图的引用。</p>
<p>第二种情况是当我们想要减少跨标记文件的常见标记内容的重复时。当我们需要在标记文件中使用相同的标记元素时，我们可以将该标记内容移动到一个部分视图中并重用它。这样，将来对该标记的更改只需要在一个地方进行，我们就提高了代码的模块化。</p>
<p>然而，部分视图不是维护常见布局元素的推荐方法。在下一节中，我们将学习创建常见布局元素的最佳实践。</p>
<p>假设我们需要在BookStore应用程序的多个地方显示作者信息。创建一个用于显示作者信息的部分视图将是理想的做法。</p>
<p>右击Shared文件夹并选择添加 -&gt; 视图：<br />
<img src="https://zhangxihai.cn/wp-content/uploads/2024/01/a4-1.jpg" alt="" /></p>
<p>在“添加MVC视图”对话框中，我们将视图名称设置为_Authors，然后勾选“创建为部分视图”选项，点击添加：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/a5.jpg" alt="" /></p>
<p>让我们向<code>_authors.cshtml</code>文件中添加一些虚拟文本：</p>
<pre><code>&lt;h3&gt;Authors&lt;/h3&gt;
&lt;p&gt;This section is used to display information about authors.&lt;/p&gt;</code></pre>
<p>现在，让我们使用<code>partial</code>标签助手将这个部分视图添加到书籍详情视图中：</p>
<pre><code>&lt;div&gt;
        &lt;partial name=&quot;_Authors&quot; /&gt;
&lt;/div&gt;</code></pre>
<p>就是这样。我们可以看到，书籍详细信息页面现在也显示了“作者”部分：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/a6-1.jpg" alt="" /></p>
<p>我们可以通过在其他视图中放置这个部分视图来重用这一部分。</p>
<p>在这一部分中，我们学习了如何创建部分视图以及如何在视图中使用它。</p>
<h3>ASP.NET Core中的布局</h3>
<p>大多数Web应用程序都有一个共同的布局，为用户在页面间导航时提供一致的体验。在ASP.NET Core MVC应用程序中，我们使用布局文件为整个页面提供一致的体验。</p>
<p>布局通常包括诸如头部、菜单和页脚之类的常见用户界面元素。应用程序中的许多页面共享诸如脚本和样式表之类的公共资源。我们可以在一个布局文件中定义所有这些共享元素，然后该布局文件可以被应用程序中的任何视图引用。布局有助于减少视图中的重复代码。</p>
<p>当我们使用Visual Studio提供的默认模板创建一个ASP.Net Core MVC应用程序时，它会生成一个默认的布局文件(<code>_Layout.cshtml</code>)并将其放置在Shared文件夹中。在创建视图时，我们可以选择指定一个布局文件。我们可以稍后通过设置视图的Layout属性来更改这一点：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/a7.png" alt="" /></p>
<p>现在让我们来检查默认的布局文件。</p>
<p>布局文件的顶部包含一个 <code>&lt;head&gt;</code> 部分，其中包含标题、指向样式表等的链接。</p>
<pre><code>&lt;head&gt;
    &lt;meta charset=&quot;utf-8&quot; /&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot; /&gt;
    &lt;title&gt;@ViewData[&quot;Title&quot;] - WorkingWithViews&lt;/title&gt;
    &lt;link rel=&quot;stylesheet&quot; href=&quot;~/css/site.css&quot; /&gt;
&lt;/head&gt;</code></pre>
<p>接着是包含带有菜单的页眉的 <code>&lt;body&gt;</code> 部分。<code>&lt;body&gt;</code> 还有一个容器 div，在其中调用了 <code>RenderBody()</code> 方法。这个方法渲染内容页：</p>
<pre><code>&lt;div class=&quot;container&quot;&gt;
    &lt;partial name=&quot;_CookieConsentPartial&quot; /&gt;
    &lt;main role=&quot;main&quot; class=&quot;pb-3&quot;&gt;
        @RenderBody()
    &lt;/main&gt;
&lt;/div&gt;</code></pre>
<p>接下来是一个 <code>&lt;footer&gt;</code> 部分。</p>
<p>我们通常在文档末尾加载脚本，以确保加载所有依赖项：</p>
<pre><code>&lt;script src=&quot;~/js/site.js&quot; asp-append-version=&quot;true&quot;&gt;&lt;/script&gt;
@RenderSection(&quot;Scripts&quot;, required: false)</code></pre>
<p>在这一部分中，我们学习了如何使用布局文件为我们的应用程序维护一致的外观和感觉。</p>
<h3>总结</h3>
<p>在这篇文章中，我们探讨了以下主题：</p>
<p>使用Razor标记构建视图<br />
使用部分视图重用页面的部分内容<br />
使用布局文件为应用程序创建共同的外观和感觉<br />
在本系列的下一部分中，我们将探讨ASP.NET Core MVC中的状态管理。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://zhangxihai.cn/archives/322/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>ASP.NET Core MVC处理数据</title>
		<link>https://zhangxihai.cn/archives/305</link>
					<comments>https://zhangxihai.cn/archives/305#respond</comments>
		
		<dc:creator><![CDATA[胖爷]]></dc:creator>
		<pubDate>Fri, 29 Dec 2023 03:05:31 +0000</pubDate>
				<category><![CDATA[笔记]]></category>
		<category><![CDATA[编码]]></category>
		<guid isPermaLink="false">https://zhangxihai.cn/?p=305</guid>

					<description><![CDATA[在这个系列的前一部分中，我们使用一些模拟数据构建了一个ASP.NET Core MVC应用程序。在这一部分中，我们将探讨如何将应用程序连接到数据库并处理数据。 我们将使用EF Core Code-First方法，这是在开始新项目时处理数据的首选方式。 我们强烈建议访问本系列的完整导航：ASP.NET Core MVC系列。 要下载本文的源代码，请访问：在AS...]]></description>
										<content:encoded><![CDATA[<p>在这个系列的前一部分中，我们使用一些模拟数据构建了一个ASP.NET Core MVC应用程序。在这一部分中，我们将探讨如何将应用程序连接到数据库并处理数据。</p>
<p>我们将使用<a href="6926376813415765612
">EF Core Code-First</a>方法，这是在开始新项目时处理数据的首选方式。</p>
<p>我们强烈建议访问本系列的完整导航：<a href="https://code-maze.com/asp-net-core-mvc-series/">ASP.NET Core MVC</a>系列。</p>
<p>要下载本文的源代码，请访问：<a href="https://github.com/CodeMazeBlog/aspnetcore-mvc-series">在ASP.NET Core MVC中处理数据的源代码</a>。</p>
<p>首先，我们需要创建一个模型。然后，我们将对模型进行脚手架，以生成与模型的创建、读取、更新和删除（CRUD）操作相对应的控制器操作方法和视图。在此之后，我们将使用EF Core迁移来创建数据库。最后，我们将用一些初始数据填充数据库。</p>
<p>一旦完成这些步骤，我们就会有一个能够连接到数据库的工作中的ASP.NET Core MVC应用程序。</p>
<h3>在ASP.NET MVC中创建Model</h3>
<p>让我们创建Book模型，这是一个与我们在本系列前一部分中创建的对象类似的对象，只是没有Authors属性：</p>
<pre><code>public class Book
{
   public int Id { get; set; }
   [Display(Name = &quot;Book Title&quot;)]
   [Required]
   public string Title { get; set; }
   public string Genre { get; set; }
   [DataType(DataType.Currency)]
   [Range(1, 100)]
   public decimal Price { get; set; }
   [Display(Name = &quot;Publish Date&quot;)]
   [DataType(DataType.Date)]
   public DateTime PublishDate { get; set; }
}</code></pre>
<p>这个模型将作为构建项目的基础。</p>
<h3>脚手架Model</h3>
<p>下一步是对模型进行脚手架操作，以生成控制器操作方法和视图。脚手架将创建一个新的、功能完备的控制器。</p>
<p>右击 <code>Controllers</code> 文件夹 &gt; <code>添加</code> &gt; <code>新的脚手架项</code>：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/aa.jpg" alt="" /></p>
<p>在<code>添加脚手架</code>对话框中，我们将选择使用<code>Entity Framework的MVC控制器与视图</code> &gt; <code>添加</code>：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/add-scaffold.png" alt="" /></p>
<p>让我们完成添加控制器对话框：</p>
<ol>
<li>Model类: Book (BookStoreWithData.Models)</li>
<li>数据上下文类: 请悬着 + 图标.</li>
<li>保留模型名称 (BookStoreWithData.Models.BookStoreWithDataContext)</li>
<li>点击添加</li>
</ol>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/a1.png" alt="" /></p>
<p>添加数据上下文：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/a2.png" alt="" /></p>
<ol>
<li>Views: 保持每个选项的勾选状态</li>
<li>Controller Name: 保持默认的 BooksController</li>
<li>点击Add</li>
</ol>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/a3.png" alt="" /></p>
<p>Visual Studio创建了：</p>
<p>一个<code>Entity Framework Core</code>数据库上下文类（<code>Data/BookStoreWithDataContext.cs</code>）<br />
一个控制器（<code>Controllers/BooksController.cs</code>）<br />
用于创建、删除、详情、编辑和索引页面的Razor视图文件（<code>Views/Books/*.cshtml</code>）<br />
这个自动创建数据库上下文、CRUD（创建、读取、更新和删除）操作方法和视图的过程被称为脚手架。<br />
脚手架是可选的，您也可以手动完成整个过程。在不需要或不适用脚手架的情况下，我们可以控制整个创建过程。</p>
<h3>数据迁移</h3>
<p><a href="https://code-maze.com/migrations-and-seed-data-efcore/">迁移</a>自动基于我们的模型创建数据库。</p>
<p>首先，让我们在包管理器控制台中运行以下命令：</p>
<p><code>Add-Migration BookStoreWithData.Data.BookStoreWithDataContext</code></p>
<p>这将创建支持迁移的类。</p>
<p>现在，我们需要将这些更改应用到数据库。在此之前，请确保在<code>appsettings.json</code>文件中指定的连接字符串指向我们想要连接的数据库服务器。默认情况下，脚手架过程会使它指向SQL Server Express本地数据库。</p>
<p>为了将更改应用到数据库，让我们运行以下命令：</p>
<p><code>PM&gt; update-database</code></p>
<p>这将根据我们的模型更新数据库。</p>
<p>我们在《ASP.NET Core Web API与EF Core Code-First方法》一文中详细介绍了这种方法。</p>
<p>一旦我们完成所有步骤，就会根据模型定义创建数据库和列。我们的数据库表和列将如下所示：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/a4.jpg" alt="" /></p>
<p>我们已经成功地使用EF Core Code-First迁移从我们的代码创建了数据库。</p>
<h3>数据填充</h3>
<p>下一步是进行数据填充。数据填充允许我们在创建数据库时提供初始数据。</p>
<p>正如上面链接的文章中提到的，让我们在<code>BookContext</code>中重写<code>OnModelCreating</code>方法：</p>
<pre><code>      protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity&lt;Book&gt;().HasData(new Book
            {
                Id = 1,
                Title = &quot;Book1&quot;,
                Genre = &quot;Genre1&quot;,
                Price = 20,
                PublishDate = new DateTime(2012, 4, 23)
            }, new Book
            {
                Id = 2,
                Title = &quot;Book2&quot;,
                Genre = &quot;Genre2&quot;,
                Price = 30,
                PublishDate = new DateTime(2008, 6, 13)
            });
        }</code></pre>
<p>一旦我们应用迁移并更新数据库，我们可以看到数据库表已经用我们提供的种子数据进行了更新：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/a6.jpg" alt="" /></p>
<h3>ASP.NET Core MVC</h3>
<p>现在，让我们运行这个应用。</p>
<h4>列表页面</h4>
<p>让我们导航到<code>/Books</code>;</p>
<p>这将调用控制器中的<code>Index()</code>方法：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/a7.jpg" alt="" /></p>
<p>我们可以看到，索引页面显示了数据库中所有书籍的列表。在网格上还有编辑、查看详情和删除书籍的链接。在顶部，还有一个创建新书的链接。</p>
<h4>Create 页面</h4>
<p>让我们点击<code>Create new</code>链接</p>
<p>点击链接将带我们进入 <code>/Books/Create</code>。这将通过GET请求调用控制器中的<code>Create()</code>方法：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/a8.jpg" alt="" /></p>
<p>这个页面可以用来创建新书。在输入书籍详细信息并点击创建按钮后，将调用带有<code>[HttpPost]</code>属性的<code>Create()</code>方法。这将是一个<code>POST</code>请求，表单数据将被提交。</p>
<p>底部有一个链接，可以返回到列表页面。</p>
<h4>Detials页面</h4>
<p>在列表页面上，如果我们点击任何一本书的详情链接，我们将被带到 <code>/Books/Details/{id}</code></p>
<p>这将调用控制器中的<code>Details()</code>方法：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/a9.jpg" alt="" /></p>
<p>这个页面展示了一本书的详细信息。在详情页面上，我们可以看到用于编辑和返回列表的按钮。</p>
<h4>Edit页面</h4>
<p>如果我们从这里或列表页面点击编辑链接，它将带我们到 <code>/Book/Edit/{id}</code></p>
<p>这将调用控制器中第一个仅支持<code>GET</code>请求的<code>Edit()</code>方法：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/a10.jpg" alt="" /></p>
<p>点击保存按钮将调用带有<code>[HttpPost]</code>属性的<code>Edit()</code>方法。这将使用我们在页面上提供的值更新记录。根据请求类型，<code>MVC</code>决定调用哪个<code>Edit</code>方法。</p>
<p>对于编辑记录，<code>PUT</code>请求是更合适的方法。但在这里，自动生成的代码使用了<code>POST</code>方法，这也可以使用。然而，当我们自己创建控制器方法时，推荐的做法是使用<code>PUT</code>方法来更新记录。</p>
<h3>Delete页面</h3>
<p>最后，如果我们在列表页面点击删除链接，我们将被导航到 <code>/Book/Delete/{id}</code></p>
<p>这将调用控制器中第一个仅支持<code>GET</code>请求的<code>Delete()</code>方法。</p>
<p>这是删除确认页面：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/a11.jpg" alt="" /></p>
<p>一旦我们通过点击删除按钮确认删除，带有 [HttpPost] 属性的 DeleteConfirmed() 方法将被调用。这将从数据库中删除记录。</p>
<h3>代码解释</h3>
<p>通过遵循上述步骤，我们已经成功创建了一个具有数据库集成的完全功能的应用程序。现在，让我们来看看自动生成的代码，并尝试理解应用程序是如何运作的。</p>
<p>Visual Studio作为脚手架的一部分生成了以下文件：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/a12.jpg" alt="" /></p>
<h4>DB Context</h4>
<p>数据库上下文(DB Context)文件负责促进与数据库的所有通信。要了解更多关于DBContext的详细信息，请查看我们的文章：<a href="https://code-maze.com/net-core-web-development-part4/#context">Context Class and the Database Connection</a>。</p>
<p>本应用的上下文文件在脚手架的一部分中自动生成，位于<code>Data/BookStoreWithDataContext</code>。</p>
<h4>Controller</h4>
<p>在<code>Controllers/BooksController.cs</code>生成了一个控制器文件，其中包含与<code>CRUD</code>操作相对应的操作方法。如果我们查看<code>BooksController</code>文件，我们可以看到以下操作方法：</p>
<p><code>GET api/books</code> - 列出所有书籍。</p>
<p><code>GET api/books/details/{id}</code> - 获取一本书的详细信息。</p>
<p><code>GET api/books/create</code> - 显示初始创建书籍页面。</p>
<p><code>POST api/books/create</code> - 创建一本新书。</p>
<p><code>GET api/books/edit/{id}</code> - 显示初始编辑页面。</p>
<p><code>POST api/books/edit/{id}</code> - 更新一本书的详细信息。</p>
<p><code>GET api/books/delete/{id}</code> - 显示删除确认页面。</p>
<p>POST api/books/delete/{id} - 删除一本书。</p>
<p>控制器中的操作方法访问上下文以执行数据操作。</p>
<p>例如，在<code>Details()</code>方法中，我们可以看到通过访问上下文获取书籍记录：</p>
<pre><code> var book = await _context.Book
                .FirstOrDefaultAsync(m =&gt; m.Id == id);</code></pre>
<p>同样，在带有<code>[HttpPost]</code>属性的<code>Create()</code>方法中，我们访问上下文以添加新记录：</p>
<pre><code>_context.Add(book);</code></pre>
<p>理想情况下，控制器不应直接访问上下文文件。相反，我们应该在中间引入一个仓储层。我们在其他文章的一个部分中详细解释了仓库模式：实现仓库模式。</p>
<h4>Razor view文件</h4>
<p>在<code>Views/Books</code>文件夹内，我们可以看到为<code>Create</code>、<code>Delete</code>、<code>Details</code>、<code>Edit</code>和<code>Index</code>方法创建了视图页面。这些视图页面基于<code>razor</code>语法。我们将在后续的文章中详细讨论使用<code>razor</code>语法创建视图页面。</p>
<h3>总结</h3>
<p>在这篇文章中，我们研究了以下内容：</p>
<p>创建模型。<br />
对模型进行脚手架操作以生成上下文、控制器和视图文件。<br />
使用迁移创建数据库。<br />
数据填充。<br />
脚手架自动生成的代码。<br />
在本系列的下一部分中，我们将探讨使用razor语法创建视图页面。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://zhangxihai.cn/archives/305/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>开始使用ASP.NET Core MVC</title>
		<link>https://zhangxihai.cn/archives/283</link>
					<comments>https://zhangxihai.cn/archives/283#respond</comments>
		
		<dc:creator><![CDATA[胖爷]]></dc:creator>
		<pubDate>Fri, 29 Dec 2023 01:24:27 +0000</pubDate>
				<category><![CDATA[笔记]]></category>
		<category><![CDATA[编码]]></category>
		<guid isPermaLink="false">https://zhangxihai.cn/?p=283</guid>

					<description><![CDATA[在本文中，我们将看到构建ASP.NET Core MVC Web应用程序的基础知识。 我们将从使用Visual Studio提供的默认模板创建一个简单的ASP.NET Core MVC应用程序开始。默认模板本身将转化为一个可工作的应用程序。 对于默认模板，我们将添加一个控制器和一些操作方法。 之后，我们将使用Razor语法引入视图，并从控制器方法中返回它们。...]]></description>
										<content:encoded><![CDATA[<p>在本文中，我们将看到构建ASP.NET Core MVC Web应用程序的基础知识。</p>
<p>我们将从使用Visual Studio提供的默认模板创建一个简单的ASP.NET Core MVC应用程序开始。默认模板本身将转化为一个可工作的应用程序。</p>
<p>对于默认模板，我们将添加一个控制器和一些操作方法。</p>
<p>之后，我们将使用Razor语法引入视图，并从控制器方法中返回它们。</p>
<p>最后，我们将定义一些模型，并了解如何将它们传递到视图中。我们还将看到如何在Web页面上呈现模型数据。</p>
<p>强烈建议查看本系列的完整导航：<a href="https://zhangxihai.cn/archives/280">ASP.NET Core MVC系列教程</a>。</p>
<p>要下载本文的源代码，请访问：<a href="https://github.com/CodeMazeBlog/aspnetcore-mvc-series">开始使用ASP.NET Core MVC 源代码</a>。</p>
<h3>创建一个ASP.NET Core MVC项目</h3>
<p>首先，让我们创建一个新的ASP.NET Core MVC项目。</p>
<p>从Visual Studio 2019中，使用<strong>ASP.NET Core Web应用程序</strong>模板创建一个新项目。</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/create-new-project.png" alt="" /></p>
<p>下一步是配置<strong>项目名称</strong>、<strong>位置</strong>和<strong>解决方案</strong>名称。</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/configure-new-project.png" alt="" /></p>
<p>最后一步中，我们需要选择框架版本和MVC项目模板：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/create-new-asp.net-core-web-app.png" alt="" /></p>
<p>完成这些步骤后，Visual Studio将使用默认模板创建一个MVC项目。很棒的是，现在我们已经有了一个工作的应用程序，只需输入项目名称并选择一些选项。这是一个基本的入门项目，是一个很好的起点。</p>
<h3>项目结构</h3>
<p>现在，让我们检查项目结构，并查看默认MVC模板生成的文件：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/Solution-Explorer.jpg" alt="" /></p>
<p>我们可以看到，该项目被很好地组织成了用于模型（Models）、控制器（Controllers）和视图（Views）的独立文件夹。 视图进一步分为与每个视图对应的子文件夹。 每个文件夹中还生成了一些默认文件。 然后还有与.NET Core项目模板一起提供的通常的配置和启动文件。</p>
<p>现在让我们使用Ctrl+F5运行该应用程序。 我们可以看到一个基于ASP.NET Core MVC提供的默认布局的网站：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/default-mvc-app.jpg" alt="" /></p>
<p>恭喜！我们刚刚使用ASP.NET Core MVC创建了一个网站。</p>
<h3>添加Controller</h3>
<p>既然我们有一个可用的<code>ASP.NET Core MVC</code>应用程序，让我们开始对其进行实验。</p>
<p>让我们在<code>Controllers</code>文件夹中添加一个空的控制器，将其命名为<code>BooksController</code>。我们可以通过右键单击Controllers &gt; 添加 &gt; 控制器来完成这个操作。</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/add-controller.jpg" alt="" /></p>
<p>在<code>Add Scaffold dialog box</code>弹出框中, 选择<code>MVC Controller - Empty</code>。</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/add-scaffold-1024x711-1.jpg" alt="" /></p>
<p>然后，在&quot;添加空MVC控制器&quot;对话框中，将控制器名称设置为<code>BooksController</code>，然后点击添加：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/add-empty-mvc-controller.jpg" alt="" /></p>
<p>这将创建一个带有默认操作方法的<code>BooksController</code>。让我们修改代码并在其中创建两个操作方法：</p>
<pre><code>public string Index()
{
    return &quot;This is the book index.&quot;;
}
public string Details()
{
     return &quot;This is the details of a book.&quot;;
}</code></pre>
<p>让我们运行应用程序并通过将URL更改为 <code>https://localhost:44323/books</code> 来导航到<code>BooksController</code>。</p>
<pre><code>https://zhangxihai.cn/wp-content/uploads/2024/01/books-index-page.jpg</code></pre>
<p>我们将在接下来的文章中详细介绍路由，但现在让我们先了解一些基础知识。MVC根据传入的URL来调用控制器类及其中的操作方法。MVC使用类似于以下格式的默认URL路由逻辑来确定要调用的代码：</p>
<p><code>/[Controller]/[ActionName]/[Parameters]</code></p>
<p>当未提供<code>ActionName</code>时，默认为<code>Index</code>。参数也是可选的。</p>
<p>因此，在这种情况下，当我们访问上面的URL时，应用程序会执行<code>BooksController</code>的<code>Index</code>方法。此方法返回一个字符串，我们看到的是使用提供的字符串生成的HTML页面。</p>
<p>类似地，如果我们将URL更改为<code>https://localhost:44323/books/details</code>，我们可以看到<code>BooksController</code>的<code>Details</code>方法被执行：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/books-details.jpg" alt="" /></p>
<p>我们已经创建了自己的控制器，并执行了其中的两个方法，这太棒了。</p>
<h3>创建视图</h3>
<p>尽管从控制器返回纯字符串是有效的，但这并不是一个良好的实践。控制器操作方法理想情况下应返回一个视图。然后，视图应负责显示页面输出。</p>
<p>因此，让我们为<code>Index</code>操作方法添加一个视图文件。右键单击<code>Index</code>操作方法，然后单击“添加视图”：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/add-view.jpg" alt="" /></p>
<p>将视图名称命名为<code>Index</code>，然后单击“添加”：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/add-mvc-view-1.png" alt="" /></p>
<p>这将在Views下创建一个名为Books的新文件夹，并在其中创建一个名为Index.cshtml的视图文件：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/solution-explorer-view.jpg" alt="" /></p>
<p>这是一个Razor视图文件。我们将在后续的文章中详细学习如何使用Razor语法创建视图。现在，让我们在视图文件中添加一些文本，如下所示：</p>
<pre><code>@{
    ViewData[&quot;Title&quot;] = &quot;Index&quot;;
}
&lt;h1&gt;This is the book index generated by the view.&lt;/h1&gt;</code></pre>
<p>让我们也更改BooksController的Index方法，以返回视图而不是字符串：</p>
<pre><code>public IActionResult Index()
{
   return View();
}</code></pre>
<p>现在让我们再次运行应用程序。</p>
<p><a href="https://localhost:44323/books">https://localhost:44323/books</a></p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/book-details-view-page.jpg" alt="" /></p>
<p>我们可以看到，根据我们刚刚创建的视图文件显示了一个新页面。此外，我们可以看到应用了默认的布局模板，我们将在稍后的文章中再次讨论布局文件。</p>
<p>因此，我们已经创建了一个视图文件，从控制器动作方法返回它，并验证了在运行应用程序时显示它。</p>
<h3>定义模型</h3>
<p>到目前为止，我们已经看到了控制器和视图的运作方式。现在，让我们将模型引入到这个方程中。</p>
<p>让我们在 <code>Models</code> 文件夹中添加一个名为 <code>Book</code> 的新类，其中包含一些属性：</p>
<pre><code>public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Genre { get; set; }
    public List&lt;string&gt; Authors { get; set; }
    public decimal Price { get; set; }
    public DateTime PublishDate { get; set; }
}</code></pre>
<p>我们将在BooksController的Details操作方法中返回这个模型。但在此之前，我们需要创建一个用于显示书籍详情的视图。</p>
<p>为此，我们将添加一个名为Details的新视图，就像我们之前为Index所做的那样。</p>
<p>让我们还修改Details操作方法以返回此视图。我们将把模型传递到视图中，并在页面上显示书籍详情。</p>
<p>理想情况下，我们会从数据库中获取模型数据。我们将在即将发布的文章中学习如何做到这一点。现在，我们只需生成一些模拟数据来返回：</p>
<pre><code>public IActionResult Details()
{
     Book book = new Book()
     {
         Id = 1,
         Title = &quot;Learning ASP.NET Core 2.0&quot;,
         Genre = &quot;Programming &amp; Software Development&quot;,
         Price = 45,
         PublishDate = new System.DateTime(2012, 04, 23),
         Authors = new List&lt;string&gt; { &quot;Jason De Oliveira&quot;, &quot;Michel Bruchet&quot; }
     };
     return View(book);
}</code></pre>
<p>让我们也修改视图以显示模型数据：</p>
<pre><code>@model BookStore.Models.Book
@{
    ViewData[&quot;Title&quot;] = &quot;Details&quot;;
}
&lt;h1&gt;Details&lt;/h1&gt;
&lt;div&gt;
    &lt;h4&gt;Book&lt;/h4&gt;
    &lt;hr /&gt;
    &lt;dl class=&quot;row&quot;&gt;
        &lt;dt class=&quot;col-sm-2&quot;&gt;
            @Html.DisplayNameFor(model =&gt; model.Title)
        &lt;/dt&gt;
        &lt;dd class=&quot;col-sm-10&quot;&gt;
            @Html.DisplayFor(model =&gt; model.Title)
        &lt;/dd&gt;
        &lt;dt class=&quot;col-sm-2&quot;&gt;
            @Html.DisplayNameFor(model =&gt; model.Genre)
        &lt;/dt&gt;
        &lt;dd class=&quot;col-sm-10&quot;&gt;
            @Html.DisplayFor(model =&gt; model.Genre)
        &lt;/dd&gt;
        &lt;dt class=&quot;col-sm-2&quot;&gt;
            @Html.DisplayNameFor(model =&gt; model.Price)
        &lt;/dt&gt;
        &lt;dd class=&quot;col-sm-10&quot;&gt;
            @Html.DisplayFor(model =&gt; model.Price)
        &lt;/dd&gt;
        &lt;dt class=&quot;col-sm-2&quot;&gt;
            @Html.DisplayNameFor(model =&gt; model.PublishDate)
        &lt;/dt&gt;
        &lt;dd class=&quot;col-sm-10&quot;&gt;
            @Html.DisplayFor(model =&gt; model.PublishDate)
        &lt;/dd&gt;
    &lt;/dl&gt;
    &lt;table&gt;
        &lt;thead&gt;
            &lt;tr&gt;
                &lt;th&gt;
                    Authors
                &lt;/th&gt;
            &lt;/tr&gt;
        &lt;/thead&gt;
        &lt;tbody&gt;
            @foreach (var item in Model.Authors)
            {
                &lt;tr&gt;
                    &lt;td&gt;
                        @Html.DisplayFor(modelItem =&gt; item)
                    &lt;/td&gt;
                &lt;/tr&gt;
            }
        &lt;/tbody&gt;
    &lt;/table&gt;
&lt;/div&gt;
&lt;hr /&gt;
&lt;div&gt;
    &lt;a asp-action=&quot;Edit&quot; asp-route-id=&quot;@Model.Id&quot;&gt;Edit&lt;/a&gt; |
    &lt;a asp-action=&quot;Index&quot;&gt;Back to List&lt;/a&gt;
&lt;/div&gt;</code></pre>
<p>在这里，我们使用强类型模型的方法。通过在视图文件顶部包含一个<code>@model</code>语句，我们指定视图期望的对象类型。因此，我们的视图期望一个类型为<code>Book</code>的模型。我们可以通过<code>Visual Studio</code>中可用的<code>IntelliSense</code>访问<code>Book</code>类的任何属性。</p>
<p>接下来，我们定义一个HTML模板来显示视图数据。<code>DisplayNameFor()</code>和<code>DisplayFor()</code>是HTML助手方法，它们显示模型中属性的名称和值。</p>
<p>Model.Authors是一个集合，我们使用@foreach语法遍历它并显示值。</p>
<p>现在，让我们再次运行应用程序并导航到详情页面：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/book-details-with-view.jpg" alt="" /></p>
<p>瞧！我们现在已经创建了一个带有模型、视图和控制器的MVC应用程序。</p>
<p>接下来，让我们研究如何使用数据注解来验证模型。</p>
<h3>数据注解</h3>
<p>数据注解提供了一组内置的验证属性集，我们可以声明性地将其应用于任何类或属性。它还包含有助于格式化数据的属性：</p>
<pre><code>    public class Book
    {
        public int Id { get; set; }
        [Display(Name = &quot;Book Title&quot;)]
        [Required]
        [StringLength(maximumLength: 20, ErrorMessage = &quot;The Title length should be between 2 and 20.&quot;, MinimumLength = 2)]
        public string Title { get; set; }
        public string Genre { get; set; }
        public List&lt;string&gt; Authors { get; set; }
        [DataType(DataType.Currency)]
        [Range(1, 100)]
        public decimal Price { get; set; }
        [Display(Name = &quot;Publish Date&quot;)]
        [DataType(DataType.Date)]
        public DateTime PublishDate { get; set; }
    }</code></pre>
<p>在上述代码中，我们已经对Book模型类应用了一些注解。</p>
<p>这些验证属性指定了您希望在其应用的模型属性上强制执行的行为：</p>
<p><code>[Required]</code> 属性表明一个属性必须有值。<br />
使用 <code>[MinLength]</code> 属性表示属性应具有最小长度，这也意味着它不能为空。<br />
<code>[RegularExpression]</code> 属性用于限制可以输入的字符。<br />
通过使用 <code>[Range]</code> 属性，我们可以将属性值限制在指定范围内。<br />
<code>[StringLength]</code> 属性让我们设置字符串属性的最大长度，以及可选的最小长度。<br />
<code>[DataType]</code> 用于指定字段的数据类型，它们本身是必需的，不需要 <code>[Required]</code> 属性。<br />
现在，让我们再次运行应用程序并导航到书籍详情页面：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/book-details-view-page-with-annotations-1.jpg" alt="" /></p>
<p>注意，由于我们应用了<code>Display</code>属性，标题现在已更改为<code>Book Title</code>，<code>PublishDate</code>更改为<code>Publish Date</code>。另外，请注意，价格和发布日期已根据特定区域设置格式化为货币和日期。</p>
<p>现在，让我们创建一个用于添加新书的页面，并查看验证如何起作用。</p>
<p>在控制器中，我们将添加两个<code>Create</code>方法：</p>
<pre><code> public IActionResult Create()
 {
     return View();
 }
 [HttpPost]
 [ValidateAntiForgeryToken]
 public IActionResult Create(Book book)
 {
     if (ModelState.IsValid)
     {
         // Logic to add the book to DB
         return RedirectToAction(&quot;Index&quot;);
     }
     return View(book);
 }</code></pre>
<h3>创建视图</h3>
<p>第一个<code>Create</code>操作方法显示初始创建表单。</p>
<p>让我们为这个操作创建一个视图。为此，我们将右击第一个<code>Create</code>操作并选择“添加视图”选项（就像我们为<code>Index</code>和<code>Details</code>操作所做的那样）。</p>
<p>在下一个窗口中，我们将为视图命名，选择视图的模板，并选择一个模型类来连接这个视图：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/a.png" alt="" /></p>
<p>在视图创建后，只需移除生成Id控件的div部分，因为我们在创建视图时不需要它。</p>
<p>第二个<code>Create</code>方法有一个<code>[HttpPost]</code>属性，这表明它只能处理POST请求。</p>
<p>由于这是一个<code>post</code>请求，我们正在提交一个表单，我们可以使用<code>ModelState.IsValid</code>来检查<code>Boo</code>k是否有任何验证错误。调用这个方法会评估已应用于对象的任何验证属性。如果对象不符合我们的验证标准，<code>Create</code>方法将重新显示表单。</p>
<p>如果没有错误，该方法理想情况下应该将新书保存在数据库中。（现在还未实现）</p>
<p>在不输入有效数据的情况下点击创建按钮，我们将看到验证消息：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/b.jpg" alt="" /></p>
<p>所以，我们已经成功地使用数据注解实现了模型验证和数据格式化。</p>
<h3>总结</h3>
<p>在这篇文章中，我们研究了以下主题：</p>
<p>如何创建一个ASP.NET MVC Core项目<br />
检查项目结构<br />
添加控制器、视图和模型<br />
使用注解进行数据验证和格式化<br />
在本系列的下一部分中，我们将学习如何在ASP.NET Core MVC中处理数据。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://zhangxihai.cn/archives/283/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>ASP.NET Core MVC 系列教程</title>
		<link>https://zhangxihai.cn/archives/280</link>
					<comments>https://zhangxihai.cn/archives/280#respond</comments>
		
		<dc:creator><![CDATA[胖爷]]></dc:creator>
		<pubDate>Fri, 29 Dec 2023 00:12:53 +0000</pubDate>
				<category><![CDATA[笔记]]></category>
		<category><![CDATA[编码]]></category>
		<guid isPermaLink="false">https://zhangxihai.cn/?p=280</guid>

					<description><![CDATA[翻译文章，原文点这里 在这个系列中，我们将详细讨论ASP.Net Core MVC。我们将从MVC架构开始，然后转向ASP.NET Core MVC框架。之后，我们将检查项目结构，看看各个组件如何适应其中。然后，我们将讨论框架支持的各种功能，处理数据，实施单元测试等等。 我们遵循的方法是通过进行小而易于遵循的代码示例来学习每个主题。我们的想法是在这个系列结束...]]></description>
										<content:encoded><![CDATA[<p>翻译文章，<a href="https://code-maze.com/asp-net-core-mvc-series/">原文点这里</a></p>
<p>在这个系列中，我们将详细讨论ASP.Net Core MVC。我们将从MVC架构开始，然后转向ASP.NET Core MVC框架。之后，我们将检查项目结构，看看各个组件如何适应其中。然后，我们将讨论框架支持的各种功能，处理数据，实施单元测试等等。</p>
<p>我们遵循的方法是通过进行小而易于遵循的代码示例来学习每个主题。我们的想法是在这个系列结束时，我们应该对ASP.NET Core MVC的概念有相当好的了解。此外，我们应该能够开始开发ASP.NET Core MVC应用程序。</p>
<h3>MVC架构</h3>
<p>模型-视图-控制器（MVC）架构模式将一个应用程序分为三个组件：模型（Models）、视图（Views）和控制器（Controllers）。这种模式有助于实现关注点分离。在这种模式中，用户的请求被路由到一个控制器（Controller）。控制器调用模型（Model）来执行用户操作或检索数据。然后，控制器将这个模型传递给一个视图（View），并将其返回给用户。</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/mvc-architecture-e1599083120851.jpg" alt="" /></p>
<p>在MVC应用程序中，模型（Model）代表应用程序的状态以及应该由其执行的任何业务逻辑或操作。模型还可以包含持久化应用程序状态的逻辑。</p>
<p>视图（View）负责通过用户界面呈现内容。视图应该尽量包含最少的逻辑，它只与呈现内容相关。</p>
<p>控制器（Controller）是处理用户交互、与模型一起工作并最终选择要渲染的视图的组件。在MVC模式中，控制器是初始入口点，负责选择要使用哪些模型类型以及要渲染哪个视图。换句话说，控制器控制应用程序如何响应特定请求。</p>
<p>因此，这种模式的优势在于每个组件都具有单一的职责，易于单独编写、调试和测试。</p>
<h3>ASP.NET CORE MVC</h3>
<p>在这个系列中，我们将详细讨论ASP.Net Core MVC。我们将从MVC架构开始，然后转向ASP.NET Core MVC框架。之后，我们将查看项目结构，了解各个组件如何适应。然后，我们将讨论框架支持的各种特性，处理数据，实施单元测试等等。</p>
<p>我们的学习方法是通过小而易于跟随的代码示例来学习每个主题。我们的目标是，在这个系列结束时，我们应该对ASP.NET Core MVC概念有相当好的了解。此外，我们应该能够开始开发ASP.NET Core MVC应用程序。</p>
<ul>
<li><a href="https://zhangxihai.cn/archives/283">入门</a></li>
<li><a href="https://zhangxihai.cn/archives/305">处理数据</a></li>
<li><a href="https://zhangxihai.cn/archives/322">Razor页面和局部视图</a></li>
<li>状态管理</li>
<li>路由</li>
<li>文件上传</li>
<li>依赖注入</li>
<li>单元测试</li>
<li>过滤器</li>
</ul>
<p>在系列的第一部分，我们将从头创建一个全新的ASP.NET Core MVC项目。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://zhangxihai.cn/archives/280/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>asp.net core中如何自定义注解</title>
		<link>https://zhangxihai.cn/archives/278</link>
					<comments>https://zhangxihai.cn/archives/278#respond</comments>
		
		<dc:creator><![CDATA[胖爷]]></dc:creator>
		<pubDate>Thu, 28 Dec 2023 02:02:28 +0000</pubDate>
				<category><![CDATA[笔记]]></category>
		<category><![CDATA[编码]]></category>
		<guid isPermaLink="false">https://zhangxihai.cn/?p=278</guid>

					<description><![CDATA[翻译文章,原文 .NET（Core）中的自定义属性是一种有助于附加额外信息到类、结构甚至它们的成员的机制。在本文中，我们将通过一些实际示例来解释如何在.NET中创建、访问和获取自定义属性中的信息。 让我们开始。 声明自定义注解 我们可以通过创建一个类来定义一个属性。这个类应该继承自Attribute类。 Microsoft建议在类的名称末尾添加Attribu...]]></description>
										<content:encoded><![CDATA[<p>翻译文章,<a href="https://code-maze.com/dotnet-custom-attributes/">原文</a></p>
<p>.NET（Core）中的自定义属性是一种有助于附加额外信息到类、结构甚至它们的成员的机制。在本文中，我们将通过一些实际示例来解释如何在.NET中创建、访问和获取自定义属性中的信息。</p>
<p>让我们开始。</p>
<h3>声明自定义注解</h3>
<p>我们可以通过创建一个类来定义一个属性。这个类应该继承自Attribute类。</p>
<p>Microsoft建议在类的名称末尾添加<code>Attribute</code>后缀。之后，我们派生类的每个属性将成为所需数据类型的参数。</p>
<h3>自定义自定义属性的使用</h3>
<p>AttributeUsageAttribute类通过定义一些基本特性来指定另一个属性类的使用方式。</p>
<p>This class has three members:</p>
<ul>
<li>AttributeTargets enum</li>
<li>Inherited property (bool)</li>
<li>AllowMultiple property (bool)</li>
</ul>
<h4>AttributeTargets 枚举</h4>
<p><code>AttributeTargets</code>枚举指定了我们可以将自定义属性应用于的应用程序元素。</p>
<p>为了看看它是如何工作的，让我们创建一个名为<code>TaskDescriptorAttribute</code>的新类：</p>
<pre><code>[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class TaskDescriptorAttribute : Attribute
{
    public string? Name { get; set; }
    public string? Description { get; set; }
    public bool NeedsManager { get; set; }
    public int DeveloperCount { get; set; }
}</code></pre>
<p>我们可以将这个属性（<code>TaskDescriptorAttribute</code>）只应用于类和结构，因为我们已经将它的目标设置为两者都使用了按位组合。当创建属性时，除了类和结构之外，我们还可以使用方法、枚举和其他应用程序元素。如果使用<code>AttributeTargets.All</code>值（它是默认值），这些属性也会应用于所有的应用程序元素。</p>
<p>让我们在<code>MyTasks</code>类中使用我们的属性：</p>
<pre><code>[TaskDescriptor(Name = &quot;The task&#039;s name&quot;,
    Description = &quot;Some descriptions for the task&quot;,
    NeedsManager = true,
    DeveloperCount = 5)]
public class MyTasks
{
}</code></pre>
<p>当我们将<code>TaskDescriptorAttribute</code>应用于一个类时，我们只使用<code>TaskDescriptor</code>部分，因为编译器允许我们在没有<code>Attribute</code>后缀的情况下使用它。</p>
<h4>AllowMultiple属性</h4>
<p><code>AllowMultiple</code>属性允许多个实例的属性。这个属性可以是false（默认值）或true。</p>
<p>让我们创建另一个名为<code>DeveloperTaskAttribute</code>的属性：</p>
<pre><code>[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class DeveloperTaskAttribute : Attribute
{
    public Priorities Priority { get; set; }
    public string? Description { get; set; }
    public DeveloperTaskAttribute(Priorities priority)
    {
        Priority = priority;
    }
}</code></pre>
<p>我们只能将这个属性应用于方法上，并且可以对它们应用多个实例。它有必需的<code>Priority</code>和可选的<code>Description</code>参数。</p>
<p>要应用这个属性，我们将在<code>MyTasks</code>类中创建一个新的<code>ScheduleMeeting()</code>方法：</p>
<pre><code>public class MyTasks
{
    [DeveloperTask(Priorities.Low)]
    [DeveloperTask(Priorities.High, Description = &quot;High level description&quot;)]
    public void ScheduleMeeting()
    {
    }
}</code></pre>
<p>ScheduleMeeting()方法现在有两个<code>DeveloperTask</code>属性。我们使用第一个属性只声明了必需的参数，但是第二个属性声明了必需和可选参数。但是，我们不能定义一个没有<code>Priorities</code>参数的<code>DeveloperTask</code>属性，否则会得到编译器错误。</p>
<h4>Inherited 属性</h4>
<p><code>Inherited</code>属性是我们可以应用于自定义属性的另一个关键特性。它指示该属性是否可以被继承。这个属性的默认值为true。</p>
<p>为了看到这个属性的用法，让我们创建<code>ManagerTaskAttribute</code>属性：</p>
<pre><code>[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public class ManagerTaskAttribute : Attribute
{
    public Priorities Priority { get; set; }
    public bool NeedsReport { get; set; }
}</code></pre>
<p>这个新属性不能被继承，它的两个参数都是可选的。</p>
<p>现在，我们将在<code>MyTasks</code>类中创建<code>ScheduleInterview</code>方法来利用它：</p>
<pre><code>public class MyTasks
{
    [ManagerTask(Priority = Priorities.Mid, NeedsReport = true)]
    [DeveloperTask(Priorities.High, Description = &quot;High level description&quot;)]
    public virtual void ScheduleInterview()
    {
    }
}</code></pre>
<p>这个方法有两个属性，一个<code>DeveloperTask</code>和一个<code>ManagerTask</code>属性。我们还添加了<code>virtual</code>关键字，因为我们想在另一个类中重写它。</p>
<p>所以，让我们创建一个继承自<code>MyTasks</code>类的<code>YourTasks</code>类：</p>
<pre><code>public class YourTasks : MyTasks
{
    [DeveloperTask(Priorities.Mid, Description = &quot;Mid level description&quot;)]
    public override void ScheduleInterview()
    {
    }
}</code></pre>
<p><code>YourTasks</code>类中的<code>ScheduleInterview</code>方法覆盖了基类<code>MyTasks</code>中的先前<code>ScheduleInterview</code>方法。这个方法没有<code>ManagerTask</code>属性，因为它的<code>Inherited</code>属性值为false。</p>
<p>但是，<code>DeveloperTask</code>属性的默认<code>Inherited</code>值为true。所以，<code>YourTasks.ScheduleInterview</code>方法有两个<code>DeveloperTask</code>属性。我们在<code>YourTasks</code>类内声明了第一个，而在<code>MyTasks</code>类内声明了第二个。</p>
<h3>访问自定义属性的实例</h3>
<p>一旦我们想要从属性中检索值，我们可以使用Attribute类的静态GetCustomAttribute方法。所以，让我们创建一个获取存储在TaskDescriptor实例中的信息的GetAttribute方法：</p>
<pre><code>public static string? GetAttribute(Type desiredType, Type desiredAttribute)
{
    var attributeInstance = Attribute.GetCustomAttribute(desiredType, desiredAttribute);
    if (attributeInstance == null)
        Console.WriteLine($&quot;The class {desiredType} does not have atributes.&quot;);
    else
        WriteOnTheConsole(attributeInstance);
    return attributeInstance?.ToString();
}</code></pre>
<p>我们的<code>GetAttribute</code>方法接受一个类类型和一个属性类型作为输入参数，并在控制台上打印信息。</p>
<p>在<code>Attribute</code>基类内部，我们可以找到<code>GetCustomAttribute</code>方法的不同重载。对于我们的示例，我们使用了<code>GetCustomAttribute(MemberInfo element, Type attributeType)</code>重载来获取我们想要的信息。</p>
<p><code>Type</code>类继承自<code>MemberInfo</code>基类，所以我们可以将它作为第一个参数传递给方法。我们还将我们的自定义属性类型作为第二个参数发送。我们使用<code>WriteOnTheConsole()</code>方法内部的一些反射来在控制台上打印所有信息。（您可以查看<a href="https://github.com/CodeMazeBlog/CodeMazeGuides/tree/main/aspnetcore-features/CustomAttributes">源代码以获取实现</a>）</p>
<h5>创建自定义属性的实例</h5>
<p><code>GetCustomAttribute</code>方法要么返回一个属性实例，要么返回一个null值。因此，如果存在，<code>attributeInstance</code>变量将存储我们自定义属性的一个实例。现在，我们将检索该实例的信息。</p>
<h5>检索自定义属性的信息</h5>
<p>为了检索我们自定义属性的信息，我们将调用<code>GetAttribute</code>方法，并将<code>typeof(MyTasks)</code>和<code>typeof(TaskDescriptorAttribute)</code>作为其参数传递：</p>
<pre><code>GetAttribute(typeof(MyTasks), typeof(TaskDescriptorAttribute));</code></pre>
<p>如果<code>GetAttribute</code>方法找到了<code>TaskDescriptorAttribute</code>类的一个实例，我们应该将它的所有属性作为结果获取。</p>
<pre><code>The CustomAttributes.TaskDescriptorAttribute attribute:
The Name property is: The task&#039;s name
The Description property is: Some descriptions for the task
The NeedsManager property is: True
The DeveloperCount property is: 5</code></pre>
<p>我们成功地检索了自定义属性类的信息。</p>
<h3>获取不同自定义属性的实例</h3>
<p>有时，我们需要访问类成员的所有属性。<code>Attribute</code>基类有另一个<code>GetCustomAttributes</code>方法，可以将它们作为数组返回。</p>
<p>让我们创建一个<code>GetAttributesOfMethods</code>方法，以访问所有属性的实例并检索它们的信息：</p>
<pre><code>public static List&lt;string&gt; GetAttributesOfMethods(Type elementType)
{
    List&lt;string&gt; attributes = new List&lt;string&gt;();
    var methodInfoList = elementType.GetMethods(BindingFlags.Public |
        BindingFlags.Instance |
        BindingFlags.DeclaredOnly);
    if (methodInfoList == null || methodInfoList.Length == 0)
    {
        Console.WriteLine($&quot;The type {elementType} does not have any methods.&quot;);
        return attributes;
    }
    foreach (var methodInfo in methodInfoList)
    {
        var attributeList = Attribute.GetCustomAttributes(methodInfo, true);
        if (attributeList.Length == 0)
        {
            Console.WriteLine($&quot;The {elementType.Name}.{methodInfo.Name} method does not have attributes.&quot;);
            continue;
        }
        Console.WriteLine($&quot;The {elementType.Name}.{methodInfo.Name} method&#039;s attribute:&quot;);
        foreach (var att in attributeList)
        {
            WriteOnTheConsole(att);
            attributes.Add(methodInfo.Name + &quot;-&quot; + att.ToString());
        }
        Console.WriteLine();
    }
    return attributes;
}</code></pre>
<p>我们想要获取每个已声明方法的所有属性。因此，我们使用适当的枚举调用<code>GetMethods</code>方法。</p>
<p>在第一个<code>foreach</code>循环内，我们使用<code>GetCustomAttribute(MemberInfo element, bool inherit)</code>重载调用<code>GetCustomAttributes</code>方法，并逐个发送已获取的<code>MethodInfo</code>值。我们还将true作为第二个参数传递，因为我们需要继承的属性。</p>
<p>在内部的<code>foreach</code>循环内，我们顺序获取<code>attributeList</code>数组的项目，并使用<code>WriteOnTheConsole</code>方法将它们打印到控制台上。</p>
<p>现在，让我们为<code>MyTasks</code>类调用<code>GetAttributesOfMethods</code>方法：</p>
<pre><code>GetAttributesOfMethods(typeof(MyTasks));</code></pre>
<p>我们将<code>MyTasks</code>类的类型发送给这个方法，作为结果，我们希望看到其方法的所有属性：</p>
<pre><code>The MyTasks.ScheduleMeeting method&#039;s attribute:
The CustomAttributes.DeveloperTaskAttribute attribute:
The Description property is:
The Priority property is: Low
The CustomAttributes.DeveloperTaskAttribute attribute:
The Description property is: High level description
The Priority property is: High
The MyTasks.ScheduleInterview method&#039;s attribute:
The CustomAttributes.ManagerTaskAttribute attribute:
The Priority property is: Mid
The NeedsReport property is: True
The CustomAttributes.DeveloperTaskAttribute attribute:
The Description property is: High level description
The Priority property is: High</code></pre>
<p><code>GetAttributesOfMethods</code>打印了<code>ScheduleMeeting和ScheduleInterview</code>方法的属性。</p>
<p>我们也可以对<code>YourTasks</code>类执行相同的操作：</p>
<pre><code>GetAttributesOfMethods(typeof(YourTasks));</code></pre>
<p>并且发现结果非常相似：</p>
<pre><code>The YourTasks.ScheduleInterview method&#039;s attribute:
The CustomAttributes.DeveloperTaskAttribute attribute:
The Description property is: Mid level description
The Priority property is: Mid
The CustomAttributes.DeveloperTaskAttribute attribute:
The Description property is: High level description
The Priority property is: High</code></pre>
<p>我们在控制台上看不到<code>ManagerTask</code>属性，因为这个属性的<code>Inherited</code>值为false。我们看到两个<code>DeveloperTask</code>属性的原因是因为我们在<code>MyTasks</code>类内部声明了第一个属性，而在<code>YourTasks</code>类内部声明了第二个属性。</p>
<h3>总结</h3>
<p>在本文中，我们学习了如何在.NET中声明自定义属性。我们已经了解到我们可以将它们用于类及其成员。我们还触及了访问单个和多个属性实例的方式。最后，我们找出了如何检索它们的信息。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://zhangxihai.cn/archives/278/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>如何在ASP.NET Core中自定义一个Authorize注解</title>
		<link>https://zhangxihai.cn/archives/273</link>
					<comments>https://zhangxihai.cn/archives/273#respond</comments>
		
		<dc:creator><![CDATA[胖爷]]></dc:creator>
		<pubDate>Wed, 27 Dec 2023 15:08:38 +0000</pubDate>
				<category><![CDATA[笔记]]></category>
		<category><![CDATA[编码]]></category>
		<guid isPermaLink="false">https://zhangxihai.cn/?p=273</guid>

					<description><![CDATA[原文 授权是一种安全机制，用于确定用户对资源的访问级别。我们经常需要按照组织或项目设定的规则来实现自定义授权逻辑。 在本文中，我们将学习如何在ASP.NET Core中实现自定义授权属性。 需要下载本文中的源码, 你可以访问我们的Github代码仓. 尽管在本文中我们深入讨论了自定义授权属性，但如果您对这些主题不熟悉，我们也有关于自定义属性和通用属性的入门文...]]></description>
										<content:encoded><![CDATA[<p><a href="https://code-maze.com/custom-authorize-attribute-aspnetcore/">原文</a></p>
<p>授权是一种安全机制，用于确定用户对资源的访问级别。我们经常需要按照组织或项目设定的规则来实现自定义授权逻辑。</p>
<p>在本文中，我们将学习如何在ASP.NET Core中实现自定义授权属性。</p>
<blockquote>
<p>需要下载本文中的源码, 你可以访问我们的<a href="https://github.com/CodeMazeBlog/CodeMazeGuides/tree/main/aspnetcore-features/CustomAuthorizeAttribute">Github代码仓</a>.</p>
</blockquote>
<p>尽管在本文中我们深入讨论了自定义授权属性，但如果您对这些主题不熟悉，我们也有关于<a href="https://code-maze.com/dotnet-custom-attributes/">自定义属性</a>和<a href="https://code-maze.com/csharp-generic-attributes/">通用属性</a>的入门文章。</p>
<p>要了解更多关于基于角色的授权的信息，请参考<a href="https://code-maze.com/angular-role-based-authorization-with-aspnet-identity/">Angular中使用ASP.NET Core Identity进行基于角色的授权</a>和<a href="https://code-maze.com/blazor-webassembly-role-based-authorization/">使用Blazor WebAssembly进行基于角色的授权</a>。</p>
<h3>自定义Authorize注解</h3>
<p>ASP.NET Core提供了<a href="https://code-maze.com/filters-in-asp-net-core-mvc/">过滤器</a>，用于在操作方法之前或之后执行用户定义的代码。其中一个在操作方法调用之前帮助授权请求的过滤器是<code>IAuthorizationFilter</code>。</p>
<p>现在，让我们利用这个过滤器并实现一个简单的自定义授权属性。</p>
<h5>实现一个简单的自定义的Authorize注解</h5>
<p><code>IAuthorizationFilter</code>公开了一个<code>OnAuthorization()</code>方法，该方法在每次操作方法调用之前执行：</p>
<pre><code>void OnAuthorization(AuthorizationFilterContext context);</code></pre>
<p>因此，要创建一个自定义授权属性，我们可以创建一个继承自IAuthorizationFilter接口并实现OnAuthorization()方法的属性：</p>
<pre><code>public sealed class CustomAuthorizeAttribute : Attribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        if (context != null)
        {
            // Auth logic
        }
    }
}</code></pre>
<p>我们可以在操作方法或控制器上使用这个属性。现在，只有在OnAuthorization()方法内的授权检查成功时，操作方法才会执行：</p>
<pre><code>[HttpGet]
[CustomAuthorize]
public IEnumerable&lt;WeatherForecast&gt; Get()
{
    return Enumerable.Range(1, 5).Select(index =&gt; new WeatherForecast
    {
        Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
        TemperatureC = Random.Shared.Next(-20, 55),
        Summary = Summaries[Random.Shared.Next(Summaries.Length)]
    })
    .ToArray();
}</code></pre>
<p>这是一个不错的方法，但有时我们需要在过滤器内部注入外部依赖项以执行授权逻辑。例如，为了获取已登录用户的声明，我们将注入IHttpContextAccessor来访问声明：</p>
<pre><code>public class CustomAuthorizeAttribute : Attribute
{
    private readonly IEnumerable&lt;Claim&gt; _claims;
    public CustomAuthorizeAttribute(IHttpContextAccessor httpContextAccessor)
    {
        _claims = httpContextAccessor.HttpContext.User.Claims;
    }
}</code></pre>
<p>我们讨论的简单方法的缺点是我们无法将外部依赖项注入到过滤器中。这是因为在使用属性时，必须提供它们的构造函数参数。</p>
<p>这就是我们可以使用<code>TypeFilterAttribute</code>的地方。它具有<code>System.Type</code>数据类型的<code>ImplementationType</code>属性，可以通过构造函数进行初始化：</p>
<pre><code>public class TypeFilterAttribute : Attribute, IFilterFactory, IOrderedFilter
{
    public TypeFilterAttribute(Type type)
    {
        ImplementationType = type ?? throw new ArgumentNullException(nameof(type));
    }
    public Type ImplementationType { get; }
    // ...more code omitted for brevity
}</code></pre>
<p>这个<code>TypeFilterAttribute</code>使用<code>Microsoft.Extensions.DependencyInjection.ObjectFactory</code>来实例化<code>ImplementationType</code>，而不是从IoC容器中解析。有了这个，我们现在可以在属性中定义依赖关系，而运行时会负责依赖注入。</p>
<p>现在，通过结合<code>IAuthorizationFilter</code>和<code>TypeFilterAttribute</code>，我们可以创建一个支持注入外部依赖项的自定义<code>AuthorizeAttribute</code>。</p>
<p>现在让我们看看这个实际的例子。</p>
<h5>实现一个具有依赖项的自定义授权属性</h5>
<p>现在，让我们实现一个简单的自定义授权属性，验证传递的自定义会话标头的HTTP请求。</p>
<p>首先，我们启动一个ASP.NET Core Web API项目，并在<code>Program.cs</code>文件中配置<code>HttpContextAccessor</code>以进行依赖注入：</p>
<pre><code>builder.Services.AddHttpContextAccessor();</code></pre>
<p>之后我们定义<code>SessionRequirementFilter</code>:</p>
<pre><code>public class SessionRequirementFilter : IAuthorizationFilter
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    public SessionRequirementFilter(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        if (!_httpContextAccessor.HttpContext!.Request.Headers[&quot;X-Session-Id&quot;].Any())
        {
            context.Result = new UnauthorizedObjectResult(string.Empty);
            return;
        }
    }
}</code></pre>
<p>我们继承自<code>IAuthorizationFilter</code>并实现了<code>OnAuthorization</code>方法，在其中检查请求中是否存在自定义会话标头<code>X-Session-Id</code>。正如您注意到的，我们注入了<code>IHttpContextAccessor</code>以访问<code>HttpContext</code>。</p>
<p>接下来，我们创建一个继承自<code>TypeFilterAttribute</code>的属性：</p>
<pre><code>public class SessionRequirementAttribute : TypeFilterAttribute
{
    public SessionRequirementAttribute() : base(typeof(SessionRequirementFilter))
    {
    }
}</code></pre>
<p>继承自<code>TypeFilterAttribute</code>允许我们传递<code>SessionRequirementFilter</code>类，当我们使用这个属性时它会执行。</p>
<p>最后，我们可以在自动生成的<code>WeatherForecast</code>控制器的<code>Get()</code>方法上装饰该属性：</p>
<pre><code>[HttpGet(&quot;WithCustomAuthorizeAttribute&quot;)]
[SessionRequirement]
public IEnumerable&lt;WeatherForecast&gt; Get()
{
    return Enumerable.Range(1, 5).Select(index =&gt; new WeatherForecast
    {
        Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
        TemperatureC = Random.Shared.Next(-20, 55),
        Summary = Summaries[Random.Shared.Next(Summaries.Length)]
    })
    .ToArray();
}</code></pre>
<p>现在让我们执行这段代码，但不传递X-Session-Id标头。 Get()方法不会被调用，而是返回未授权的响应(401)：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/custom-auth-1.png" alt="" /></p>
<p>另一方面，如果我们添加会话标头，我们将得到带有OK(200) HTTP状态代码的响应：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/custom-auth-2.png" alt="" /></p>
<h3>基于策略的授权</h3>
<p>虽然我们可以使用自定义授权属性来构建授权逻辑，但<strong>Microsoft建议使用基于策略的授权来构建自定义授权</strong>。基于策略的授权将授权与应用程序逻辑解耦，提供了一种灵活、可重用和可扩展的ASP.NET Core安全模型。</p>
<p>要实现基于策略的授权，我们需要了解三个概念：</p>
<ul>
<li>策略（Policies）</li>
<li>需求（Requirements）</li>
<li>处理程序（Handlers）<br />
一个策略包括多个需求。需求是一个类，接受参数以验证授权逻辑。最后，一个授权处理程序包含根据添加到它的需求验证策略的逻辑。</li>
</ul>
<p>现在，让我们将这个实践起来。我们将实现与前一节相同的授权检查，以验证是否传递了自定义会话标头的HTTP请求。</p>
<h5>Implement Policy-Based Authorization</h5>
<p>首先，让我们创建一个用于会话标头验证的需求：</p>
<pre><code>public class SessionRequirement : IAuthorizationRequirement
{
    public SessionRequirement(string sessionHeaderName)
    {
        SessionHeaderName = sessionHeaderName;
    }
    public string SessionHeaderName { get; }
}</code></pre>
<p>需求必须实现空的标记接口<code>IAuthorizationRequirement</code>。我们还接受一个构造函数参数，用于传递我们要检查其存在的会话标头的名称。</p>
<p>接下来，我们创建一个包含授权逻辑的处理程序：</p>
<pre><code>public class SessionHandler : AuthorizationHandler&lt;SessionRequirement&gt;
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    public SessionHandler(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }
    protected override Task HandleRequirementAsync
        (AuthorizationHandlerContext context, SessionRequirement requirement)
    {
        var httpRequest = _httpContextAccessor.HttpContext!.Request;
        if (!httpRequest.Headers[requirement.SessionHeaderName].Any())
        {
            context.Fail();
            return Task.CompletedTask;
        }
        context.Succeed(requirement);
        return Task.CompletedTask;
    }
}</code></pre>
<p>要创建一个授权处理程序，我们继承自<code>AuthorizationHandler&lt;TRequirement&gt;</code>。这确保了对需求类型TRequirement的授权处理程序的调用。</p>
<p>然后，我们实现<code>AuthorizationHandler</code>类的<code>HandleRequirementAsync()</code>方法。该方法接受授权上下文和需求实例本身。然后，我们从<code>SessionRequirement</code>实例中获取标头名称，并使用通过构造函数注入的外部<code>IHttpContextAccessor</code>服务进行验证。</p>
<p>在评估需求之后，我们在<code>AuthorizationHandlerContext</code>实例上调用<code>Succeed()</code>方法，并将需求实例作为参数传递给该方法。这表示需求成功。</p>
<p>另一方面，我们在<code>AuthorizationHandlerContext</code>实例上使用<code>Fail()</code>方法来标记需求失败。这会阻止进一步访问所请求的资源。</p>
<p>接下来，让我们将处理程序注册到服务集合中：</p>
<pre><code>builder.Services.AddSingleton&lt;IAuthorizationHandler, SessionHandler&gt;();</code></pre>
<p>现在，我们可以将策略注册到<code>Authorization</code>服务中：</p>
<pre><code>builder.Services.AddAuthorization(options =&gt;
{
    options.AddPolicy(&quot;SessionPolicy&quot;, policy =&gt;
    {
        policy.Requirements.Add(new SessionRequirement(&quot;X-Session-Id&quot;));
    });
});</code></pre>
<p>在这里，我们创建了一个名为<code>SessionPolicy</code>的策略，并配置了相关的需求。虽然我们可以向策略添加多个需求，但在我们的示例中，我们添加了一个单独的SessionRequirement，并通过构造函数传递了会话标头的名称。</p>
<p>最后，让我们在我们的控制器操作方法中使用策略：</p>
<pre><code>[HttpGet(&quot;WithCustomAuthorizationPolicy&quot;)]
[Authorize(Policy = &quot;SessionPolicy&quot;)]
public IEnumerable&lt;WeatherForecast&gt; Get()
{
    return Enumerable.Range(1, 5).Select(index =&gt; new WeatherForecast
    {
        Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
        TemperatureC = Random.Shared.Next(-20, 55),
        Summary = Summaries[Random.Shared.Next(Summaries.Length)]
    })
    .ToArray();
}</code></pre>
<p>使用起来非常简单，因为我们将创建的策略名称传递给Authorize属性的构造函数。这要求在执行操作方法时必须满足策略。</p>
<p>现在让我们测试一下代码。</p>
<p>我们在不传递<code>X-Session-Id</code>标头的情况下执行代码，然后我们会收到一个未授权的响应：</p>
<p><img src="https://zhangxihai.cn/wp-content/uploads/2024/01/custom-auth-3.png" alt="" /></p>
<p>相反，添加会话标头会产生成功的响应：<br />
<img src="https://zhangxihai.cn/wp-content/uploads/2024/01/custom-auth-4.png" alt="" /></p>
<h3>总结</h3>
<p>我们学习了在ASP.NET Core中创建自定义授权的两种方法。尽管使用<code>IAuthorizationFilter</code>实现自定义授权属性很简单，但基于策略的授权更灵活，通过将授权与应用程序逻辑解耦来帮助我们构建一个松散耦合的安全模型。这就是为什么基于策略的授权更适合实现可扩展的授权解决方案的原因。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://zhangxihai.cn/archives/273/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>.NET 8: 身份验证及授权的新特性</title>
		<link>https://zhangxihai.cn/archives/266</link>
					<comments>https://zhangxihai.cn/archives/266#respond</comments>
		
		<dc:creator><![CDATA[胖爷]]></dc:creator>
		<pubDate>Wed, 27 Dec 2023 10:06:12 +0000</pubDate>
				<category><![CDATA[笔记]]></category>
		<category><![CDATA[编码]]></category>
		<guid isPermaLink="false">https://zhangxihai.cn/?p=266</guid>

					<description><![CDATA[翻译文章，原文链接点我。 .NET 8的发布即将到来。在为开发人员带来的惊人功能中，它在身份验证和授权支持方面进行了一次小的革命：将ASP.NET Core Identity从面向页面的方法转变为面向API的方法。 让我们探讨一下发生了什么。 ASP.NET Core Identity和基于令牌的身份验证 ASP.NET Core开发人员可以使用内置的ASP...]]></description>
										<content:encoded><![CDATA[<p>翻译文章，<a href="https://auth0.com/blog/whats-new-dotnet8-authentication-authorization/">原文链接点我</a>。</p>
<p>.NET 8的发布即将到来。在为开发人员带来的惊人功能中，它在身份验证和授权支持方面进行了一次小的革命：将ASP.NET Core Identity从面向页面的方法转变为面向API的方法。</p>
<p>让我们探讨一下发生了什么。</p>
<h3>ASP.NET Core Identity和基于令牌的身份验证</h3>
<p>ASP.NET Core开发人员可以使用内置的<a href="https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity">ASP.NET Core Identity框架</a>来添加对本地身份验证和授权的支持。身份框架包括开发人员通常需要的一切，用于对本地用户存储进行用户身份验证和授权。默认情况下，在Windows上创建一个SQL Server数据库，在macOS上创建一个SQLite数据库，但您可以将其更改为您选择的数据库管理系统（DBMS）。</p>
<p>ASP.NET Core Identity框架专为服务器呈现的Web应用程序设计，例如ASP.NET Core MVC或Razor Pages应用程序，并在单页应用程序（SPA）中存在一些挑战，其中基于令牌的身份验证似乎更为合适。为解决这个问题，自.NET Core 3.1起，Microsoft提供了基于Angular和React的SPA的内置项目模板，并支持Identity Server。然而，.NET社区<a href="https://github.com/dotnet/aspnetcore/issues/42158">对这种方法并不完全满意</a>。为了<a href="https://github.com/dotnet/aspnetcore/issues/42158#issuecomment-1481742187">满足社区的需求</a>，Microsoft在.NET 8中移除了对Identity Server的默认支持，并重新设计了ASP.NET Core Identity的内部架构，使其更适用于SPA和本地应用程序。.NET 8引入了一组新的Identity API端点和对基于令牌的身份验证的支持。但让我们按顺序来看。</p>
<h3>Bearer Token身份验证处理程序</h3>
<p>这个新基础设施的第一个构建块是新的<a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.bearertokenextensions.addbearertoken">Bearer Token身份验证处理程序</a>。该处理程序类似于ASP.NET Core Identity默认使用的经典<a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.cookieextensions.addcookie">Cookie身份验证处理程序</a>。Cookie身份验证处理程序负责两件事：</p>
<ul>
<li>在用户验证后创建新的会话Cookie。 </li>
<li>根据接收到的传入HTTP请求中的有效会话Cookie构建一个<code>ClaimsPrincipal</code>用户对象。</li>
</ul>
<p>同样，Bearer Token身份验证处理程序负责：</p>
<ul>
<li>在用户验证后创建新的令牌。</li>
<li>根据接收到的传入HTTP请求中的有效令牌构建一个<code>ClaimsPrincipal</code>用户对象。</li>
</ul>
<p>换句话说，Bearer Token处理程序模仿Cookie处理程序的行为，以基于令牌而不是Cookie来管理已验证的会话。</p>
<blockquote>
<p>请注意，Bearer Token处理程序生成的令牌不是JWT格式的。而且，它们不遵循任何特定的标准。尽管文档提到了<a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.bearertoken.accesstokenresponse.accesstoken?view=aspnetcore-8.0">访问令牌</a>和<a href="https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.bearertoken.accesstokenresponse.refreshtoken?view=aspnetcore-8.0">刷新令牌</a>，但它们并未实现<a href="https://oauth.net/2/">OAuth 2.0授权框架</a>。</p>
</blockquote>
<p>这个Bearer Token处理程序在实践中如何使用呢？</p>
<p>考虑以下代码：</p>
<pre><code>
// Program.cs

using Microsoft.AspNetCore.Authentication.BearerToken; //&#x1f448;using System.Security.Claims;

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddAuthentication()
    .AddBearerToken();  //&#x1f448;
builder.Services.AddAuthorization();

var app = builder.Build();

app.MapGet(&quot;/login&quot;, (string username) =&gt;
    {
        var claimsPrincipal = new ClaimsPrincipal(
          new ClaimsIdentity(
            new[] { new Claim(ClaimTypes.Name, username)},
            BearerTokenDefaults.AuthenticationScheme  //&#x1f448;
          )
        );

        return Results.SignIn(claimsPrincipal);
    });

app.MapGet(&quot;/user&quot;, (ClaimsPrincipal user) =&gt;
    {
        return Results.Ok($&quot;Welcome {user.Identity.Name}!&quot;);
    })
    .RequireAuthorization();

app.Run();</code></pre>
<p>该应用程序定义了两个API端点：</p>
<ul>
<li><code>/login</code>端点根据HTTP请求中发送的用户名创建一个<code>ClaimsPrincipal</code>用户对象，并返回签入的结果。</li>
<li><code>/user</code>端点返回经过身份验证的用户的用户名。</li>
</ul>
<p>请注意代码中的突出显示的行。您引用了新的<code>Microsoft.AspNetCore.Authentication.BearerToken</code>命名空间，这使您可以访问<code>BearerTokenDefaults.AuthenticationScheme</code>值。您还使用<code>AddBearerToken()</code>扩展方法配置了<code>Bearer Token</code>中间件。</p>
<p>现在，假设您使用curl调用登录端点，如下所示：</p>
<pre><code>curl &#039;https://&lt;YOUR_HOST&gt;/login?username=joe&#039;</code></pre>
<p>你将会看到以下的结果:</p>
<pre><code>
{
  &quot;token_type&quot;: &quot;Bearer&quot;,
  &quot;access_token&quot;: &quot;CfDJ8Ha5YkqG...omitted content...&quot;,
  &quot;expires_in&quot;: 3600,
  &quot;refresh_token&quot;: &quot;CfDJ8Ha5YkqG...omitted content...&quot;
  }</code></pre>
<p>这个JSON包含了一个访问令牌和一个刷新令牌，您可以使用它们来调用您应用程序提供的受保护的API。例如，您现在可以调用受保护的<code>/user</code>端点，如下所示：</p>
<pre><code>curl -i https://&lt;YOUR_HOST&gt;/user \
-H &#039;Authorization: Bearer CfDJ8Ha5YkqG...omitted content...&#039;</code></pre>
<p>然后你会得到以下结果：</p>
<pre><code>Welcome joe!</code></pre>
<p>总的来说，Bearer Token身份验证处理程序使得建立基于令牌的身份验证变得非常容易。这是整个ASP.NET Core Identity过渡到基于令牌身份验证的基石。</p>
<h3>身份验证API端点</h3>
<p>在向ASP.NET Core Identity添加基于令牌的身份验证的第二步是引入身份验证API端点。基本上，这是ASP.NET Core Identity经典Web UI可以执行的操作的API版本。一旦启用了<a href="https://github.com/dotnet/aspnetcore/blob/main/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs">身份验证API端点</a>，您将获得像<code>/register</code>、<code>/login</code>、<code>/forgotPassword</code>、<code>confirmEmail</code>等端点。</p>
<p>身份验证API端点解决了两个最常抱怨的问题：</p>
<ul>
<li>您可以构建自己的用户身份验证和账户管理的UI，同时保持应用程序的整体UI风格。</li>
<li>在身份验证后，您的应用程序可以获取访问令牌而不是Cookie，因此对于SPA和本地应用程序来说，这是一种更合适的方法。</li>
</ul>
<blockquote>
<p>请记住，身份验证API端点不实现<a href="https://auth0.com/docs/authenticate/protocols/openid-connect-protocol"><code>OpenID Connect（OIDC）</code></a>。它们只是通过自定义API公开了ASP.NET Core Identity UI提供的标准功能。它们用于第一方身份验证。</p>
</blockquote>
<ul>
<li><code>AddIdentityApiEndpoints()</code> ，该代码使用Bearer Token处理程序配置基于令牌的身份验证，同时添加Cookie身份验证，并添加一组身份验证服务。</li>
<li><code>MapIdentityApi()</code>，该代码实际上将身份验证端点添加到您的应用程序。</li>
</ul>
<p>你的<code>Program.cs</code> 文件里的代码应该像如下所示：</p>
<pre><code>
// Program.cs

using System.Security.Claims;using Microsoft.AspNetCore.Identity;using Microsoft.AspNetCore.Identity.EntityFrameworkCore;using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddAuthorization();

builder.Services.AddDbContext&lt;ApplicationDbContext&gt;(
    options =&gt; options.UseSqlite(builder.Configuration[&quot;ConnectionString&quot;]));

builder.Services.AddIdentityApiEndpoints&lt;IdentityUser&gt;()
    .AddEntityFrameworkStores&lt;ApplicationDbContext&gt;();

var app = builder.Build();

app.MapIdentityApi&lt;IdentityUser&gt;();

app.MapGet(&quot;/user&quot;, (ClaimsPrincipal user) =&gt;
    {
        return Results.Ok($&quot;Welcome {user.Identity.Name}!&quot;);
    })
    .RequireAuthorization();

app.Run();</code></pre>
<p>您的客户端将能够通过调用<code>/register</code>端点注册用户，通过<code>/login</code>端点对用户进行身份验证，等等。</p>
<p>尽管身份验证API端点似乎解决了关于经典ASP.NET Core Identity的两个主要问题，<a href="https://andrewlock.net/should-you-use-the-dotnet-8-identity-api-endpoints/">但在生产环境中使用它们仍存在一些关于安全性和可伸缩性方面的顾虑</a>。</p>
<h3>Blazor UI 验证</h3>
<p>除了对ASP.NET Core Identity的更改之外，.NET 8将身份验证框架集成到Blazor项目模板中。您可以通过运行以下命令创建一个具有身份验证支持的新.NET 8 Blazor应用程序：</p>
<pre><code>dotnet new blazor -au Individual</code></pre>
<p>您将获得一个Blazor应用程序，其中包含一切您需要在本地管理用户和进行身份验证的内容。您将获得一个SQLite数据库来存储用户帐户。如果您使用Visual Studio而不是.NET CLI对应用程序进行脚手架生成，您将获得一个SQL Server数据库。您还将获得所有允许用户注册、进行身份验证和管理其帐户的页面。</p>
<p>以下图片显示了Blazor应用程序的内置登录页面：</p>
<p><a href="https://zhangxihai.cn/wp-content/uploads/2023/12/blazor-identity-ui-login.png"><img src="https://zhangxihai.cn/wp-content/uploads/2023/12/blazor-identity-ui-login.png" alt="" /></a></p>
<p>实现Blazor Identity UI的用户身份验证和管理页面不是像经典的ASP.NET Core Identity那样的Razor页面，而是<a href="https://learn.microsoft.com/en-us/aspnet/core/blazor/components">Razor组件</a>。模板为您提供了项目中<code>Pages/Account</code>文件夹中所有UI组件的源代码。这使您可以根据应用程序设计自定义它们。</p>
<p><a href="https://zhangxihai.cn/wp-content/uploads/2023/12/dotnet-auth0-ebook.png"><img src="https://zhangxihai.cn/wp-content/uploads/2023/12/dotnet-auth0-ebook.png" alt="" /></a></p>
<h3>自定义授权策略简化</h3>
<p>除了与ASP.NET Core Identity相关的新功能之外，.NET 8还引入了对自定义授权策略定义的简化。以前，您必须编写大量代码来定义一个参数化的授权策略，以应用于API端点。</p>
<p>例如，假设您想要为一个仅授权18岁以上用户的端点定义一个策略，并且可以按以下方式使用：</p>
<pre><code>[ApiController]
[Route(&quot;api/[controller]&quot;)]
public class DrinkController : Controller
{
    [MinimumAgeAuthorize(18)]

    [HttpGet(&quot;beer&quot;)]

    public string BeerDispenser(ClaimsPrincipal user) =&gt; $&quot;Here is your beer, {user.Identity?.Name}!&quot;;
}</code></pre>
<p>在之前的.NET版本中，为了实现自定义授权策略，您必须编写如下代码片段中概述的代码：</p>
<pre><code>
class MinimumAgeAuthorizeAttribute : AuthorizeAttribute
{
  ...
}

class MinimumAgePolicyProvider : IAuthorizationPolicyProvider
{
  ...
}

class MinimumAgeRequirement : IAuthorizationRequirement
{
  ...
}

class MinimumAgeAuthorizationHandler : AuthorizationHandler&lt;MinimumAgeRequirement&gt;
{
  ..
.}</code></pre>
<p>您必须定义一个<code>AuthorizeAttribute</code>。然后，您需要实现一个<code>AuthorizationPolicyProvider</code>来处理您的自定义策略，以及一个<code>AuthorizationRequirement</code>来定义需求。最后，您必须实现<code>AuthorizationHandler</code>。</p>
<p>面显示的代码片段概述了您需要实现的类。要查看实际实现的示例，请看这个<a href="https://gist.github.com/captainsafia/309e408cb5e2c2e5acd44d2e19f7f88e">代码样例</a>。</p>
<p>使用新的<code>IAuthorizationRequirementData</code>接口，您的实现代码变成如下：</p>
<pre><code>
class MinimumAgeAuthorizeAttribute : AuthorizeAttribute, IAuthorizationRequirement, IAuthorizationRequirementData{
  public MinimumAgeAuthorizeAttribute(int age) =&gt; Age =age;
  public int Age { get; }

  public IEnumerable&lt;IAuthorizationRequirement&gt; GetRequirements()
  {
    yield return this;
  }}

class MinimumAgeAuthorizationHandler : AuthorizationHandler&lt;MinimumAgeAuthorizeAttribute&gt;{
  protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MinimumAgeAuthorizeAttribute requirement) 
  {
    ...
  }}</code></pre>
<p>您只需实现两个类，而不是像前面的示例中那样的四个类。查看<a href="https://gist.github.com/captainsafia/7c54e92d12df695ff0908e989fb8531f">此处</a>获取完整的代码。</p>
<h3>总结</h3>
<p>在本文中，您了解了.NET 8为支持身份验证和授权带来的新功能。其中大多数功能与扩展ASP.NET Core Identity框架以支持基于令牌的身份验证有关，为SPA和本地应用程序的身份验证铺平了道路。</p>
<p>作为一个副产品，您可以访问Bearer Token身份验证处理程序，它允许您发放访问令牌，以及身份验证API端点，它允许您在Web API中嵌入身份验证支持。</p>
<p>此外，Blazor项目模板现在通过Identity API端点支持身份验证，还提供了一套丰富的Razor组件，专为用户身份验证和管理而设计。</p>
<p>最后，新的<code>IAuthorizationRequirementData</code>接口使您能够以更少的代码创建自定义授权策略。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://zhangxihai.cn/archives/266/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Aspnet Blazor SEO支持</title>
		<link>https://zhangxihai.cn/archives/265</link>
					<comments>https://zhangxihai.cn/archives/265#respond</comments>
		
		<dc:creator><![CDATA[胖爷]]></dc:creator>
		<pubDate>Sun, 24 Dec 2023 03:40:16 +0000</pubDate>
				<category><![CDATA[笔记]]></category>
		<category><![CDATA[编码]]></category>
		<guid isPermaLink="false">https://zhangxihai.cn/?p=265</guid>

					<description><![CDATA[翻译文章，原文Search Engine Optimization (SEO)。 SEO在任何网站中都起着至关重要的作用，特别是对于SPA而言更是如此。在这个指南中，提到SEO时，我们指的是技术方面，而不是非技术方面。我们只关注如何向搜索引擎提供您网站的信息。 您可以在GitHub上下载本主题中使用的示例代码。 先决条件 本指南假定您对一下内容有基本理解： ...]]></description>
										<content:encoded><![CDATA[<p>翻译文章，原文<a href="https://blazorschool.com/tutorial/blazor-server/dotnet5/search-engine-optimization-(seo)-938345">Search Engine Optimization (SEO)</a>。</p>
<p>SEO在任何网站中都起着至关重要的作用，特别是对于SPA而言更是如此。在这个指南中，提到SEO时，我们指的是技术方面，而不是非技术方面。我们只关注如何向搜索引擎提供您网站的信息。</p>
<blockquote>
<p>您可以在GitHub上下载本主题中使用的<a href="https://github.com/Blazor-School/seo-blazor-server">示例代码</a>。</p>
</blockquote>
<h3>先决条件</h3>
<p>本指南假定您对一下内容有基本理解：</p>
<ul>
<li><a href="https://blazorschool.com/tutorial/blazor-server/dotnet5/component-interaction-550894">组件交互</a></li>
<li><a href="https://blazorschool.com/tutorial/blazor-server/dotnet5/dependency-injection-478834">依赖注入</a></li>
<li><a href="https://developers.google.com/search/docs/beginner/seo-starter-guide?hl=en&amp;visit_id=637639370449073519-226133402&amp;rd=1">谷歌SEO指南</a></li>
</ul>
<h3>SEO技术部分</h3>
<p>SEO的技术部分的目标是根据请求的URL在<head>标签中更改元数据标签。例如，当用户导航到新的组件时，<code>&lt;title&gt;</code>标签会发生变化。Blazor Server确实支持SEO。然而，Blazor Server的默认模板不支持SEO，您需要自己实现。</p>
<h4>使用Blazor Server进行SEO时常见的错误</h4>
<p>在为Blazor Server实施SEO时，有一些常见的错误需要避免。其中一些在开发阶段可能有效，但在生产阶段可能不起作用。</p>
<ol>
<li>使用JS更改元数据标签： Blazor Server是一个在服务器上渲染的SPA框架，因此这种方法将不起作用。</li>
<li>尝试更新已呈现的HTML标签： Blazor Server是在Razor Pages之上渲染的，Razor Pages不是一个SPA框架，因此不支持更新已呈现的HTML标签。</li>
</ol>
<h5>翻译补充</h5>
<ol start="3">
<li>缺乏关键元数据： 确保每个页面都有适当的元数据，如标题（<code>&lt;title&gt;</code>）、描述（<code>&lt;meta name=&quot;description&quot; content=&quot;...&quot; /&gt;</code>）和关键词。</li>
<li>动态加载内容： 如果您使用JavaScript等技术在页面上动态加载内容，确保搜索引擎可以正确地抓取和索引这些内容。使用预渲染技术或服务器端渲染（SSR）可以有助于解决这个问题。</li>
<li>不友好的URL结构： 使用有意义、清晰且易于理解的URL结构。避免使用过长、难以理解的参数和动态生成的URL。</li>
<li>不合理的页面层次结构： 组织您的页面以便搜索引擎可以轻松地理解页面之间的关系。使用适当的HTML标记来定义页面的结构。忽略性能问题： 确保您的网站加载速度快，因为搜索引擎通常对速度较慢的网站排名较低。优化图像、减少HTTP请求和使用浏览器缓存可以提高性能。</li>
<li>不处理404错误： 确保当用户访问不存在的页面时，返回适当的404错误页面，而不是默认的错误页面。这有助于搜索引擎了解页面是否真的不存在。</li>
<li>不使用合适的标题结构： 使用适当的标题标签（<code>&lt;h1&gt;</code>到<code>&lt;h6&gt;</code>）来定义页面的标题结构。确保标题具有层次性，反映页面的内容结构。</li>
<li>忽略移动友好性： 确保您的网站在移动设备上有良好的用户体验，因为搜索引擎对移动友好性有所考虑，并可能根据此因素进行排名。</li>
</ol>
<h3>Blazor服务器的SEO实现</h3>
<p>以下步骤是在Blazor Server中实施SEO的步骤。这些步骤使用本地的Blazor Server技术，无需第三方工具。</p>
<ol>
<li>创建一个名为<code>MetadataProvider.cs</code>的服务；</li>
<li>创建爱你一个名为<code>MetadataTransferService.cs</code>的数据转换服务;</li>
<li>在Startup.cs中将这两个服务都添加为scoped服务。</li>
<li>创建一个名为<code>Metadata.razor</code>的组件。</li>
<li>使用<code>MetadataTransferService</code>和<code>MetadataProvider</code>来初始化元数据。</li>
<li>在<code>_Host.cshtml</code>页面中使用<code>Metadata.razor</code>。</li>
</ol>
<h4>创建服务</h4>
<p>我们将要创建<code>MetadataProvider</code>和<code>MetadataTransferService</code>服务。正如名称所示，<code>MetadataProvider</code>将提供路由及其元数据详细信息。您可以将数据存储在数据库中，并使用此类获取沿路由的元数据。这是我们<code>MetadataProvider</code>实现的一个示例。</p>
<pre><code>public class MetadataProvider
{
    public Dictionary&lt;string, MetadataValue&gt; RouteDetailMapping { get; set; } = new()
    {
        {
            &quot;/&quot;,
            new()
            {
                Title = &quot;Blazor School Example&quot;,
                Description = &quot;Visit more at https://blazorschool.com&quot;
            }
        },
        {
            &quot;/about&quot;,
            new()
            {
                Title = &quot;About us&quot;,
                Description = &quot;Email us: dotnetprotech@gmail.com - The DotNetPro team.&quot;
            }
        }
    };
}</code></pre>
<p>下一步是创建<code>MetadataTransferService</code>服务。正如名称所示，<code>MetadataTransferService</code>将充当组件之间数据传输的服务。有关传输服务的更多信息，请参见组件交互。</p>
<pre><code>
public class MetadataTransferService : INotifyPropertyChanged, IDisposable
{
    public event PropertyChangedEventHandler PropertyChanged;

    private string _title;

    public string Title
    {
        get =&gt; _title;
        set
        {
            _title = value;
            OnPropertyChanged();
        }
    }

    private string _description;
    private readonly NavigationManager _navigationManager;
    private readonly MetadataProvider _metadataProvider;

    public string Description
    {
        get =&gt; _description;
        set
        {
            _description = value;
            OnPropertyChanged();
        }
    }

    public void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new(propertyName));
    }

    public MetadataTransferService(NavigationManager navigationManager, MetadataProvider metadataProvider)
    {
        _navigationManager = navigationManager;
        _metadataProvider = metadataProvider;
    }

    public void Dispose()
    {

    }
}</code></pre>
<p>您需要两个接口：<code>INotifyPropertyChanged</code>和<code>IDisposable</code>用于传输服务。<code>INotifyPropertyChanged</code>用于等同对待每个属性更改，您也可以为每个属性声明一个事件，但这个接口不是必需的。<code>IDisposable</code>也不是必需的，但建议使用它，因为您将订阅<code>NavigationManager</code>的<code>LocationChanged</code>事件，需要使用<code>Dispose</code>方法取消订阅该事件。添加<code>UpdateMetadata</code>方法以更新元数据。</p>
<pre><code>
private void UpdateMetadata(string url)
{
    var metadataValue = _metadataProvider.RouteDetailMapping.FirstOrDefault(vp =&gt; url.EndsWith(vp.Key)).Value;

    if (metadataValue is null)
    {
        metadataValue = new()
        {
            Title = &quot;Default&quot;,
            Description = &quot;Default&quot;
        };
    }

    Title = metadataValue.Title;
    Description = metadataValue.Description;
}</code></pre>
<p>订阅<code>LocationChanged</code>事件。</p>
<pre><code>
public MetadataTransferService(NavigationManager navigationManager, MetadataProvider metadataProvider)
{
    ...
    _navigationManager.LocationChanged += UpdateMetadata;
}

private void UpdateMetadata(object sender, LocationChangedEventArgs e)
{
    UpdateMetadata(e.Location);
}

public void Dispose()
{
    _navigationManager.LocationChanged -= UpdateMetadata;
}</code></pre>
<p>您需要两个接口：<code>INotifyPropertyChanged</code>和<code>IDisposable</code>，用于传输服务。<code>INotifyPropertyChanged</code>用于等同对待每个属性更改，您也可以为每个属性声明一个事件，但这个接口不是必需的。<code>IDisposable</code>也不是必需的，但建议使用它，因为您将订阅<code>NavigationManager</code>的<code>LocationChanged</code>事件，需要使用Dispose方法取消订阅该事件。添加<code>UpdateMetadata</code>方法以更新元数据。</p>
<pre><code>
public MetadataTransferService(NavigationManager navigationManager, MetadataProvider metadataProvider)
{
    ...
    UpdateMetadata(_navigationManager.Uri);
}</code></pre>
<p>在<code>Startup.cs</code>中注册两个文件。</p>
<pre><code>
public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddSingleton&lt;MetadataProvider&gt;();
    services.AddScoped&lt;MetadataTransferService&gt;();
}</code></pre>
<blockquote>
<p>MetadataProvider也可以是一个作用域服务。您应该根据您的情况选择最合适的方式，而不总是将其设置为单例，就像示例中一样。</p>
</blockquote>
<h4>创建一个metadata组件</h4>
<p><code>Metadata.razor</code>包含您想要在网站上显示的所有元数据标记。每个网站必须具有的一些基本标签是<code>&lt;title&gt;</code>和<meta name=“description”/><code>。</code>Metadata.razor<code>将使用传输服务中的数据来呈现HTML元素。这是我们的</code>Metadata.razor`实现的一个示例。</p>
<pre><code>
@inject MetadataTransferService MetadataTransferService@implements IDisposable

&lt;title&gt;@MetadataTransferService.Title&lt;/title&gt;&lt;meta name=&quot;description&quot; content=&quot;@MetadataTransferService.Description&quot; /&gt;

@code {
    protected override void OnInitialized()
    {
        MetadataTransferService.PropertyChanged += OnMetadataChanged;
    }

    private void OnMetadataChanged(object sender, PropertyChangedEventArgs e)
    {
        StateHasChanged();
    }

    public void Dispose()
    {
        MetadataTransferService.PropertyChanged -= OnMetadataChanged;
    }</code></pre>
<h4>使用metadata组件</h4>
<p>一旦您拥有了<code>Metadata.razor</code>，您需要在<code>_Host.cshtml</code>中使用它。请记住，<code>_Host.cshtml</code>是一个Razor页面，而您的<code>Metadata.razor</code>是一个Razor组件。在Razor页面中使用Razor组件与在另一个Razor组件中使用Razor组件有些不同。请移除<code>_Host.cshtml</code>中现有的<code>&lt;title&gt;</code>标签。</p>
<pre><code>&lt;head&gt;
    ...
    &lt;component type=&quot;typeof(Metadata)&quot; render-mode=&quot;ServerPrerendered&quot;/&gt;
&lt;/head&gt;</code></pre>
]]></content:encoded>
					
					<wfw:commentRss>https://zhangxihai.cn/archives/265/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
