c# - SignalR 集线器方法参数序列化

标签 c# signalr signalr-hub

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

我开始将我的项目从 WCF 轮询双工 (Silverlight 5 - ASP.NET 4.5) 迁移到 SignalR (1.1.2)。消息(数据契约)是基于接口(interface)的多态的。 (如 IMessage、MessageA : IMessage 等 - 实际上有一个由类实现的接口(interface)层次结构,但对于这个问题来说意义不大)。
(我知道多态对象对客户端不利,但客户端会将其作为 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);

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

现在,我最终定义了具有 JObject 参数类型的方法:
public void SendMessage(JObject data)

然后使用手动反序列化数据
JObject.ToObject<IMessage>(JsonSerializer)

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

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

最佳答案

我尝试使用 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文件。有人关心检查吗?

关于c# - SignalR 集线器方法参数序列化,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18020897/

相关文章:

c# - 按角色获取 ASP.NET Identity 用户

c# - 在 .NET Core 中从 HttpResponseMessage 转换为 IActionResult

c# - 域对象应该实现 IXmlSerializable 吗?

c# - 更新到 MVC5 后找不到/signalr/hubs 404

asp.net - 网站重启时自动恢复/重新连接 SignalR 连接

c# - 无法从 singelton 事件取消订阅 signalR 集线器功能

asp.net - signalR OnDisconnected() 取消任务

c# - 使用for循环删除数据库中的数据

.net-core - SignalR Core 如何获取服务器端的连接参数

c# - 如何在 MyClass :Hub 中存储数据