【问题标题】:SignalR hub method parameter serializationSignalR 集线器方法参数序列化
【发布时间】:2020-03-24 19:14:11
【问题描述】:

我需要 SignalR 开发人员的一些指导,什么是调整 HUB 方法参数序列化的最佳方法。

我开始将我的项目从 WCF 轮询双工 (Silverlight 5 - ASP.NET 4.5) 迁移到 SignalR (1.1.2)。消息(数据契约)是基于接口的多态的。 (像 IMessage、MessageA : IMessage 等 - 实际上有一个由类实现的接口层次结构,但它对这个问题没有多大意义)。 (我知道多态对象对客户端不利,但客户端会将其作为 JSON 处理,并且仅在服务器端或客户端(如果它是 .NET/Silverlight)上完成到对象的映射)

在集线器上我定义了这样的方法:

public void SendMessage(IMessage data) { .. }

我创建了自定义 JsonConverters 并验证了消息可以使用 Json.NET 进行序列化/反序列化。然后我用适当的设置替换了 DependencyResolver 中的 JsonNetSerializer。 Silverlight 客户端也是如此。到目前为止一切顺利。

但是当我将消息从客户端发送到服务器时(消息已正确序列化为 JSON - 在 Fiddler 中验证),服务器返回参数无法反序列化的错误。 在调试器的帮助下,我在 SignalR 中发现了一个错误(负责反序列化参数的 JRawValue 类在内部创建自己的 JsonSerializer 实例,忽略提供的实例)。通过替换似乎很容易修复

var settings = new JsonSerializerSettings
{
    MaxDepth = 20
};
var serializer = JsonSerializer.Create(settings);
return serializer.Deserialize(jsonReader, type);

var serializer = GlobalHost.DependencyResolver.Resolve<IJsonSerializer>();
return serializer.Parse(jsonReader, type);

但我也发现接口 IJsonSerializer 将在 SignalR 的未来版本中删除。基本上,我需要的是从 HUB 方法获取原始 JSON(或字节流),以便我可以自己反序列化它,或者可以通过指定转换器等来调整序列化程序。

现在,我最终定义了具有 JObject 参数类型的方法:

public void SendMessage(JObject data)

随后使用手动反序列化数据

JObject.ToObject<IMessage>(JsonSerializer)

方法。但我更愿意自定义序列化程序并在集线器方法上使用类型/接口。关于下一个 SignalR 的设计,什么是“正确的方法”?

我还发现有可能从我的代码中将原始 JSON 发送回客户端很有用,即,这样对象就不会再次被 SignalR 序列化。我怎样才能做到这一点?

【问题讨论】:

  • 好问题。很遗憾看到没有答案。我也对实现多态消息合约感兴趣。
  • 这个问题是否仍然与 2.x 相关?使用 2.x,您应该能够将更新后的 DependencyResolver 传递给 HubConfiguration 以进行 app.MapSignalR(config) 调用
  • 您如何找到有关 SignalR 中参数序列化如何工作的信息?一般来说,我很难找到有用的 SignalR 文档(尤其是关于 C# 客户端)。
  • @Qwertie 这是一篇相当老的帖子,但我想我已经反编译了(直接在 ReSharper 中或使用 Reflector、JustDecompile、DotPeek 等工具或任何你能找到的用于此目的的工具)。代码甚至可能已经开源了..

标签: c# signalr signalr-hub


【解决方案1】:

我尝试使用 EnableJsonTypeNameHandlingConverter 发布的 here 以及以下用于双向连接的客户端和服务器代码来更改客户端和服务器序列化配置。

如您所见,有代码可以在客户端和服务器上设置自定义序列化...但它不起作用!

using System;
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Client;
using Newtonsoft.Json;
using Owin;

class Program
{
    static void Main(string[] args)
    {
        // Ensure serialization and deserialization works outside SignalR
        INameAndId nameId = new NameAndId(5, "Five");
        string json = JsonConvert.SerializeObject(nameId, Formatting.Indented, new EnableJsonTypeNameHandlingConverter());
        var clone = JsonConvert.DeserializeObject(json, typeof(INameAndId), new EnableJsonTypeNameHandlingConverter());
        Console.WriteLine(json);

        // Start server
        // http://+:80/Temporary_Listen_Addresses is allowed by default - all other routes require special permission
        string url = "http://+:80/Temporary_Listen_Addresses/example";
        using (Microsoft.Owin.Hosting.WebApp.Start(url))
        {
            Console.WriteLine("Server running on {0}", url);

            // Start client side
            HubConnection conn = new HubConnection("http://127.0.0.1:80/Temporary_Listen_Addresses/example");
            conn.JsonSerializer.Converters.Add(new EnableJsonTypeNameHandlingConverter());

            // Note: SignalR requires CreateHubProxy() to be called before Start()
            var hp = conn.CreateHubProxy(nameof(SignalRHub));
            var proxy = new SignalRProxy(hp, new SignalRCallback());

            conn.Start().Wait();

            proxy.Foo();
            // AggregateException on server: Could not create an instance of type 
            // SignalRSelfHost.INameAndId. Type is an interface or abstract class 
            // and cannot be instantiated.
            proxy.Bar(nameId); 

            Console.ReadLine();
        }
    }
}

class Startup
{
    // Magic method expected by OWIN
    public void Configuration(IAppBuilder app)
    {
        //app.UseCors(CorsOptions.AllowAll);
        var hubCfg = new HubConfiguration();
        var jsonSettings = new JsonSerializerSettings();
        jsonSettings.Converters.Add(new EnableJsonTypeNameHandlingConverter());
        hubCfg.EnableDetailedErrors = true;
        hubCfg.Resolver.Register(typeof(JsonSerializer), () => JsonSerializer.Create(jsonSettings));
        GlobalHost.DependencyResolver.Register(typeof(JsonSerializer), () => JsonSerializer.Create(jsonSettings));
        app.MapSignalR(hubCfg);
    }
}

// Messages that can be sent to the server
public interface ISignalRInterface
{
    void Foo();
    void Bar(INameAndId param);
}
// Messages that can be sent back to the client
public interface ISignalRCallback
{
    void Baz();
}

// Server-side hub
public class SignalRHub : Hub<ISignalRCallback>, ISignalRInterface
{
    protected ISignalRCallback GetCallback(string hubname)
    {
        // Note: SignalR hubs are transient - they connection lives longer than the 
        // Hub - so it is generally unwise to store information in member variables.
        // Therefore, the ISignalRCallback object is not cached.
        return GlobalHost.ConnectionManager.GetHubContext<ISignalRCallback>(hubname).Clients.Client(Context.ConnectionId);
    }

    public virtual void Foo() { Console.WriteLine("Foo!"); }
    public virtual void Bar(INameAndId param) { Console.WriteLine("Bar!"); }
}

// Client-side proxy for server-side hub
public class SignalRProxy
{
    private IHubProxy _Proxy;

    public SignalRProxy(IHubProxy proxy, ISignalRCallback callback)
    {
        _Proxy = proxy;
        _Proxy.On(nameof(ISignalRCallback.Baz), callback.Baz);
    }

    public void Send(string method, params object[] args)
    {
        _Proxy.Invoke(method, args).Wait();
    }

    public void Foo() => Send(nameof(Foo));
    public void Bar(INameAndId param) => Send(nameof(Bar), param);
}
public class SignalRCallback : ISignalRCallback
{
    public void Baz() { }
}

[Serializable]
public class NameAndId : INameAndId
{
    public NameAndId(int id, string name)
    {
        Id = id;
        Name = name;
    }
    public int Id { get; set; }
    public string Name { get; set; }
}

[EnableJsonTypeNameHandling]
public interface INameAndId
{
    string Name { get; }
    int Id { get; }
}

SignalR 调用传递给GlobalHost.DependencyResolver 的 lambda 不少于 8 次,但最终它忽略了提供的序列化程序。

我找不到任何有关 SignalR 参数序列化的文档,因此我使用了 Rider 的反编译调试器来帮助了解发生了什么。

在 SignalR 内部有一个 HubRequestParser.Parse 方法,它使用正确的 JsonSerializer,但它实际上并没有反序列化参数。稍后在 DefaultParameterResolver.ResolveParameter() 中对参数进行反序列化,从而在以下调用堆栈中间接调用 CreateDefaultSerializerSettings()

JsonUtility.CreateDefaultSerializerSettings() in Microsoft.AspNet.SignalR.Json, Microsoft.AspNet.SignalR.Core.dll
JsonUtility.CreateDefaultSerializer() in Microsoft.AspNet.SignalR.Json, Microsoft.AspNet.SignalR.Core.dll
JRawValue.ConvertTo() in Microsoft.AspNet.SignalR.Json, Microsoft.AspNet.SignalR.Core.dll
DefaultParameterResolver.ResolveParameter() in Microsoft.AspNet.SignalR.Hubs, Microsoft.AspNet.SignalR.Core.dll
Enumerable.<ZipIterator>d__61<ParameterDescriptor, IJsonValue, object>.MoveNext() in System.Linq, System.Core.dll
new Buffer<object>() in System.Linq, System.Core.dll
Enumerable.ToArray<object>() in System.Linq, System.Core.dll
DefaultParameterResolver.ResolveMethodParameters() in Microsoft.AspNet.SignalR.Hubs, Microsoft.AspNet.SignalR.Core.dll
HubDispatcher.InvokeHubPipeline() in Microsoft.AspNet.SignalR.Hubs, Microsoft.AspNet.SignalR.Core.dll
HubDispatcher.OnReceived() in Microsoft.AspNet.SignalR.Hubs, Microsoft.AspNet.SignalR.Core.dll
PersistentConnection.<>c__DisplayClass64_1.<ProcessRequestPostGroupRead>b__5() in Microsoft.AspNet.SignalR, Microsoft.AspNet.SignalR.Core.dll
TaskAsyncHelper.FromMethod() in Microsoft.AspNet.SignalR, Microsoft.AspNet.SignalR.Core.dll
PersistentConnection.<>c__DisplayClass64_0.<ProcessRequestPostGroupRead>b__4() in Microsoft.AspNet.SignalR, Microsoft.AspNet.SignalR.Core.dll
WebSocketTransport.OnMessage() in Microsoft.AspNet.SignalR.Transports, Microsoft.AspNet.SignalR.Core.dll
DefaultWebSocketHandler.OnMessage() in Microsoft.AspNet.SignalR.WebSockets, Microsoft.AspNet.SignalR.Core.dll
WebSocketHandler.<ProcessWebSocketRequestAsync>d__25.MoveNext() in Microsoft.AspNet.SignalR.WebSockets, Microsoft.AspNet.SignalR.Core.dll
AsyncMethodBuilderCore.MoveNextRunner.InvokeMoveNext() in System.Runtime.CompilerServices, mscorlib.dll [5]
ExecutionContext.RunInternal() in System.Threading, mscorlib.dll [5]
ExecutionContext.Run() in System.Threading, mscorlib.dll [5]
AsyncMethodBuilderCore.MoveNextRunner.Run() in System.Runtime.CompilerServices, mscorlib.dll [5]
...

SignalR source code 中问题很明显:

// in DefaultParameterResolver
public virtual object ResolveParameter(ParameterDescriptor descriptor, IJsonValue value)
{
    // [...]
    return value.ConvertTo(descriptor.ParameterType);
}
// in JRawValue
public object ConvertTo(Type type)
{
    // A non generic implementation of ToObject<T> on JToken
    using (var jsonReader = new StringReader(_value))
    {
        var serializer = JsonUtility.CreateDefaultSerializer();
        return serializer.Deserialize(jsonReader, type);
    }
}
// in JsonUtility
public static JsonSerializer CreateDefaultSerializer()
{
    return JsonSerializer.Create(CreateDefaultSerializerSettings());
}
public static JsonSerializerSettings CreateDefaultSerializerSettings()
{
    return new JsonSerializerSettings() { MaxDepth = DefaultMaxDepth };
}

所以 SignalR 将您的自定义(反)序列化器用于其部分工作,而不是用于参数反序列化。

我想不通的是2015 answer on this other question 有 8 票,这似乎意味着这个解决方案在过去 4 年的某个时间点对某人有效,但如果是这样,那么一定有一个技巧我们不知道。

也许.NET Core version of SignalR 解决了这个问题。看起来该版本已被显着重构,不再具有DefaultParameterResolver.cs 文件。有人愿意检查吗?

【讨论】:

    【解决方案2】:

    如果您使用连接 API 而不是 Hub API,您可以处理 OnReceive 事件并将请求作为原始 JSON(字符串)获取。看看this example

    在 2.x 版本中添加了使用 Hub API 向客户端发送预序列化数据的功能,我不知道在 1.x 中有什么方法可以做到这一点(请参阅github issue

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-09-29
      • 2020-09-12
      • 2018-12-18
      • 1970-01-01
      • 2023-04-06
      • 1970-01-01
      相关资源
      最近更新 更多