作者:Daniel Roth - Principal Program Manager, ASP.NET
翻译:Alan Wang
排版:Alan Wang
.NET 8 Preview 4 现已可用,并包括了许多对 ASP.NET Core 的新改进。
以下是本预览版本中的新内容摘要:
- Blazor
- 使用 Blazor 组件进行流式渲染
- 使用 Blazor SSR 处理表单提交
- 在 Blazor 中路由到命名元素
- Webcil 为 Blazor WebAssembly 应用程序打包
- API 编写
- minimal API 中表单绑定的扩展支持
- API 项目模板包含
.http
文件
- Native AOT
- 编译时生成的 minimal API 的日志记录和异常处理
- ASP.NET Core 顶级 API 注释以去除警告
- 通过可配置的 HTTPS 支持缩小应用程序大小
- Worker Service 模板更新
- 在 slim builder 中配置的其他默认服务
- API 模板 JSON 配置更改
- 支持编译器生成的
IAsyncEnumerable
无法表达类型的 JSON 序列化
- 认证和授权
- 标识 API 端点
- 改进使用
IAuthorizationRequirementData
的自定义认证策略的支持
- ASP.NET Core metrics
更多有关 .NET 8 中 ASP.NET Core 的计划,请在 GitHub 中查看完整的 ASP.NET Core .NET 8 路线图。
开始
在 .NET 8 Preview 4 中开始 ASP.NET Core,请安装 .NET 8 SDK。
如果您在 Windows 上使用 Visual Studio,我们建议您安装最新的 Visual Studio 2022 preview。目前 Visual Studio for Mac 尚不支持 .NET 8 预览版。
升级现有项目
将现有 ASP.NET Core 应用从 .NET 8 Preview 3 升级到 .NET 8 Preview 4:
- 将应用程序的目标框架更新为
net8.0
- 将所有 Microsoft.AspNetCore.* 包引用更新为
8.0.0-preview.4.*
- 将所有 Microsoft.Extensions.* 包引用更新为
8.0.0-preview.4.*
查看 .NET 8 中 ASP.NET Core 所有突破性改变的完整列表。
Blazor
使用 Blazor 组件进行流式渲染
在 .NET 8 中,使用 Blazor 进行服务器端渲染(SSR)时,您现在可以在响应流上流式传输内容更新。流式渲染可以改善服务器端渲染页面的用户体验,这些页面需要执行长时间运行的异步任务才能完全渲染。
例如,为了渲染页面,您可能需要进行长时间运行的数据库查询或 API 调用。通常,渲染页面的所有异步任务都必须在渲染的响应可以发送之前完成,这可能会导致页面延迟加载。流式渲染最初使用占位符内容渲染整个页面,同时执行异步操作。一旦异步操作完成,更新的内容将通过同一响应连接发送到客户端,然后由 Blazor 修补到 DOM 中。这种方法的好处在于应用程序的主要布局可以尽可能快地渲染,一旦内容准备好,页面就会更新。
要启用流式渲染,您首先需要添加新的 Blazor 脚本。
<script src="_framework/blazor.web.js" suppress-error="BL9992"></script>
注意,如果你正在将此脚本添加到 Blazor 组件(如布局组件)中,则需要添加 suppress-error="BL9992"
属性,以避免出现在组件中使用脚本标记的错误。
然后,要为特定组件启用流式渲染,请使用 [StreamRendering(true)]
属性。这通常使用 @attribute Razor
指令完成:
@page "/fetchdata"
@using BlazorSSR.Data
@inject WeatherForecastService ForecastService
@attribute [StreamRendering(true)]
<PageTitle>Weather forecast</PageTitle>
<h1>Weather forecast</h1>
@if (forecasts is null)
{
<p><em>Loading...</em></p>
}
else
{
// Render weather forecasts
}
@code {
private string message;
protected override async Task OnInitializedAsync()
{
forecasts = await ForecastService.GetForecastAsync(DateOnly.FromDateTime(DateTime.Now));
}
}
如今,组件不再需要等待任何异步任务即可使用占位符内容(“Loading…”)完成初始渲染。异步任务完成后,更新的内容被流式传输到响应,然后由 Blazor 修补到 DOM 中。
使用 Blazor SSR 处理表单提交
现在可以使用 Blazor 组件来处理服务器端渲染的表单提交。
要启用从服务器处理表单提交,首先需要使用 CascadingModelBinder
组件设置模型绑定上下文。一个简单的方法是在应用程序的主布局中:
<CascadingModelBinder>
@Body
</CascadingModelBinder>
要在 Blazor 中定义一个表单,可以使用现有的 EditForm
组件和相应的输入组件,比如 InputText
、InputSelect
等等。
EditForm
组件将渲染一个标准的 HTML form
元素,因此您可以使用 method
属性来指定表单是否应该发送 POST 请求。 GET 请求不支持 EditForm
事件处理程序。
提交表单时,请求将被路由到相应的页面,然后由具有 handler
处理程序查询字符串参数指定的匹配表单处理程序名称的表单处理。可以使用 FormHandlerName
属性 为 EditForm
指定表单处理程序名称。如果页面上只有一个表单,则不需要指定名称。然后你可以使用 EditForm
事件处理表单提交。
对模型绑定和验证请求数据的支持尚未实现(即将实现!)但您可以使用FormDataProvider
服务手动处理请求数据。 FormDataProvider.Entries
属性提供对表单数据的访问, FormDataProvider.Name
属性指定所需的表单处理程序。
以下是 Blazor 中一个简单的服务器端渲染表单的样子:
@inject FormDataProvider FormData
<EditForm method="POST" Model="exampleModel" OnValidSubmit="HandleSubmit">
<InputText @bind-Value="exampleModel.Name" />
<button type="submit">Submit</button>
</EditForm>
@code {
ExampleModel exampleModel = new();
protected override void OnInitialized()
{
// Manually model bind the form data using the FormDataProvider service
if (FormData.Entries.TryGetValue("Name", out var nameValues))
{
exampleModel.Name = nameValues.First();
}
}
void HandleSubmit()
{
// Handle the submitted form data
}
public class ExampleModel
{
public string? Name { get; set; }
}
}
在 Blazor 中路由到命名元素
Blazor 现在支持使用客户端路由,使用标准 URL 片段导航到页面上的特定 HTML 元素。如果使用标准id
属性为 HTML 元素指定标识符,那么当 URL 片段与元素标识符匹配时,Blazor 将正确滚动到该元素。
Webcil 为 Blazor WebAssembly 应用程序打包
您现在可以使用 Blazor WebAssembly 应用程序试用新的 Webcil 软件包。Webcil .NET 程序集的 web 友好包。它会删除任何特定于本机 Windows 执行的内容,以避免在部署到组织下载或使用 .dll 文件的环境时出现问题。
要使 Blazor WebAssembly 应用程序能够使用 Webcil,请将 WasmEnableWebcil
属性添加到项目文件中:
<PropertyGroup>
<WasmEnableWebcil>true</WasmEnableWebcil>
</PropertyGroup>
如果您在环境中使用 .webcil 文件时遇到问题,请在 GitHub 上创建 issue 让我们知道。
API 编写
minimal API 中表单绑定的扩展支持
此预览版本介绍了对在 minimal API 中绑定到表单类型的扩展支持。现在可以在不需要FromForm
属性的情况下推断基于表单的参数。对基于表单的参数支持包括:IFormCollection
、IFormFile
以及 IFormFileCollection
。为表单参数推断 OpenAPI 元数据,以支持与 Swagger UI 的集成。
以下代码样例展示了如何实现一个 minimal API,通过利用从IFormFile
类型推断的绑定来处理文件上传。
var app = WebApplication.Create();
string GetOrCreateFilePath(string fileName, string filesDirectory = "uploadFiles")
{
var directoryPath = Path.Combine(app.Environment.ContentRootPath, filesDirectory);
Directory.CreateDirectory(directoryPath);
return Path.Combine(directoryPath, fileName);
}
async Task UploadFileWithName(IFormFile file, string fileSaveName)
{
var filePath = GetOrCreateFilePath(fileSaveName);
await using var fileStream = new FileStream(filePath, FileMode.Create);
await file.CopyToAsync(fileStream);
}
app.MapPost("/upload", async (IFormFile file) => {
var fileSaveName = Guid.NewGuid().ToString("N") + Path.GetExtension(file.FileName);
await UploadFileWithName(file, fileSaveName);
return TypedResults.Ok("File uploaded successfully!");
});
app.Run();
这一功能在使用基于运行时的代码生成以及在利用新的编译时代码生成的本地 AOT 场景的 minimal API 中得到支持。
注意:在应用程序中实现表单时,防御 XSRF 攻击 非常重要。 此 代码样例 概述了如何使用 ASP.NET 中的防伪服务来支持在 minimal API 中生成和验证防伪令牌。
API 项目模板包含 .http
文件
API 项目模板(通过dotnet new api
生成)现在包含一个.http
文件,可用于从 Visual Studio 中的新 HTTP 编辑器向应用程序中定义的端点发送请求。
@MyApi_HostAddress = http://localhost:5233
GET {{MyApi_HostAddress}}/todos/
Accept: application/json
###
GET {{MyApi_HostAddress}}/todos/1
Accept: application/json
###
Native AOT
编译时生成的 minimal API 的日志记录和异常处理
运行时生成的最小 API 支持在参数绑定失败时自动记录日志(或在开发环境中抛出异常)。在此预览中,我们引入了对在编译时通过 Request Delegate Generator (RDG)生成的 API 的相同支持。
考虑以下 API 端点,我们通过设置<EnableRequestDelegateGenerator>true</EnableRequestDelegateGenerator>
启用了 RDG:
var app = WebApplication.Create();
app.MapGet("/hello/{name}", (string name)
=> $"Hello {name}!");
app.MapGet("/age", (DateTime birthDate)
=> $"You're about {DateTime.Now.Year - birthDate.Year} years old!");
app.Run();
发送以下请求将抛出一个BadHttpRequestException
,因为路由或查询字符串中没有提供所需的name
参数。
curl "http://localhost:5056/hello"
Microsoft.AspNetCore.Http.BadHttpRequestException: Required parameter "string name" was not provided from route or query string.
....
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
类似地,向/age
端点发送带有不可解析的birthDate
值将抛出异常。
curl "http://localhost:5056/age?birthDate=invalidDate"
Microsoft.AspNetCore.Http.BadHttpRequestException: Failed to bind parameter "DateTime birthDate" from "invalidDate".
...
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
ASP.NET Core 顶级 API 注释以去除警告
为帮助开发人员了解哪些功能与 Native AOT 不兼容,我们注释了不能与 Native AOT 可靠工作的子系统的主要入口点。当从启用了 Native AOT 的应用程序调用这些方法时,开发人员将收到警告。例如,以下代码段将在调用AddControllers
时生成警告,以指示此 API 不安全。
通过可配置的 HTTPS 支持缩小应用程序大小
在预览版4中,我们进一步减少了不需要 HTTPS 或 HTTP/3 支持的应用程序的 Native AOT 二进制大小。这对于在 TLS 终端代理(例如,托管在 Azure 上)后面运行的应用程序来说非常常见。
当您使用新的 new WebApplication.CreateSlimBuilder
时,默认情况下不会包括此功能。它可以通过分别调用builder.WebHost.UseKestrelHttpsConfiguration()
或 builder.WebHost.UseQuic()
来重新添加。
由于这些和其他更改,我们可以从预览3更新我们的表:
我们在基准测试实验室中运行了一个简单的 ASP.NET Core API 应用程序,来比较使用和不使用 Native AOT 发布的应用程序大小、内存使用、启动时间和 CPU 负载的差异:
发布种类 | 启动时间(ms) | 应用程序大小(MB) |
---|---|---|
Default | 169 | 88.5 |
Native AOT – Preview 3 | 34 | 11.3 |
Native AOT – Preview 4 | 32 | 9.3 |
请注意应用程序大小这 2 MB 的减小。
您可以在我们的公共基准仪表盘上探索这些以及更多指标。
Worker Service 模板更新
ASP.NET Core 中的 Worker Service 模板(可通过dotnet new worker
获得)现在包括对--aot
标志的支持以启用 AOT 发布来创建 Worker Service 项目。
dotnet new worker -o WorkerWithAot --aot
模板也进行了更新,以利用简化的HostApplicationBuilder
来配置应用程序主机。
using WorkerWithAot;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
var host = builder.Build();
host.Run();
在 slim builder 中配置的其他默认服务
.NET 8 Preview 3 中引入的 WebApplication.CreateSlimBuilder
API,初始化应用程序中的基本功能,以最小化其部署大小。在 .NET 8 Preview 4 中,我们更新了SlimBuilder
使其包含以下功能,以改善开发体验,同时仍将应用程序的总体大小保持在10 MB 以下。
- appsettings.json 和 appsettings 的 JSON 文件配置
{EnvironmentName}.json - 用户机密配置
- 控制台日志记录
- 日志记录配置
API 模板 JSON 配置更改
我们在 .NET 8 Preview 3 中引入了新的 API 项目模板。在 Preview 4 中,使用 --aot
选项使用此模板创建的项目,已更改为将应用程序的源代码生成的 JsonSerializationContext
插入到 JsonSerializerOptions.TypeInfoResolverChain
的开头。以前生成的代码使用了JsonSerializerOptions.AddContext<T>
API,并且使用 Preview 3 版本的模板创建的任何项目都应更新为调用新的 API。
您可在 .NET 8 Preview 4发布 中阅读有关JsonSerializerOptions.TypeInfoResolverChain
API 的更多信息。
支持编译器生成的 IAsyncEnumerable 无法表达类型的 JSON 序列化
现在支持由 C# 编译器实现的IAsyncEnumerable<T>
实现的 JSON 序列化,从而在配置为发布 native AOT 的 ASP.NET Core 项目中开房了它们的使用。如果路由处理程序返回调用 API 的结果,该 API 使用IAsyncEnumerable<T>
和 yield return
异步返回枚举的结果,例如从数据库查询中具体化行(示例),那么这将非常有用。
您可以在 .NET 8 Preview 4发布中了解更多关于 JSON 序列化程序对无法表达类型的支持。
认证和授权
标识 API 端点
我们很高兴介绍MapIdentityApi<TUser>()
,它是一种扩展方法,添加了两个新的 API 端点(/register
和 /login
)。MapIdentityApi
的主要目标是使开发人员能够轻松地在基于 JavaScript 的单页应用程序(SPA)或 Blazor 应用程序中使用 ASP.NET Core Identity 进行身份验证。MapIdentityApi
没有使用基于 Razor Pages 的 ASP.NET Core Identity 提供的默认 UI,而是添加了更适合 SPA 应用程序和非浏览器应用程序的 JSON API 端点。
除了用户注册和登录之外,身份 API 端点还将在即将到来的预览中支持双重身份验证和电子邮件验证等功能。您可以在 ASP.NET Core GitHub 存储库上标记为 feature-token-identity 的 issue 中找到计划功能的列表。
以下展示了 ASP.NET Core 应用程序的Program.cs
,该应用程序使用 MapIdentityApi
来启用 opaque bearer token 和 cookie 身份验证。要单独启用 cookie 或令牌身份验证,您可以直接调用现有的AddCookie
或新的AddBearerToken
AuthenticationBuilder
扩展方法。两者都是通过下面的AddIdentityApiEndpoints
方法完成的:
// usings ...
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization();
builder.Services.AddDbContext<ApplicationDbContext>(
options => options.UseSqlite(builder.Configuration["ConnectionString"]));
builder.Services.AddIdentityApiEndpoints<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();
var app = builder.Build();
app.MapGroup("/identity").MapIdentityApi<IdentityUser>();
app.MapGet("/requires-auth", (ClaimsPrincipal user) => $"Hello, {user.Identity?.Name}!").RequireAuthorization();
app.Run();
// public class ApplicationDbContext : IdentityDbContext<IdentityUser> ...
在客户端上,假设httpClient
、 username
和 password
已在 .NET 控制台应用程序中初始化,则可以如下调用 /register
端点:
// Email confirmation will be added later.
// The request body is: { "username": "<username>", "password": "<password>" }
await httpClient.PostAsJsonAsync("/identity/register", new { username, password });
您可以使用 /login
端点登录并获得 opaque bearer token:
// 2fa flow will be added later.
// The request body is: { "username": "<username>", "password": "<password>" }
var loginResponse = await httpClient.PostAsJsonAsync("/identity/login", new { username, password });
// loginResponse is similar to the "Access Token Response" defined in the OAuth 2 spec
// {
// "token_type": "Bearer",
// "access_token": "...",
// "expires_in": 3600
// }
// refresh token is likely to be added later
var loginContent = await loginResponse.Content.ReadFromJsonAsync<JsonElement>();
var accessToken = loginContent.GetProperty("access_token").GetString();
httpClient.DefaultRequestHeaders.Authorization = new("Bearer", accessToken);
Console.WriteLine(await httpClient.GetStringAsync("/requires-auth"));
或者,如果您想获得一个 cookie,您可以在 /login
队列字符串中设置 ?cookieMode=true
:
// HttpClientHandler.UseCookies is true by default on supported platforms.
// The request body is: { "username": "<username>", "password": "<password>" }
await httpClient.PostAsJsonAsync("/identity/login?cookieMode=true", new { username, password });
Console.WriteLine(await httpClient.GetStringAsync("/requires-auth"));
我们期待您对我们早期工作的反馈,以改善 SPA 和移动应用程序的身份体验。
改进使用 IAuthorizationRequirementData
的自定义认证策略的支持
在此预览版本之前,向端点添加参数化授权策略需要编写大量代码。
- 为每个策略实现
AuthorizeAttribute
- 实现
AuthorizationPolicyProvider
以处理基于字符串的合同中的自定义策略 - 为策略实现
AuthorizationRequirement
- 为每个需求实现一个
AuthorizationHandler
下面是自定义参数化策略的部分实现。未删节版本包含完整的代码。
var builder = WebApplication.CreateBuilder();
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
builder.Services.AddSingleton<IAuthorizationPolicyProvider, MinimumAgePolicyProvider>();
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeAuthorizationHandler>();
var app = builder.Build();
app.MapControllers();
app.Run();
[ApiController]
[Route("api/[controller]")]
public class GreetingsController : Controller
{
[MinimumAgeAuthorize(16)]
[HttpGet("hello")]
public string Hello(ClaimsPrincipal user) => $"Hello {(user.Identity?.Name ?? "world")}!";
}
class MinimumAgeAuthorizeAttribute : AuthorizeAttribute { }
class MinimumAgePolicyProvider : IAuthorizationPolicyProvider { }
class MinimumAgeRequirement : IAuthorizationRequirement { }
class MinimumAgeAuthorizationHandler : AuthorizationHandler<MinimumAgeRequirement> { }
此预览引入了IAuthorizationRequirementData
接口,该接口允许属性定义也指定与授权策略相关联的要求。通过利用这一变化,我们可以用更少的代码行重新实现我们的自定义授权策略。未删节版本包含完整的代码。
var builder = WebApplication.CreateBuilder();
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeAuthorizationHandler>();
var app = builder.Build();
app.MapControllers();
app.Run();
[ApiController]
[Route("api/[controller]")]
public class GreetingsController : Controller
{
[MinimumAgeAuthorize(16)]
[HttpGet("hello")]
public string Hello(ClaimsPrincipal user) => $"Hello {(user.Identity?.Name ?? "world")}!";
}
class MinimumAgeAuthorizeAttribute : AuthorizeAttribute, IAuthorizationRequirement, IAuthorizationRequirementData
{
public MinimumAgeAuthorizeAttribute(int age) => Age =age;
public int Age { get; }
public IEnumerable<IAuthorizationRequirement> GetRequirements()
{
yield return this;
}
}
class MinimumAgeAuthorizationHandler : AuthorizationHandler<MinimumAgeAuthorizeAttribute>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MinimumAgeAuthorizeAttribute requirement) { ... }
}
ASP.NET Core metrics
Metrics 是随着时间的推移报告的测量结果,最常用于监控应用程序的运行状况和生成警报。例如报告失败 HTTP 请求的计数器可以显示在仪表盘中,或者在失败超过阈值时生成警报。
此预览版本使用 System.Diagnostics.Metrics 在整个 ASP.NET Core 中添加了新的指标。Metrics 是一个现代 API,用于报告和收集有关应用程序的信息。
与现有事件计数器相比,Metrics 提供了许多改进:
- 具有计数器、仪表和直方图的新型测量
- 具有多维价值的强大报告
- 通过与 Open Telemetry 标准保持一致,集成到更广泛的云原生生态系统中
Metrics 已经为 ASP.NET Core 托管、Kestrel 和 SignalR 添加了度量。预计未来会有更多的 .NET API 来获取 metrics。
如果您有兴趣尝试 metrics,我们已将 Grafana 仪表盘组合在一起,用于报告 Prometheus 收集的 ASP.NET Core metrics。您可以在 aspnetcore-grafana repository 获取仪表盘,并将其导入到您自己的 Grafana 环境中。
提供反馈
我们希望您喜欢 .NET 8 中的 ASP.NET Core 预览版。请在 GitHub 上提交 issue 让我们知道您对这些新改进的看法。
感谢您试用 ASP.NET Core!