SignalR hub method parameter serialization

2020-06-07 07:28发布

问题:

I would need some guidelines from SignalR developers what is the best way to tweak HUB method's parameters serialization.

I started migrating my project from WCF polling duplex (Silverlight 5 - ASP.NET 4.5) to SignalR (1.1.2). The message (data contract) is polymorphic based on interfaces. (Like IMessage, MessageA : IMessage, etc. - there is actually a hierarchy of interfaces implemented by classes but it is not of much significancy for the question). (I know polymorphic objects are not good for clients but the client will handle it as JSON and mapping to objects is done only on the server side or client if it is .NET/Silverlight)

On the hub I defined method like this:

public void SendMessage(IMessage data) { .. }

I created custom JsonConverters and verified the messages could be serialized/deserialized using Json.NET. Then I replaced JsonNetSerializer in DependencyResolver with proper settings. Similarly on the Silverlight client-side. So far so good.

But when I sent the message from client to server (message got serialized to JSON correctly - verified in Fiddler), the server returned an error that the parameter cannot be deserialized. With help of debugger, I found a bug in SignalR (JRawValue class responsible for deserialization of parameter creates internally its own instance of JsonSerializer ignoring the provided one). Seemed to be quite easy fix by replacing

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

with

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

but I also found that the interface IJsonSerializer is going to be removed in a future version of SignalR. What I need, basically, is to get either raw JSON (or byte stream) from HUB method so I could deserialize it by myself or a possibility to tweak the serializer by specifying converters, etc.

For now, I ended up with defining the method with JObject parameter type:

public void SendMessage(JObject data)

followed by manual deserialization of data using

JObject.ToObject<IMessage>(JsonSerializer)

method. But I would prefer to customize the serializer and having the type/interface on the hub method. What is the "right way" to do it regarding design of the next SignalR?

I also found useful to have a possibility to send back to clients raw JSON from my code, i.e. so that the object is not serialized again by SignalR again. How could I achieve this?

回答1:

I tried changing the client and server serialization configuration using the EnableJsonTypeNameHandlingConverter published here plus the following client and server code for a bidirectional connection.

As you can see, there is code to set up custom serialization on both the client and server... but it doesn't work!

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 calls the lambda passed to GlobalHost.DependencyResolver no less than 8 times, yet in the end it ignores the serializer provided.

I couldn't find any documentation on SignalR parameter serialization, so I used Rider's decompiling debugger to help find out what was going on.

Inside SignalR there's a HubRequestParser.Parse method which uses the correct JsonSerializer, but it does not actually deserialize the parameters. The parameters are deserialized later in DefaultParameterResolver.ResolveParameter() which indirectly calls CreateDefaultSerializerSettings() in the following call stack:

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]
...

In the SignalR source code the problem is evident:

// 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 };
}

So SignalR uses your custom (de)serializer for part of its job, just not for parameter deserialization.

What I can't figure out is that the 2015 answer on this other question has 8 votes, which seems to imply that this solution worked at some point for somebody in the last 4 years, but if so there must be a trick to it that we don't know about.

Perhaps the .NET Core version of SignalR fixes this problem. It looks like that version has been refactored significantly and no longer has a DefaultParameterResolver.cs file. Anyone care to check?



回答2:

If you use connection API instead of Hub API, you can handle OnReceive event and get requests as raw JSON (string). Take a look at this example.

Ability to send pre-serialized data to clients using Hub API was added in 2.x version and I don't know about any way to do that in 1.x (see github issue)