ASP.NET Core SignalR 配置与集成测试究极指南

2024-05-07 20:28

本文主要是介绍ASP.NET Core SignalR 配置与集成测试究极指南,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

这篇文章也可以在我的博客中查看

前言

哥们最近都在埋头苦干,沉默是金,有一段时间没更新博客了。然而今儿SignalR集成测试实属是给我整破防了。虽说SignalR是.NET官方维护的实时通信库,已经开发了有十几年,甚至已经编入至了core dll,然而更新迭代异常迅速,导致文档不全,出了事不知所措。这不最近在集成测试SignalR这点上就踩了大坑。

今天就给大伙分享一下如何配置SignalR,并重点讲解如何在 .NET 8 中使用xUnitMicrosoft.AspNetCore.Mvc.Testing.WebApplicationFactory对最新版(ASP.NET Core)SignalR进行集成测试,希望后来者可以少走弯路。

痛点

SignalR测试为何困难,原因有下:

  1. WebApplicationFactory,或者说其背后的TestServer,并不提供真的服务器环境,所有默认配置下的网络客户端(当然包括HttpClient)都无法连接至该模拟的服务器。
    • 然而SignalR客户端所有连接都是在默认网络环境下的,需要替换成TestServer环境下的客户端
  2. HttpClient并不提供WebSocket连接支持。
    • 然而SignalR实时通讯首选的是WebSocket,所以我们还要配个TestServer环境下的WebSocket客户端
  3. Hub受身份验证保护。
    • 替换成TestServer客户端的时候还需要考虑身份验证

汗流浃背了家人们

关于本文

本文按这三个问题为思路逐步进行,最合理的解决方案会在文末给出。

如果你觉得TL;DR、不想关注过程、或者认为看代码比看文章舒服,可以跳转到文章最后获取项目源码👇

本文只介绍SignalR配置与集成测试,阅读本文前建议做以下准备工作(本文可能不会介绍以下内容):

  1. SignalR的使用(只提及部分)
  2. 配置[Authorize]身份认证(只一笔带过)
  3. 配置.NET集成测试框架,如 xUnit
  4. 配置WebApplicationFactory

本文操作环境:

  1. .NET 8
  2. xUnit 测试框架

无身份验证SignalR

在引入复杂性之前,应先处理最核心的配置,因此先不配置身份验证。

基本配置

配置Hub

在 .NET 8 中,SignalR已经集成至ASP.NET Core中,因此不需要下载任何Nuget包就能够使用。

配置也十分简洁,首先需要创建一个HubHub相当于是SignalR中的控制器。
创建Hub非常简单,只需要继承Hub即可。以下例子展示了一个最基本的收发消息ChatHubSendMessage向所有连接广播一条消息:

using Microsoft.AspNetCore.SignalR;namespace SignalR.IntegrationTests;public class ChatHub : Hub
{public async Task SendMessage(string message){await Clients.All.SendAsync("ReceiveMessage", message);}
}
  1. SendMessage是客户端向服务端发送消息的入口
    • 该方法可以有返回值,返回值会传回调用者
  2. ReceiveMessage是服务端向客户端发送消息的入口
    • message是参数,参数不一定只有一个,也不一定为string
  3. A向B发送一条聊天信息其实需要经历两次交互
    1. A向服务器发送消息
    2. 服务器向B发送消息

配置Program.cs

Program.cs中注册SignalR组件,最简单的配置如下:

const string HubsPrefix = "/hubs"; // <-- Grouped by prefix /hubsvar builder = WebApplication.CreateBuilder(args);builder.Services.AddSignalR(); // <-- Add SignalRvar app = builder.Build();app.MapGroup(HubsPrefix).MapHub<ChatHub>("/chat"); // <-- Map your ChatHub to /hubs/chatapp.Run();

强类型Hub

上面的例子中,服务端消息方法ReceiveMessage是字符串,众所周知字符串意味着弱类型,无编译时提示,稍不留神可能就会写错。
.NET提供了一个做法强类型化这些方法。

首先定义一个接口:

public interface IChatClientProxy
{public Task ReceiveMessage(string message);
}

由于客户端还是需要以字符串订阅消息,因此函数应以客户端的角度进行命名:

  1. Receive而不是Send
  2. 虽然是异步方法,但不加Async后缀

然后将ChatHub修改如下:

public class ChatHub : Hub<IChatClientProxy>
{public async Task SendMessage(string message){await Clients.All.ReceiveMessage(message);}
}

SignalR会自动实现IChatClientProxy接口,当调用这个接口的方法时,对应名称的消息就会被发出。

在Hub外向客户端发送消息

更多时候我们会在Hub之外发送消息,就需要借助IHubContext获取Hub上下文。这个接口也支持强类型化。
以下实现了一个简单的服务,先做一系列检测和记录,再使用IHubContext实现实时发送消息:

using Microsoft.AspNetCore.SignalR;namespace SignalR.IntegrationTests;public class ChatService(IHubContext<ChatHub, IChatClientProxy> _hubContext)
{public async Task SendMessageToAllAsync(string message){// Chek for permissions...// Record to database...// ...await _hubContext.Clients.All.ReceiveMessage(message);}
}

为了保持程序中的一致性,通常情况下也会希望在Hub中引用自己的服务,而不是直接发送消息:

public class ChatHub(ChatService _chatService) : Hub<IChatClientProxy>
{public async Task SendMessage(string message){await _chatService.SendMessageToAllAsync(message);}
}

别忘了在Program.cs中为自己的服务注册依赖注入:

builder.Services.AddScoped<ChatService>();

在客户端中接收SignalR消息

呃,严格意义上你无法在服务端中接收SignalR消息,你需要一个客户端接收服务端发出的信息。

以下代码是客户端代码,它可能位于另一个项目,可以是另一种语言实现,甚至可以处于另一个平台(e.g. Android)
但是它也可以碰巧是同一个平台,又碰巧是C#实现,甚至碰巧在同一个项目 😉

总之如果要在C#中接收SignalR消息,你需要安装客户端Nuget包Microsoft.AspNetCore.SignalR.Client
下面的例子展示了如何向服务端的ChatHub收发消息:

using Microsoft.AspNetCore.SignalR.Client;
using System.Diagnostics;var connection = new HubConnectionBuilder().WithUrl("http://localhost/hubs/chat").Build();
// Add receive message handler.
connection.On<string>("ReceiveMessage", (message) => Debug.WriteLine(message));await connection.StartAsync();// Send message.
await connection.InvokeAsync("SendMessage", "Hello World");await connection.StopAsync();
  1. On方法用于接收消息。注意泛型参数一定要与服务端的类型兼容,否则可能收不到对应消息
  2. InvokeAsync方法用于发送消息。第一个参数是远程方法名,第二个起是远程方法对应的参数
    1. 该方法可以有泛型参数TResult,以接受对应类型的返回值
  3. HubConnectionBuilder还可以配置断线重连、身份验证等功能,具体请查阅官方文档

集成测试

准备工作

进行下一步之前,需要先:

  1. 新建一个 xUnit 项目
  2. 添加主项目为依赖项
  3. 在测试项目中安装并配置WebApplicationFactory
  4. 在测试项目中安装Microsoft.AspNetCore.SignalR.ClientNuget包

测试用例

根据含义,我们会尝试使用SignalR客户端发送一条消息,然后断言能够收到消息:

using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.SignalR.Client;public class WebAppFactory : WebApplicationFactory<Program> { }public class HubIntegrationTests(WebAppFactory _factory) : IClassFixture<WebAppFactory>
{private HubConnection SetupHubConnection(string path){var uri = new Uri(_factory.Server.BaseAddress, path);return new HubConnectionBuilder().WithUrl(uri).Build();}[Fact]public async Task MessageTest(){// --> Arrangevar connection = SetupHubConnection("/hubs/chat");string? received = null;connection.On<string>("ReceiveMessage", (m) => received = m);await connection.StartAsync();string message = "Hello World";// --> Actawait connection.InvokeAsync("SendMessage", message);// Wait for messages to be received. You may need to increase the delay if you're running in a slow environment.await Task.Delay(1);// --> AssertAssert.Equal(message, received);}
}
  • SetupHubConnection函数用作连接SignalR服务器。
  • 其中等待了1毫秒以确保有足够的时间接收消息
    • 如果你的测试环境是老爷机,可能需要增加等待时间

然而这个用例会失败,错误如下:

System.Net.Http.HttpRequestException : No connection could be made because the target machine actively refused it. (localhost:80)

原因是TestServer并不是真的服务器,它只模拟ASP.NET应用服务器的行为,而不会在宿主机环境中启动真的服务器。因此我们使用常规的方式进行连接是无法访问的。但没有关系……

非WebSocket传输模式的测试

TestServer提供了一个用于连接至测试服务器的HttpMessageHandler对象,也就是任何支持HttpMessageHandler进行Http数据交换的库都可以通过使用该对象访问TestServer
经常接触.NET测试的伙伴此时已经要素察觉了:HttpClientHttpMessageHandler就是原生支持的!

然后还有两个好消息:

  1. WebSocket模式下的SignalR发起的连接使用的就是HttpClient
    • 没错,只是非WebSocket,但总比连接失败要好!
  2. SignalR提供了一个配置项,可以替换内部HttpClient使用的HttpMessageHandler

所以解决方案很简单,只需要将上述SetupHubConnection函数修改成以下形式:

private HubConnection SetupHubConnection(string path)
{var server = _factory.Server;var uri = new Uri(server.BaseAddress, path);return new HubConnectionBuilder().WithUrl(uri, o =>{o.HttpMessageHandlerFactory = _ => server.CreateHandler();}).Build();
}

TestServer.CreateHandler()生成了一个HttpMessageHandler,将它赋值给HttpMessageHandlerFactory,可以改变其内部HttpClient的连接行为,使其得以与TestServer进行交互。

问题

虽然测试是能通过了,但是注意到测试时间长达4秒,这对于本地服务器来讲显然是不正常的:

========== Starting test run ==========
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.5.3.1+6b60a9e56a (64-bit .NET 8.0.4)
[xUnit.net 00:00:00.04]   Starting:    SignalR.IntegrationTests.Tests
[xUnit.net 00:00:04.37]   Finished:    SignalR.IntegrationTests.Tests
========== Test run finished: 1 Tests (1 Passed, 0 Failed, 0 Skipped) run in 4.4 sec ==========

原因是因为产生了等待。事实上,这个用例并没有建立WebSocket连接,而是在等待WebSocket连接超时后,转为了使用LongPolling模式连接。
如果我们强制限制SignalR客户端使用WebSocket连接:

private HubConnection SetupHubConnection(string path)
{var server = _factory.Server;var uri = new Uri(server.BaseAddress, path);return new HubConnectionBuilder().WithUrl(uri, o =>{o.Transports = Microsoft.AspNetCore.Http.Connections.HttpTransportType.WebSockets; // WebSockets only.o.HttpMessageHandlerFactory = _ => server.CreateHandler();}).Build();
}

这个用例会在4秒后超时失败:

System.AggregateException : Unable to connect to the server with any of the available transports. (WebSockets failed: Unable to connect to the remote server) (ServerSentEvents failed: The transport is disabled by the client.) (LongPolling failed: The transport is disabled by the client.)

轮询并不是一般情况下的连接方式,而且我们也不希望每个连接都等待4秒,所以,有没有办法能够进行Socket连接?

WebSocket传输模式的测试

WebSocket连接失败的原因是WebSocketClient独立于HttpClient,虽然我们构建了SignalR内部HttpClientTestServer之间的连接,但是并没有改变WebSocketClient,它仍然是向真正的宿主机环境建立连接,所以必然会失败。

但是没有关系,这个问题早在几年前就被SignalR团队注意到,并提供了替换WebSocketClient的配置项:

private HubConnection SetupHubConnection(string path)
{var server = _factory.Server;var uri = new Uri(server.BaseAddress, path);return new HubConnectionBuilder().WithUrl(uri, o =>{o.Transports = HttpTransportType.WebSockets;o.HttpMessageHandlerFactory = _ => server.CreateHandler();// Support WebSocket transports.o.WebSocketFactory = async (context, cancellationToken) =>{var wsClient = server.CreateWebSocketClient();return await wsClient.ConnectAsync(context.Uri, cancellationToken);};o.SkipNegotiation = true;}).Build();
}

通过配置WebSocketFactory,可以将默认的WebSocketClient换成TestServer提供的客户端。从而能够对其进行WebSocket访问。
在WebSocket模式下,顺便设置了SkipNegotiation,可以减少协商时间,而不会影响结果。

这里其实可以省略HttpMessageHandlerFactory的配置,因为使用WebSocket时不会用到HttpClient。但如果使用LongPolling则很重要,因此还是保留以供选择。

修改了WebSoketClient配置后,重新运行测试用例,这次可以快速以WebSocket模式通过测试:

========== Starting test run ==========
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.5.3.1+6b60a9e56a (64-bit .NET 8.0.4)
[xUnit.net 00:00:00.03]   Starting:    SignalR.IntegrationTests.Tests
[xUnit.net 00:00:00.21]   Finished:    SignalR.IntegrationTests.Tests
========== Test run finished: 1 Tests (1 Passed, 0 Failed, 0 Skipped) run in 216 ms ==========

带身份验证SignalR

身份配置

SignalR的身份验证方式

SignalR可以使用CookieToken令牌两种方式进行身份认证。

Cookie是浏览器环境下的首选方式,可以自动传递凭证;而Token则是非浏览器客户端下最简便的做法。
由于Cookie开箱即用,不需要做额外配置,因此本文只重点介绍Token做法。

SignalR Token令牌传递方式

根据SignalR文档,在不同情况下有不同的传达方式:

  1. 在非浏览器环境中,以Authorization请求头的方式传递
  2. 在浏览器环境的WebSocket, Server Side Event模式下,无法使用自定义请求头,需要以查询字符串的方式传递
    • 该查询字符串需要在身份验证服务器自行读取接收

服务端配置接收access_token

所有无法自定义连接请求头的情况下,都约定使用一个写死的(😅微软你也干这事啊)查询字符串access_token作为身份认证的参数。

你写死不要紧,要紧的是我们使用SignalR是需要手动处理这个查询字符串的,否则这种情况下永远无法触发身份验证。

虽然官网有说明,但是总有像我一样的愣头青不喜欢看官方文档然后捣鼓了一整天才发现涅麻麻的要手动配置这个查询字符串。

所以为了减少愣头青,请你务必:
按照以下操作配置查询字符串!
按照以下操作配置查询字符串!
按照以下操作配置查询字符串!

接收查询字符串

需要在SignalR服务端中主动接收这个查询字符串。
使用不同的身份验证库,需要以不同的方式进行接收:

  1. 你使用了内置的JWT库或者Identity Server,可以参照官方文档进行配置
  2. 你使用了Identity内置的BearerToken,可以在Bearer Token中间件进行配置(见下文)
  3. 你使用了其它的身份验证库,基本也是相同的套路:需要在验证请求事件中手动将该查询字符串赋值为用户凭证
Identity 内置Bearer Token身份验证

我这里使用了 .NET 8 Identity的内置BearerToken,所以能够实现目标的最小配置Program.cs是这样的:

using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using SignalR.IntegrationTests;const string HubsPrefix = "/hubs";var builder = WebApplication.CreateBuilder(args);builder.Services.AddAuthorization();
builder.Services.AddAuthentication(IdentityConstants.BearerScheme).AddCookie(IdentityConstants.ApplicationScheme).AddBearerToken(IdentityConstants.BearerScheme, o =>{o.Events = new(){OnMessageReceived = context =>{var accessToken = context.Request.Query["access_token"];var path = context.HttpContext.Request.Path;// If the request is for our hub...if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments(HubsPrefix)){// Read the token out of the query stringcontext.Token = accessToken;}return Task.CompletedTask;}};});
builder.Services.AddIdentityCore<IdentityUser>().AddApiEndpoints().AddEntityFrameworkStores<IdentityDbContext>();
builder.Services.AddDbContext<IdentityDbContext>(x => x.UseInMemoryDatabase("db"));builder.Services.AddSignalR();var app = builder.Build();app.MapIdentityApi<IdentityUser>();app.MapGroup(HubsPrefix).MapHub<ChatHub>("/chat");app.Run();

使用身份验证保护Hub

与Controller一样,通过使用AuthorizeAllowAnonymous特性控制对Hub的访问

[Authorize]
public class ChatHub(ChatService _chatService) : Hub<IChatClientProxy>
{// ......
}

为Hub连接提供身份验证

Cookie验证
  1. 浏览器环境中,正常使用Cookie登录,凭证会在请求时自动携带
  2. 非浏览器环境中,可以通过手动设置Cookie请求头实现Cookie验证
    • 但这种做法不如使用Token更加正规
Token令牌验证

Token可以在客户端发起连接前使用AccessTokenProvider提供。

var connection = new HubConnectionBuilder().WithUrl("http://localhost/hubs/chat", options =>{options.AccessTokenProvider = () => Task.FromResult(token);}).Build();

考虑到重连与Token过期问题,AccessTokenProvider接受的是一个工厂函数,你可以选择动态获取新Token,而不是写死一个值

集成测试

由于我们替换了默认的WebSocketClient,我们需要手动携带Token令牌,以支持WebSocket模式下的身份验证;非WebSocket的身份验证仍然使用AccessTokenProvider配置项,无需修改。因此修改SetupHubConnection方法:

  1. 配置AccessTokenProvider参数,使非WebSocket连接方式能够携带令牌
  2. token添加至WebSocketClient中,使WebSocket连接方式能够携带令牌。由于是非浏览器环境,有两种方案可以选择:
    1. 添加名为access_token的查询字符串
    2. 添加Authorization请求头

小孩子才做选择,我全都要。

private HubConnection SetupHubConnection(string path, string? token = null)
{var server = _factory.Server;var uri = new Uri(server.BaseAddress, path);return new HubConnectionBuilder().WithUrl(uri, o =>{o.Transports = HttpTransportType.WebSockets;o.HttpMessageHandlerFactory = _ => server.CreateHandler();o.WebSocketFactory = async (context, cancellationToken) =>{var wsClient = server.CreateWebSocketClient();if (token != null){// Authentication for socket transports. (Chooses one of these.)// Option1: Use request headers.wsClient.ConfigureRequest = request => request.Headers.Authorization = new($"Bearer {token}");// Option2: Add access token to query string.uri = new Uri(QueryHelpers.AddQueryString(context.Uri.ToString(), "access_token", token));// I like both ;)}else{uri = context.Uri;}return await wsClient.ConnectAsync(uri, cancellationToken);};o.SkipNegotiation = true;// Authentication for non-socket transports. (Can be omitted here.)o.AccessTokenProvider = () => Task.FromResult(token);}).Build();
}

最后在用例中指定token参数,即可成功通过测试。

Q: 我应该如何生成token令牌?

如何生成令牌取决于你身份验证的实现方式。

在使用WebApplicationFactory的集成测试中,你可以比较容易地使用真实的用户与正常的登录方式获取令牌;如果身份认证本身并不是集成测试的关键,你可以设法使用测试替身替换掉原有的身份验证程序。(但一般情况下这只会更麻烦)

如果你想了解如何以正常登录方式获取令牌,可以查看我的源码👇


至此,所有问题解决!现在我们可以用SignalR WebSocket模式对带身份认证的Hub进行集成测试了!

项目源码

  • Github

参考资料

  1. Authentication and authorization in ASP.NET Core SignalR
  2. [SignalR] Better integration with TestServer
  3. SignalR Hub auth?

这篇关于ASP.NET Core SignalR 配置与集成测试究极指南的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/968302

相关文章

Zookeeper安装和配置说明

一、Zookeeper的搭建方式 Zookeeper安装方式有三种,单机模式和集群模式以及伪集群模式。 ■ 单机模式:Zookeeper只运行在一台服务器上,适合测试环境; ■ 伪集群模式:就是在一台物理机上运行多个Zookeeper 实例; ■ 集群模式:Zookeeper运行于一个集群上,适合生产环境,这个计算机集群被称为一个“集合体”(ensemble) Zookeeper通过复制来实现

CentOS7安装配置mysql5.7 tar免安装版

一、CentOS7.4系统自带mariadb # 查看系统自带的Mariadb[root@localhost~]# rpm -qa|grep mariadbmariadb-libs-5.5.44-2.el7.centos.x86_64# 卸载系统自带的Mariadb[root@localhost ~]# rpm -e --nodeps mariadb-libs-5.5.44-2.el7

性能测试介绍

性能测试是一种测试方法,旨在评估系统、应用程序或组件在现实场景中的性能表现和可靠性。它通常用于衡量系统在不同负载条件下的响应时间、吞吐量、资源利用率、稳定性和可扩展性等关键指标。 为什么要进行性能测试 通过性能测试,可以确定系统是否能够满足预期的性能要求,找出性能瓶颈和潜在的问题,并进行优化和调整。 发现性能瓶颈:性能测试可以帮助发现系统的性能瓶颈,即系统在高负载或高并发情况下可能出现的问题

hadoop开启回收站配置

开启回收站功能,可以将删除的文件在不超时的情况下,恢复原数据,起到防止误删除、备份等作用。 开启回收站功能参数说明 (1)默认值fs.trash.interval = 0,0表示禁用回收站;其他值表示设置文件的存活时间。 (2)默认值fs.trash.checkpoint.interval = 0,检查回收站的间隔时间。如果该值为0,则该值设置和fs.trash.interval的参数值相等。

NameNode内存生产配置

Hadoop2.x 系列,配置 NameNode 内存 NameNode 内存默认 2000m ,如果服务器内存 4G , NameNode 内存可以配置 3g 。在 hadoop-env.sh 文件中配置如下。 HADOOP_NAMENODE_OPTS=-Xmx3072m Hadoop3.x 系列,配置 Nam

字节面试 | 如何测试RocketMQ、RocketMQ?

字节面试:RocketMQ是怎么测试的呢? 答: 首先保证消息的消费正确、设计逆向用例,在验证消息内容为空等情况时的消费正确性; 推送大批量MQ,通过Admin控制台查看MQ消费的情况,是否出现消费假死、TPS是否正常等等问题。(上述都是临场发挥,但是RocketMQ真正的测试点,还真的需要探讨) 01 先了解RocketMQ 作为测试也是要简单了解RocketMQ。简单来说,就是一个分

wolfSSL参数设置或配置项解释

1. wolfCrypt Only 解释:wolfCrypt是一个开源的、轻量级的、可移植的加密库,支持多种加密算法和协议。选择“wolfCrypt Only”意味着系统或应用将仅使用wolfCrypt库进行加密操作,而不依赖其他加密库。 2. DTLS Support 解释:DTLS(Datagram Transport Layer Security)是一种基于UDP的安全协议,提供类似于

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

Retrieval-based-Voice-Conversion-WebUI模型构建指南

一、模型介绍 Retrieval-based-Voice-Conversion-WebUI(简称 RVC)模型是一个基于 VITS(Variational Inference with adversarial learning for end-to-end Text-to-Speech)的简单易用的语音转换框架。 具有以下特点 简单易用:RVC 模型通过简单易用的网页界面,使得用户无需深入了

poj 1258 Agri-Net(最小生成树模板代码)

感觉用这题来当模板更适合。 题意就是给你邻接矩阵求最小生成树啦。~ prim代码:效率很高。172k...0ms。 #include<stdio.h>#include<algorithm>using namespace std;const int MaxN = 101;const int INF = 0x3f3f3f3f;int g[MaxN][MaxN];int n