Alby's blog

世上没有巧合,只有巧合的假象。

0%

ASP.NET Core SignalR 迟到的、隐藏在角落的 RawResult

一、概述

ASP.NETASP.NET Core 中,如果服务端得到一个 JSON 字符串(比如从 Redis 缓存中获取),我们可以通过 Content 方法或直接创建 ContentResult 对象来作为 Action 的返回值。

1
2
var json = "{\"key\": \"value\"}";
return Content(json, "application/json");
1
2
3
4
5
return new ContentResult
{
Content = json,
ContentType = "application/json"
};

而在 SignalR 中,在 ASP.NET Core 7.0 之前,对于一个 JSON 字符串只能先反序列化,否则 Web 前端得用 JSON.parse() 处理一次。

本文主要记录测试 RawResult 的结果。

二、服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class KVObject
{
public string Key { get; set; }
}

public class ChatHub : Hub
{
public string Test1()
{
var json = "{\"Key\": \"1\"}";
return json;
}

public KVObject Test2()
{
var json = "{\"Key\": \"2\"}";
var obj = JsonSerializer.Deserialize<KVObject>(json)!;
return obj;
}

public RawResult Test3()
{
var json = "{\"Key\": \"3\"}";
return new RawResult(new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes(json)));
}

public async Task Test4()
{
var json = "{\"Key\": \"4\"}";
await Clients.Caller.SendAsync("Notification", json);
}

public async Task Test5()
{
var json = "{\"Key\": \"5\"}";
var obj = JsonSerializer.Deserialize<KVObject>(json)!;
await Clients.Caller.SendAsync("Notification", obj);
}

public async Task Test6()
{
var json = "{\"Key\": \"6\"}";
var rawResult = new RawResult(new ReadOnlySequence<byte>(Encoding.UTF8.GetBytes(json)));
// 异常:
// System.InvalidOperationException: The type 'System.ReadOnlySpan`1[System.Byte]' of property 'FirstSpan' on type 'System.Buffers.ReadOnlySequence`1[System.Byte]' is invalid for serialization or deserialization because it is a pointer type, is a ref struct, or contains generic parameters that have not been replaced by specific types.
await Clients.Caller.SendAsync("Notification", rawResult);
}
}

上述代码中,json 字符串变量可能是从其他进程(如数据库)、文本文件或网络获取。

在 .Net 7 中,Test6 会有异常,.Net 8 尚未测试。

三、客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<!DOCTYPE html>
<html>
<head>
<title>SignalR RawResult Test</title>
</head>
<body>
<script src="//cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.1/signalr.js"></script>
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', async function () {
var connection = new signalR.HubConnectionBuilder()
.withUrl('/chat')
.build();

connection.on('Notification', function (message) {
console.log("Notification:", message, typeof(message));
});

await connection.start();
var result1 = await connection.invoke('Test1');
console.log("Test1:", result1, typeof (result1));
var result2 = await connection.invoke('Test2');
console.log("Test2:", result2, typeof (result2));
var result3 = await connection.invoke('Test3');
console.log("Test3:", result3, typeof (result3));
await connection.invoke('Test4'); // 无返回值,但会收到一个通知
await connection.invoke('Test5'); // 无返回值,但会收到一个通知
await connection.invoke('Test6'); // 无返回值,但会收到一个通知
});
</script>
</body>
</html>

四、测试

注释 Test6 的调用后。console.log 输出如下:

1
2
3
4
5
Test1: {"Key": "1"} string
Test2: {key: '2'} object
Test3: {Key: '3'} object
Notification: {"Key": "4"} string
Notification: {key: '5'} object

通过查看 WebSocket 消息日志也能确认这一点(下面的日志经过手工整理):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Test1 的调用和返回值。返回值的类型是 string。
{"arguments":[],"invocationId":"0","target":"Test1","type":1}
{"type":3,"invocationId":"0","result":"{\"Key\": \"1\"}"}

// Test2 的调用和返回值。返回值的类型是 object。
{"arguments":[],"invocationId":"1","target":"Test2","type":1}
{"type":3,"invocationId":"1","result":{"key":"2"}}

// Test3 的调用和返回值。返回值的类型是 object。
{"arguments":[],"invocationId":"2","target":"Test3","type":1}
{"type":3,"invocationId":"2","result":{"Key": "3"}}

// Test4 的调用、返回值(null)和通知。通知的第一个值的类型是 string。
{"arguments":[],"invocationId":"3","target":"Test4","type":1}
{"type":3,"invocationId":"3","result":null}
{"type":1,"target":"Notification","arguments":["{\"Key\": \"4\"}"]}

// Test5 的调用、返回值(null)和通知。通知的第一个值的类型是 object。
{"arguments":[],"invocationId":"4","target":"Test5","type":1}
{"type":3,"invocationId":"4","result":null}
{"type":1,"target":"Notification","arguments":[{"key":"5"}]}

参考资料

RawResult 文档
RawResult 源码
ASP.NET Core SignalR