使用 ASP.NET Core SignalR 重构
在本单元中,你将了解如何重构客户端轮询并添加 ASP.NET Core SignalR。 服务器和客户端应用程序都需要重构。 需要创建、映射并配置服务器中心。 然后,客户端需要建立与中心的连接,并处理订单状态更改通知。
NuGet 包
BlazingPizza.Server.csproj 文件添加了以下 NuGet 包引用:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>true</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="$(BlazorVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.AspNetCore.ApiAuthorization.IdentityServer" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="$(SignalRVersion)" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="$(EntityFrameworkVersion)" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="$(EntityFrameworkVersion)" />
<PackageReference Include="WebPush" Version="1.0.11" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BlazingPizza.Client\BlazingPizza.Client.csproj" />
<ProjectReference Include="..\BlazingPizza.Shared\BlazingPizza.Shared.csproj" />
</ItemGroup>
</Project>
BlazingPizza.Client.csproj 文件添加了以下 NuGet 包引用:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>true</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="$(BlazorVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="$(BlazorVersion)" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="$(BlazorVersion)" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="$(SignalRVersion)" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="$(SignalRVersion)" />
<PackageReference Include="Microsoft.Extensions.Http" Version="$(SystemNetHttpJsonVersion)" />
<PackageReference Include="System.Net.Http.Json" Version="$(SystemNetHttpJsonVersion)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BlazingComponents\BlazingComponents.csproj" />
<ProjectReference Include="..\BlazingPizza.ComponentsLibrary\BlazingPizza.ComponentsLibrary.csproj" />
<ProjectReference Include="..\BlazingPizza.Shared\BlazingPizza.Shared.csproj" />
</ItemGroup>
</Project>
添加 MessagePack NuGet 包是为了让客户端与服务器之间发送的消息变小。
重构的服务器应用
服务器负责公开 SignalR 终结点。 终结点映射到 Hub 或 Hub<T> 子类。 服务器公开可以从客户端调用的中心方法,并公开客户端可以订阅的事件。
添加 OrderStatusHub
ASP.NET Core SignalR 支持强类型中心实现 Hub<T>,其中 T
是指客户端类型。 请参考以下 IOrderStatusHubs.cs 文件:
namespace BlazingPizza.Server.Hubs;
public interface IOrderStatusHub
{
/// <summary>
/// This event name should match <see cref="OrderStatusHubConsts.EventNames.OrderStatusChanged"/>,
/// which is shared with clients for discoverability.
/// </summary>
Task OrderStatusChanged(OrderWithStatus order);
}
前面的接口定义了一种方法,该方法将充当客户端可以订阅的事件。 请参考以下 OrderStatusHub.cs 文件:
namespace BlazingPizza.Server.Hubs;
[Authorize]
public class OrderStatusHub : Hub<IOrderStatusHub>
{
/// <summary>
/// Adds the current connection to the order's unique group identifier, where
/// order status changes will be notified in real-time.
/// This method name should match <see cref="OrderStatusHubConsts.MethodNames.StartTrackingOrder"/>,
/// which is shared with clients for discoverability.
/// </summary>
public Task StartTrackingOrder(Order order) =>
Groups.AddToGroupAsync(
Context.ConnectionId, order.ToOrderTrackingGroupId());
/// <summary>
/// Removes the current connection from the order's unique group identifier,
/// ending real-time change updates for this order.
/// This method name should match <see cref="OrderStatusHubConsts.MethodNames.StopTrackingOrder"/>,
/// which is shared with clients for discoverability.
/// </summary>
public Task StopTrackingOrder(Order order) =>
Groups.RemoveFromGroupAsync(
Context.ConnectionId, order.ToOrderTrackingGroupId());
}
前面的中心实现公开了两个可从客户端调用的方法。 客户端调用 StartTrackingOrder
,并收到一个 order
实例,然后客户端的唯一连接将添加到将要发送通知的组中。 同样,如果调用 StopTrackingOrder
,该组的连接将断开,并且不再接收通知。
SignalR 服务器配置
需要更新 Startup
类,以便添加 ASP.NET Core SignalR 和 MessagePack 协议。 "/orderstatus"
终结点也将映射到 OrderStatusHub
实现。 请参考以下 Startup.cs 文件:
namespace BlazingPizza.Server;
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddRazorPages();
services.AddDbContext<PizzaStoreContext>(options =>
options.UseSqlite("Data Source=pizza.db"));
services.AddDefaultIdentity<PizzaStoreUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddEntityFrameworkStores<PizzaStoreContext>();
services.AddIdentityServer()
.AddApiAuthorization<PizzaStoreUser, PizzaStoreContext>();
services.AddAuthentication()
.AddIdentityServerJwt();
services.AddSignalR(options => options.EnableDetailedErrors = true)
.AddMessagePackProtocol();
services.AddHostedService<FakeOrderStatusService>();
services.AddSingleton<IBackgroundOrderQueue, DefaultBackgroundOrderQueue>();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseWebAssemblyDebugging();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<OrderStatusHub>("/orderstatus");
endpoints.MapRazorPages();
endpoints.MapControllers();
endpoints.MapFallbackToFile("index.html");
});
}
}
前面的代码突出显示了这些更改:
- 添加 SingalR 和 MessagePack 协议。
- 将
OrderStatusHub
映射到"/orderstatus"
终结点。
重构的客户端应用
客户端应用程序需要重构 OrderDetails.razor 文件:
@page "/myorders/{orderId:int}"
@attribute [Authorize]
<div class="main">
@if (_invalidOrder)
{
<h2>Nope</h2>
<p>Sorry, this order could not be loaded.</p>
}
else if (_orderWithStatus is null)
{
<text>Loading...</text>
}
else
{
<div class="track-order">
<div class="track-order-title">
<h2>
Order placed @_orderWithStatus.Order.CreatedTime.ToLongDateString()
</h2>
<p class="ml-auto mb-0">
Status: <strong>@_orderWithStatus.StatusText</strong>
</p>
</div>
<div class="track-order-body">
<div class="track-order-details">
<OrderReview Order="_orderWithStatus.Order" />
</div>
<div class="track-order-map">
<Map Zoom="13" Markers="_orderWithStatus.MapMarkers" />
</div>
</div>
</div>
}
</div>
你会注意到,该过程移除了多个指令:
- 标记中不再需要
@using System.Threading
指令。 @inject OrdersClient OrdersClient
指令现属于代码隐藏文件的一部分。- 在代码隐藏文件中,
@implements IDisopsable
已替换为 IAsyncDisposable 的实现。 @code { ... }
指令已被移除,因为所有的逻辑目前都位于代码隐藏文件中。
此外,Blazor 组件现在更加纯净,因为它主要表示绑定和模板化。 而逻辑已移到代码隐藏文件中。
使用代码隐藏文件扩展组件
随着 Blazor 组件中引入的逻辑越来越多,它们通常会演变成两个单独的文件:
- OrderDetails.razor:Razor 标记。
- OrderDetails.razor.cs:相应的 C# 代码隐藏文件。 它是同一组件,但只有一个
partial class
。
请参考 OrderDetails.razor.cs 文件:
namespace BlazingPizza.Client.Pages;
public sealed partial class OrderDetails : IAsyncDisposable
{
private HubConnection _hubConnection;
private OrderWithStatus _orderWithStatus;
private bool _invalidOrder;
[Parameter] public int OrderId { get; set; }
[Inject] public NavigationManager Nav { get; set; } = default!;
[Inject] public OrdersClient OrdersClient { get; set; } = default!;
[Inject] public IAccessTokenProvider AccessTokenProvider { get; set; } = default!;
protected override async Task OnInitializedAsync()
{
_hubConnection = new HubConnectionBuilder()
.WithUrl(
Nav.ToAbsoluteUri("/orderstatus"),
options => options.AccessTokenProvider = GetAccessTokenValueAsync)
.WithAutomaticReconnect()
.AddMessagePackProtocol()
.Build();
_hubConnection.On<OrderWithStatus>(
OrderStatusHubConsts.EventNames.OrderStatusChanged, OnOrderStatusChangedAsync);
await _hubConnection.StartAsync();
}
private async Task<string> GetAccessTokenValueAsync()
{
var result = await AccessTokenProvider.RequestAccessToken();
return result.TryGetToken(out var accessToken)
? accessToken.Value
: null;
}
protected override async Task OnParametersSetAsync()
{
try
{
_orderWithStatus = await OrdersClient.GetOrder(OrderId);
if (_orderWithStatus is null || _hubConnection is null)
{
return;
}
if (_orderWithStatus.IsDelivered)
{
await _hubConnection.InvokeAsync(
OrderStatusHubConsts.MethodNames.StopTrackingOrder, _orderWithStatus.Order);
await _hubConnection.StopAsync();
}
else
{
await _hubConnection.InvokeAsync(
OrderStatusHubConsts.MethodNames.StartTrackingOrder, _orderWithStatus.Order);
}
}
catch (AccessTokenNotAvailableException ex)
{
ex.Redirect();
}
catch (Exception ex)
{
_invalidOrder = true;
Console.Error.WriteLine(ex);
}
finally
{
StateHasChanged();
}
}
private Task OnOrderStatusChangedAsync(OrderWithStatus orderWithStatus) =>
InvokeAsync(() =>
{
_orderWithStatus = orderWithStatus;
StateHasChanged();
});
public async ValueTask DisposeAsync()
{
if (_hubConnection is not null)
{
if (_orderWithStatus is not null)
{
await _hubConnection.InvokeAsync(
OrderStatusHubConsts.MethodNames.StopTrackingOrder, _orderWithStatus.Order);
}
await _hubConnection.DisposeAsync();
_hubConnection = null;
}
}
}
前面的 C# 组件表示 OrderDetails
组件的代码隐藏文件,这可能是因为类被定义为 partial
。 它是 IAsyncDisposable
的实现,用于清理 HubConnection 实例。 该组件定义了多个类范围的字段,每个字段都有各自的意图:
_hubConnection
:用于调用 SignalR 服务器上的方法的连接。_orderWithStatus
:上下文订单及其当前状态。_invalidOrder
:指示订单是否无效的值。
在此次重构中,原来 @code { ... }
指令中所有需要的代码如今都在代码隐藏文件中:
[Parameter] public int OrderId { get; set; }
完全相同。- 由特性修饰的属性
[Inject] public OrdersClient OrdersClient { get; set; }
将取代@inject OrdersClient OrdersClient
指令。
SignalR 连接的配置需要一些额外的注入属性:
[Inject] public NavigationManager Nav { get; set; }
属性用于解析中心的终结点。[Inject] public IAccessTokenProvider AccessTokenProvider { get; set; }
属性用于将AccessTokenProvider
分配给中心连接的 options 对象。 这样可确保所有通信均正确提供已通过身份验证的用户的访问令牌。
OnInitializedAsync
替代方法使用 HubConnectionBuilder 对象来生成 _hubConnection
实例。 它配置为自动重新连接并指定 MessagePack 协议。 对于 _hubConnection
实例,该页面将订阅 "OrderStatusChanged"
中心事件,并将 OnOrderStatusChangedAsync
设置为其相应的事件处理程序。 然后,使用 HubConnection.StartAsync 启动中心连接。
OnOrderStatusChangedAsync
事件处理程序方法接受 OrderWithStatus
实例作为其参数,然后返回 Task。 它重新分配 _orderWithStatus
类变量并调用 StateHasChanged()。
分配 OrderId
后,将调用 OnParametersSetAsync
替代方法。 _orderWithStatus
对象的初始状态是通过 OrdersClient.GetOrder
调用分配的。 如果订单已送达,系统将不再跟踪订单,并使用 HubConnection.StopAsync 停止中心连接。 如果订单尚未送达,将继续跟踪订单。