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?