作者:Daniel Roth
排版:Alan Wang
.NET 8 Preview 7 现在已经发布,其中包括了对 ASP.NET Core 的许多重要更新。
以下是预览版本中新增功能的摘要:
- 服务器和中间件
- 防伪中间件
- API 编写
- 最小 API 的防伪集成
- Native AOT
- 请求委托生成器支持拦截器功能
- Full TrimMode 用于启用修剪功能编译的 Web 项目
WebApplication.CreateEmptyBuilder
- Blazor
- 防伪集成
- 服务器端表单处理改进
- Auto 渲染模式
- 注册 root-level 级联值
- 改进了交互式组件与服务器端渲染的集成
Virtualize
的新参数EmptyContent
- Identity
- 新的承载令牌身份验证处理程序
- 新的 API 端点
- 单页应用程序(SPA)
- 新的 Visual Studio 模板
有关为 .NET 8的 ASP.NET Core 规划的更多详细信息,请参阅 GitHub 上的 .NET 8 的完整 ASP.NET Core 路线图。
入门
如果您想要在 .NET 8 Preview 7 中使用 ASP.NET Core,请安装 .NET 8 SDK。
如果您在 Windows 上使用 Visual Studio,我们建议您安装最新的 Visual Studio 2022 预览版。如果您使用的是 Visual Studio Code,那么可以尝试新的 C# 开发工具包。如果您使用的是 macOS,那么可以在“Preferences”中启用 .NET 8 的预览功能后使用 Visual Studio for Mac 17.6.1 进行开发。
升级现有项目
若要将现有 ASP.NET Core 应用程序从 .NET 8 Preview 6 升级到 .NET 8 Preview 7,请执行以下操作:
- 将应用程序的目标框架更新为
.NET 8.0
。 - 将所有 Microsoft.AspNetCore.* 包引用更新为
8.0.0-preview.7.*
。 - 将所有 Microsoft.Extensions.* 包引用更新为
8.0.0-preview.7.*
。
具体信息请参阅 .NET 8 的 ASP.NET Core 重要更新的完整列表。
服务器和中间件
防伪中间件
此版本添加了一个用于验证防伪令牌以及减轻跨站点请求伪造攻击的中间件。当通过 AddAntiforgery
方法注册防伪服务时,防伪中间件会自动在目标应用程序中启用。
var builder = WebApplication.CreateBuilder();
builder.Services.AddAntiforgery();
var app = builder.Build();
// 由 WebApplicationBuilder 隐式添加
// app.UseAntiforgery();
app.Run();
防伪中间件本身不会中断请求流水线的执行。相反,该中间件会在当前请求的 HttpContext.Features
中设置 IAntiforgeryValidationFeature
。中间件期望每个框架实现(最小 API、MVC、Blazor 等)都会对此功能做出反应并按预期中断执行。
只有在以下情况下才会验证防伪令牌:
- 端点包含实现了
IAntiforgeryMetadata
接口的元数据,其中
RequiresValidation=true
。 - 与端点关联的 HTTP 方法是相关 HTTP 方法(不是 TRACE、OPTIONS、HEAD、GET)。
- 请求与有效端点相关联。
请注意,为了避免在用户未经身份验证时无意中读取表单数据,防伪中间件必须在身份验证和授权中间件之后运行 。
API 编写
最小 API 的防伪集成
现在默认情况下,接受表单数据的最小 API 需要防伪令牌验证。
在下面的代码示例中:
- 防伪服务在 DI 中注册,因此防伪中间件会自动启用。
- 提供了两个端点用于显示表单:
/antiforgery
:会渲染一个带有隐藏防伪令牌字段的表单;/no-antiforgery
:则渲染一个没有防伪令牌字段的表单。 /todo
端点处理表单中的Todo
对象,并自动要求防伪令牌验证。
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder();
builder.Services.AddAntiforgery();
var app = builder.Build();
app.MapGet("/antiforgery", (HttpContext context, IAntiforgery antiforgery) =>
{
var token = antiforgery.GetAndStoreTokens(context);
var html = $"""
<html>
<body>
<form action="/todo" method="POST" enctype="multipart/form-data">
<input name="{token.FormFieldName}" type="hidden" value="{token.RequestToken}" />
<input type="text" name="name" />
<input type="date" name="dueDate" />
<input type="checkbox" name="isCompleted" />
<input type="submit" />
</form>
</body>
</html>
""";
return Results.Content(html, "text/html");
});
app.MapGet("/no-antiforgery", () =>
{
var html = """
<html>
<body>
<form action="/todo" method="POST" enctype="multipart/form-data">
<input type="text" name="name" />
<input type="date" name="dueDate" />
<input type="checkbox" name="isCompleted" />
<input type="submit" />
</form>
</body>
</html>
""";
return Results.Content(html, "text/html");
});
app.MapPost("/todo", ([FromForm] Todo todo) => Results.Ok(todo));
app.Run();
class Todo
{
public string Name { get; set; }
public bool IsCompleted { get; set; }
public DateTime DueDate { get; set; }
}
在 /antiforgery
提交表单将会得到一个成功的响应。另一方面,在 /no-antiforgery
提交表单将在运行时产生异常,因为没有提供有效的防伪令牌。在 Production 环境中,这将生成一个日志而不是抛出异常:
Microsoft.AspNetCore.Http.BadHttpRequestException: Invalid antiforgery token found when reading parameter "Todo todo" from the request body as form.
An unhandled exception has occurred while executing the request.
Microsoft.AspNetCore.Http.BadHttpRequestException: Invalid antiforgery token found when reading parameter "Todo todo" from the request body as form.
---> Microsoft.AspNetCore.Antiforgery.AntiforgeryValidationException: The required antiforgery request token was not provided in either form field "__RequestVerificationToken" or header value "RequestVerificationToken".
at Microsoft.AspNetCore.Antiforgery.DefaultAntiforgery.ValidateRequestAsync(HttpContext httpContext)
at Microsoft.AspNetCore.Antiforgery.Internal.AntiforgeryMiddleware.InvokeAwaited(HttpContext context)
Native AOT
请求委托生成器支持拦截器功能
.NET 8 Preview 3 中引入的请求委托生成器已更新为使用新的 C# 12 拦截器编译器功能,以支持在运行时使用静态生成的变体拦截对最小 API 的 Map
操作方法的调用。 由于这个变化,用户可以期望在启用 PublishAot
编译中看到应用程序的启动性能有所提高。
下表概述了启动时间的变化情况(即处理在应用程序中所有端点所需的时间)在三种情况下的变化:
- 基线,端点的
RequestDelegates
通过反射和动态代码生成在运行时生成。在编译时没有拦截器功能生成的端点,是 Preview 3 到 Preview 6 中的默认设置。 - 在编译时生成的端点,使用了拦截器功能 ,是 Preview 7 中的默认设置。
迭代 | 基线(运行时生成的端点) | 请求委托生成器(无拦截器) | 请求委托生成器(带拦截器) |
---|---|---|---|
1 | 716.2313毫秒 | 372.5172毫秒 | 31.5255毫秒 |
2 | 747.279毫秒 | 355.1435毫秒 | 64.188毫秒 |
3 | 730.975毫秒 | 350.5353毫秒 | 32.9315毫秒 |
4 | 729.2775毫秒 | 345.8684毫秒 | 34.1169毫秒 |
5 | 711.0555毫秒 | 351.2683毫秒 | 38.7152毫秒 |
Full TrimMode 用于启用修剪功能编译的 Web 项目
在此预览版中,我们引入了一项重大变更将影响通过 PublishTrimmed=true
启用了裁剪的 Web 项目。 在此版本之前,项目默认使用 partial 的 TrimMode。未来,所有面向 .NET 8 或更高版本的项目都将启用 TrimMode=full
。有关此重大变更的更多信息,请参阅公告。
WebApplication.CreateEmptyBuilder
我们添加了一个新的 WebApplicationBuilder
工厂方法,用于构建仅包含必要功能的小型应用程序:WebApplication.CreateEmptyBuilder(WebApplicationOptions options)
。此 WebApplicationBuilder
是没有内置行为的。您的应用程序将仅包含您配置的服务和中间件。
以下是使用此 API 创建小型 Web 应用程序的示例:
var builder = WebApplication.CreateEmptyBuilder(new WebApplicationOptions());
builder.WebHost.UseKestrelCore();
var app = builder.Build();
app.Use(async (context, next) =>
{
await context.Response.WriteAsync("Hello, World!");
await next(context);
});
Console.WriteLine("Running...");
app.Run();
在 linux-x64 计算机上使用 .NET 8 Preview 7 通过 Native AOT 发布此代码会生成约 8.5 MB
的自包含本机可执行文件。
Blazor
防伪集成
Blazor 端点现在默认需要防伪保护。您可以使用新的防伪中间件启用防伪支持,并使用 AntiforgeryToken
组件为渲染的表单生成令牌。EditForm
组件将自动为您添加防伪令牌。
在 Blazor 组件页面中使用 [RequireAntiforgeryToken]
属性来指示组件是否需要防伪投影。 例如,您可以像这样为页面禁用防伪要求(不建议!):
@using Microsoft.AspNetCore.Antiforgery;
@attribute [RequireAntiforgeryToken(required: false)]
服务器端表单处理改进
现在,您可以在使用服务器端渲染且不使用 EditForm
时在 Blazor 中构建标准 HTML 表单。只需使用普通 HTML form
标签创建表单,并指定 @onsubmit
处理程序来处理提交的表单请求。
<form method="post" @formname="contact" @onsubmit="AddContact">
<div>
<label for="name">Name</label>
<InputText id="name" @bind-Value="NewContact.Name" />
</div>
<div>
<label for="email">Email</label>
<InputText id="email" @bind-Value="NewContact.Email" />
</div>
<div>
<InputCheckbox id="send-me-deals" @bind-Value="NewContact.SendMeDeals" />
<label for="send-me-deals">Send me deals</label>
</div>
<button>Submit</button>
<AntiforgeryToken />
</form>
@code {
[SupplyParameterFromForm]
public Contact NewContact { get; set; } = new();
public class Contact
{
public string Name { get; set; }
public string Email { get; set; }
public bool SendMeDeals { get; set; }
}
private async Task AddContact()
{
// Add contact...
NewContact = new();
}
}
现在,所有服务器端渲染的表单都需要一个名称,该名称用于将提交的请求映射到相应的表单处理程序方法和模型绑定。 要指定普通 HTML 表单的名称,请使用新的 @formname
属性。 使用 EditForm
时,请使用 FormName
参数指定名称。
默认情况下,表单名称必须是唯一的,但您可以使用 FormMappingScope
组件定义表单名称的范围。
<FormMappingScope Name="parent-context">
<ComponentWithFormBoundParameter />
</FormMappingScope>
基于 InputBase<TValue>
的输入将生成与 Blazor 用于模型绑定的名称相匹配的表单值名称。您可以使用 [SupplyParameterFromForm]
上的 Name
属性指定 Blazor 用于将表单数据绑定到模型的名称。您还可以使用 Handler
属性指定要绑定其数据的表单的名称(在早期预览中,Name
属性用于此目的)。
要将表单分解为多个子组件,请从 Editor<T>
派生子组件。这将确保您的子组件根据模型生成正确的表单字段名称。
Index.razor
<EditForm Model="Customer" method="post" OnSubmit="DisplayCustomer" FormName="customer">
<div>
<label>Name</label>
<InputText @bind-Value="Customer.Name" />
</div>
<AddressEditor @bind-Value="Customer.BillingAddress" />
<button>Send</button>
</EditForm>
@if (submitted)
{
<!-- Display customer data -->
<h3>Customer</h3>
<p>Name: @Customer.Name</p>
<p>Street: @Customer.BillingAddress.Street</p>
<p>City: @Customer.BillingAddress.City</p>
<p>State: @Customer.BillingAddress.State</p>
<p>Zip: @Customer.BillingAddress.Zip</p>
}
@code {
public void DisplayCustomer()
{
submitted = true;
}
[SupplyParameterFromForm] Customer? Customer { get; set; }
protected override void OnInitialized() => Customer ??= new();
bool submitted = false;
public void Submit() => submitted = true;
}
AddressEditor.razor
@inherits Editor<Address>
<div>
<label for="street">Street</label>
<InputText id="street" @bind-Value="Value.Street" />
</div>
<div>
<label for="state">State</label>
<InputText id="state" @bind-Value="Value.State" />
</div>
<div>
<label id="city">City</label>
<InputText for="city" @bind-Value="Value.City" />
</div>
<div>
<label for="zip">Zip</label>
<InputText id="zip" @bind-Value="Value.Zip" />
</div>
Blazor 中的模型绑定现在支持绑定到以下附加类型:
- 递归类型
- 带有构造函数的类型
- 枚举
您现在还可以使用 [DataMember]
和 [IgnoreDataMember]
属性来自定义您正在编写的类型的模型绑定 。 您可以使用这些属性来重命名属性、忽略属性以及根据需要标记属性。 调用 AddRazorComponents
时,可以从 RazorComponentOptions
获得其他模型绑定选项。
Auto 渲染模式
Blazor Web 应用程序的新 Auto
交互式渲染模式将 Server
和 WebAssembly
渲染模式的优势结合到一个动态选项中。如果 .NET WebAssembly 运行时可以快速加载(100 毫秒内),则 Auto
渲染模式将使用基于 WebAssembly 的渲染。这种情况通常发生在当运行时之前已下载并缓存或使用高速网络时。否则,当 .NET WebAssembly 运行时在后台下载时,Auto
渲染模式会回退到使用 Server
渲染模式。
如果您要使用 Auto
渲染模式,请在组件实例上指定 @rendermode="@RenderMode.Auto"
属性,或在组件定义上指定 @attribute [RenderModeAuto]
。请注意,该组件需要设置为从服务器和浏览器运行,因为它必须位于您的客户端项目中,并且它的实现不得与 Server
或 WebAssembly
绑定。请查看 Blazor Auto 渲染模式示例,了解如何正确设置。
注册 root-level 级联值
在 Blazor 中,级联值是一种便捷的方式,可以将状态提供给组件层次结构的子树。现在,您可以注册 root-level 级联值,以便整个组件层次结构都可以使用它们。
// 注册固定级联值
services.AddCascadingValue(sp => new MyCascadedThing { Value = 123 });
// 按名称注册固定级联值
services.AddCascadingValue("thing", sp => new MyCascadedThing { Value = 123 });
// 使用 CascadingValueSource<TValue> 注册级联值
services.AddCascadingValue(sp =>
{
var thing = new MyCascadedThing { Value = 456 };
var source = new CascadingValueSource<MyCascadedThing>(thing, isFixed: false);
return source;
});
改进了交互式组件与服务器端渲染的集成
.NET 8 中的 Blazor 具有先进的服务器端渲染功能,例如增强的导航和表单处理。此预览版改进了交互式组件与服务器端渲染的集成。增强的导航、增强的表单处理和流式渲染现在可以添加和删除交互式组件并对其进行参数设置。
Virtualize
的新参数 EmptyContent
Virtualize
组件现在有一个 EmptyContent
属性,您可以使用该属性来定义在没有项目时或当 ItemsProvider
返回 TotalItemCount
为零时应显示的内容。
感谢 @etemi 的贡献!
Identity
之前在 .NET 8 Preview 4 中,我们添加了新的 Identity API 端点来注册和登录用户,以简化自托管身份管理,使得在单页应用程序 (SPA) 和 Blazor 应用程序中实现和自定义 identity 更加容易。默认体验是基于 cookie 的,因此它“仅适用于”单域应用程序。对于需要令牌的场景,例如从移动客户端访问您的 Web 应用程序,我们现在提供对“内置”令牌的支持。这些令牌是自包含的,并使用与 cookie 身份验证相同的技术来生成。值得注意的是,这些不是 JWT,而是自包含的,并且针对没有委托身份验证的第一方应用程序进行了优化。在多服务器环境中,您需要配置数据保护才能使用共享存储。
承载令牌身份验证处理程序
新的承载令牌身份验证处理程序与 ASP.NET Core 的内置身份验证系统无缝集成。 它可以独立使用(不依赖 ASP.NET Core Identity)。它支持发行和验证令牌。从 Preview 6 开始,它还支持刷新令牌。ASP.NET Core Identity 使用 AddBearerToken
扩展将 handler 与 identity 集成。
Identity API 端点
新的 .NET 8 identity API 端点提供基于 HTTP 的 API :
- 在 identity 系统中注册新用户
- 登录并交换经过验证的凭据以获取 cookie 或令牌
- 使用刷新令牌刷新凭据,使用户保持登录状态,而无需重新输入其凭据
- 在注册过程中确认电子邮件以进行额外验证
- 如果未收到或已过期,请重新发送确认电子邮件
- 重置密码以支持“忘记密码”功能
还有一些受保护的端点需要对用户进行身份验证:
- 管理双因素身份验证(2FA)。
- 检索或更新用户 identity 配置文件中的信息,包括声明。
入门
David Fowler 的 identity 端点示例展示了如何配置 identity 以使用新的 handler 和端点。 它还包含一个 .http 文件,用于测试 Visual Studio 内的新端点。
首先,启用新的 handler 并向应用添加身份验证和授权。
builder.Services.AddAuthentication().AddBearerToken(IdentityConstants.BearerScheme);
builder.Services.AddAuthorizationBuilder();
下一步:
- 映射 identity 模型
- 为 identity 存储指定 DbContext
- 选择使用新端点
builder.Services.AddIdentityCore<MyUser>()
.AddEntityFrameworkStores<AppDbContext>()
.AddApiEndpoints();
调用 Build()
后,将 identity 端点映射到应用程序中的路由。
app.MapIdentityApi<MyUser>();
现在您可以将 API 配置为使用 identity。此端点访问 identity 以返回登录用户的名称,并且仅适用于经过身份验证的用户。
app.MapGet("/", (ClaimsPrincipal user) => $"Hello {user.Identity!.Name}").RequireAuthorization();
示例会话如下所示:
POST
到/register
端点以注册用户。
{
"user" : "test",
"password" : "@T35t!",
"email" : "test@notadomain.xyz"
}
POST
到/login?cookieMode=false&persistCookies=false
端点以登录用户。
{
"user" : "test",
"password" : "@T35t!"
}
- 接收
access_token
、expiration 和refresh_token
。
{
"token_type": "Bearer",
"access_token": "CfDJ9NHobblyWobblyGobblyGoop...",
"expires_in": 3600,
"refresh_token": "TokenBabbelYabbaDabbaDoo..."
}
- 通过将请求的
Authentication
标头设置为Bearer xxx
(其中xxx
是access_token
),使用令牌调用受保护的 API。 - 当用户的凭据过期或即将过期时,
POST
到/refresh
端点并传递refresh_token
。
{
"refreshToken": "TokenBabbelYabbaDabbaDoo..."
}
- 这将生成新的
access_token
、expiration 和refresh_token
。
这些新的构建模块可以更轻松地构建非委托验证的身份验证程序。
单页应用程序
新的 Visual Studio 模板
我们一直与 Visual Studio 团队密切合作,以确保 Visual Studio JavaScript 和 TypeScript 开发体验能给 ASP.NET Core 开发人员带来出色的体验。Visual Studio 包含适用于 Angular、React 和 Vue 的新项目模板,这些模板基于新的 JavaScript 项目系统 (.esproj) 构建,并与 ASP.NET Core 后端项目集成。
这些 Visual Studio 模板为 .NET 和 JavaScript 开发人员提供了丰富的功能:
- 快速入门 JavaScript 前端和 ASP.NET Core 后端。
- 及时了解最新的前端框架版本。
- 与最新的前端框架命令行工具集成。
- JavaScript 和 TypeScript 的模板。
- 丰富的 JavaScript 和 TypeScript 代码编辑经验。
- 清晰的前端和后端项目分离。
- 将 JavaScript 构建工具与您的 .NET 构建集成。
- npm 依赖管理 UI。
- 与 Visual Studio Code 调试和启动配置兼容。
- 使用您最喜欢的 JavaScript 测试框架在测试资源管理器中运行前端单元测试。
为了专注于为前端 JavaScript 框架与 ASP.NET Core 的使用提供最佳的开发体验,我们从 .NET 8 SDK 中删除了现有的 Angular 和 React 模板,转而采用新的 Visual Studio 模板。 我们正在与 Visual Studio 团队合作,进一步改进带有 ASP.NET Core 模板的新 Visual Studio JavaScript,以支持跨平台开发、与 ASP.NET Core 客户端 Web 资产集成、简化发布并面向所有受支持的 .NET 版本。
您可以通过安装最新的 Visual Studio 预览版,然后按照 Visual Studio 文档中的 Angular、React 和 Vue 教程中的任意一个来尝试新的 Visual Studio JavaScript 模板。 如果您对新模板有反馈,可以使用 Visual Studio 发送反馈工具与我们分享。
提供反馈
我们希望您喜欢 .NET 8 中的 ASP.NET Core 预览版。请在 GitHub 上提交问题,让我们知道您对这些新改进的看法。
感谢您使用 ASP.NET Core!