.NET Framework 4.8 SignalR返回IAsyncEnumerable序列化异常修复
你使用的ASP.NET SignalR 2.4.3(适配.NET Framework 4.8的版本)原生不支持IAsyncEnumerable<T>类型返回值的自动处理。该版本发布时间早于C# 8.0正式推出IAsyncEnumerable<T>的时间点,既不会自动枚举异步流返回元素集合,也不识别该类型为流式返回类型,默认JSON序列化逻辑会直接把编译器生成的异步迭代器状态机实例当作普通公共对象序列化,最终输出带<>1__state、`<>t__builder``这类编译器生成字段的异常JSON结构。
根据业务是否需要服务端流式推送能力,二选一即可:
方案1:返回全量结果集合(适配成本最低)
如果不需要边生成数据边向客户端推送,只需要最终拿到全量对象数组,直接在Hub方法中完成异步流的全量枚举,转成普通强类型集合返回即可,修改代码如下:
public class CargoListHub : Hub { public async Task<List<OrtecDetails>> GetOrtecDetails(int cargoListID) { var detailsList = new List<OrtecDetails>(); await foreach (var item in OrtecDetails.GetOrtecDetails(cargoListID)) { detailsList.Add(item); } return detailsList; } }
注意:使用该方案需要将项目的C#语言版本调整为8.0及以上,.NET Framework 4.8兼容C# 8.0语法,仅需在项目生成配置中修改语言版本参数即可使用await foreach语法。
方案2:实现服务端流式推送
SignalR 2.x原生支持流式返回,但仅识别IObservable<T>、ChannelReader<T>作为流式返回类型。你可以将IAsyncEnumerable<T>适配为ChannelReader<T>实现逐元素推送,首先在项目中安装兼容.NET Framework 4.8的System.Threading.ChannelsNuGet包,再修改Hub代码:
public class CargoListHub : Hub { public ChannelReader<OrtecDetails> GetOrtecDetails(int cargoListID) { var channel = Channel.CreateUnbounded<OrtecDetails>(); _ = ProcessStreamAsync(channel.Writer, cargoListID); return channel.Reader; } private async Task ProcessStreamAsync(ChannelWriter<OrtecDetails> writer, int cargoListID) { try { await foreach (var item in OrtecDetails.GetOrtecDetails(cargoListID)) { await writer.WriteAsync(item); } writer.TryComplete(); } catch (Exception ex) { writer.TryComplete(ex); } } }
采用该方案时,客户端可按照SignalR 2.x的流式调用规范,逐段接收服务端推送的OrtecDetails对象,无需等待所有数据加载完成。
内容的提问来源于stack exchange,提问作者André Reichelt

