ASP-NET-Core-6-0-自定义指南-全-
ASP.NET Core 6.0 自定义指南(全)
原文:
zh.annas-archive.org/md5/22612f10e0b86a284b54eb7eeed4f4d2译者:飞龙
前言
ASP.NET Core 是微软提供的最强大的 Web 框架,它充满了使它更强大和有用的隐藏功能。
不应该让你的应用程序去匹配框架;你的框架应该能够完成你的应用程序真正需要的功能。通过这本书,你将学习如何找到可以旋转以获得框架最大化的隐藏螺丝。
使用 ASP.NET Core 开发的开发者将能够通过这本关于自定义 ASP.NET Core 的实用指南来运用他们的知识。本书提供了一种动手实践的方法,以及与其相关的实现方法和方法论,这将使你迅速投入工作并变得高效。
本书是默认 ASP.NET Core 行为的紧凑集合,这些行为你可能想要更改,以及如何一步步进行更改的详细说明。
到这本书的结尾,你将知道如何根据你个人的需求自定义 ASP.NET Core,从而获得一个优化的应用程序。
ASP.NET Core 架构概述
要继续阅读下一章,你应该熟悉 ASP.NET Core 的基本架构及其组件。本书几乎涵盖了架构的所有组件。
下图显示了 ASP.NET Core 6.0 的基本架构概述。让我们快速浏览一下从底部到顶部层显示的组件:

在底部,是 Host 层。这是启动 web 服务器以及启动 ASP.NET Core 应用程序所需的所有内容的引导层,包括日志记录、配置和服务提供者。这一层创建了实际请求对象及其依赖项,这些依赖项在上述层中使用。
在 Host 层之上的是 Middleware 层。这一层与请求对象一起工作或对其进行操作。它将中间件附加到请求对象上。它执行中间件,如错误处理、HSTS 认证、CORS 等。
在那之上,是 Routing 层,它根据定义的路由模式将请求路由到端点。端点路由是 ASP.NET Core 3.1 中的新参与者,它将路由从上面的 UI 层中分离出来,以实现对不同端点的路由,包括 Blazor、gRPC 和 SignalR。提醒一下:在 ASP.NET Core 的早期版本中,路由是 MVC 层的一部分,而每个其他的 UI 层都需要实现自己的路由。
实际端点由第四层,即 UI 层提供,该层包含知名的 UI 框架 Blazor、gRPC、SignalR 和 MVC。作为 ASP.NET Core 开发者,你将在这里做大部分工作。
最后,在 MVC 之上,你会发现 WebAPI 和 Razor Pages。
本书涵盖了哪些内容?
本书不涵盖架构概述中提到的所有主题。本书主要涵盖托管层的主题,因为这是包含最多可能需要定制的功能的层。本书探讨了中间件和路由,以及 MVC 功能和一些额外的 WebAPI 主题,在这些主题中你可以施展一些魔法技巧。
每章开头,我们将指出该主题属于哪个级别。
未涵盖的内容及其原因?
本书不涵盖 Razor Pages、SignalR、gRPC 和 Blazor。
原因在于 gRPC 和 SignalR 已经非常专业化,实际上并不需要太多定制。Blazor 是 ASP.NET Core 家族的新成员,目前尚未得到广泛应用。此外,作者对 Blazor 的了解还不够深入,以至于不知道如何调整所有细节来定制它。Razor Pages 建立在 MVC 框架之上,因此对 MVC 的定制也适用于 Razor Pages。
本书面向的对象
本书面向的是使用 ASP.NET Core 的网络开发者,他们可能需要更改默认行为以完成任务。读者应具备 ASP.NET Core 和 C# 的基础知识,因为本书不涵盖这些技术的基础知识。读者还应熟悉 Visual Studio、Visual Studio Code 或任何支持 ASP.NET Core 和 C# 的其他代码编辑器。
本书涵盖的内容
第一章,自定义日志记录,教你如何自定义日志行为以及如何添加自定义日志提供程序。
第二章,自定义应用程序配置,帮助你了解如何使用不同的配置源并添加自定义配置提供程序。
第三章,自定义依赖注入,教你了解 依赖注入 (DI) 的工作原理以及如何使用不同的 DI 容器。
第四章,使用 Kestrel 配置和自定义 HTTPS,探讨了不同的 HTTPS 配置方法。
第五章,配置 WebHostBuilder,帮助你了解如何在托管层设置配置。
第六章,使用不同的托管模型,教你了解不同平台上的不同托管类型。
第七章,使用 IHostedService 和 BackgroundService,让你了解如何在后台执行任务。
第八章,编写自定义中间件,处理 HTTP 上下文使用中间件。
第九章,使用端点路由,帮助你了解如何使用新的路由来提供自定义端点。
第十章,自定义 ASP.NET Core 身份验证,解释了如何扩展应用程序的用户属性,并帮助你更改身份 UI。
第十一章, 配置身份管理,帮助您管理您的用户及其角色。
第十二章, 使用自定义输出格式化程序进行内容协商,教您如何根据 HTTP Accept 头输出不同的内容类型。
第十三章, 使用自定义 ModelBinder 管理输入,帮助您创建具有不同类型内容的输入模型。
第十四章, 创建自定义 ActionFilter,介绍了使用 ActionFilter 的面向方面编程。
第十五章, 使用缓存,帮助您使您的应用程序更快。
第十六章, 创建自定义 TagHelper,使您能够通过创建 TagHelper 简化 UI 层。
要充分利用本书
读者应具备基本的 ASP.NET Core 和 C#知识,以及 Visual Studio、Visual Studio Code 或任何支持 ASP.NET Core 和 C#的其他代码编辑器。

您应该在您的机器上安装最新的.NET 6.0 SDK。请访问dotnet.microsoft.com/download/dotnet-core/以获取最新版本。
随意使用您喜欢的任何支持 ASP.NET Core 和 C#的代码编辑器。我们推荐使用 Visual Studio Code([code.visualstudio.com/](https://code.visualstudio.com/)),它适用于所有平台,并且本书的作者也在使用它。
本书中的所有项目都将使用控制台、命令提示符、shell 或 PowerShell 创建。请随意使用您感到舒适的任何控制台。作者使用 Windows 命令提示符,托管在 cmder shell 中([https://cmder.net/](https://cmder.net/))。我们不推荐使用 Visual Studio 创建项目,因为基本配置可能会更改,并且 Web 项目将启动在本书中描述的端口之外。
您是否卡在.NET Core 3.1 或.NET 5.0 上?如果您由于任何原因无法在您的机器上使用.NET 6.0,所有示例也都可以使用.NET Core 3.1 和.NET 5.0。有些章节在.NET 6.0 与.NET 5.0 有差异时,会包含与.NET 5.0 的比较。
如果您正在使用本书的数字版,我们建议您亲自输入代码或从本书的 GitHub 仓库(下一节中有一个链接)获取代码。这样做将帮助您避免与代码复制和粘贴相关的任何潜在错误。
下载示例代码文件
您可以从 GitHub 下载本书的示例代码文件https://github.com/PacktPublishing/Customizing-ASP.NET-Core-6.0-Second-Edition。如果代码有更新,它将在 GitHub 仓库中更新。如果代码有更新,它将在 GitHub 仓库中更新。
我们还有其他来自我们丰富的书籍和视频目录的代码包可供在github.com/PacktPublishing/找到。查看它们吧!
下载彩色图像
我们还提供了一份包含本书中使用的截图和图表彩色图像的 PDF 文件。您可以从这里下载:static.packt-cdn.com/downloads/9781803233604_ColorImages.pdf。
使用的约定
本书使用了多种文本约定。
文本中的代码:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“您可以使用ConfigureAppConfiguration来配置应用程序配置。”
代码块按以下方式设置:
builder.Configuration.AddJsonFile(
"appsettings.json",
optional: false,
reloadOnChange: true);
当我们希望将您的注意力引到代码块的一个特定部分时,相关的行或项目将以粗体显示:
builder.Logging.AddConfiguration(builder.Configuration.
GetSection("Logging"));
builder.Logging.AddConsole();
builder.Logging.AddDebug();
任何命令行输入或输出都按以下方式编写:
cd LoggingSample
code .
粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词以粗体显示。以下是一个示例:“点击左上角的注册,您将看到以下页面。”
小贴士或重要提示
看起来是这样的。
联系我们
我们始终欢迎读者的反馈。
一般反馈:如果您对本书的任何方面有疑问,请通过电子邮件发送至 customercare@packtpub.com,并在邮件主题中提及书名。
勘误表:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将不胜感激,如果您能向我们报告这一点。请访问www.packtpub.com/support/errata并填写表格。
盗版:如果您在互联网上遇到我们作品的任何形式的非法副本,如果您能提供位置地址或网站名称,我们将不胜感激。请通过 copyright@packt.com 与我们联系,并提供材料的链接。
如果您有兴趣成为作者:如果您在某个主题上具有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
分享您的想法
读完Customizing ASP.NET Core 6.0后,我们很乐意听听您的想法!请点击此处直接进入此书的亚马逊评论页面并分享您的反馈。
您的评论对我们和科技社区都很重要,并将帮助我们确保我们提供高质量的内容。
第一章:第一章:自定义日志记录
在本书关于自定义 ASP.NET Core 的第一章节中,您将了解如何自定义 日志记录。默认的日志记录仅写入控制台或调试窗口。这对于大多数情况来说相当不错,但有时您需要将日志记录到其他位置,例如文件或数据库。或者,也许您想通过附加额外信息来扩展日志记录器。在这些情况下,您需要了解如何更改默认的日志记录。
在本章中,我们将涵盖以下主题:
-
配置日志记录
-
创建自定义日志记录器
-
插入现有的第三方日志记录提供程序
本章中的主题涉及 ASP.NET Core 架构的托管层:

图 1.1 – ASP.NET Core 架构
技术要求
要遵循本章中的描述,您需要创建一个 ASP.NET Core MVC 应用程序。为此,打开您的控制台、shell 或 Bash 终端,并切换到您的当前工作目录。然后,使用以下命令创建一个新的 MVC 应用程序:
dotnet new mvc -n LoggingSample -o LoggingSample
现在,通过双击项目文件或在 Visual Studio Code 中在已打开的控制台中输入以下命令来打开项目在 Microsoft Visual Studio 中的项目:
cd LoggingSample
code .
本章中的所有代码示例都可以在本书的 GitHub 仓库中找到,网址为 github.com/PacktPublishing/Customizing-ASP.NET-Core-6.0-Second-Edition/tree/main/Chapter01。
配置日志记录
在 ASP.NET Core 的早期版本(即 2.0 版本之前),日志记录是在 Startup.cs 中配置的。提醒一下,自 2.0 版本以来,Startup.cs 文件已被简化,许多配置已移动到默认的 WebHostBuilder,该 WebHostBuilder 在 Program.cs 中被调用。此外,日志记录也已移动到默认的 WebHostBuilder。
在 ASP.NET Core 3.1 及更高版本中,Program.cs 文件变得更加通用,首先会创建 IHostBuilder。IHostBuilder 对于无需所有 ASP.NET 网络功能即可启动应用程序非常有用。我们将在本书的后面部分了解更多关于 IHostBuilder 的内容。使用这个 IHostBuilder,我们创建 IWebHostBuilder 来配置 ASP.NET Core。在 ASP.NET Core 3.1 及更高版本中,我们通过 webBuilder 变量获得 IWebHostBuilder:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(
string[]args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
在 ASP.NET Core 6.0 中,Microsoft 引入了 Startup 文件,并将所有配置添加到 Program.cs 文件中。让我们看看它是什么样子:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
var app = builder.Build();
// The rest of the file isn't relevant for this chapter
在 ASP.NET Core 中,你可以几乎覆盖和自定义几乎所有内容。这包括日志记录。IWebHostBuilder 有许多扩展方法,允许我们覆盖不同功能的默认行为。要覆盖日志记录的默认设置,我们需要使用 ConfigureLogging 方法。下面的代码片段显示了几乎与 ConfigureWebHostDefaults() 方法内部配置的相同日志记录:
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder
.ConfigureLogging((hostingContext, logging) =>
{
logging.AddConfiguration(
hostingContext.Configuration.GetSection(
"Logging"));
logging.AddConsole();
logging.AddDebug();
})
.UseStartup<Startup>();
使用最小 API 方法,我们不再需要 ConfigureLogging 方法,可以直接使用 WebApplicationBuilder 的 Logging 属性:
builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging"));
builder.Logging.AddConsole();
builder.Logging.AddDebug();
现在我们已经看到了如何配置日志记录,让我们来看看如何构建自定义日志记录器。
创建自定义日志记录器
为了演示自定义日志记录器,让我们使用一个小巧简单的日志记录器,它能够将特定日志级别的日志条目着色到控制台。这个日志记录器被称为 ColoredConsoleLogger,它将通过 LoggerProvider 创建并添加,我们还需要为自己编写它。为了指定要着色的颜色和日志级别,我们需要添加一个配置类。
在接下来的代码片段中,展示了三个部分(Logger、LoggerProvider 和 Configuration):
-
让我们在与
Program.cs文件相同的文件夹中创建一个新的文件CustomLogger.cs,作为我们日志记录器的配置类。在文件的顶部添加以下using语句:namespace LoggingSample;我们将其命名为
ColoredConsoleLoggerConfiguration。这个类包含三个属性来定义 –LogLevel、EventId和Color– 这些都可以设置:public class ColoredConsoleLoggerConfiguration { public LogLevel LogLevel { get; set; } = LogLevel.Warning; public int EventId { get; set; } = 0; public ConsoleColor Color { get; set; } = ConsoleColor.Yellow; } -
接下来,我们需要一个提供者来检索配置并创建实际的日志记录器实例:
public class ColoredConsoleLoggerProvider : ILoggerProvider { private readonly ColoredConsoleLoggerConfiguration _config; private readonly ConcurrentDictionary<string, ColoredConsoleLogger> _loggers = new ConcurrentDictionary<string, ColoredConsoleLogger>(); public ColoredConsoleLoggerProvider (ColoredConsoleLoggerConfiguration config) { _config = config; } public ILogger CreateLogger(string categoryName) { return _loggers.GetOrAdd(categoryName, name => new ColoredConsoleLogger(name, _config)); } public void Dispose() { _loggers.Clear(); } }不要忘记添加
System.Collections.Concurrent的using语句。 -
第三个类是我们想要使用的实际日志记录器:
public class ColoredConsoleLogger : ILogger { private static object _lock = new Object(); private readonly string _name; private readonly ColoredConsoleLoggerConfiguration _config; public ColoredConsoleLogger( string name, ColoredConsoleLoggerConfiguration config) { _name = name; _config = config; } public IDisposable BeginScope<TState>(TState state) { return null; } public bool IsEnabled(LogLevel logLevel) { return logLevel == _config.LogLevel; } public void Log<TState>( LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) { if (!IsEnabled(logLevel)) { return; } lock (_lock) { if (_config.EventId == 0 || _config.EventId == eventId.Id) { var color = Console.ForegroundColor; Console.ForegroundColor = _config.Color; Console.Write($"{logLevel} - "); Console.Write($"{eventId.Id} - {_name} - "); Console.Write($"{formatter(state, exception)}\n"); Console.ForegroundColor = color; } } } }我们现在需要锁定实际的控制台输出 – 这是因为我们将遇到一些竞争条件,其中错误的日志条目会以错误的颜色着色,因为控制台本身并不是真正的线程安全的。
-
完成此操作后,我们可以开始将新的日志记录器插入到
Program.cs中的配置中:builder.Logging.ClearProviders(); var config = new ColoredConsoleLoggerConfiguration { LogLevel = LogLevel.Information, Color = ConsoleColor.Red }; builder.Logging.AddProvider(new ColoredConsoleLoggerProvider(config));
你可能需要在 LoggerSample 命名空间中添加一个 using 语句。
如果你不想使用现有的日志记录器,你可以清除之前添加的所有日志记录器提供者。然后,我们调用 AddProvider 来添加我们 ColoredConsoleLoggerProvider 类的一个新实例,并带有特定的设置。我们也可以添加一些具有不同设置的提供者实例。
这显示了如何以不同的方式处理日志级别。你可以使用这种方法来发送有关严重错误的电子邮件,或将调试消息记录到与常规信息消息不同的日志接收器,等等。
图 1.2 展示了之前创建的自定义日志记录器的彩色输出:

图 1.2 – 自定义日志记录器的截图
在许多情况下,编写自定义日志记录器是没有意义的,因为已经有许多优秀的第三方日志记录器可用,例如ELMAH、log4net和NLog。在下一节中,我们将看到如何在 ASP.NET Core 中使用NLog。
插入现有的第三方日志记录器提供程序
NLog是首批可用的日志记录器之一,因为NLog也提供了一个日志提供程序,可以轻松地将其插入到 ASP.NET Core 中。
你可以通过NLog找到它尚未明确支持 ASP.NET Core 6.0,但它仍然可以与版本 6.0 一起工作:
-
我们需要添加一个
NLog.Config文件,该文件定义了两个不同的接收器,将所有标准消息记录在单个日志文件中,并将自定义消息仅记录在另一个文件中。由于此文件过长,无法打印,你可以直接从 GitHub 查看或下载:github.com/PacktPublishing/Customizing-ASP.NET-Core-6.0-Second-Edition/blob/main/Chapter01/LoggingSample6.0/NLog.Config -
然后,我们需要从 NuGet 添加
NLogASP.NET Core 包:dotnet add package NLog.Web.AspNetCore重要提示
在执行前面的命令之前,请确保你处于项目目录中!
-
现在,你只需要在
Program.cs中的ConfigureLogging方法中清除所有其他提供程序,并使用IWebHostBuilder的UseNLog()方法来使用NLog:Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder .ConfigureLogging((hostingContext, logging) => { logging.ClearProviders(); logging.SetMinimumLevel( LogLevel.Trace); }) .UseNLog() .UseStartup<Startup>(); });使用最小 API,它看起来要简单得多:
using NLog.Web; var builder = WebApplication.CreateBuilder(args); builder.Logging.ClearProviders(); builder.Logging.SetMinimumLevel(LogLevel.Trace); builder.WebHost.UseNLog();在这里,你可以添加你需要的任意数量的日志提供程序。
这就涵盖了使用现有的第三方日志记录器。现在,让我们回顾一下本章所涵盖的内容。
摘要
隐藏应用程序的基本配置的好处在于,它允许你清理新搭建的项目,并尽可能简化实际的启动过程。开发者能够专注于实际的功能。然而,随着应用程序的增长,日志记录变得越来越重要。默认的日志配置既简单又有效,但在生产环境中,你需要一个持久的日志来查看过去的错误。因此,你需要添加自定义的日志配置或更灵活的第三方日志记录器,例如NLog或log4net。
你将在下一章中了解更多关于如何配置 ASP.NET Core 6.0 的信息。
第二章:第二章:自定义应用程序配置
本章的第二个部分是关于应用程序配置,如何使用它,以及如何自定义 ASP.NET 配置以采用不同的方式配置你的应用程序。也许你已经有了一个现有的 可扩展标记语言 (XML) 配置,或者想要在不同类型的应用程序之间共享一个 YAML 不是标记语言 (YAML) 配置文件。有时,从数据库中读取配置值也是有意义的。
本章将涵盖以下主题:
-
配置配置
-
使用类型化配置
-
使用 初始化 (INI) 文件进行配置
-
配置提供程序
本章讨论的主题涉及 ASP.NET Core 架构的托管层:

图 2.1 – ASP.NET Core 架构
技术要求
为了遵循本章的描述,你需要创建一个 ASP.NET Core 模型-视图-控制器 (MVC) 应用程序。打开你的控制台、shell 或 Bash 终端,并切换到你的工作目录。使用以下命令创建一个新的 MVC 应用程序:
dotnet new mvc -n ConfigureSample -o ConfigureSample
现在,通过双击项目文件或在 Visual Studio Code (VS Code) 中在已打开的控制台中输入以下命令来在 Visual Studio 中打开项目:
cd ConfigureSample
code .
本章中的所有代码示例都可以在本书的 GitHub 仓库中找到,网址为 github.com/PacktPublishing/Customizing-ASP.NET-Core-6.0-Second-Edition/tree/main/Chapter02。
配置配置
让我们先看看如何配置你的各种配置选项。
自从 ASP.NET Core 2.0 以来,配置被隐藏在 WebHostBuilder 的默认配置中,不再是 Startup.cs 的一部分。这有助于保持启动过程的简洁和简单。
在 ASP.NET Core 3.1 到 ASP.NET Core 5.0 中,代码看起来是这样的:
// ASP.NET Core 3.0 and later
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[]
args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
}
}
在 ASP.NET Core 6.0 中,Microsoft 引入了最小的 Startup 并将所有配置添加到 Program.cs 文件中。让我们看看它的样子:
Var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
var app = builder.Build();
// The rest of the file isn't relevant for this chapter
幸运的是,在两个版本中,你也能够覆盖默认设置以根据需要自定义配置。在两个版本中,我们通过 ConfigureAppConfiguration() 方法扩展了 IWebHostBuilder,这里的魔法就会发生。
这就是 ASP.NET Core 3.1 和 ASP.NET Core 5.0 中的配置样子。
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder
.ConfigureAppConfiguration((builderContext,
config) =>
{
// configure configuration here
})
.UseStartup<Startup>();
});
这就是使用最小 API 方法时的代码样子。你也可以使用 ConfigureAppConfiguration 来配置应用程序配置:
builder.WebHost.ConfigureAppConfiguration((builderContext, config) =>
{
// configure configuration here
});
但有一个更简单的方法,通过访问构建器的 Configuration 属性:
builder.Configuration.AddJsonFile(
"appsettings.json",
optional: false,
reloadOnChange: true);
当你创建一个新的 ASP.NET Core 项目时,你将已经配置了appsettings.json和appsettings.Development.json。你可以,并且应该使用这些配置文件来配置你的应用程序;这是预配置的方式,大多数 ASP.NET Core 开发者都会寻找一个appsettings.json文件来配置应用程序。这是绝对可以接受的,并且工作得很好。
以下代码片段展示了封装的默认配置,用于读取appsettings.json文件:
var env = builder.Environment;
builder.Configuration.SetBasePath(env.ContentRootPath);
builder.Configuration.AddJsonFile(
"appsettings.json",
optional: false,
reloadOnChange: true);
builder.Configuration.AddJsonFile(
$"appsettings.{env.EnvironmentName}.json",
optional: true,
reloadOnChange: true);
builder.Configuration.AddEnvironmentVariables();
此配置还设置了应用程序的基本路径,并通过环境变量添加了配置。
无论何时自定义应用程序配置,你都应该通过使用AddEnvironmentVariables()方法将配置作为最终步骤添加到环境变量中。配置的顺序很重要,你稍后添加的配置提供程序将覆盖之前添加的配置。确保环境变量始终覆盖通过文件设置的配置。这样,你也可以确保在 Azure App Service 上配置的应用程序配置将以环境变量的形式传递给应用程序。
IConfigurationBuilder有很多扩展方法来添加更多配置,例如 XML 或 INI 配置文件和内存中的配置。你可以找到社区构建的附加配置提供程序,用于读取 YAML 文件、数据库值以及更多。在接下来的部分中,我们将看到如何读取 INI 文件。首先,我们将探讨使用类型化配置。
使用类型化配置
在尝试读取 INI 文件之前,先看看如何使用类型化配置而不是通过IConfiguration逐个读取配置。
要读取类型化配置,你需要定义要配置的类型。我通常创建一个名为AppSettings的类,如下所示:
namespace ConfigureSample;
public class AppSettings
{
public int Foo { get; set; }
public string Bar { get; set; }
}
这是在 ASP.NET Core 5.0 之前的Startup.cs中的简单ConfigureServices方法:
services.Configure<AppSettings>
(Configuration.GetSection("AppSettings"));
使用最小 API 方法,你需要像这样配置AppSettings类:
builder.Services.Configure<AppSettings>(
builder.Configuration.GetSection("AppSettings"));
这样,类型化配置也注册为依赖注入(DI)容器中的服务,可以在应用程序的任何地方使用。你可以为每个配置部分创建不同的配置类型。在大多数情况下,一个部分就足够了,但有时将设置分成不同的部分是有意义的。下一个片段展示了如何在 MVC 控制器中使用配置:
using Microsoft.Extensions.Options;
// ...
public class HomeController : Controller
{
private readonly AppSettings _options;
public HomeController(IOptions<AppSettings> options)
{
_options = options.Value;
}
public IActionResult Index()
{
ViewData["Message"] = _options.Bar;
return View();
}
IOptions<AppSettings>是我们AppSettings类型的包装器,Value属性包含实际的AppSettings实例,包括来自配置文件的值。
要尝试读取设置,appsettings.json文件需要配置AppSettings部分,否则值将为 null 或未设置。现在让我们将部分添加到appsettings.json文件中,如下所示:
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*",
"AppSettings": {
"Foo": 123,
"Bar": "Bar"
}
}
接下来,我们将检查 INI 文件如何用于配置。
使用 INI 文件进行配置
要使用 INI 文件配置应用程序,您需要在 Program.cs 中的 ConfigureAppConfiguration() 方法内添加 INI 配置,如下所示:
builder.Configuration.AddIniFile(
"appsettings.ini",
optional: false,
reloadOnChange: true);
builder.Configuration.AddJsonFile(
$"appsettings.{env.EnvironmentName}.ini",
optional: true,
reloadOnChange: true);
此代码以与 JavaScript 对象表示法(JSON)配置文件相同的方式加载 INI 文件。第一行是必需的配置,第二行是可选配置,取决于当前运行时环境。
INI 文件可能看起来像这样:
[AppSettings]
Bar="FooBar"
如您所见,此文件包含一个名为 AppSettings 的部分和一个名为 Bar 的属性。
之前我们说过配置的顺序很重要。如果您在配置 JSON 文件之后添加配置 INI 文件的两个行,INI 文件将覆盖 JSON 文件的设置。Bar 属性被覆盖为 "FooBar",而 Foo 属性保持不变,因为它不会被覆盖。此外,INI 文件外的值将通过之前创建的类型化配置提供。
每个其他配置提供程序都将以相同的方式工作。现在让我们看看配置提供程序将是什么样子。
配置提供程序
配置提供程序是 IConfigurationProvider 的实现,由配置源创建,配置源是 IConfigurationSource 的实现。然后配置提供程序从某处读取数据,并通过 Dictionary 提供它。
要将自定义或第三方配置提供程序添加到 ASP.NET Core,您需要在 ConfigurationBuilder 上调用 Add 方法并插入配置源。这是一个示例:
// add new configuration source
builder.Configuration.Add(new MyCustomConfigurationSource
{
SourceConfig = //configure whatever source
Optional = false,
ReloadOnChange = true
});
通常,您会创建一个扩展方法来更轻松地添加配置源,如下所示:
builder.Configuration.AddMyCustomSource("source", optional: false, reloadOnChange: true);
关于如何创建自定义配置提供程序的详细具体示例是由 Andrew Lock 编写的。您可以在本章的 进一步阅读 部分找到它。
摘要
在大多数情况下,您不需要添加不同的配置提供程序或创建自己的配置提供程序,但了解如何更改它是很好的,以防万一。此外,使用类型化配置是读取和提供设置的不错方式。在经典 ASP.NET 中,我们使用手动创建的界面以类型化方式读取应用程序设置。现在,只需提供类型即可自动完成此操作。此类型将被自动实例化、填充并提供,通过依赖注入。
要了解有关在 ASP.NET Core 6.0 中自定义依赖注入的更多信息,让我们看看下一章。
进一步阅读
您可以参考以下来源获取更多信息:
- 在 ASP.NET Core 中创建自定义 ConfigurationProvider 以解析 YAML,作者:Andrew Lock:
andrewlock.net/creating-a-custom-iconfigurationprovider-in-asp-net-core-to-parse-yaml/
第三章:第三章:自定义依赖注入
在本章的第三部分,我们将探讨 ASP.NET Core 依赖注入(DI)以及如何根据需要自定义它以使用不同的 DI 容器。
在本章中,我们将涵盖以下主题:
-
使用不同的 DI 容器
-
探索
ConfigureServices方法 -
使用不同的
ServiceProvider -
介绍 Scrutor
本章中的主题涉及 ASP.NET Core 架构的宿主层:

图 3.1 – ASP.NET Core 架构
技术要求
要遵循本章的描述,您需要创建一个 ASP.NET Core MVC 应用程序。打开您的控制台、shell 或 Bash 终端,切换到您的工作目录。使用以下命令创建一个新的 MVC 应用程序:
dotnet new mvc -n DiSample -o DiSample
现在,通过双击项目文件或在 Visual Studio Code 中在已打开的控制台中输入以下命令来在 Visual Studio 中打开项目:
cd DiSample
code .
本章中的所有代码示例都可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Customizing-ASP.NET-Core-6.0-Second-Edition/tree/main/Chapter03。
使用不同的 DI 容器
在大多数项目中,您实际上并不需要使用不同的 DI 容器。ASP.NET Core 中现有的 DI 实现支持主要的基本功能,并且既有效又快速。然而,一些其他 DI 容器支持您可能在应用程序中想要使用的许多有趣功能:
-
使用 Ninject 创建一个支持模块作为轻量级依赖项的应用程序,例如,您可能希望将其放入特定目录并自动在应用程序中注册的模块。
-
在应用程序外部的配置文件中配置服务,在 XML 或 JSON 文件中而不是仅使用 C#。这是各种 DI 容器中的常见功能,但 ASP.NET Core 中尚未支持。
-
在运行时添加服务,可能是因为您不希望有一个不可变的 DI 容器。这也是一些 DI 容器中的常见功能。
现在我们来看看 ConfigureServices 方法如何使您能够使用替代的 DI 容器。
探索 ConfigureServices 方法
让我们比较当前的 ConfigureServices 方法与之前的长期支持版本,看看有什么变化。如果您使用版本 3.1 创建了新的 ASP.NET Core 项目并打开 Startup.cs,您将找到配置服务的函数,如下所示:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user
// consent for non-essential cookies is
// needed for a given request.
options.CheckConsentNeeded = context => true;
});
services.AddControllersWithViews();
services.AddRazorPages();
}
相比之下,在 ASP.NET Core 6.0 中,已经不再有 Startup.cs,服务的配置是在 Program.cs 中完成的,如下所示:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
var app = builder.Build();
// The rest of the file isn't relevant for this chapter
在这两种情况下,方法都会获取 IServiceCollection,它已经包含了一堆 ASP.NET Core 所需的服务。这个服务是由托管服务和在调用 ConfigureServices 方法之前执行的 ASP.NET Core 的部分添加的。
在方法内部,还会添加一些更多的服务。首先,添加一个包含 cookie 策略选项的配置类到 ServiceCollection。之后,AddMvc() 方法添加了 MVC 框架所需的另一堆服务。到目前为止,我们已经将大约 140 个服务注册到了 IServiceCollection。然而,服务集合并不是实际的 DI 容器。
实际的 DI 容器被所谓的 IServiceCollection 包装,并注册了一个扩展方法来从服务集合中创建 IServiceProvider,如下面的代码片段所示:
IServiceProvider provider = services.BuildServiceProvider()
ServiceProvider 包含一个不可变的容器,在运行时无法更改。使用默认的 ConfigureServices 方法,在调用此方法之后,会在后台创建 IServiceProvider。
接下来,我们将学习如何在 DI 自定义过程中应用替代的 ServiceProvider。
使用不同的 ServiceProvider
如果其他容器已经支持 ASP.NET Core,那么切换到不同的或自定义的 DI 容器相对容易。通常,其他容器会使用 IServiceCollection 来填充自己的容器。第三方 DI 容器通过遍历集合将已注册的服务移动到其他容器中:
-
让我们首先使用
Autofac作为第三方容器。在你的命令行中输入以下命令来加载 NuGet 包:Autofac is good for this because you are easily able to see what is happening here. -
要注册自定义的 IoC 容器,你需要注册不同的
IServiceProviderFactory。在这种情况下,如果你使用Autofac,你将想要使用AutofacServiceProviderFactory。IServiceProviderFactory将创建一个ServiceProvider实例。如果第三方容器支持 ASP.NET Core,它应该提供一个。你应该将这个小的扩展方法放在
Program.cs中,以将AutofacServiceProviderFactory注册到IHostBuilder:using Autofac; using Autofac.Extensions.DependencyInjection; namespace DiSample; public static class IHostBuilderExtension { public static IHostBuilder UseAutofacServiceProviderFactory( this IHostBuilder hostbuilder) { hostbuilder.UseServiceProviderFactory <ContainerBuilder>( new AutofacServiceProviderFactory()); return hostbuilder; } }不要忘记添加
Autofac和Autofac.Extensions.DependencyInjection的 using 语句。 -
要使用这个扩展方法,你可以在
Program.cs中使用AutofacServiceProvider:var builder = WebApplication.CreateBuilder(args); builder.Host.UseAutofacServiceProviderFactory(); // Add services to the container. builder.Services.AddControllersWithViews();
这将 AutofacServiceProviderFactory 函数添加到 IHostBuilder 并启用 Autofac IoC 容器。如果你已经设置了它,那么在默认方式下向 IServiceCollection 添加服务时,你将使用 Autofac。
介绍 Scrutor
你并不总是需要替换现有的 .NET Core DI 容器来获取和使用一些酷炫的功能。在本章的开头,我提到了服务的自动注册,这可以通过其他 DI 容器来完成。这也可以通过一个名为 IServiceCollection 的不错的 NuGet 包来实现,该包可以自动将服务注册到 .NET Core DI 容器中。
注意
安德鲁·洛克(Andrew Lock)发布了一篇相当详细的关于 Scrutor 的博客文章。与其重复他的话,我建议您直接阅读那篇文章来了解更多信息:使用 Scrutor 自动将服务注册到 ASP.NET Core DI 容器,可在andrewlock.net/using-scrutor-to-automatically-register-your-services-with-the-asp-net-core-di-container/找到。
摘要
使用本章中展示的方法,您将能够使用任何 .NET Standard 兼容的 DI 容器来替换现有的容器。如果您选择的容器不包含 ServiceProvider,请创建自己的实现 IServiceProvider 并在内部使用 DI 容器。如果您选择的容器不提供填充容器中注册的服务的方法,请创建自己的方法。遍历注册的服务并将它们添加到其他容器中。
实际上,最后一步听起来很简单,但可能是一项艰巨的任务,因为您需要将所有可能的 IServiceCollection 注册转换为其他容器的注册。这项任务的复杂性取决于其他 DI 容器的实现细节。
无论如何,您可以选择使用任何与 .NET Standard 兼容的 DI 容器。您可以在 ASP.NET Core 中更改很多默认实现。
这也是您可以在 Windows 上默认的 HTTPS 行为中完成的事情,我们将在下一章中了解更多。
第四章:第四章:使用 Kestrel 配置和自定义 HTTPS
在 ASP.NET Core 中,HTTPS 默认启用,并且是一个一等特性。在 Windows 上,启用 HTTPS 所需的证书是从 Windows 证书存储中加载的。如果你在 Linux 或 Mac 上创建项目,证书是从证书文件中加载的。
即使你想创建一个在 IIS 或 NGINX 网络服务器后面运行的项目,HTTPS 也会被启用。通常情况下,你会在 IIS 或 NGINX 网络服务器上管理证书。然而,在这里启用 HTTPS 不会成为问题,所以不要在 ASP.NET Core 设置中禁用它。
如果你在防火墙后面运行服务,例如不可从互联网访问的服务,如基于微服务的应用程序的后台服务,或者自托管的 ASP.NET Core 应用程序中的服务,直接在 ASP.NET Core 应用程序中管理证书是有意义的。
在 Windows 上,也有一些场景下从文件中加载证书是有意义的。这可能是你将在 Docker for Windows 或 Linux 上运行的应用程序中。我个人喜欢从文件中灵活加载证书的方式。
本简短章节将涵盖两个主题:
-
介绍 Kestrel
-
设置 Kestrel
本章中的主题涉及 ASP.NET Core 架构的托管层:

图 4.1 – ASP.NET Core 架构
技术要求
要遵循本章的描述,你需要创建一个 ASP.NET Core MVC 应用程序。为此,打开你的控制台、shell 或 Bash 终端,切换到你的工作目录。然后,使用以下命令创建一个新的 MVC 应用程序:
dotnet new mvc -n HttpSample -o HttpSample
现在,通过双击项目文件在 Visual Studio 中打开项目,或在 Visual Studio Code 中通过在已打开的控制台中输入以下命令来打开项目:
cd HttpSample
code .
本章中的所有代码示例都可以在本书的 GitHub 仓库中找到,网址为 github.com/PacktPublishing/Customizing-ASP.NET-Core-6.0-Second-Edition/tree/main/Chapter04。
介绍 Kestrel
Kestrel 是一个新实现的 HTTP 服务器,它是 ASP.NET Core 的托管引擎。每个 ASP.NET Core 应用程序都会在 Kestrel 服务器上运行。经典的 ASP.NET 应用程序(在 .NET Framework 上运行)通常直接在 IIS 上运行。在 ASP.NET Core 中,微软受到了 Node.js 的启发,它也提供了一个名为 libuv 的 HTTP 服务器。在 ASP.NET Core 的第一个版本中,微软也使用了 libuv,然后它添加了一个名为 Kestrel 的层。当时,Node.js 和 ASP.NET Core 共享同一个 HTTP 服务器。
由于.NET Core 框架已经发展,并且在其上实现了.NET 套接字,因此微软基于.NET 套接字构建了自己的 HTTP 服务器,并移除了 libuv,这是一个他们不拥有和控制依赖项。现在,Kestrel 是一个功能齐全的 HTTP 服务器,可以运行 ASP.NET Core 应用程序。
IIS 充当反向代理,将流量转发到 Kestrel 并管理 Kestrel 进程。在 Linux 上,通常使用 NGINX 作为 Kestrel 的反向代理。
设置 Kestrel
正如我们在本书的前两章中所做的那样,我们需要稍微覆盖默认的WebHostBuilder来设置 Kestrel。从 ASP.NET Core 3.0 开始,可以替换默认的 Kestrel 基础配置为自定义配置。这意味着 Kestrel 网络服务器被配置到主机构建器。让我们看看设置的步骤:
-
您只需使用它,就可以手动添加和配置 Kestrel。以下代码展示了在
IwebHostBuilder上调用UseKestrel()方法时会发生什么。现在让我们看看这是如何与CreateWebHostBuilder方法结合的:public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder .UseKestrel(options => { }) .UseStartup<Startup>(); } }上述代码显示了直到 ASP.NET Core 5.0 的
Program.cs看起来如何。在 ASP.NET Core 6.0 中,使用新的最小 API 方法来配置您的应用程序:var builder = WebApplication.CreateBuilder(args); builder.WebHost.UseKestrel(options => { }); // Add services to the container. builder.Services.AddControllersWithViews(); var app = builder.Build(); // the rest of this file is not relevant我们将在本章的剩余部分专注于
UseKestrel()方法。UseKestrel()方法接受一个操作来配置 Kestrel 网络服务器。 -
我们实际上需要做的是配置网络服务器监听的地址和端口。对于 HTTPS 端口,我们还需要配置证书应该如何加载:
builder.WebHost.UseKestrel(options => { options.Listen(IPAddress.Loopback, 5000); options.Listen(IPAddress.Loopback, 5001, listenOptions => { listenOptions.UseHttps("certificate.pfx", "topsecret"); }); });不要忘记添加一个
using语句到System.Net命名空间以解析IPAddress。在这个片段中,我们添加了要监听的地址和端口。配置被定义为使用 HTTPS 的安全端点。
UseHttps()方法被多次重载,以便从 Windows 证书存储以及从文件中加载证书。在这种情况下,我们将使用位于项目文件夹中的名为certificate.pfx的文件。 -
要创建一个证书文件来玩这个配置,请打开证书存储并导出 Visual Studio 创建的开发证书。它位于当前用户证书下的个人证书中:

图 4.2 – 证书
右键单击此条目。在上下文菜单中,转到所有任务并单击导出。在证书导出向导中,单击下一步然后单击是,导出私钥,然后单击下一步。现在,在下一屏幕中选择.PFX格式并单击下一步。在这里,您需要设置一个密码。这就是您在代码中需要使用的密码,如以下代码示例所示。选择一个文件名和一个存储文件的位置,然后单击下一步。最后一个屏幕将显示摘要。单击完成将证书保存到文件。
为了您的安全
只使用以下行来尝试这个配置:
listenOptions.UseHttps("certificate.pfx", "topsecret");
为了阐明原因——问题是硬编码的密码。永远,永远不要将密码存储在会被推送到任何源代码仓库的代码文件中。请确保您从 ASP.NET Core 的配置 API 中加载密码。在您的本地开发机器上使用用户密钥,在服务器上使用环境变量。在 Azure 上,使用应用程序设置来存储密码。如果将它们标记为密码,密码将在 Azure 门户 UI 中被隐藏。
摘要
这只是一个小的定制,但如果您想在不同的平台之间共享代码,或者想在 Docker 上运行应用程序而不想担心证书存储等问题,它应该会有所帮助。
通常,如果您在 IIS 或 NGINX 等网络服务器后面运行应用程序,您不需要关心您的 ASP.NET Core 6.0 应用程序中的证书。然而,如果您在另一个应用程序内部、Docker 上或没有 IIS 或 NGINX 的情况下托管应用程序,您将需要这样做。
在下一章中,我们将讨论如何配置 ASP.NET Core 网络应用程序的托管。
第五章:第五章: 配置 WebHostBuilder
当你在 第四章,使用 Kestrel 配置和自定义 HTTPS 中阅读时,你可能自己问过一个问题:
我如何使用用户密钥来将密码传递给 HTTPS 配置?
你甚至可能会想知道是否可以从 Program.cs 中获取配置。
在本章中,我们将通过以下主题来回答这些问题:
- 重新审视
WebHostBuilderContext
本章中的主题指的是 ASP.NET Core 架构的托管层:

图 5.1 – ASP.NET Core 架构
技术要求
要遵循本章中的描述,你需要创建一个 ASP.NET Core 应用程序。为此,打开你的控制台、shell 或 Bash 终端,切换到你的工作目录。然后,使用以下命令创建一个新的 Web 应用程序:
dotnet new web -n HostBuilderConfig -o HostBuilderConfig
现在通过双击项目文件或在 Visual Studio Code 中在已打开的控制台中输入以下命令来在 Visual Studio 中打开项目:
cd HostBuilderConfig
code .
注意
在 .NET 6.0 中,简单的 web 项目模板发生了变化。在 6.0 版本中,Microsoft 引入了 最小 API 并将项目模板更改为使用最小 API 方法。我将在本章中向您展示这些模板之间的差异。
本章中的所有代码示例都可以在本书的 GitHub 仓库中找到,网址为 github.com/PacktPublishing/Customizing-ASP.NET-Core-6.0-Second-Edition/tree/main/Chapter05。
重新审视 WebHostBuilderContext
记得我们在 第四章,使用 Kestrel 配置和自定义 HTTPS 中查看的 WebHostBuilder Kestrel 配置吗?在那个章节中,我们看到了你应该使用用户密钥来配置证书密码,如下面的代码片段所示:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(
string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder
.UseKestrel(options =>
{
options.Listen(
IPAddress.Loopback,
5000);
options.Listen(
IPAddress.Loopback,
5001,
listenOptions =>
{
listenOptions.UseHttps(
"certificate.pfx",
"topsecret");
});
})
.UseStartup<Startup>();
});
}
}
这个片段对于 .NET 5.0 及之前的版本仍然有效,对于几乎所有的 .NET 6.0 中的 Web 项目也仍然有效。但是,它不适用于我们在技术要求部分使用的 web 项目模板。最小 API 的 Program.cs 文件如下所示:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
这意味着我们在上一章中实现的配置在最小 API 中看起来是这样的:
using System.Net;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseKestrel(options =>
{
options.Listen(IPAddress.Loopback, 5000);
options.Listen(IPAddress.Loopback, 5001,
listenOptions =>
{
listenOptions.UseHttps(
"certificate.pfx",
"topsecret");
});
});
以下适用于 .NET 6.0 及之前版本的所有项目模板。
通常,你无法在这个代码中获取配置。你需要知道 UseKestrel() 方法是重载的,就像你在这里看到的那样:
.UseKestrel((host, options) =>
{
// ...
})
第一个参数是一个 WebHostBuilderContext 实例,你可以使用它来访问配置。所以,让我们稍微修改一下 lambda 表达式来使用这个上下文:
builder.WebHost.UseKestrel((host, options) =>
{
var filename = host.Configuration.GetValue(
"AppSettings:certfilename", "");
var password = host.Configuration.GetValue(
"AppSettings:certpassword", "");
options.Listen(IPAddress.Loopback, 5000);
options.Listen(IPAddress.Loopback, 5001,
listenOptions =>
{
listenOptions.UseHttps(filename, password);
});
});
在此示例中,我们使用冒号分隔符写入密钥,因为这是从 appsettings.json 读取嵌套配置的方式:
{
"AppSettings": {
"certfilename": "certificate.pfx",
"certpassword": "topsecret"
},
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}
重要提示
这只是如何读取配置以配置 Kestrel 的一个示例。请永远不要,永远不要 在代码中存储任何凭据。相反,使用以下概念。
您还可以从用户密钥存储中读取,其中密钥是通过以下 .NET CLI 命令设置的,您需要在项目文件夹中执行这些命令:
dotnet user-secrets init
dotnet user-secrets set "AppSettings:certfilename"
"certificate.pfx"
dotnet user-secrets set "AppSettings:certpassword"
"topsecret"
这也适用于环境变量:
SET APPSETTINGS_CERTFILENAME=certificate.pfx
SET APPSETTINGS_CERTPASSWORD=topsecret
重要提示
由于用户密钥存储仅用于本地开发,因此您应在生产环境中通过环境变量将凭据传递给应用程序,或者类似生产的应用程序。此外,您在 Azure 中创建的应用程序设置配置将作为环境变量传递给您的应用程序。
那么,这一切是如何工作的呢?
它是如何工作的?
您还记得那些需要设置 ASP.NET Core 1.0 中 Startup.cs 文件中的应用程序配置的日子吗?它们是在 StartUp 类的构造函数中配置的,如果您添加了用户密钥,它们看起来类似于此:
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json")
.AddJsonFile($"appsettings.{env.EnvironmentName}.json",
optional: true);
if (env.IsDevelopment())
{
builder.AddUserSecrets();
}
builder.AddEnvironmentVariables();
Configuration = builder.Build();
此代码现在被封装在 CreateDefaultBuilder 方法中(如您在 GitHub 上所见 – 详细信息请参阅 进一步阅读 部分),其外观如下:
builder.ConfigureAppConfiguration((hostingContext, config)
=>
{
var env = hostingContext.HostingEnvironment;
config
.AddJsonFile(
"appsettings.json",
optional: true,
reloadOnChange: true)
.AddJsonFile(
$"appsettings.{env.EnvironmentName}.json",
optional: true,
reloadOnChange: true);
if (env.IsDevelopment())
{
var appAssembly = Assembly.Load(
new AssemblyName(env.ApplicationName));
if (appAssembly != null)
{
config.AddUserSecrets(appAssembly,
optional: true);
}
}
config.AddEnvironmentVariables();
if (args != null)
{
config.AddCommandLine(args);
}
});
这几乎与之前的代码相同,并且是构建 WebHost 时最早执行的事情之一。
这需要是我们设置的第一件事之一,因为 Kestrel 可以通过应用程序配置进行配置。您可以通过使用环境变量或 appsettings.json 来指定端口、URL 等。
您可以在 GitHub 上的 WebHost.cs 文件中找到这些行:
builder.UseKestrel((builderContext, options) =>
{
options.Configure(
builderContext.Configuration.GetSection("Kestrel"));
})
这意味着您可以将这些行添加到 appsettings.json 中来配置 Kestrel 端点:
"Kestrel": {
"EndPoints": {
"Http": {
"Url": "http://localhost:5555"
}
}
}
或者,可以使用以下环境变量来配置端点:
SET KESTREL_ENDPOINTS_HTTP_URL=http://localhost:5555
让我们现在回顾一下本章中我们涵盖的所有内容。
概述
在 Program.cs 中,如果您有权访问 WebHostBuilderContext,则可以在配置方法的 lambda 表达式中进行应用程序配置。这样,您可以使用所有喜欢的配置来配置 WebHostBuilder。
在下一章中,我们将查看托管细节。您将了解不同的托管模型以及如何以不同的方式托管 ASP.NET Core 应用程序。
进一步阅读
ASP.NET GitHub 仓库中的 WebHost.cs 文件:github.com/aspnet/MetaPackages/blob/d417aacd7c0eff202f7860fe1e686aa5beeedad7/src/Microsoft.AspNetCore/WebHost.cs.
第六章:第六章:使用不同的托管模型
在本章中,我们将讨论如何在 ASP.NET Core 中自定义托管。我们将探讨托管选项和不同类型的托管,并简要介绍在 IIS 上的托管。本章只是一个概述。对于每个主题,都有可能进行更深入的探讨,但这将需要一本完整的书来阐述!
在本章中,我们将涵盖以下主题:
-
设置
WebHostBuilder -
设置 Kestrel
-
设置
HTTP.sys -
在 IIS 上托管
-
在 Linux 上使用 Nginx 或 Apache
本章中的主题涉及 ASP.NET Core 架构的托管层:

图 6.1 – ASP.NET Core 架构
本章探讨了以下服务器架构主题:

图 6.2 – ASP.NET 服务器架构
技术要求
对于本章,我们只需要设置一个小型的空 Web 应用程序:
dotnet new web -n ExploreHosting -o ExploreHosting
就这些了。用 Visual Studio Code 打开它:
cd ExploreHosting
code .
Et voilà!一个简单的项目在 Visual Studio Code 中打开。
本章的代码可以在 GitHub 上找到:github.com/PacktPublishing/Customizing-ASP.NET-Core-6.0-Second-Edition/tree/main/Chapter06。
设置 WebHostBuilder
与上一章一样,在本节中我们将重点关注 Program.cs。WebHostBuilder 是我们的朋友。这是配置和创建 Web 服务器的地方。
以下代码片段是使用 .NET CLI 的 dotnet new 命令创建的每个新 ASP.NET Core Web 项目的默认配置:
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
如我们从之前的章节中已经了解到的,默认构建器已经预先配置了所有必要的功能。为了在 Azure 或本地 IIS 上成功运行应用程序,所有需要配置的内容都已为您配置好。
但是,您可以覆盖几乎所有这些默认配置,包括托管配置。
接下来,让我们设置 Kestrel。
设置 Kestrel
在创建 WebHostBuilder 之后,我们可以使用各种功能来配置构建器。在这里,我们可以看到其中之一,它指定了应该使用的 Startup 类。
注意
如在 第四章 中讨论的,使用 Kestrel 配置和自定义 HTTPS,Kestrel 是托管应用程序的一种可能选择。Kestrel 是内置在 .NET 中并基于 .NET 套接字实现的 Web 服务器。之前,它是基于 libuv 构建的,这是 Node.js 使用的相同 Web 服务器。Microsoft 移除了对 libuv 的依赖,并基于 .NET 套接字创建了自己的 Web 服务器实现。
在上一章中,我们看到了 UseKestrel 方法来配置 Kestrel 选项:
.UseKestrel((host, options) =>
{
// ...
})
第一个参数是WebHostBuilderContext,用于访问已配置的托管设置或配置本身。第二个参数是一个用于配置 Kestrel 的对象。这个代码片段显示了我们在上一章中配置套接字端点时所做的工作,主机需要监听这些端点:
builder.WebHost.UseKestrel((host, options) =>
{
var filename = host.Configuration.GetValue(
"AppSettings:certfilename", "");
var password = host.Configuration.GetValue(
"AppSettings:certpassword", "");
options.Listen(IPAddress.Loopback, 5000);
options.Listen(IPAddress.Loopback, 5001,
listenOptions =>
{
listenOptions.UseHttps(filename, password);
});
});
(你可能需要向System.Net添加一个using语句。)
这将覆盖默认配置,你可以传递 URL,例如,使用launchSettings.json的applicationUrl属性或环境变量。
现在,让我们看看如何设置HTTP.sys。
设置 HTTP.sys
另外还有一个托管选项,一个不同的 Web 服务器实现。HTTP.sys是 Windows 内部一个相当成熟的库,可以用来托管你的 ASP.NET Core 应用程序:
.UseHttpSys(options =>
{
// ...
})
HTTP.sys与 Kestrel 不同。它不能用于 IIS,因为它与 IIS 的 ASP.NET Core 模块不兼容。
使用HTTP.sys而不是 Kestrel 的主要原因是在不需要 IIS 的情况下,你需要将你的应用程序暴露于互联网。
注意
IIS 已经在HTTP.sys上运行多年。这意味着UseHttpSys()和 IIS 使用相同的 Web 服务器实现。要了解更多关于HTTP.sys的信息,请阅读文档,相关链接可以在进一步阅读部分找到。
接下来,让我们看看如何使用 IIS 进行托管。
在 IIS 上托管
ASP.NET Core 应用程序不应当直接暴露于互联网,即使它支持 Kestrel 或HTTP.sys。最好在两者之间有一个反向代理,或者至少有一个监控托管进程的服务。对于 ASP.NET Core 来说,IIS 不仅仅是一个反向代理。它还负责托管进程,以防因错误而中断。如果发生这种情况,IIS 将重新启动进程。在 Linux 上,Nginx 可以用作反向代理,同时也负责托管进程。
注意
确保你创建了一个新项目或移除了上一节中 Kestrel 的配置。这不会与 IIS 一起工作。
要在 IIS 或 Azure 上托管 ASP.NET Core Web,你需要先发布它。发布不仅编译项目;它还准备项目在 IIS、Azure 或 Linux 上的 Web 服务器(如 Nginx)上托管。
以下命令将发布项目:
dotnet publish -o ..\published -r win-x64
在系统浏览器中查看时,它应该如下所示:

图 6.3 – .NET 发布文件夹
这会产生一个可以在 IIS 中映射的输出。它还创建了一个web.config文件,用于添加 IIS 或 Azure 的设置。它包含了一个作为 DLL 编译的 Web 应用程序。
如果你发布了一个自包含的应用程序,它也包含了运行时本身。自包含的应用程序会自带.NET Core 运行时,但交付的大小会增加很多。
在 IIS 上?只需创建一个新的 Web,并将其映射到放置发布输出的文件夹:

图 6.4 – .NET 发布对话框
如果您需要更改安全性,如果您有一些数据库连接等,事情会变得稍微复杂一些。这可以是一个单独章节的主题。

图 6.5 – 在浏览器中查看的“Hello World!”
图 6.5显示了示例项目中Program.cs中小的MapGet的输出:
app.MapGet("/", () => "Hello World!");
接下来,我们将讨论一些 Linux 的替代方案。
在 Linux 上使用 Nginx 或 Apache
在 Linux 上发布 ASP.NET Core 应用程序看起来与在 IIS 上看起来非常相似,但为反向代理做准备需要一些额外的步骤。您需要一个像 Nginx 或 Apache 这样的反向代理服务器,它将流量转发到 Kestrel 和 ASP.NET Core 应用程序:
-
首先,您需要允许您的应用程序接受两个特定的转发头。为此,打开
Startup.cs,并在UseAuthentication中间件之前将以下行添加到Configure方法中:app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto }); -
您还需要信任来自反向代理的传入流量。这需要您将以下行添加到
ConfigureServices方法中:Builder.Services.Configure<ForwardedHeadersOptions>( options => { options.KnownProxies.Add( IPAddress.Parse("10.0.0.100")); });您可能需要向
Microsoft.AspNetCore.HttpOverrides添加一个using。 -
在这里添加代理的 IP 地址。这只是一个示例。
-
然后,您需要发布应用程序:
dotnet publish --configuration Release -
将构建输出复制到名为
/var/www/yourapplication的文件夹中。您还应该在 Linux 上通过调用以下命令进行快速测试:dotnet <yourapplication.dll> -
在这里,
yourapplication.dll是编译后的应用程序,包括路径。如果一切正常,您应该能够在http://localhost:5000/上调用您的 Web 应用程序。如果一切正常,应用程序应作为服务运行。这需要您在
/etc/systemd/system/上创建一个服务文件。将文件命名为kestrel-yourapplication.service,并在其中放置以下内容:[Unit] Description=Example .NET Web API App running on Ubuntu [Service] WorkingDirectory=/var/www/yourapplication ExecStart=/usr/bin/dotnet/var/www/yourapplication/yourapplication.dll Restart=always # Restart service after 10 seconds if the dotnet service crashes: RestartSec=10 KillSignal=SIGINT SyslogIdentifier=dotnet-example User=www-data Environment=ASPNETCORE_ENVIRONMENT=Production Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false [Install] WantedBy=multi-user.target确保第 5 行和第 6 行中的路径指向您放置构建输出的文件夹。此文件定义了您的应用程序应在默认端口上作为服务运行。它还监视应用程序,并在它崩溃时重新启动它。它还定义了传递给配置应用程序的环境变量。请参阅第一章,自定义日志记录,了解如何使用环境变量配置您的应用程序。
接下来,我们将看到如何配置 Nginx。
配置 Nginx
现在,您可以使用以下代码告诉 Nginx 要做什么:
server {
listen 80;
server_name example.com *.example.com;
location / {
proxy_pass http://localhost:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Forwarded-For
$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
这告诉 Nginx 将端口80上的调用转发到example.com,以及它的子域名到http://localhost:5000,这是您应用程序的默认地址。
配置 Apache
Apache 的配置看起来与 Nginx 方法非常相似,并在最后做同样的事情:
<VirtualHost *:*>
RequestHeader set "X-Forwarded-Proto
expr=%{REQUEST_SCHEME}
</VirtualHost>
<VirtualHost *:80>
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:5000/
ProxyPassReverse / http://127.0.0.1:5000/
ServerName www.example.com
ServerAlias *.example.com
ErrorLog ${APACHE_LOG_DIR}yourapplication-error.log
CustomLog ${APACHE_LOG_DIR}yourapplication-access.log
common
</VirtualHost>
Nginx 和 Apache 就到这里。现在让我们总结这一章。
摘要
ASP.NET Core 和 .NET CLI 已经包含了所有工具,可以将它们部署到各种平台,并设置好以准备在 Azure、IIS 以及 Nginx 上运行。这非常简单,在文档中有详细描述。
目前,我们拥有 WebHostBuilder,它可以创建应用程序的托管环境。在 3.0 版本中,我们有了 HostBuilder,它能够创建一个完全独立于任何 Web 上下文的托管环境。
ASP.NET Core 6.0 具有在应用程序内部运行后台任务的功能。要了解更多信息,请阅读下一章。
进一步阅读
更多信息,您可以参考以下链接:
-
Kestrel 文档:
docs.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel?view=aspnetcore-6.0 -
HTTP.sys 文档:
docs.microsoft.com/en-us/aspnet/core/fundamentals/servers/httpsys?view=aspnetcore-6.0 -
ASP.NET Core:
docs.microsoft.com/en-us/aspnet/core/host-and-deploy/aspnet-core-module?view=aspnetcore-6.0
第七章:第七章:使用 IHostedService 和 BackgroundService
这第七章并不是真的关于定制;它更多的是关于你可以用来创建后台服务以在应用程序内部异步运行任务的特性。我在一个小型的 ASP.NET Core 应用程序中定期从远程服务获取数据时使用了这个特性。
我们将探讨以下主题:
-
介绍
IHostedService -
介绍
BackgroundService -
实现新的 Worker Service 项目
本章的主题涉及 ASP.NET Core 架构的宿主层:

图 7.1 – ASP.NET Core 架构
技术要求
要遵循本章的描述,你需要创建一个 ASP.NET Core 应用程序。打开你的控制台、shell 或 Bash 终端,切换到你的工作目录。使用以下命令创建一个新的 MVC 应用程序:
dotnet new mvc -n HostedServiceSample -o HostedServiceSample
现在通过双击项目文件或在 VS Code 中更改文件夹到项目,并在已经打开的控制台中输入以下命令来在 Visual Studio 中打开项目:
cd HostedServiceSample
code .
本章中所有的代码示例都可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Customizing-ASP.NET-Core-6.0-Second-Edition/tree/main/Chapter07.
介绍 IHostedService
自从 ASP.NET Core 2.0 以来,托管服务一直是一个东西,可以用来在应用程序的后台异步运行任务。它们可以用来定期获取数据,后台进行一些计算,或者进行一些清理。你还可以使用它们来发送预配置的电子邮件——或者你需要在后台做的任何事情。
托管服务基本上是实现了 IHostedService 接口的简单类。你可以用以下代码调用它们:
public class SampleHostedService : IHostedService
{
public Task StartAsync(CancellationToken
cancellationToken)
{
}
public Task StopAsync(CancellationToken
cancellationToken)
{
}
}
IHostedService 需要实现一个 StartAsync() 方法和一个 StopAsync() 方法。StartAsync() 方法是执行逻辑的地方。该方法在应用程序启动后立即执行一次。另一方面,StopAsync() 方法在应用程序停止前执行。这也意味着要启动一个计划中的服务,你需要自己实现它。你需要实现一个循环,定期执行代码。
要执行一个 IHostedService,你需要将其注册到 ASP.NET Core 依赖注入容器中作为一个单例实例:
builder.Services.AddSingleton<IHostedService, SampleHostedService>();
下一个示例展示了托管服务的工作原理。它在启动时、停止时以及每 2 秒向控制台写入一条日志消息:
-
首先,编写一个通过
DependencyInjection获取ILogger的类骨架:namespace HostedServiceSample; public class SampleHostedService : IHostedService { private readonly ILogger<SampleHostedService> logger; // inject a logger public SampleHostedService(ILogger<SampleHostedService> logger) { this.logger = logger; } public Task StartAsync(CancellationToken cancellationToken) { } public Task StopAsync(CancellationToken cancellationToken) { } } -
下一步是实现
StopAsync方法。该方法用于清理,以防你需要关闭连接、流等:public Task StopAsync(CancellationToken cancellationToken) { logger.LogInformation("Hosted service stopping"); return Task.CompletedTask; } -
实际工作将在
StartAsync方法中完成:public Task StartAsync(CancellationToken cancellationToken) { logger.LogInformation("Hosted service starting"); return Task.Factory.StartNew(async () => { // loop until a cancelation is requested while (!cancellationToken.IsCancellationRequested) { logger.LogInformation($"Hosted service executing - {DateTime.Now}"); try { // wait for 2 seconds await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken); } catch (OperationCanceledException) { } } }, cancellationToken); } -
为了测试这一点,请在控制台中调用以下命令来启动应用程序:
dotnet run或者,在 Visual Studio 或 VS Code 中按F5。这将产生以下控制台输出:

图 7.2 – dotnet run 输出的截图
如您所见,日志输出每 2 秒写入控制台。
在下一节中,我们将探讨BackgroundService。
介绍 BackgroundService
BackgroundService类是在 ASP.NET Core 3.0 中引入的,它基本上是一个实现IHostedService接口的抽象类。它还提供了一个名为ExecuteAsync()的抽象方法,该方法返回一个Task。
如果您想重用上一节中的托管服务,则需要重写代码。按照以下步骤学习如何操作:
-
首先,编写通过
DependencyInjection检索ILogger的类骨架:namespace HostedServiceSample; public class SampleBackgroundService : BackgroundService { private readonly ILogger<SampleHostedService> logger; // inject a logger public SampleBackgroundService( ILogger<SampleHostedService> logger) { this.logger = logger; } } -
下一步将是重写
StopAsync方法:public override async Task StopAsync(CancellationToken cancellationToken) { logger.LogInformation("Background service stopping"); await Task.CompletedTask; } -
在最后一步,我们将重写
ExecuteAsync方法,该方法执行所有工作:protected override async Task ExecuteAsync(CancellationToken cancellationToken) { logger.LogInformation("Background service starting"); await Task.Factory.StartNew(async () => { while (!cancellationToken.IsCancellationRequested) { logger.LogInformation($"Background service executing - {DateTime.Now}"); try { await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken); } catch (OperationCanceledException) {} } }, cancellationToken); }甚至注册也是新的。
此外,在 ASP.NET Core 3.0 及以后版本中,ServiceCollection有一个新的扩展方法来注册托管服务或后台工作线程:
builder.Services.AddHostedService<SampleBackgroundService>();
为了测试这一点,请在控制台中调用以下命令来启动应用程序:
dotnet run
或者,在 Visual Studio 或 VS Code 中按F5。它应该显示与您在上一节中创建的SampleHostedService几乎相同的输出。
接下来,让我们看看 Worker Service 项目。
实现新的 Worker Service 项目
在 ASP.NET Core 3.0 及以后版本中,工作服务和通用托管使得创建简单的服务类应用程序变得非常容易,这些应用程序可以做一些事情,而不需要完整的 ASP.NET 堆栈 – 以及不需要 Web 服务器。
您可以使用以下命令创建此项目:
dotnet new worker -n BackgroundServiceSample -o BackgroundServiceSample
基本上,这会创建一个包含Program.cs和Worker.cs文件的控制台应用程序。Worker.cs文件包含继承自BackgroundService类的Worker类。在 ASP.NET 5.0 及更早版本中,Program.cs文件看起来与我们在 ASP.NET Core 的早期版本中看到的样子非常相似,但没有WebHostBuilder:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[]
args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<Worker>();
});
}
在 ASP.NET Core 6.0 中,Program.cs与最小 API 一样简化了。它看起来像这样:
using BackgroundServiceSample;
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddHostedService<Worker>();
})
.Build();
await host.RunAsync();
这创建了一个启用依赖注入的IHost。这意味着我们可以在任何类型的.NET Core 应用程序中使用依赖注入,而不仅仅是 ASP.NET Core 应用程序。
然后将工作线程添加到服务集合中。
这有什么用?您可以将此应用程序作为 Windows 服务运行,或者作为 Docker 容器中的后台应用程序运行,该容器不需要 HTTP 端点。
摘要
您现在可以使用IHostedService和BackgroundService开始做一些更复杂的事情。请注意,后台服务因为它们都在同一个应用程序中运行;如果您使用过多的 CPU 或内存,这可能会减慢您的应用程序。
对于更大的应用程序,我建议在一个专门用于执行后台任务的应用程序中运行此类任务:一个单独的 Docker 容器、Azure 上的BackgroundWorker、Azure Functions 或类似的东西。然而,在这种情况下,它应该与主应用程序分开。
在下一章中,我们将学习中间件,以及如何使用它们在请求管道上实现特殊逻辑或在不同的路径上提供特定逻辑。
第八章:第八章:编写自定义中间件
哇,我们这本书已经进入了第八章!在本章中,我们将学习关于 中间件 的知识以及如何使用它来进一步定制你的应用程序。我们将快速浏览中间件的基础知识,然后我们会探索一些你可以用它做的特别事情。
在本章中,我们将涵盖以下主题:
-
介绍中间件
-
编写自定义中间件
-
探索中间件的潜力
-
在 ASP.NET Core 3.0 及更高版本中使用中间件
本章涉及的内容与 ASP.NET Core 架构的中间件层相关:

图 8.1 – ASP.NET Core 架构
技术要求
要跟随本章的描述,你需要创建一个 ASP.NET Core MVC 应用程序。为此,打开你的控制台、shell 或 Bash 终端,切换到你的工作目录。然后,使用以下命令创建一个新的 MVC 应用程序:
dotnet new web -n MiddlewaresSample -o MiddlewaresSample
现在,通过双击项目文件在 Visual Studio 中打开项目,或者在 Visual Studio Code 中,在已经打开的控制台中输入以下命令:
cd MiddlewaresSample
code .
注意
.NET 6.0 中的简单 web 项目模板发生了变化。在 6.0 版本中,Microsoft 引入了 最小 API 并将项目模板更改为使用最小 API 方法。这是一种更简单的方式来引导和开始一个 Web 应用程序。
本章中所有的代码示例都可以在本书的 GitHub 仓库中找到,网址为 github.com/PacktPublishing/Customizing-ASP.NET-Core-6.0-Second-Edition/tree/main/Chapter08。
介绍中间件
大多数人可能已经知道什么是中间件,但有些人可能不知道。即使你已经使用 ASP.NET Core 有一段时间了,你也不必详细了解中间件实例,因为它们大多隐藏在诸如 UseMvc()、UseAuthentication()、UseDeveloperExceptionPage() 等命名良好的扩展方法之后。每次你在 Startup.cs 文件中的 Configure 方法中调用 Use 方法时,你都会隐式地使用至少一个——或者可能更多——中间件组件。
中间件组件是一段处理请求管道的代码。想象一下请求管道就像一个巨大的管子,你可以在里面调用某个东西,然后会有回声。中间件负责产生这个回声——它操纵声音以丰富信息,处理原始声音,或者处理回声。
中间件组件按照它们配置的顺序执行。首先配置的中间件组件是第一个被执行的。
在 ASP.NET Core Web 应用程序中,如果客户端请求一个图像或任何其他静态文件,StaticFileMiddleware会搜索该资源,如果找到,则返回该资源。如果没有找到,这个中间件组件除了调用下一个中间件外,不做任何事情。如果没有处理请求管道的最终中间件,请求将返回空。MvcMiddleware组件也会检查请求的资源,尝试将其映射到配置的路由,执行控制器,创建视图,并返回 HTML 或 Web API 结果。如果MvcMiddleware找不到匹配的控制器,它仍然会返回一个结果——在这种情况下,是一个404状态结果。所以,在任何情况下,它都会返回一个回声。这就是为什么MvcMiddleware是配置的最后一个中间件组件。

图 8.2 – 中间件工作流程图
500状态:
-
如果你创建一个空的 ASP.NET Core 应用程序,如技术要求部分所述,你将能够看到管道是如何执行的。
-
使用你喜欢的编辑器打开
Program.cs。与常规 ASP.NET Core 应用程序相比,这应该相当小。在 ASP.NET Core 6.0 中,Microsoft 引入了最小 API 方法,它简化了应用程序配置,并隐藏了大量的默认配置,不让开发者看到。Microsoft 还在 ASP.NET Core 中实现了默认的using语句。正因为如此,你最初看不到任何using语句。这是 ASP.NET Core 6.0 中Program.cs的样貌:var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapGet("/", () => "Hello World!"); app.Run();在这里,特殊的 lambda 中间件绑定到默认路由,并且只将
"Hello World!"写入响应流。响应流是我们之前学过的回声。这个特殊的中间件停止了管道,并返回类似回声的东西。因此,它是最后运行的中间件。 -
将调用
app.MapGet()的行替换为以下代码,在app.Run()函数之前:app.Use(async (context, next) =>{ await context.Response.WriteAsync("==="); await next(); await context.Response.WriteAsync("==="); }); app.Use(async (context, next) => { await context.Response.WriteAsync(">>>>>> "); await next(); await context.Response.WriteAsync(" <<<<<<"); }); app.Run(async context => { await context.Response.WriteAsync("Hello World!"); });这两个
app.Use()调用也创建了两个 lambda 中间件,但这次,除了处理特定请求外,中间件组件还会调用它们的后续者:每个中间件组件都知道应该跟随哪个中间件组件,因此会调用它。app.Run()的调用替换了app.MapGet()的调用,但它们基本上做的是同一件事,只是app.Run()直接写入响应流。使用app.Use()创建的 lambda 中间件在调用下一个中间件之前和之后写入响应流。在调用下一个中间件之前,实际请求被处理,在调用下一个中间件之后,响应(回声)被处理。这应该展示了管道是如何工作的。 -
如果你现在运行应用程序(使用
dotnet run),并在浏览器中打开显示的 URL,你应该看到如下所示的纯文本结果:===>>>>>> Hello World! <<<<<<===
这对你来说有道理吗?如果有,让我们继续看看如何使用这个概念为请求管道添加一些额外的功能。
编写自定义中间件
ASP.NET Core 基于中间件。在请求过程中执行的所有逻辑都基于中间件。因此,我们可以使用它来向 Web 添加自定义功能。在以下过程中,我们想要找出每个通过请求管道的请求的执行时间:
-
我们可以通过在调用下一个中间件之前创建并启动一个计时器,并在调用下一个中间件之后停止测量执行时间来实现这一点,如下所示:
app.Use(async (context, next) => { var s = new Stopwatch(); s.Start(); // execute the rest of the pipeline await next(); s.Stop(); //stop measuring var result = s.ElapsedMilliseconds; // write out the milliseconds needed await context.Response.WriteAsync($" Time needed: {result} milliseconds"); });你可能需要添加一个
using语句来引用System.Diagnostics。之后,我们将经过的毫秒数返回到响应流中。
-
如果你编写了更多的中间件组件,
Program.cs中的配置会变得相当混乱。这就是为什么大多数中间件组件都是作为单独的类编写的。这可能看起来像这样:using System.Diagnostics; public class StopwatchMiddleware { private readonly RequestDelegate _next; public StopwatchMiddleware(RequestDelegate next) { _next = next; } public async Task Invoke(HttpContext context) { var s = new Stopwatch(); s.Start(); // execute the rest of the pipeline await _next(context); s.Stop(); //stop measuring var result = s.ElapsedMilliseconds; // write out the milliseconds needed await context.Response.WriteAsync($" Time needed: {result} milliseconds"); } }这样,我们通过构造函数和
Invoke()方法中的当前上下文来获取下一个中间件组件执行。注意
中间件在应用程序启动时初始化,构造函数在整个应用程序生命周期中只运行一次。另一方面,
Invoke()方法对每个请求只调用一次。 -
要使用此中间件,有一个通用的
UseMiddleware()方法可用,你可以使用它:app.UseMiddleware<StopwatchMiddleware>(); -
然而,更优雅的方法是创建一个
extension方法来封装这个调用:public static class StopwatchMiddlewareExtension { public static IApplicationBuilder UseStopwatch(this IApplicationBuilder app) { app.UseMiddleware<StopwatchMiddleware>(); return app; } } -
现在,你可以简单地这样调用它:
app.UseStopwatch();
这样,你就可以通过请求管道为 ASP.NET Core 应用程序提供额外的功能。在你的中间件中,你可以使用整个HttpContext来操作请求或甚至响应。
例如,AuthenticationMiddleware试图从请求中收集用户信息。如果没有找到任何信息,它将通过发送特定的响应回客户端来请求信息。如果找到了一些信息,它将通过这种方式将其添加到请求上下文中,并使其对整个应用程序可用。
探索中间件的潜力
你可以用中间件做很多事情。例如,你知道你可以将请求管道分成两个或更多管道吗?在本节中,我们将探讨如何做到这一点以及其他几个方面。
使用/map分支管道
下面的代码片段展示了如何根据特定的路径创建请求管道的分支:
app.Map("/map1", app1 =>
{
// some more Middleware
app1.Run(async context =>
{
await context.Response.WriteAsync("Map Test 1");
});
});
app.Map("/map2", app2 =>
{
// some more Middleware
app2.Run(async context =>
{
await context.Response.WriteAsync("Map Test 2");
});
});
// some more Middleware
/map1路径是一个特定的分支,它继续在内部扩展请求管道——这与/map2路径相同。两个映射都有自己的中间件配置在内。所有其他未指定的路径都将遵循主分支。
使用MapWhen()分支管道
此外,还有一个MapWhen()方法可以根据条件分支管道,而不是基于路径的分支:
public void Configure(IApplicationBuilder app)
{
app.MapWhen(
context =>
context.Request.Query.ContainsKey("branch"),
app1 =>
{
// some more Middleware
app1.Run(async context =>
{
await context.Response.WriteAsync(
"MapBranch Test");
});
});
// some more Middleware
app.Run(async context =>
{
await context.Response.WriteAsync(
"Hello from non-Map delegate.");
});
}
接下来,我们将看看如何使用中间件来创建条件。
使用中间件创建条件
您可以根据配置值创建条件,或者,如这里所示,根据请求上下文属性创建条件。在之前的示例中,使用了查询字符串属性。您可以使用 HTTP 头、表单属性或请求上下文的任何其他属性。
您还可以嵌套映射以创建所需的子分支和孙分支。
我们可以使用 Map() 或 MapWhen() 分别提供基于特定路径或特定条件的特殊 API 或资源。ASP.NET Core 的 HealthCheck API 就是这样的:首先,它使用 MapWhen() 来指定要使用的端口,然后,它使用 Map() 来设置 HealthCheck API 的路径(或者,如果没有指定端口,则使用 Map())。最后,使用 HealthCheckMiddleware。以下代码只是一个示例,以展示其外观:
private static void UseHealthChecksCore(IApplicationBuilder
app, PathString path, int? port, object[] args)
{
if (port == null)
{
app.Map(path,
b =>
b.UseMiddleware<HealthCheckMiddleware>(args));
}
else
{
app.MapWhen(
c => c.Connection.LocalPort == port,
b0 => b0.Map(path,
b1 =>
b1.UseMiddleware<HealthCheckMiddleware>(args)
)
);
};
}
接下来,让我们看看如何在 ASP.NET Core 更新版本中使用终止中间件组件。
在 ASP.NET Core 3.0 及以后的版本中使用中间件
在 ASP.NET Core 3.0 及以后的版本中,有两种新的中间件元素,它们被称为 UseRouting 和 UseEndpoints:
public void Configure(IApplicationBuilder app,
IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Hello
World!");
});
});
}
第一个是一个使用路由的中间件组件,另一个使用端点。那么,我们到底在关注什么呢?
这就是新的 端点路由。之前,路由是 MVC 的一部分,并且它只与 MVC、Web API 和基于 MVC 框架的框架一起工作。然而,在 ASP.NET Core 3.0 及以后的版本中,路由不再在 MVC 框架中。现在,MVC 和其他框架被映射到特定的路由或端点。有不同类型的端点定义可用。
在前面的代码片段中,将 GET 请求映射到页面根 URL。在下一个代码片段中,MVC 被映射到路由模式,而 Razor Pages 被映射到基于 Razor Pages 特定文件结构的路由:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
已经没有 UseMvc() 方法了,即使它仍然存在于 IApplicationBuilder 对象级别并且仍然可以工作,以防止现有代码损坏。现在,有新的方法可以更细致地激活 ASP.NET Core 功能。
这些是 ASP.NET Core 5.0 或更高版本中最常用的 Map 方法:
-
endpoints.MapAreaControllerRoute(...); -
endpoints.MapControllerRoute(...); -
endpoints.MapBlazorHub(...); -
endpoints.MapHub(...); -
endpoints.MapRazorPages(...); -
endpoints.MapHealthChecks(...);
有许多更多方法来定义回退端点、将路由和 HTTP 方法映射到委托,以及用于中间件组件。
如果您想创建适用于所有请求的中间件,例如 StopWatchMiddleware,它将在 IApplicationBuilder 上像以前一样工作。如果您想编写适用于特定路径或路由的中间件,您需要为它创建一个 Map 方法来将其映射到该路由。
重要提示
现在不再建议在中间件内部处理路由。相反,你应该使用新的端点路由。这种方法使得中间件更加通用,并且它将使用单个配置在多个路由上工作。
我最近编写了一个中间件,在 ASP.NET Core 应用程序中提供 GraphQL 端点。然而,我不得不重写它以遵循新的 ASP.NET Core 路由。旧的方法仍然可以工作,但它将路径和路由与新的 ASP.NET Core 路由分开处理。让我们看看如何处理这些情况。
重写终止中间件以满足当前标准
如果你有一个提供不同端点的现有中间件,你应该将其更改为使用新的端点路由:
-
例如,让我们创建一个小型的、模拟的中间件,它将应用程序状态写入特定的路由。在这个例子中,没有自定义路由处理:
namespace MiddlewaresSample; public class AppStatusMiddleware { private readonly RequestDelegate _next; private readonly string _status; public AppStatusMiddleware( RequestDelegate next, string status) { _next = next; _status = status; } public async Task Invoke(HttpContext context) { await context.Response.WriteAsync( $"Hello {_status}!"); } }我们需要做的第一件事是在
IEndpointRouteBuilder对象上编写一个extension方法。这个方法有一个作为可选参数的路由模式,并返回一个IEndpointConventionBuilder对象,以便为路由启用 跨源资源共享(CORS)、身份验证或其他条件。 -
现在,我们应该添加一个扩展方法来简化中间件的使用:
public static class MapAppStatusMiddlewareExtension { public static IEndpointConventionBuilder MapAppStatus( this IEndpointRouteBuilder routes, string pattern = "/", string name = "World") { var pipeline = routes .CreateApplicationBuilder() .UseMiddleware<AppStatusMiddleware>(name) .Build(); return routes.Map(pattern, pipeline) .WithDisplayName("AppStatusMiddleware"); } } -
完成之后,我们可以使用
MapAppStatus方法将其映射到特定的路由:app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapGet("/", () => "Hello World!"); endpoints.MapAppStatus("/status", "Status"); }); -
现在,我们可以在浏览器中通过输入以下地址来调用该路由:
http://localhost:5000/status。
我们将在 第九章 中学习更多关于端点路由以及如何自定义它,与端点路由一起工作。现在,让我们回顾一下关于中间件我们已经学到了什么。
摘要
大多数 ASP.NET Core 功能都是基于中间件的,在本章中,你学习了中间件的工作原理以及如何创建自己的中间件组件以扩展 ASP.NET 框架。你还学习了如何使用新的路由向自己的自定义终止中间件组件添加路由。
在下一章中,我们将探讨 ASP.NET Core 中的新端点路由,它允许你以简单灵活的方式创建自己的托管端点。
第九章:第九章:使用端点路由
在本章中,我们将讨论ASP.NET Core中的新端点路由。我们将学习端点路由是什么,它是如何工作的,它在哪些地方被使用,以及你如何创建自己的路由到自己的端点。
在本章中,我们将涵盖以下主题:
-
探索端点路由
-
创建自定义端点
-
创建一个更复杂的端点
本章中的主题涉及 ASP.NET Core 架构的路由层:

图 9.1 – ASP.NET Core 架构
技术要求
对于这个系列,我们只需要设置一个小型的空 Web 应用程序:
dotnet new mvc -n RoutingSample -o RoutingSample
就这些了!使用 Visual Studio Code 打开应用程序:
cd RoutingSample
code .
本章中的所有代码示例都可以在本书的GitHub仓库中找到:github.com/PacktPublishing/Customizing-ASP.NET-Core-6.0-Second-Edition/tree/main/Chapter09。
探索端点路由
要了解端点路由,你需要了解什么是端点以及什么是路由。
端点是应用程序的一部分,当路由将进入的请求映射到它时,它会被执行。让我们更详细地分析这个定义。
客户端通常从服务器请求资源。在大多数情况下,客户端是浏览器。资源由 URL 定义,指向特定的目标。在大多数情况下,目标是网页。它也可能是一个请求特定数据的移动应用,该数据来自 JSON Web API。应用请求的数据在 URL 中定义。
这意味着进入的请求也由 URL 定义。另一方面,正在执行的端点映射到特定的路由。路由是一个 URL 或 URL 的模式。ASP.NET Core 开发者已经熟悉这样的路由模式:
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
如果路由或路由模式与进入请求的 URL 匹配,请求就会被映射到该端点。在这种情况下,请求被映射到 MVC 端点。
ASP.NET Core 可以映射到以下端点:
-
控制器(例如,MVC 或 Web API)
-
Razor Pages
-
SignalR(和 Blazor Server)
-
gRPC 服务
-
健康检查
大多数端点都有非常简单的路由模式。只有 MVC 和 Web API 端点使用更复杂的模式。Razor 页面的路由定义基于实际页面的文件夹和文件结构。
在 ASP.NET Core 2.2 引入端点之前,路由仅在 MVC 和 Web API 中存在。Razor Pages中的隐式路由是内置的,而 SignalR 还没有准备好。那时,Blazor 和 gRPC 还没有出现,健康检查最初被实现为一个中间件组件。
端点路由被引入以将路由与实际端点分离。这使得框架更加灵活,并且意味着新的端点不需要实现自己的路由方式。这样,端点可以使用现有的灵活路由技术来映射到特定的路由。
接下来,我们将看到如何创建自己的自定义端点。
创建自定义端点
创建端点最简单的方法是使用基于 lambda 的端点:
app.Map("/map", async context =>
{
await context.Response.WriteAsync("OK");
});
这将 /map 路由映射到一个简单的端点,该端点将单词 "OK" 写入响应流。
关于先前 .NET 6.0 版本的一则说明
在 .NET 6.0 之前,你会在 Startup.cs 文件中传递给 UseEndpoints 方法的 lambda 中定义的 endpoints 对象上映射自定义端点。在 .NET 6.0 及 Program.cs 文件中的新 app 对象中。
你可能需要将 Microsoft.AspNetCore.Http 命名空间添加到 using 语句中。
你还可以将特定的 HTTP 方法(如 GET、POST、PUT 和 DELETE)映射到一个端点。以下代码展示了如何映射 GET 和 POST 方法:
app.MapGet("/mapget", async context =>
{
await context.Response.WriteAsync("Map GET");
});
app.MapPost("/mappost", async context =>
{
await context.Response.WriteAsync("Map POST");
});
我们还可以将两个或多个 HTTP 方法映射到一个端点:
app.MapMethods(
"/mapmethods",
new[] { "DELETE", "PUT" },
async context =>
{
await context.Response.WriteAsync("Map Methods");
});
这些端点看起来像我们在 第八章 编写自定义中间件 中看到的基于 lambda 的终止中间件组件。这些是终止管道并返回结果的中间件组件,例如基于 HTML 的视图、JSON 结构化数据或类似内容。端点路由是创建输出的更灵活方式,并且应该从 ASP.NET Core 3.0 及以后的版本开始使用。
在 第八章 编写自定义中间件 中,我们看到了我们可以像这样分支管道:
app.Map("/map", mapped =>
{
// some more Middlewares
});
这也创建了一个路由,但这个路由只会监听以 /map 开头的 URL。如果你希望有一个能够处理类似 /map/{id:int?} 的模式的路由引擎,同时匹配 /map/456 而不是 /map/abc,你应该使用新的路由,就像在本节前面所展示的那样。
这些基于 lambda 的端点对于简单场景很有用。然而,由于它们定义在 Program.cs 中,如果你开始使用这种基于 lambda 的方法实现更复杂的场景,事情会很快变得混乱。
因此,我们应该尝试找到一种更结构化的方式来创建自定义端点。
创建更复杂的端点
在本节中,我们将逐步创建一个更复杂的端点。让我们通过编写一个非常简单的健康检查端点来实现,这类似于如果你在 Kubernetes 集群内部运行应用程序或只是告诉他人你的健康状态时可能需要的情况:
-
微软建议从定义 API 开始,从开发者的角度添加端点。我们在这里也是这样做。这意味着我们首先会添加一个
MapSomething方法,但不包含实际实现。这将是一个在IEndpointRouteBuilder对象上的扩展方法。我们将称之为MapMyHealthChecks:// the new endpoint app.MapMyHealthChecks("/myhealth"); app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");新的端点应该以与预构建端点相同的方式添加,以免混淆需要使用它的开发者。
现在我们已经知道了这个方法应该是什么样子,让我们来实施它。
-
创建一个新的静态类
MapMyHealthChecksExtensions,并在MapMyHealthChecks对象内部放置一个扩展方法,该扩展方法扩展IEndpointRouteBuilder并返回一个IEndpointConventionBuilder对象。我将其放置在MapMyHealthChecksExtensions.cs文件中:namespace RoutingSample; public static class MapMyHealthChecksExtensions { public static IEndpointConventionBuilder MapMyHealthChecks ( this IEndpointRouteBuilder endpoints, string pattern = "/myhealth") { // ... } }这只是一个框架。让我们先从实际的端点开始,然后再使用它。
-
实际的端点将被编写为一个终止中间件组件——也就是说,一个不调用下一个组件的中间件组件(参见第八章,编写自定义中间件)并创建输出到响应流:
namespace RoutingSample; public class MyHealthChecksMiddleware { private readonly ILogger<MyHealthChecksMiddleware> _logger; public MyHealthChecksMiddleware ( RequestDelegate next, ILogger<MyHealthChecksMiddleware> logger) { _logger = logger; } public async Task Invoke(HttpContext context) { // add some checks here... context.Response.StatusCode = 200; context.Response.ContentType = "text/plain"; await context.Response.WriteAsync("OK"); } }实际的工作是在
Invoke方法中完成的。目前,这实际上并没有做更多的事情,只是以纯文本形式响应OK和200HTTP 状态,如果你只是想显示你的应用程序正在运行,这是完全可以接受的。你可以自由地扩展这个方法,添加实际的检查,例如检查数据库或相关服务的可用性,例如。然后,你需要更改 HTTP 状态和与检查结果相关的输出。让我们使用这个终止中间件。
-
让我们回到
MapMyHealthChecks方法的框架。我们现在需要创建自己的管道,并将其映射到给定的路由。将以下行放置在该方法中:var pipeline = endpoints .CreateApplicationBuilder() .UseMiddleware<MyHealthChecksMiddleware>() .Build(); return endpoints.Map(pattern, pipeline) .WithDisplayName("My custom health checks"); -
这种方法允许你只为这个新的管道添加一些额外的中间件。
WithDisplayName扩展方法将配置的显示名称设置为端点。 -
就这样!在你的 IDE 中按F5启动应用程序,并在浏览器中调用
https://localhost:7111/myhealth。你应该在浏览器中看到OK:

图 9.2 – 端点路由输出的截图
请注意端口号可能不同。你还可以将现有的终止中间件组件转换为路由端点,以获得更灵活的路由。这就是本章的全部内容!
摘要
ASP.NET Core 知道许多处理请求和向请求客户端提供信息的方式。端点路由是一种基于请求的 URL 和请求方法提供资源的方式。
在本章中,您学习了如何使用终止中间件组件作为端点,并将其映射到新的路由引擎,以实现更高的灵活性,匹配您希望用于向请求客户端提供信息的路由。
每个网络应用程序都需要了解其用户,以便允许或限制对应用程序特定区域或特定数据的访问。在下一章中,我们将展示如何配置身份验证以识别您的用户。
第十章:第十章: 自定义 ASP.NET Core Identity
在本章中,我们将学习如何自定义 ASP.NET Core Identity。安全性是应用程序最重要的方面之一。Microsoft 将 ASP.NET Core Identity 作为 ASP.NET Core 框架的一部分提供,以向 ASP.NET Core 应用程序添加身份验证和用户管理。
在本章中,您将学习如何自定义 ASP.NET Core Identity 的基本实现,以及如何向 IdentityUser 添加自定义信息。我们将涵盖以下内容:
-
介绍 ASP.Net Core Identity
-
自定义 IdentityUser
-
自定义身份视图
本章的主题与 ASP.NET Core 架构的 MVC 层相关:

图 10.1 – ASP.NET Core 架构
技术要求
要跟随本章的练习,您需要创建一个 ASP.NET Core MVC 应用程序。打开您的控制台、shell 或 Bash 终端,切换到您的工作目录。使用以下命令创建一个新的 MVC 应用程序:
dotnet new mvc -n AuthSample -o AuthSample --auth Individual
现在,通过双击项目文件在 Visual Studio 中打开项目,或者通过在已打开的控制台中输入以下命令在 Visual Studio Code 中打开它:
cd AuthSample
code .
本章的所有代码示例都可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Customizing-ASP.NET-Core-6.0-Second-Edition/tree/main/Chapter10。
介绍 ASP.NET Core Identity
一个 writer 告诉应用程序身份允许写入某些内容。身份也可以嵌套。用户可以是某个组的成员,而组也可以是另一个组的成员,依此类推。
ASP.NET Core Identity 是一个框架,它将此概念结构化为 .NET 对象,以帮助您存储和读取用户信息。该框架还提供了一个机制来添加登录表单、注册表单、会话处理等。它还帮助您以加密和安全的方式存储凭据。
ASP.NET Core Identity 提供了多种方式来验证您的用户:
-
个人: 应用程序自行管理身份。它有一个数据库,其中存储用户信息,并自行管理登录、注销、注册等。
-
个人 B2C: 自行管理用户数据,但从中获取 Azure B2C 的数据。
-
单组织: 身份由 Azure 活动目录(AD)管理;登录、注销等由 Azure AD 完成。应用程序只需从网络服务器获取一个现成的身份。
-
多组织: 与之前相同,但支持多个 Azure AD 组织。
-
Windows: 这意味着传统的 Windows 身份验证,仅在应用程序与 IIS 一起托管时才可用。用户还可以从网络服务器获得一个现成的身份。
本章不是关于不同身份验证方式的内容,因为这个主题可以填满整本书。
让我们探索一个启用了个人身份验证的应用程序。
如您可能从技术要求中记得的那样,--auth 标志用于创建应用程序。它设置为 Individual 以创建一个启用了个人身份验证的 ASP.NET Core MVC 应用程序。这意味着它附带一个用于存储用户的数据库。--auth 标志将添加所有相关代码和依赖项,以启用您新创建的应用程序中的身份验证。

图 10.2 – ASP.NET Identity UI 布局参考
--auth 标志创建一个名为 Identity 的区域,其中包含一个 _ViewStart.cshtml 文件,该文件引用了新项目的 _Layout.cshtml 文件。实际的登录或注册屏幕由一个引用到此项目的编译库提供。
AUTHSAMPLE 包含一个 Data 文件夹,其中包含一个 Entity Framework Core DbContext,以及用于创建和更新此处使用的数据库的数据库迁移。
除了 Program.cs 之外的所有其他部分都与常规 MVC 应用程序完全相同。
如果您使用技术要求中所示的方式使用 .NET CLI 创建了应用程序,则使用 SQLite 数据库。如果您使用 Visual Studio 创建此应用程序,则使用 SQL Server 来存储用户数据。
在启动应用程序之前,在终端中调用以下命令:
dotnet ef database update
这将创建和更新数据库。
如果不起作用,你可能需要首先在 .NET CLI 中安装 Entity Framework 工具:
dotnet tool install -g dotnet-ef
然后,调用以下命令:
dotnet watch
应用程序现在将以监视模式启动,并启用热重载。它还会打开一个浏览器窗口并调用应用程序:

图 10.3 – AuthSample 主页
如您所见,在右上角有一个菜单,其中包含此应用程序的 注册 和 登录 选项。点击 登录 链接将带您到以下登录界面:

图 10.4 – 登录界面
如前所述,此视图来自一个编译的 Razor 库,它为 Identity 区域提供必要的视图。我们从框架中自动获取此 UI。
在本节的最后,我们应该快速查看 Program.cs 文件,它也与我们之前章节中看到的文件不同。
在服务注册的上部区域,有注册 DbContext 以及数据库异常页面的代码行:
var connectionString = builder.Configuration
.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<ApplicationDbContext>(
options => options.UseSqlite(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
builder.Services.AddDefaultIdentity<IdentityUser>(
options => options.SignIn.RequireConfirmedAccount =
true)
.AddEntityFrameworkStores<ApplicationDbContext>();
还有一个用于默认身份的注册,它添加了 EntityFramework 存储。它还配置为仅允许已确认的账户,这意味着您作为用户需要在登录之前确认您的电子邮件地址。
在使用中间件的较低部分,我们看到身份验证和授权被使用:
app.UseAuthentication();
app.UseAuthorization();
这两个中间件启用身份验证和授权。第一个通过读取身份验证 cookie 来尝试识别用户。它还将所有相关信息添加到 Identity 对象中。
你可能需要通过向用户添加更多属性来扩展用户配置文件。让我们看看如何在下一节中这样做。
自定义 IdentityUser
IdentityUser 有以下字段:Id、Username、Password、Email 和 Phonenumber。
由于显示名称可能与用户名不同,我们应该添加一个 Name 属性。比如说,如果我们想向用户发送生日祝福,我们就想了解他们的出生日期。
要做到这一点,需要在 Data 文件夹中添加一个名为 WebAppUser.cs 的文件,其中包含以下行:
using Microsoft.AspNetCore.Identity;
namespace AuthSample.Data;
public class WebAppUser : IdentityUser
{
[PersonalData]
public string? Name { get; set; }
[PersonalData]
public DateTime DOB { get; set; }
}
如此所示,WebAppUser 从 IdentityUser 继承并扩展了前面提到的两个属性。
在 Program.cs 中,我们需要修改服务注册以使用新的 WebAppUser:
builder.Services.AddDefaultIdentity<WebAppUser>
我们还需要以更改基类的方式更改 DbContext 以使用此 WebAppUser。
public class ApplicationDbContext :
IdentityDbContext<WebAppUser, IdentityRole, string>
你可能需要向 Microsoft.AspNetCore.Identity 添加一个 using 语句。
第一步就到这里。我们现在需要更新数据库:
dotnet ef migrations add CustomUserData
dotnet ef database update
一旦你将 IdentityUser 扩展了自定义属性,你就可以开始在用户配置文件中使用它。这需要在 ASP.NET Core Identity UI 中进行一些自定义。
自定义 Identity 视图
即使 ASP.NET Core Identity 视图来自编译的 Razor 库,你也可以自定义这些视图以添加更多字段或更改布局。为此,你只需在预定义的文件夹结构中的区域内用自定义视图覆盖给定的视图即可。
如前所述,项目中已经有一个名为 Identity 的区域。在这个区域内部,有一个 Pages 文件夹。在这里,需要创建一个新的名为 Account 的文件夹,以匹配 Register 页面的路由。
如果已经完成,请在这个文件夹内放置一个新的 Razor 页面,命名为 Register.cshtml,并将以下内容放入其中,以查看视图覆盖是否生效:
@page
@{
}
<h1>Hello Register Form</h1>
如果你现在运行应用程序并点击左上角的 Register,你会看到以下页面:

图 10.5 – 注册页面
它正在工作。
实际上,你不需要自己覆盖视图。有一个代码生成器可用于构建你想要覆盖的视图。
通过调用以下命令安装代码生成器:
dotnet tool install -g dotnet-aspnet-codegenerator
如果还没有完成,你还需要在你的项目中安装以下包:
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
dotnet add package Microsoft.AspNetCore.Identity.UI
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
要了解代码生成器能做什么,请运行以下命令:
dotnet aspnet-codegenerator identity -h
你可以构建整个身份 UI 以及特定的页面。如果你没有指定默认 UI 的页面,所有页面都将生成到你的项目中。要查看你可以生成的页面,请输入以下命令:
dotnet aspnet-codegenerator identity -lf
第一次更改的想法是让用户在注册页面上填写名称属性。
因此,让我们生成注册页面:
dotnet aspnet-codegenerator identity -dc AuthSample.Data.ApplicationDbContext --files "Account.Register" -sqlite
此命令告诉代码生成器使用已存在的ApplicationDbContext和Sqlite。如果你没有指定,它将创建一个新的DbContext或注册现有的DbContext以使用 SQL Server 而不是 SQLite。
如果一切设置正确,代码生成器应该只添加Register.cshtml页面以及一些基础设施文件:

图 10.6 – 代码生成器添加的文件
代码生成器也知道项目正在使用自定义的WebAppUser而不是IdentityUser,这意味着WebAppUser在生成的代码中使用。
现在,你应该将Register.cshtml更改为添加显示名称到表单中。在 15 行电子邮件字段之前的表单元素中添加以下行:
<div class="form-floating">
<input asp-for="Input.Name" class="form-control"
autocomplete="name" aria-required="true" />
<label asp-for="Input.Name"></label>
<span asp-validation-for="Input.Name"
class="text-danger"></span>
</div>
此外,Regiser.cshtml.cs也需要更改。ImportModel类需要Name属性:
public class InputModel
{
[Required]
[Display(Name = "Display name")]
public string Name { get; set; }
在PostAsync方法中,将Name属性分配给新创建的用户:
var user = CreateUser();
user.Name = Input.Name;
就这样。
启动应用程序后,你会看到以下注册表单:

图 10.7 – 注册表单
尝试一下,你就会看到它正在工作。
由于用户可能需要更新名称,我们还需要更改个人资料页面的视图。在这里,还需要添加出生日期:
dotnet aspnet-codegenerator identity -dc AuthSample.Data.ApplicationDbContext --files "Account.Manage.Index" -sqlite
打开位于/Areas/Identity/Pages/Account/Manage/文件夹中的新创建的Index.cshtml.cs,并在InputModel类中放置以下属性:
public class InputModel
{
[Required]
[Display(Name = "Display name")]
public string Name { get; set; }
[Display(Name = "Date of birth")]
public DateTime DOB { get; set; }
你现在可以在相应的Index.cshtml中使用这些属性。下一个代码片段需要放置在验证摘要和用户名之间:
<div class="form-floating">
<input asp-for="Input.Name" class="form-control"
autocomplete="name" aria-required="true" />
<label asp-for="Input.Name"></label>
<span asp-validation-for="Input.Name"
class="text-danger"></span>
</div>
<div class="form-floating">
<input asp-for="Input.DOB" class="form-control"
type="date"/>
<label asp-for="Input.DOB" class="form-label"></label>
</div>
这足以显示字段,但还需要进行一些更改来填充保存的数据。在LoadAsync方法中,InputModel的实例化需要扩展到新属性:
Input = new InputModel
{
PhoneNumber = phoneNumber,
Name = user.Name,
DOB = user.DOB
};
当用户保存表单时,更改的值也需要保存。在OnPostAsync方法的倒数第三行之前放置以下代码片段:
user.Name = Input.Name;
user.DOB = Input.DOB;
await _userManager.UpdateAsync(user);
这将InputModel的值设置为WebAppUser属性,并在数据库中保存更改。
让我们在终端中调用dotnet watch来试一下。
现在个人资料页面将看起来类似于这个:

图 10.8 – 管理账户页面
你现在可以更改显示名称并添加你的出生日期。
如果用户提供了显示名称,他们可能会在登录后左上角显示它。
打开位于Views/Shared文件夹中的_LoginPartial.cshtml,并将前四行替换为以下代码片段:
@using Microsoft.AspNetCore.Identity
@using AuthSample.Data
@inject SignInManager<WebAppUser> SignInManager
@inject UserManager<WebAppUser> UserManager
@{
var user = await @UserManager.GetUserAsync(User);
}
这将 SignInManager 和 UserManager 的泛型类型参数从 IdentityUser 类型更改为 WebAppUser 类型。在代码块内部,通过传递当前用户,使用 UserManager 加载当前的 WebAppUser。
现在,需要将第 12 行上用户名的输出更改为写入显示名称:
Hello @user?.Name!
当 dotnet watch 仍在运行时,浏览器中运行的应用程序应该已经更新。可能你需要重新登录。现在你应该在右上角看到显示名称:

图 10.9 – 显示名称
就这样。
摘要
在本章中,你学习了如何通过添加额外的属性来扩展 ASP.NET Core Identity,以增强用户对象。你还学习了如何增强 Identity UI 以加载、保存和更新新用户属性的值。
但你将如何管理你应用程序的用户角色?
这将是下一章你要学习的内容,关于配置身份管理。
第十一章:第十一章:配置身份管理
在上一章中,我们学习了如何添加和自定义 ASP.NET Core Identity UI,以便用户可以注册、登录和管理他们的个人资料。不幸的是,ASP.NET Core Identity 默认不提供身份管理。
在本章中,我们将学习如何通过使用 IdentityManager2 创建用户和角色来管理 ASP.NET Core Identity。
我们将涵盖以下部分:
-
介绍 IdentityManager2
-
设置 IdentityManager2
本章的主题与 ASP.NET Core 架构的 MVC 层相关:

图 11.1 – ASP.NET Core 架构
技术要求
要跟随本章的示例,你需要创建一个 ASP.NET Core MVC 应用程序。打开你的控制台、shell 或 Bash 终端,切换到你的工作目录。使用以下命令创建一个新的 MVC 应用程序:
dotnet new mvc -n IdentityManagementSample -o IdentityManagementSample --auth Individual
现在,通过双击项目文件在 Visual Studio 中打开项目,或者在 VS Code 中通过在已打开的控制台中输入以下命令来打开项目:
cd IdentityManagementSample
code .
本章的所有代码示例都可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Customizing-ASP.NET-Core-6.0-Second-Edition/tree/main/Chapter11。
重要提示
本章假设你已经完成了上一章中的步骤。作为替代方案,你可以重用上一章的项目,可能只需要调整项目名称。
介绍 IdentityManager2
IdentityManager 是一个最初由 Brock Allen 创建并拥有的项目,他同时也与 Dominick Baier 一起创建了 IdentityServer。Scott Brady (www.scottbrady91.com/aspnet-identity) 及其雇主接管了该项目,将其移植到 ASP.NET Core,并作为 IdentityManager2 (brockallen.com/2018/07/09/identitymanager2/) 发布。
它通过 NuGet 提供 (www.nuget.org/packages/identitymanager2)。
设置 IdentityManager2
第一步是加载包。使用已经打开的命令行或 VS Code 或 Visual Studio 中的终端:
dotnet add package IdentityManager2
如果包已加载,打开 Program.cs 并将 IdentityManager2 添加到服务集合中:
builder.Services.AddIdentityManager();
将 ASP.NET Identity 的服务注册从以下内容更改:
builder.Services.AddDefaultIdentity<ApplicationUser>{ …
更改为以下内容:
builder.Services.AddIdentity<ApplicationUser, IdentityRole>( …
这向服务集合中添加了一些相关的服务。
此外,还需要添加 DefaultTokenProvider:
builder.Services.AddIdentity<ApplicationUser, IdentityRole>( … )
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
至此,服务配置就完成了。
然后,需要将 IdentityServer 添加到管道中。在身份验证和授权中间件之后添加它:
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseIdentityManager();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.MapRazorPages();
现在我们需要将 IdentityManager2 与已经配置好的 ASP.NET Identity 数据库连接连接起来。
这需要安装以下包:
dotnet add package IdentityManager2.AspNetIdentity
现在,你可以将 IdentityManager2 连接到已经存在的ApplicationDbContext,它是IdentityDbContext,处理IdentityUsers和IdentityRoles。别忘了添加using到IdentityManager2.AspNetIdentity。在代码中,需要使用已经存在的ApplicationUser:
builder.Services.AddIdentityManager()
.AddIdentityMangerService<
AspNetCoreIdentityManagerService<ApplicationUser,
string, IdentityRole, string>>();
运行IdentityManager就到这里。在命令提示符中输入dotnet watch以启动应用程序,或在 VS Code 或 VS 中按F5。如果你现在在浏览器中调用应用程序,你将看到管理数据的 UI:

图 11.2 - IdentityManager2
现在你可以创建角色和用户:
-
创建一个管理员角色和一个用户角色。之后,为自己创建一个用户角色。
-
用户角色创建后,转到所有用户并编辑新创建的用户。在这里,你可以更改用户属性并为他们分配两个角色:

图 11.3 - 编辑角色
通过使用 IdentityManager,你获得了一个管理用户和角色的完整工具。它还可以与自定义用户和自定义用户属性一起工作。
保护 IdentityManager2
我相信你已经注意到 IdentityManager2 可以在不登录的情况下访问。这是设计的一部分。你需要限制对其的访问。
Scott Brady 描述了一种使用 IdentityServer 来实现这一功能的方法(www.scottbrady91.com/aspnet-identity/getting-started-with-identitymanager2)。我们也会建议这样做。设置 IdentityServer 并不直接,且本书没有涵盖。不幸的是,无法使用默认的 ASP.NET Core 个人认证来保护 IdentityManager2。看起来创建 IdentityManager2 UI 的中间件不支持个人认证,并重定向到 ASP.NET Core Identity UI。
创建一个单独的 ASP.NET Core 应用程序来托管 IdentityManager2 是有意义的。这样,这种类型的行政 UI 将完全与公开可用的应用程序分离,你将能够使用 OAuth 或 Azure Active Directory 认证来保护应用程序。
摘要
在本章中,你学习了如何为你的应用程序添加用户界面来管理用户和角色。IdentityManager2 是管理你的身份的最佳和最完整的解决方案。
在下一章中,你将学习如何使用内容协商,仅通过单个 HTTP 端点创建不同类型的输出。
第十二章:第十二章:使用自定义 OutputFormatter 进行内容协商
在本章中,我们将学习如何以不同的格式和类型将数据发送到客户端。默认情况下,ASP.NET Core Web API 以 JSON 格式发送数据,但还有一些其他的数据分发方式。
本章将涵盖以下内容:
-
介绍
OutputFormatter对象 -
创建自定义
Outputformatter对象
本章讨论的主题与 ASP.NET Core 架构的 WebAPI 层相关:

图 12.1 – ASP.NET Core 架构
技术要求
要遵循本章中的示例,你需要创建一个 ASP.NET Core MVC 应用程序。为此,打开你的控制台、shell 或 Bash 终端,切换到你的工作目录。然后,使用以下命令创建一个新的 MVC 应用程序:
dotnet new webapi -n OutputFormatterSample -o OutputFormatterSample
现在,通过双击项目文件或在 Visual Studio Code 中在已打开的控制台中输入以下命令来打开项目:
cd OutputFormatterSample
code .
本章中所有的代码示例都可以在本书的GitHub仓库中找到,仓库地址为github.com/PacktPublishing/Customizing-ASP.NET-Core-6.0-Second-Edition/tree/main/Chapter12。
介绍 OutputFormatter 对象
在 ASP.NET Core 中,OutputFormatters 是将现有数据转换为不同格式以通过 HTTP 发送到客户端的类。Web API 使用默认的 OutputFormatters 将对象转换为 JSON,这是发送结构化数据的默认格式。其他内置格式化程序包括 XML 格式化程序和纯文本格式化程序。
通过所谓的 内容协商,客户端能够决定他们想要检索哪种格式。客户端需要在 Accept 标头中指定格式的内容类型。内容协商在 ObjectResult 中实现。
默认情况下,Web API 总是返回 JSON,即使你在标头中接受 text/XML。这就是为什么内置的 XML 格式化程序默认未注册。
向 ASP.NET Core 添加 XmlSerializerOutputFormatter 有两种方式:
-
以下代码片段展示了第一种方式:
builder.Services.AddControllers() .AddXmlSerializerFormatters(); -
或者,你也可以使用以下方法:
builder.Services.AddControllers() .AddMvcOptions(options => { options.OutputFormatters.Add( new XmlSerializerOutputFormatter()); });
你可能需要将 Microsoft.AspNetCore.Mvc.Formatters 命名空间添加到 using 语句中。
还有一个名为 XmlDataContractSerializerOutputFormatter 的格式化程序可用,它内部使用 DataContractSerializer,并且在配置上更加灵活。
默认情况下,任何 Accept 标头都会自动转换为 application/json,即使你使用了这些方法之一。然而,我们可以解决这个问题。
如果你想要允许客户端接受不同的标头,你需要关闭这种转换:
builder.Services.AddControllers()
.AddMvcOptions(options =>
{
options.RespectBrowserAcceptHeader = true;
// false by default
});
一些第三方组件可能不完全支持 ASP.NET Core 5.0 或更高版本,不会异步写入响应流,但自 ASP.NET Core 3.0 起的默认配置仅允许异步写入。
要启用同步写入访问,你需要在 ConfigureServices 方法中添加以下行:
builder.Services.Configure<KestrelServerOptions>(options =>
{
options.AllowSynchronousIO = true;
});
将 Microsoft.AspNetCore.Server.Kestrel.Core 命名空间添加到 using 语句中,以便访问选项。
要尝试格式化程序,让我们设置一个小型的测试项目。
准备测试项目
使用控制台,我们将创建一个小型的 ASP.NET Core 网络 API 项目,使用之前在 技术要求 部分中显示的命令:
-
首先,执行以下命令以添加必要的
GenFu是一个易于创建测试数据的优秀库,第二个包CsvHelper帮助我们轻松写入 CSV 数据。 -
现在,在 Visual Studio 或 VS Code 中打开项目,并在
controller文件夹中创建一个新的 API 控制器PersonsController:[Route("api/[controller]")] [ApiController] public class PersonsController : ControllerBase { } -
打开
PersonsController.cs文件,并添加一个Get()方法,如下所示:[HttpGet] public ActionResult<IEnumerable<Person>> Get() { var persons = A.ListOf<Person>(25); return persons; }你可能需要在文件开头添加以下
using语句:using GenFu; using Microsoft.AspNetCore.Mvc; using OutputFormatterSample.Models;使用
GenFu创建一个包含 25 个人的列表。属性将自动填充真实数据。GenFu是一个开源、快速、轻量级且可扩展的测试数据生成器。它包含内置的姓名、城市、国家、电话号码等列表,并自动将数据填充到类的正确属性中,具体取决于属性名称。例如,名为City的属性将填充城市名称,而名为Phone、Telephone或Phonenumber的属性将填充格式良好的假电话号码。你将在稍后看到GenFu的魔法和结果。 -
现在,创建一个
Models文件夹,并在其中创建一个名为Person.cs的新文件,并在其中包含Person类:public class Person { public int Id { get; set; } public string? FirstName { get; set; } public string? LastName { get; set; } public int Age { get; set; } public string? EmailAddress { get; set; } public string? Address { get; set; } public string? City { get; set; } public string? Phone { get; set; } } -
也要打开
Program.cs文件,添加 XML 格式化程序,并允许其他AcceptHeader,如前所述:builder.Services.AddControllers() .AddMvcOptions(options => { options.RespectBrowserAcceptHeader = true; // false by default options.OutputFormatters.Add( new XmlSerializerOutputFormatter()); });到此为止。现在,你能够从网络 API 中检索数据。
-
使用
dotnet run命令启动项目。
接下来,我们将测试 API。
测试网络 API
测试网络 API 的最佳工具是 Fiddler (www.telerik.com/fiddler) 或 Postman (www.postman.com/)。我更喜欢 Postman,因为它更容易使用。你可以使用这两个工具中的任何一个,但在这些建议中,我们将使用 Postman:
-
在 Postman 中创建一个新的请求。将 API URL(
localhost:5001/api/persons(URL 的端口号可能不同))输入到address字段中,然后添加一个带有Accept键和application/json值的标题。 -
在点击
GenFu对象将数据放入人员的属性中后,基于属性类型和属性名称:真实的首字母和姓氏,以及真实的城市和格式良好的电话号码。 -
接下来,让我们测试 XML 输出格式化器。在 Postman 中,将
Accept标头从application/json更改为text/xml,然后点击 发送:

图 12.3 – Postman 中 XML 输出的截图
现在,我们已经有了 XML 格式的输出。现在,让我们更进一步,创建一些自定义 OutputFormatter 对象。
创建自定义 OutputFormatter 对象
在这个例子中,我们的目标是创建一个 VCard 输出,以便能够直接将个人的联系详情导入到 Microsoft Outlook 或任何支持 VCards 的其他联系数据库。在本节后面的内容中,我们还想创建一个 CSV 输出格式化器。
这两个都是基于文本的输出格式化器,它们将从 TextOutputFormatter 继承其值。让我们看看创建 VCard 输出的步骤:
-
在新文件中创建一个名为
VcardOutputFormatter.cs的新类。 -
现在,在新文件中插入以下类骨架。你将在下面的代码片段中找到空方法的实现。构造函数包含支持的媒体类型和内容编码:
public class VcardOutputFormatter : TextOutputFormatter { public string? ContentType { get; } public VcardOutputFormatter() { SupportedMediaTypes.Add( MediaTypeHeaderValue.Parse("text/vcard")); SupportedEncodings.Add(Encoding.UTF8); SupportedEncodings.Add(Encoding.Unicode); } protected override bool CanWriteType(Type type) { } public override Task WriteResponseBodyAsync( OutputFormatterWriteContext context, Encoding selectedEncoding) { } private static void FormatVcard( StringBuilder buffer, Person person, ILogger logger) { } }你可能需要添加以下
using语句:using Microsoft.AspNetCore.Mvc.Formatters; using System.Text; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; using OutputFormatterSample.Models; -
下面的代码片段显示了
CanWriteType方法的实现。重写此方法是可选的,但限制特定条件是有意义的。在这种情况下,OutputFormatter只能格式化Person类型的对象:protected override bool CanWriteType(Type type) { if (typeof(Person).IsAssignableFrom(type) || typeof(IEnumerable<Person>) .IsAssignableFrom(type)) { return base.CanWriteType(type); } return false; } -
你需要重写
WriteResponseBodyAsync方法,将实际的Person对象转换为所需的输出。为了获取要转换的对象,你需要从传递给方法的OutputFormatterWriteContext对象中提取它们。你还可以从该上下文中获取 HTTP 响应。这是写入结果并发送给客户端所需的。 -
在方法内部,我们检查是否得到一个人或人的列表,并调用尚未实现的
FormatVcard方法:public override Task WriteResponseBodyAsync( OutputFormatterWriteContext context, Encoding selectedEncoding) { var serviceProvider = context.HttpContext.RequestServices; var logger = serviceProvider.GetService( typeof(ILogger<VcardOutputFormatter>)) as ILogger; var response = context.HttpContext.Response; var buffer = new StringBuilder(); if (context.Object is IEnumerable<Person>) { foreach (var person in context.Object as IEnumerable<Person>) { FormatVcard(buffer, person, logger); } } else { var person = context.Object as Person; FormatVcard(buffer, person, logger); } return response.WriteAsync(buffer.ToString()); } -
要将输出格式化为支持标准的
Vcard,你可能需要做一些手动工作:private static void FormatVcard( StringBuilder buffer, Person person, ILogger logger) { buffer.AppendLine("BEGIN:VCARD"); buffer.AppendLine("VERSION:2.1"); buffer.AppendLine( $"FN:{person.FirstName} {person.LastName}"); buffer.AppendLine( $"N:{person.LastName};{person.FirstName}"); buffer.AppendLine( $"EMAIL:{person.EmailAddress}"); buffer.AppendLine( $"TEL;TYPE=VOICE,HOME:{person.Phone}"); buffer.AppendLine( $"ADR;TYPE=home:;;{person.Address}; {person.City}"); buffer.AppendLine($"UID:{person.Id}"); buffer.AppendLine("END:VCARD"); logger.LogInformation( $"Writing {person.FirstName} {person.LastName}"); } -
然后,我们需要在
Program.cs中注册新的VcardOutputFormatter对象:builder.Services.AddControllers() .AddMvcOptions(options => { options.RespectBrowserAcceptHeader = true; // false by default options.OutputFormatters.Add( new XmlSerializerOutputFormatter()); // register the VcardOutputFormatter options.OutputFormatters.Add( new VcardOutputFormatter()); });你可能需要在
OutputFormatterSample中添加一个using语句。 -
使用
dotnet run再次启动应用程序。 -
现在,将
Accept标头更改为text/vcard,看看会发生什么:![图 12.4 – Postman 中 VCard 输出的截图]()
图 12.4 – Postman 中 VCard 输出的截图
现在,我们应该能看到所有数据都以
VCard格式呈现。 -
现在,让我们为 CSV 输出做同样的事情。我们已经将
CsvHelper库添加到项目中。所以,前往以下 URL 下载CsvOutputFormatter并将其放入你的项目中:github.com/PacktPublishing/Customizing-ASP.NET-Core-6.0-Second-Edition/blob/main/Chapter12/OutputFormatterSample6.0/CsvOutputFormatter.cs -
让我们快速看一下
WriteResponseBodyAsync方法:public override async Task WriteResponseBodyAsync( OutputFormatterWriteContext context, Encoding selectedEncoding) { var response = context.HttpContext.Response; var csv = new CsvWriter( new StreamWriter(response.Body), CultureInfo.InvariantCulture); IEnumerable<Person> persons; if (context.Object is IEnumerable<Person>) { persons = context.Object as IEnumerable<Person>; } else { var person = context.Object as Person; persons = new List<Person> { person }; } await csv.WriteRecordsAsync(persons); } -
它几乎与
VcardOutputFormatter以相同的方式工作。我们可以通过StreamWriter直接将响应流传递给CsvWriter。之后,我们就能将个人或个人列表喂给写入器。就是这样。 -
在测试之前,我们还需要注册
CsvOutputFormatter:builder.Services.AddControllers() .AddMvcOptions(options => { options.RespectBrowserAcceptHeader = true; // false by default options.OutputFormatters.Add( new XmlSerializerOutputFormatter()); // register the VcardOutputFormatter options.OutputFormatters.Add( new VcardOutputFormatter()); // register the CsvOutputFormatter options.OutputFormatters.Add( new CsvOutputFormatter()); }); -
在 Postman 中,将
Accept头改为text/csv,然后再次点击发送:


图 12.5 – Postman 中 text/CSV 输出的截图
我们做到了——Postman 能够打开我们测试的所有格式。
摘要
难道这不酷吗?根据Accept头改变格式的功能非常方便。这样,你就能为许多不同的客户端创建一个 Web API——一个接受多种不同格式的 API,具体取决于客户端的偏好。仍然有许多潜在的客户不使用 JSON,而更喜欢 XML 或 CSV。
反过来,也可以选择在 Web API 中消费 CSV 或其他任何格式。例如,假设你的客户端以 CSV 格式发送给你一串人名。你会如何解决这个问题?在action方法中手动解析字符串是可行的,但这不是一个容易的选择。
这就是ModelBinder对象能为我们做的。让我们看看它们在下一章是如何工作的。
第十三章:第十三章:使用自定义 ModelBinder 管理输入
在上一章关于 OutputFormatter 的内容中,我们学习了如何以不同的格式向客户端发送数据。在本章中,我们将反向操作。本章是关于从外部获取的数据;例如,如果你以特殊格式接收到数据,或者如果你需要以特殊方式验证接收到的数据时,Model Binder 将帮助你处理这种情况。
在本章中,我们将涵盖以下主题:
-
介绍
ModelBinder -
准备测试项目
-
创建
PersonsCsvBinder -
使用
ModelBinder -
测试
ModelBinder
本章中的主题涉及 ASP.NET Core 架构的 WebAPI 层:

图 13.1 – ASP.NET Core 架构
技术要求
要遵循本章的描述,你需要创建一个 ASP.NET Core MVC 应用程序。打开你的控制台、shell 或 Bash 终端,切换到你的工作目录。使用以下命令创建一个新的 MVC 应用程序:
dotnet new webapi -n ModelBinderSample -o ModelBinderSample
现在,通过双击项目文件或在 VS Code 中输入以下命令来在 Visual Studio 中打开项目:
cd ModelBinderSample
code .
本章中所有的代码示例都可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Customizing-ASP.NET-Core-6.0-Second-Edition/tree/main/Chapter13。
介绍 ModelBinder
Model Binder 负责将传入的数据绑定到特定的操作方法参数。它们将请求中发送的数据绑定到参数。默认绑定器能够绑定通过 QueryString 发送或请求正文中发送的数据。在正文中,数据可以以 URL 或 JSON 格式发送。
模型绑定尝试通过参数名称在请求中查找值。表单值、路由数据和查询字符串值存储为一个键值对集合,绑定尝试在集合的键中查找参数名称。
让我们通过一个测试项目来演示这是如何工作的。
准备测试数据
在本节中,我们将了解如何将 CSV 数据发送到 Web API 方法。我们将重用我们在 第十二章,使用自定义 OutputFormatter 进行内容协商 中创建的 CSV 数据。
这是我们想要使用的测试数据片段:
Id,FirstName,LastName,Age,EmailAddress,Address,City,Phone
48,Austin,Ward,49,Jake.Timms@live.com,"8814 Gravesend Neck Road ",Daly City,(620) 260-4410
2,Sierra,Smith,15,Elizabeth.Wright@hotmail.com,"1199 Marshall Street ",Whittier,(655) 379-4362
27,Victorina,Radcliff,40,Bryce.Sanders@rogers.ca,"2663 Sutton Street ",Bloomington,(255) 365-0521
78,Melissa,Brandzin,39,Devin.Wright@telus.net,"7439 Knight Court ",Tool,(645) 343-2144
89,Kathryn,Perry,87,Hailey.Jenkins@hotmail.com,"5283 Vanderbilt Street ",Carlsbad,(747) 369-4849
你可以在 GitHub 上找到完整的 CSV 测试数据:github.com/PacktPublishing/Customizing-ASP.NET-Core-6.0-Second-Edition/blob/main/Chapter13/testdata.csv。
准备测试项目
让我们按照以下步骤准备项目:
-
在已经创建的项目(参考技术要求部分),我们现在将创建一个新的空 API 控制器,其中包含一个小操作:
namespace ModelBinderSample.Controllers { [Route("[controller]")] [ApiController] public class PersonsController : ControllerBase { public ActionResult<object> Post( IEnumerable<Person> persons) { return new { ItemsRead = persons.Count(), Persons = persons }; } } }这个看起来基本上和任何其他操作一样。它接受人员列表并返回一个包含人员数量以及人员列表的匿名对象。这个操作相当无用,但帮助我们使用 Postman 调试
ModelBinder。 -
我们还需要
Person类:public class Person { public int Id { get; set; } public string? FirstName { get; set; } public string? LastName { get; set; } public int Age { get; set; } public string? EmailAddress { get; set; } public string? Address { get; set; } public string? City { get; set; } public string? Phone { get; set; } }如果我们想向该操作发送基于 JSON 的数据,这将实际上工作得很好。
-
作为最后的准备步骤,我们需要添加
CsvHelperNuGet 包以更轻松地解析 CSV 数据。.NET CLI 在这里也很有用:System.Linq.Async package is needed to handle the IAsyncEnumerable that gets returned by the GetRecordsAsync() method.
现在所有这些都设置好了,我们可以在下一节尝试它并创建PersonsCsvBinder。
创建PersonsCsvBinder
让我们构建一个绑定器。
要创建ModelBinder,添加一个名为PersonsCsvBinder的新类,该类实现IModelBinder接口。在BindModelAsync方法中,我们获取包含所有所需信息的ModelBindingContext,以便获取数据和反序列化。以下代码片段显示了一个通用的绑定器,它应该适用于任何模型列表。我们将其分为几个部分,以便您可以清楚地看到绑定器的每个部分是如何工作的:
public class PersonsCsvBinder : IModelBinder
{
public async Task BindModelAsync(
ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
return;
}
var modelName = bindingContext.ModelName;
if (String.IsNullOrEmpty(modelName))
{
modelName = bindingContext.OriginalModelName;
}
if (String.IsNullOrEmpty(modelName))
{
return;
}
如您从前面的代码块中看到的,首先检查上下文是否为 null。之后,如果尚未指定,我们将设置默认参数名称给模型。如果这样做,我们就可以通过之前设置的名称获取值:
var valueProviderResult =
bindingContext.ValueProvider.GetValue(modelName);
if (valueProviderResult == ValueProviderResult.None)
{
return;
}
在下一部分,如果没有值,在这种情况下我们不应该抛出异常。原因是下一个配置的ModelBinder可能负责。如果我们抛出异常,当前请求的执行将被取消,下一个配置的ModelBinder就没有机会被执行:
bindingContext.ModelState.SetModelValue(
modelName, valueProviderResult);
var value = valueProviderResult.FirstValue;
// Check if the argument value is null or empty
if (String.IsNullOrEmpty(value))
{
return;
}
如果我们有值,我们可以实例化一个新的StringReader,需要将其传递给CsvReader:
var stringReader = new StringReader(value);
var reader = new CsvReader(
stringReader, CultureInfo.InvariantCulture);
使用CsvReader,我们可以将 CSV 字符串值反序列化为Persons列表。如果我们有列表,我们需要创建一个新的、成功的ModelBindingResult,并将其分配给ModelBindingContext的Result属性:
var asyncModel = reader.GetRecordsAsync<Person>();
var model = await asyncModel.ToListAsync();
bindingContext.Result =
ModelBindingResult.Success(model);
}
}
你可能需要在文件开头添加以下using语句:
using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.IO;
using CsvHelper
using System.Globalization;
接下来,我们将使用ModelBinder。
使用模型绑定器
绑定器不会自动使用,因为它没有在依赖注入容器中注册,并且没有配置在 MVC 框架中使用。
使用此模型绑定器的最简单方法是,在模型应绑定到的操作的参数上使用ModelBinderAttribute:
[HttpPost]
public ActionResult<object> Post(
[ModelBinder(binderType: typeof(PersonsCsvBinder))]
IEnumerable<Person> persons)
{
return new
{
ItemsRead = persons.Count(),
Persons = persons
};
}
在这里,我们将我们的PersonsCsvBinder的类型设置为binderType属性。
注意
使用ModelBinderProvider将ModelBinder添加到现有列表中。
我个人更喜欢显式声明,因为大多数自定义ModelBinder将特定于某个操作或特定类型,并且可以防止在后台隐藏魔法。
现在,让我们测试一下我们所构建的内容。
测试 ModelBinder
要测试它,我们需要在 Postman 中创建一个新的请求:
-
通过在控制台运行
dotnet run或按F5在 Visual Studio 或 VS Code 中启动应用程序来启动应用程序。 -
在 Postman 中,我们将在地址栏中将请求类型设置为
https://localhost:5001/api/persons。端口号可能因您的环境而异。
-
接下来,我们需要将 CSV 数据添加到请求体中。选择
form-data作为请求体类型,添加persons键,并在值字段中粘贴以下值行:Id,FirstName,LastName,Age,EmailAddress,Address,City,Phone 48,Austin,Ward,49,Jake.Timms@live.com,"8814 Gravesend Neck Road ",Daly City,(620) 260-4410 2,Sierra,Smith,15,Elizabeth.Wright@hotmail.com,"1199 Marshall Street ",Whittier,(655) 379-4362 27,Victorina,Radcliff,40,Bryce.Sanders@rogers.ca,"2663 Sutton Street ",Bloomington,(255) 365-0521 -
按下发送后,我们得到的结果,如图图 13.2所示:

图 13.2 – Postman 中的 CSV 数据截图
现在,客户端将能够向服务器发送基于 CSV 的数据。
摘要
这是一种将输入转换为动作所需的方式的好方法。您也可以使用ModelBinder对数据库或您需要在模型传递给动作之前完成的任何操作进行一些自定义验证。
在下一章中,我们将看到您可以使用ActionFilter做什么。
进一步阅读
要了解更多关于ModelBinder的信息,您应该查看以下相对详细的文档:
-
Steve Gordon,ASP.NET MVC Core 中的自定义模型绑定: https://www.stevejgordon.co.uk/html-encode-string-aspnet-core-model-binding/
-
ASP.NET Core 中的模型绑定: https://docs.microsoft.com/en-us/aspnet/core/mvc/models/model-binding
-
ASP.NET Core 中的自定义模型绑定: https://docs.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding
第十四章:第十四章:创建自定义操作过滤器
在本章中,我们将继续在控制器级别进行自定义。我们将探讨操作过滤器以及如何创建自己的 ActionFilter 类以保持操作小而可读。
本章将涵盖以下主题:
-
介绍
ActionFilter -
使用
ActionFilter
本章的主题属于 ASP.NET Core 架构的 模型-视图-控制器(MVC)层,如图所示:

图 14.1 – ASP.NET Core 架构
技术要求
要跟随本章的练习,您需要创建一个 ASP.NET Core MVC 应用程序。打开您的控制台、shell 或 Bash 终端,切换到您的工作目录。使用以下命令创建一个新的 MVC 应用程序:
dotnet new web -n ActionFilterSample -o ActionFilterSample
现在,通过双击项目文件或在Visual Studio Code(VS Code)中在已打开的控制台中输入以下命令来在 Visual Studio 中打开项目:
cd ActionFilterSample
code .
本章中的所有代码示例都可以在本书的 GitHub 仓库中找到:github.com/PacktPublishing/Customizing-ASP.NET-Core-6.0-Second-Edition/tree/main/Chapter14。
介绍 ActionFilter
操作过滤器有点像中间件,因为它们可以操作输入和输出,但它们会在 MVC 层上的特定操作或特定控制器上的所有操作上立即执行。中间件直接在托管层上的请求对象上工作。创建 ActionFilter 类是为了在操作执行之前或之后执行代码。它们被引入来执行不属于实际操作逻辑的方面:AuthorizeAttribute 用于允许用户或组访问特定的操作或控制器。AuthorizeAttribute 是一个 ActionFilter。它检查登录用户是否有权限。如果没有,则重定向到登录页面。
注意
如果您全局应用 ActionFilter,它将在您的应用程序中的所有操作上执行。
下一个代码示例显示了普通操作过滤器和异步 ActionFilter 的骨架:
using Microsoft.AspNetCore.Mvc.Filters;
namespace ActionFilterSample;
public class SampleActionFilter : IActionFilter
{
public void OnActionExecuting(
ActionExecutingContext context)
{
// do something before the action executes
}
public void OnActionExecuted(
ActionExecutedContext context)
{
// do something after the action executes
}
}
public class SampleAsyncActionFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next)
{
// do something before the action executes
var resultContext = await next();
// do something after the action executes;
// resultContext.Result will be set
}
}
如您所见,始终有两种方法可以将代码放置在目标操作执行之前和之后。这些操作过滤器不能用作属性。如果您想在控制器中使用操作过滤器作为属性,您需要从 Attribute 或从 ActionFilterAttribute 派生它们,如下面的代码片段所示:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace ActionFilterSample;
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(
ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(
context.ModelState);
}
}
}
上述代码片段显示了一个简单的 ActionFilter,如果 ModelState 无效,则始终返回 BadRequestObjectResult。这在 Web 的 POST、PUT 和 PATCH 请求中可能很有用。这可以通过更多的验证逻辑进行扩展。我们将在稍后看到如何使用它。
ActionFilter 的另一个可能的用途是记录日志。你不需要直接在控制器操作中记录日志。你可以在动作过滤器中这样做,以保持你的操作可读性并包含相关代码,如下面的代码片段所示:
using Microsoft.AspNetCore.Mvc.Filters;
namespace ActionFilterSample;
public class LoggingActionFilter : IActionFilter
{
ILogger _logger;
public LoggingActionFilter(ILoggerFactory
loggerFactory)
{
_logger =
loggerFactory.CreateLogger<LoggingActionFilter>();
}
public void OnActionExecuting(
ActionExecutingContext context)
{
_logger.LogInformation(
$"Action '{context.ActionDescriptor.DisplayName}'
executing");
}
public void OnActionExecuted(
ActionExecutedContext context)
{
_logger.LogInformation(
$"Action '{context.ActionDescriptor.DisplayName}'
executed");
}
}
这会在控制台输出一条信息性消息。你可以从 ActionExecutingContext 或 ActionExecutedContext 中获取更多关于当前操作的信息——例如,参数、参数值等。这使得动作过滤器非常有用。
让我们看看动作过滤器在实际中是如何工作的。
使用 ActionFilter
实际上是属性的过滤器可以注册为操作或控制器的一个属性,如下面的代码片段所示:
[HttpPost]
[ValidateModel] // ActionFilter as attribute
public ActionResult<Person> Post([FromBody] Person model)
{
// save the person
return model; //just to test the action
}
在这里,我们使用 ValidateModel 属性来检查 ModelState,如果 ModelState 无效则返回 BadRequestObjectResult;我们不需要在实际操作中检查 ModelState。
要全局注册动作过滤器,你需要扩展 Startup.cs 文件中的 ConfigureServices 方法中的 MVC 注册,如下所示:
builder.Services.AddControllersWithViews()
.AddMvcOptions(options =>
{
options.Filters.Add(new SampleActionFilter());
options.Filters.Add(new SampleAsyncActionFilter());
});
以这种方式注册的动作过滤器将在每个操作上执行。这样,你就可以使用不继承自属性的过滤器。
我们之前创建的 LoggingActionFilter 稍微特殊一些。它依赖于 ILoggerFactory 的一个实例,这个实例需要传递给构造函数。因为它作为属性使用时不会很好地工作,因为 Attributes 不支持 ILoggerFactory 在 ASP.NET Core 依赖注入容器中注册,并且需要将其注入到 LoggingActionFilter 中。
由于这个原因,还有更多注册动作过滤器的方法。全局上,我们能够将它们注册为依赖注入容器实例化的类型,并且依赖关系可以由容器解决,如下面的代码片段所示:
builder.Services.AddControllersWithViews()
.AddMvcOptions(options =>
{
options.Filters.Add<LoggingActionFilter>();
})
这工作得很好。我们现在在过滤器中有 ILoggerFactory。
为了支持在 Attributes 中的自动解析,你需要在控制器或操作级别使用 ServiceFilter 属性,如下所示:
[ServiceFilter(typeof(LoggingActionFilter))]
public class HomeController : Controller
{
除了全局过滤器注册之外,ActionFilter 需要在使用 ServiceFilter 属性之前在 ServiceCollection 中注册,如下所示:
builder.Services.AddSingleton<LoggingActionFilter>();
为了完整,还有另一种使用动作过滤器的方法,它需要将参数传递给构造函数。你可以使用 TypeFilter 属性来自动实例化过滤器。但是,使用这个属性,过滤器不是由依赖注入容器实例化的;参数需要作为 TypeFilter 属性的参数指定。
请参阅官方文档中的下一个代码片段:
[TypeFilter(typeof(AddHeaderAttribute),
Arguments = new object[] { "Author", "Juergen Gutsch
(@sharpcms)" })]
public IActionResult Hi(string name)
{
return Content($"Hi {name}");
}
过滤器的类型和参数通过 TypeFilter 属性指定。
摘要
动作过滤器为我们提供了一个简单的方式来保持动作的整洁。如果我们发现动作内部有重复的任务,而这些任务实际上并不与动作的实际责任相关,我们可以将这些任务移动到 ActionFilter 中,或者可能是一个 ModelBinder 或某些 MiddleWare,具体取决于它需要如何全局工作。它与动作的相关性越强,使用 ActionFilter 就越合适。
有更多种类的过滤器,它们都以类似的方式工作。要了解更多关于不同种类的过滤器,阅读文档绝对是推荐的。
在下一章中,我们将通过使用缓存来加速您的 Web 应用程序。
进一步阅读
- 微软 ASP.NET Core 过滤器:
docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters
第十五章:第十五章:使用缓存
在本章中,我们将探讨缓存技术。ASP.NET Core 提供了多种缓存方式,我们将学习如何使用和自定义它们。
在本章中,我们将涵盖以下主题:
-
缓存的必要性
-
基于 HTTP 的缓存
-
使用 ResponseCachingMiddleware 进行缓存
-
使用缓存配置文件预定义缓存策略
-
使用 CacheTagHelper 缓存特定区域
-
手动缓存
本章中的主题涉及 ASP.NET Core 架构的 MVC 层:

图 15.1 – ASP.NET Core 架构
技术要求
要遵循本章的描述,你需要创建一个 ASP.NET Core MVC 应用程序。打开你的控制台、shell 或 Bash 终端,切换到你的工作目录。使用以下命令创建一个新的 MVC 应用程序:
dotnet new mvc -n CacheSample -o CacheSample
现在,通过双击项目文件或在 VS Code 中输入以下命令来在 Visual Studio 中打开项目:
cd CacheSample
code .
本章中的所有代码示例都可以在本书的 GitHub 仓库github.com/PacktPublishing/Customizing-ASP.NET-Core-6.0-Second-Edition/tree/main/Chapter15中找到。
我们为什么需要缓存?
缓存通过在内存或像快速 Redis 数据库这样的分布式缓存中存储结果来加速性能,如果合理的话,你还可以将缓存数据存储在文件中。
当你运行多个应用程序实例以扩展应用程序的可用性时,需要分布式缓存。这些实例将在多个 Docker 容器、Kubernetes 集群或多个 Azure App Services 上运行。在这种情况下,实例应该共享缓存。
大多数应用程序缓存都是内存缓存,存储数据的时间较短。这对于大多数场景来说都是好的。
此外,浏览器也会缓存网站或 Web 应用程序的输出。浏览器通常将整个结果存储在文件中。作为一名 ASP.NET 开发者,你可以通过添加指定浏览器是否应该缓存以及缓存项应该有效多长时间的 HTTP 头来自定义浏览器缓存。
浏览器缓存可以减少对服务器的请求次数。在你的代码中处理缓存可以减少数据库访问次数或减少对其他耗时操作的访问。
客户端缓存和服务器端缓存都对提高应用程序性能很有用。让我们详细了解一下客户端缓存。
基于 HTTP 的缓存
要控制浏览器缓存,你可以设置一个Cache-Control HTTP 头。通常,StaticFileMiddleware不会设置 Cache-Control 头。这意味着客户端可以自由地按照他们喜欢的任何方式缓存。如果你希望将缓存时间限制为仅一天,你可以通过将StaticFileOptions传递给中间件来实现这一点:
const string cacheMaxAge = "86400";
app.UseStaticFiles(new StaticFileOptions()
{
OnPrepareResponse = ctx =>
{
ctx.Context.Response.Headers.TryAdd(
"Cache-Control",
$"public, max-age={cacheMaxAge}");
}
});
这将 Cache-Control 头部设置为在发送到客户端之前请求的每个静态文件。Cache-Control 被设置为公共,这意味着它可以在每个客户端上公开缓存。缓存项的最大年龄不应超过 86,400 秒,即一天。
将头部设置为静态文件只是一个示例。您可以将头部设置为需要缓存控制的每个响应。您还可以通过将 Cache-Control 头部设置为 no-cache 来禁用缓存。
要了解更多关于 Cache-Control 头部的信息,请参阅以下 URL:https://datatracker.ietf.org/doc/html/rfc7234#section-5.2
此外,Expires 头部可能也很有用,用于指定响应内容何时失效并应从服务器刷新。请参阅 https://datatracker.ietf.org/doc/html/rfc7234#section-5.3
Vary 头部指定了一个标准,告诉客户端关于缓存变化的信息。它检查特定头部是否存在。请参阅 https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.4
这通过响应对象直接控制客户端。
使用 ResponseCachingMiddleware 进行缓存
ResponseCachingMiddleware 在服务器端缓存响应并基于缓存的响应创建响应。该中间件像客户端一样尊重 Cache-Control 头部。这意味着您可以通过设置预览部分中描述的特定头部来控制中间件。
要使其工作,您需要将 ResponseCachingMiddleware 添加到依赖注入容器中:
builder.Services.AddResponseCaching();
并且您应该在静态文件和路由添加之后使用该中间件:
app.UseResponseCaching();
如果您添加了 CORS 配置,则应先调用 UseCors 方法。
ResponseCachingMiddleware 受特定 HTTP 头部的影响。例如,如果设置了 Authentication 头部,则响应不会缓存,与 Set-Cookie 头部相同。它也仅缓存导致 200 OK 结果的响应。错误页面和其他状态代码不会缓存。
您可以在以下 URL 中找到完整的标准列表:https://docs.microsoft.com/en-us/aspnet/core/performance/caching/middleware?view=aspnetcore-6.0#http-headers-used-by-response-caching-middleware。
在控制器级别、操作级别或页面级别使用 ResponseCacheAttribute,您可以通过使用 ResponseCacheAttribute 设置正确的头部来控制 ResponseCachingMiddleware:
[ResponseCache(Duration = 86400)]
public IActionResult Index()
{
return View();
}
此代码片段将 Cache-Control 设置为公共,最大年龄为一天,就像预览部分中的示例一样。
此属性非常强大,您还可以以不同的方式设置 Vary 头部,以及指示完全不缓存输出的指示器。甚至可以设置 CacheProfileName。我们将在下一节中查看缓存配置文件。
这些是可以设置的属性:
-
Duration:秒数时间范围 -
Location:存储缓存的地点:客户端、任何或无 -
NoStore:如果设置为 true,则禁用缓存 -
VaryByHeader:一个改变缓存的头值 -
VaryByQueryKeys:一个查询键名称数组,它改变了缓存
使用缓存配置文件预定义缓存策略
你可以在所谓的缓存配置文件中预定义缓存策略,以便在需要的地方重复使用。CacheProfile 类型具有与 ResponseCache 属性相同的属性。要定义缓存配置文件,你需要将选项设置到 MVC 服务中。
在 Program.cs 文件中,AddControllersWithViews 方法有一个重载,可以配置 MvcOptions。在这里,你还可以添加缓存配置文件:
builder.Services.AddControllersWithViews(options =>
{
options.CacheProfiles.TryAdd("Duration30",
new CacheProfile
{
Duration = 30,
VaryByHeader = "User-Agent",
Location = ResponseCacheLocation.Client
});
options.CacheProfiles.TryAdd("Duration60",
new CacheProfile
{
Duration = 60,
VaryByHeader = "User-Agent",
Location = ResponseCacheLocation.Client
});
});
你可能需要添加一个 using 语句到 Microsoft.AspNetCore.Mvc。
这个片段添加了两个不同的缓存配置文件,第一个配置文件有 30 秒的缓存时间,第二个配置文件有 60 秒的缓存时间。这两个配置文件都告诉缓存根据 "User-Agent" 头来变化。
要使用配置文件,你可以在响应缓存属性中使用配置文件名称:
[ResponseCache(CacheProfileName = "Duration30")]
public IActionResult Index()
{
你不必设置 ResponseCacheAttribute 的所有属性,只需设置 CacheProfileName。让我们看看如何使用声明性方式使用缓存。
使用 CacheTagHelper 缓存特定区域
你还可以缓存视图的特定区域。在一个无法缓存整个视图的场景中,你可以通过使用 CacheTagHelper 来只缓存特定的区域。
为了测试这一点,将以下片段添加到 index.cshtml 文件中,该文件位于 /Views/Home/ 文件夹中:
<div>
<p>
The current time is: @DateTime.Now.ToLongTimeString()
</p>
</div>
<cache expires-sliding="@TimeSpan.FromSeconds(7)">
<div>
<p>
The current time is: @DateTime.Now.ToLongTimeString()
</p>
</div>
</cache>
这个片段包含两个相同的 p 标签,用于输出当前时间。
第二个被包裹在一个具有 7 秒滑动过期时间的 CacheTagHelper 中。
启动应用程序并查看发生了什么。导航到 Index 页面并多次刷新浏览器。你将看到只有第一次刷新时内容会改变,第二个是缓存的,在 7 秒内保持不变。

图 15.2 - 缓存和未缓存值
让我们看看如果我们需要更具体地缓存应该做什么
手动缓存
有时候在 C# 代码内部进行特定缓存是有意义的。例如,如果你需要从外部源或数据库获取数据,缓存结果并每次不访问结果可以节省时间和流量。
让我们通过使用两种不同的方式来创建和访问缓存项来试一试:
-
为了试一试,我们将稍微扩展一下
HomeController。首先,将IMemoryCache的一个实例注入到控制器中,并将其存储在一个字段中:using Microsoft.Extensions.Caching.Memory; public class HomeController : Controller { private readonly ILogger<HomeController> _logger; private readonly IMemoryCache _cache; public HomeController( ILogger<HomeController> logger, IMemoryCache cache ) { _logger = logger; _cache = cache; } -
在
Models文件夹中,创建一个名为Person.cs的文件,并在其中放置以下行:namespace CacheSample.Models; internal class Person { public int Id { get; set; } public string? Firstname { get; set; } public string? Lastname { get; set; } public string? Address { get; set; } public string? City { get; set; } } -
现在我们需要添加两个非常复杂的方法,这些方法为我们做一些神奇的事情。实际上,这些方法只是创建一些假数据,并不真正复杂:
private IEnumerable<Person> LoadDataFromExternalSource() { return A.ListOf<Person>(10); } private IDictionary<int, string> LoadSuperComplexCalculatedData() { return Enumerable.Range(0, 10) .ToDictionary( x => x, x => $"Item{Random.Shared.Next()}"); }第一种方法使用
GenFu,这在之前的章节中也已使用,用于创建Person列表并填充随机但有效的数据。第二种方法创建了一个包含随机数据的 10 项Dictionary。随机数据有助于展示数据实际上已被缓存。如果用户界面上的数据没有变化,那么数据是从缓存中获取的。 -
在项目文件夹中输入以下命令以安装
GenFu:dotnet add package GenFu -
在索引操作的开始处添加以下行,将第一种方法的数据存储在缓存中或从缓存中加载数据:
if (!_cache.TryGetValue<IEnumerable<Person>>( "ExternalSource", out var externalPersons)) { externalPersons = LoadDataFromExternalSource(); _cache.Set( "ExternalSource", externalPersons, new MemoryCacheEntryOptions { AbsoluteExpiration = DateTime.Now.AddSeconds(30) }); }这将首先尝试使用
ExternalSource缓存键从缓存中加载数据。如果缓存中的数据不存在,你需要从原始源加载数据,并使用Set方法将它们存储在缓存中。
创建和加载数据的另一种方式是使用GetOrCreate方法:
var calculatedValues = _cache.GetOrCreate(
"ComplexCalculate", entry =>
{
entry.AbsoluteExpiration = DateTime.Now.AddSeconds(30);
return LoadSuperComplexCalculatedData();
});
它的工作方式相同,但使用起来相当简单。需要缓存的值将直接在 lambda 表达式中返回,同时 lambda 表达式检索可以配置的缓存条目。
一旦数据存在,你就可以将它们返回到视图中:
return View(new IndexViewModel
{
Persons = externalPersons,
Data = calculatedValues
});
返回的模型看起来像这样:
internal class IndexViewModel
{
public IEnumerable<Person>? Persons { get; set; }
public IDictionary<int, string>? Data { get; set; }
}
将以下片段添加到Index.cshtml文件中,紧接在CacheTagHelper之后,以可视化数据:
<div class="row">
<div class="col-md-6">
<ul>
@foreach (var person in Model.Persons)
{
<li>
[@person.Id] @person.Firstname @person.Lastname
</li>
}
</ul>
</div>
<div class="col-md-6">
<ul>
@foreach (var data in Model.Data)
{
<li>[@data.Key] @data.Value</li>
}
</ul>
</div>
</div>
这在两个并排的列中创建了两个列表。现在运行应用程序,在浏览器中调用它,并尝试刷新页面。即使数据完全随机,显示的数据也不应该改变。如果没有缓存,每次重新加载时数据都会改变:

图 5.3 - 数据变化
就这样。缓存每 30 秒过期,如配置所示。
摘要
缓存帮助我们通过减少对性能较差的资源(如数据库、外部 API 或复杂计算)的调用,来创建高性能的应用程序。在本章中,你学习了如何使用ResponseCachingMiddleware和ResponseCacheAttribute来使用响应缓存,以及如何使用CacheTagHelper和手动在 C#代码中使用IMemoryCache来使用内存缓存。
在下一章中,你将学习如何创建自定义TagHelper。
进一步阅读
更多关于 ASP.NET Core 文档中的缓存信息:docs.microsoft.com/en-us/aspnet/core/performance/caching/response?view=aspnetcore-6.0。
第十六章:第十六章:创建自定义 TagHelper
在本章中,我们将讨论TagHelper非常有用,可以使 Razor 更加美观和易于阅读。创建自定义TagHelper将使您的生活更加轻松。
在本章中,我们将涵盖以下主题:
-
介绍
TagHelper -
创建自定义
TagHelper
本章讨论的主题涉及 ASP.NET Core 架构的 MVC 层:

图 16.1 – ASP.NET Core 架构
技术要求
要跟随本章的示例,您需要创建一个 ASP.NET Core MVC 应用程序。打开您的控制台、shell 或 Bash 终端,切换到您的工作目录。使用以下命令创建一个新的 MVC 应用程序:
dotnet new mvc -n TagHelperSample -o TagHelperSample
现在,通过双击项目文件在 Visual Studio 中打开项目,或在 Visual Studio Code 中,在已打开的控制台中输入以下命令:
cd TagHelperSample
code .
本章中的所有代码示例都可以在本书的 GitHub 存储库中找到:github.com/PacktPublishing/Customizing-ASP.NET-Core-6.0-Second-Edition/tree/main/Chapter16。
介绍 TagHelper
使用TagHelper是一种在服务器端编写更简单(更少)的 HTML 或 Razor 代码的快捷方式。TagHelper将在服务器上被解释,并为浏览器生成“真实”的 HTML 代码。
TagHelper不是 ASP.NET Core 中的新事物。它们自从框架的第一个版本起就存在了。大多数现有的和内置的TagHelper是旧式 HTML 辅助工具的替代品,这些辅助工具仍然存在于 ASP.NET Core 中,以保持 Razor 视图与 ASP.NET Core 兼容。
扩展 HTML 标签的一个非常基础的例子是内置的AnchorTagHelper:
<!-- old fashioned HtmlHelper -->
@Html.ActionLink("Home", "Index", "Home")
<!-- new TagHelper -->
<a asp-controller="Home" asp-action="Index">Home</a>
许多 HTML 开发者觉得在 HTML 标签之间有HtmlHelper有点奇怪。它难以阅读,在阅读代码时有些破坏性。也许对于习惯于阅读这种代码的 ASP.NET Core 开发者来说不是这样,但与TagHelper相比,它真的很丑。TagHelper感觉更自然,更像是 HTML,即使它们不是,即使它们在服务器上被渲染。
许多 HTML 辅助工具可以用TagHelper替代。
还有一些使用TagHelper构建的新标签,这些标签不是 HTML 标签,但看起来像 HTML。一个例子是EnvironmentTagHelper:
<environment include="Development">
<link rel="stylesheet"
href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet"
href="https://ajax.aspnetcdn.com/ajax/bootstrap/
3.3.7/css/bootstrap.min.css"
asp-fallback-href=
"~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only"
asp-fallback-test-property="position"
asp-fallback-test-value="absolute" />
<link rel="stylesheet"
href="~/css/site.min.css"
asp-append-version="true" />
</environment>
这个TagHelper根据当前的运行时环境渲染(或不渲染)内容。在这种情况下,目标环境是开发模式。第一个环境标签如果当前运行时环境设置为Development,则渲染内容;第二个标签如果它不是设置为Development,则渲染内容。这使得它在Development模式下渲染可调试的脚本或样式以及在任何其他运行时环境中渲染最小化和优化代码时非常有用。
现在我们来看看我们如何创建自己的自定义 TagHelper。
创建自定义 Tag Helper
要使用本章中创建的所有自定义 TagHelper,你需要引用当前程序集,告诉框架在哪里找到 TagHelper。打开 View/ 文件夹中的 _ViewImports.cshtml 文件,并在文件末尾添加以下行:
@addTagHelper *, TagHelperSample
这里有一个快速示例,展示了如何使用 TagHelper 扩展现有的标签:
-
假设我们需要一个配置为粗体并具有特定颜色的标签:
<p strong color="red">Use this area to provide additional information.</p>这看起来像是 90 年代的过时 HTML,但这只是为了演示一个简单的
TagHelper。 -
当前执行此任务的方法是使用
TagHelper扩展任何具有名为strong的属性的标签,如下面的代码片段所示:using Microsoft.AspNetCore.Razor.TagHelpers; namespace TagHelperSample.TagHelpers; [HtmlTargetElement(Attributes = "strong")] public class StrongTagHelper : TagHelper { public string Color { get; set; } public override void Process( TagHelperContext context, TagHelperOutput output) { output.Attributes.RemoveAll("strong"); output.Attributes.Add("style", "font-weight:bold;"); if (!String.IsNullOrWhiteSpace(Color)) { output.Attributes.RemoveAll("style"); output.Attributes.Add("style", $"font-weight:bold;color:{Color};"); } } }第一行告诉标签助手处理具有
strong目标属性的标签。这个TagHelper没有定义自己的标签,但它确实提供了一个额外的属性来指定颜色。Process方法定义了如何将 HTML 渲染到输出流中。在这种情况下,它会给当前标签添加一些内联 CSS 样式。它还会从当前标签中移除目标属性。color属性将不会显示。这将显示如下:
<p style="font-weight:bold;color:red;">Use this area to provide additional information.</p>
下一个示例展示了如何使用 TagHelper 定义一个自定义标签:
-
让我们创建这个名为
GreeterTagHelper的类:using Microsoft.AspNetCore.Razor.TagHelpers; namespace TagHelperSample.TagHelpers; public class GreeterTagHelper : TagHelper { [HtmlAttributeName("name")] public string Name { get; set; } public override void Process( TagHelperContext context, TagHelperOutput output) { output.TagName = "p"; output.Content.SetContent($"Hello {Name}"); } 2 -
这个
TagHelper处理一个具有属性名称的greeter标签。在Process方法中,当前标签将被更改为p标签,并将新内容设置为当前输出:<greeter name="Readers"></greeter>结果看起来像这样:
<p>Hello Readers</p>
但如果你需要做点更复杂的事情呢?让我们进一步探索。
检查更复杂的情况
最后一个部分的 TagHelper 非常基础,简单设计用来展示 TagHelper 的工作方式。下一个示例稍微复杂一些,展示了真实场景。这个 TagHelper 渲染了一个包含项目列表的表格。这是一个通用的 TagHelper,展示了创建自定义 TagHelper 的真实原因。有了这个,你可以重用独立的视图代码片段。例如,你可以包裹 div 标签。或者,你也可以简化你的 Razor 视图:
-
让我们首先创建
DataGridTagHelper类。接下来的代码片段并不完整,但我们将按以下步骤完成DataGridTagHelper类:using Microsoft.AspNetCore.Razor.TagHelpers; namespace TagHelperSample.TagHelpers; public class DataGridTagHelper : TagHelper { [HtmlAttributeName("Items")] public IEnumerable<object> Items { get; set; } public override void Process( TagHelperContext context, TagHelperOutput output) { output.TagName = "table"; output.Attributes.Add("class", "table"); var props = GetItemProperties(); TableHeader(output, props); TableBody(output, props); } }在
Process方法中,我们调用私有子方法来完成实际工作,使类更易于阅读。你可能需要在文件开头添加以下
using语句:using System.Reflection; using System.ComponentModel; -
因为这是一个通用的
TagHelper,传入的对象需要进行分析。GetItemProperties方法获取属性项的类型,并从类型中加载PropertyInfo。PropertyInfo将用于获取表头和值:private PropertyInfo[] GetItemProperties() { var listType = Items.GetType(); Type itemType; if (listType.IsGenericType) { itemType = listType.GetGenericArguments() .First(); return itemType.GetProperties( BindingFlags.Public | BindingFlags.Instance); } return new PropertyInfo[] { }; } -
以下代码片段展示了表头的生成。
TableHeader方法直接将必要的 HTML 标签写入TagHelperOutput。它还使用PropertyInfo列表来获取将用作表头名称的属性名:private void TableHeader( TagHelperOutput output, PropertyInfo[] props) { output.Content.AppendHtml("<thead>"); output.Content.AppendHtml("<tr>"); foreach (var prop in props) { var name = GetPropertyName(prop); output.Content.AppendHtml($"<th>{name}</th>"); } output.Content.AppendHtml("</tr>"); output.Content.AppendHtml("</thead>"); } -
使用属性名作为表头名称并不总是有用的。这就是为什么
GetPropertyName方法还尝试从DisplayNameAttribute中读取值,这是DataAnnotation的一部分,在 MVC 用户界面中广泛使用的数据模型。因此,支持这个属性是有意义的:private string GetPropertyName( PropertyInfo property) { var attribute = property .GetCustomAttribute<DisplayNameAttribute>(); if (attribute != null) { return attribute.DisplayName; } return property.Name; } -
此外,还需要显示值。
TableBody方法执行这项工作:private void TableBody( TagHelperOutput output, PropertyInfo[] props) { output.Content.AppendHtml("<tbody>"); foreach (var item in Items) { output.Content.AppendHtml("<tr>"); foreach (var prop in props) { var value = GetPropertyValue(prop, item); output.Content.AppendHtml( $"<td>{value}</td>"); } output.Content.AppendHtml("</tr>"); } output.Content.AppendHtml("</tbody>"); } -
要从实际对象中获取值,使用
GetPropertyValue方法:private object GetPropertyValue( PropertyInfo property, object instance) { return property.GetValue(instance); } -
要使用这个
TagHelper,您只需将项目列表分配给这个标签:<data-grid items="Model.Persons"></data-grid>在这种情况下,它是一个人员列表,我们通过当前模型中的
Persons属性获取它。 -
我们在这里使用的
Person类看起来是这样的:using System.ComponentModel; namespace TagHelperSample.Models; public class Person { [DisplayName("First name")] public string FirstName { get; set; } [DisplayName("Last name")] public string LastName { get; set; } public int Age { get; set; } [DisplayName("Email address")] public string EmailAddress { get; set; } }并非所有属性都有
DisplayNameAttribute,因此GetPropertyName方法中的回退是必要的,以获取实际的属性名而不是DisplayName值。将
Person类放入Models文件夹中的Person.cs文件。 -
您还需要一个服务来将数据加载到
HomeController的Index动作中。创建一个Services文件夹,并将一个名为PersonService.cs的文件放入其中。将以下代码片段放入该文件中:using TagHelperSample.Models; using GenFu; namespace TagHelperSample.Services; public interface IService { IEnumerable<Person> AllPersons(); } internal class PersonService : IService { public IEnumerable<Person> AllPersons() { return A.ListOf<Person>(25); } }在这里,我们再次使用
GenFu来自动生成人员列表。如果您还没有安装它,您需要执行以下命令来加载 NuGet 包:dotnet add package GenFu如果这样做,您应该在
Program.cs文件中将PersonService添加到ServiceCollection中:builder.Services.AddTransient<IService, PersonService>();最后但同样重要的是,应在
HomeController中使用PersonService:using Microsoft.AspNetCore.Mvc; using TagHelperSample.Models; using TagHelperSample.Services; namespace TagHelperSample.Controllers; public class HomeController : Controller { private readonly IService _service; public HomeController( IService service) { _service = service; } public IActionResult Index() { ViewData["Message"] = "Your application description page."; var persons = _service.AllPersons(); return View(new IndexViewModel { Persons = persons }); } -
这个
TagHelper在使用前需要更多的检查和验证,但它是可以工作的。它显示了一个使用GenFu生成的假数据列表(见第十二章,使用自定义输出格式化程序进行内容协商,了解GenFu):

图 16.2 – TagHelper 示例在行动
现在,您能够扩展这个TagHelper以包含更多的功能,包括排序、过滤和分页。请随意在多种环境中尝试它。
摘要
当涉及到重用视图的某些部分并简化、清理您的视图时,标签助手非常有用,就像在DataGridTagHelper的例子中一样。您还可以提供一个包含有用视图元素的库。在进一步阅读部分有一些现有的TagHelper库和示例,您可以尝试使用。
这是《定制 ASP.NET Core》第二版的最后一章。很高兴您阅读了所有章节。我们希望您觉得这些章节有用,并且它们能帮助您优化您的应用程序。
进一步阅读
-
戴米安·爱德华兹,TagHelperPack:
github.com/DamianEdwards/TagHelperPack -
大卫·帕奎特,TagHelperSamples:
github.com/dpaquette/TagHelperSamples -
Teleric 开发的 Bootstrap TagHelper:
www.red-gate.com/simple-talk/dotnet/asp-net/asp-net-core-tag-helpers-bootstrap/ -
jQuery TagHelper:
www.jqwidgets.com/asp.net-core-mvc-tag-helpers/

订阅我们的在线数字图书馆,全面访问超过 7,000 本书籍和视频,以及行业领先的工具,帮助您规划个人发展并推进职业生涯。更多信息,请访问我们的网站。
第十七章:为什么订阅?
-
使用来自 4,000 多位行业专业人士的实用电子书和视频,节省学习时间,增加编码时间
-
通过为您量身定制的技能计划提高学习效果
-
每月免费获得一本电子书或视频
-
完全可搜索,便于快速获取关键信息
-
复制粘贴、打印和收藏内容
您知道 Packt 为每本书都提供电子书版本,包括 PDF 和 ePub 文件吗?您可以在packt.com升级到电子书版本,并且作为印刷书客户,您有权获得电子书副本的折扣。有关更多信息,请联系我们 customercare@packtpub.com。
在www.packt.com上,您还可以阅读一系列免费的技术文章,订阅各种免费通讯,并享受 Packt 书籍和电子书的独家折扣和优惠。
您可能还会喜欢的其他书籍
如果您喜欢这本书,您可能对 Packt 出版的以下其他书籍也感兴趣:
ASP.NET Core 和 Vue.js
Devlin Basilan Duldulao
ISBN: 978-1-80323-279-9
-
在 ASP.NET Core 5 Web API 中探索 CQRS 和中介者模式
-
在 ASP.NET 中使用 Serilog、MediatR、FluentValidation 和 Redis
-
探索常见的 Vue.js 包,如 Vuelidate、Vuetify 和 Vuex
-
使用 Vuex 状态管理库管理复杂的应用程序状态
-
使用 xUnit 和 FluentAssertions 在 ASP.NET Core 中编写集成测试
-
使用新的 GitHub Actions 将您的应用程序部署到 Microsoft Azure,以实现持续集成和持续部署(CI/CD)
C# 10 和.NET 6 – 现代跨平台开发 – 第六版
Mark J. Price
ISBN: 978-1-80107-736-1
-
使用 Blazor、Razor Pages、模型-视图-控制器(MVC)模式和其他 ASP.NET Core 功能构建丰富的 Web 体验
-
使用面向对象编程构建自己的类型
-
编写、测试和调试函数
-
使用 LINQ 查询和操作数据
-
使用 Entity Framework Core、Microsoft SQL Server 和 SQLite 在您的应用程序中集成和更新数据库
-
使用最新的技术,包括 gRPC 和 GraphQL 构建和消费强大的服务
-
使用.NET MAUI 和 XAML 构建跨平台应用程序
Packt 正在寻找像您这样的作者
如果您有兴趣成为 Packt 的作者,请访问authors.packtpub.com并今天申请。我们已经与成千上万的开发者和技术专业人士合作,就像您一样,帮助他们将见解分享给全球科技社区。您可以提交一般申请,申请我们正在招募作者的特定热门话题,或者提交您自己的想法。
分享您的想法
现在您已经完成了 自定义 ASP.NET Core 6.0,我们非常乐意听听您的想法!如果您从亚马逊购买了这本书,请点击此处直接跳转到该书的亚马逊评论页面并分享您的反馈或在该购买网站上留下评论。
您的评论对我们和科技社区都非常重要,并将帮助我们确保我们提供高质量的内容。



浙公网安备 33010602011771号