SignalR Typenamehandling

I am trying to get SignalR to work with JsonSerializerSettings user settings for its payload, in particular, I am trying to set TypeNameHandling = TypeNameHandling.Auto .

The problem is that SignalR uses the parameters in hubConnection.JsonSerializer and GlobalHost.DependencyResolver.Resolve<JsonSerializer>() for its internal data structures and then causes all kinds of chaos (internal server crash when I set TypeNameHandling.All as the roughest for example, but with TypeNameHandling.Auto I also have problems, especially when IProgress<> ) callbacks are involved.

Is there any workaround, or am I just doing it wrong?

Sample code for demonstration:

Server:

 class Program { static void Main(string[] args) { using (WebApp.Start("http://localhost:8080")) { Console.ReadLine(); } } } public class Startup { public void Configuration(IAppBuilder app) { var hubConfig = new HubConfiguration() { EnableDetailedErrors = true }; GlobalHost.DependencyResolver.Register(typeof(JsonSerializer), ConverterSettings.GetSerializer); app.MapSignalR(hubConfig); } } public interface IFoo { string Val { get; set; } } public class Foo : IFoo { public string Val { get; set; } } public class MyHub : Hub { public IFoo Send() { return new Foo { Val = "Hello World" }; } } 

Customer:

 class Program { static void Main(string[] args) { Task.Run(async () => await Start()).Wait(); } public static async Task Start() { var hubConnection = new HubConnection("http://localhost:8080"); hubConnection.JsonSerializer = ConverterSettings.GetSerializer(); var proxy = hubConnection.CreateHubProxy("MyHub"); await hubConnection.Start(); var result = await proxy.Invoke<IFoo>("Send"); Console.WriteLine(result.GetType()); } 

General:

 public static class ConverterSettings { public static JsonSerializer GetSerializer() { return JsonSerializer.Create(new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.All }); } } 
+6
source share
2 answers

This can be done by taking advantage of the fact that your SignalR types and types are in different assemblies . The idea is to create a JsonConverter that applies to all types of your assemblies. When a type from one of your assemblies first appears in an object graph (possibly as a root object), the transformer temporarily sets jsonSerializer.TypeNameHandling = TypeNameHandling.Auto , then continue the standard serialization for the type, disconnecting itself for a while to prevent infinite recursion:

 public class PolymorphicAssemblyRootConverter : JsonConverter { [ThreadStatic] static bool disabled; // Disables the converter in a thread-safe manner. bool Disabled { get { return disabled; } set { disabled = value; } } public override bool CanWrite { get { return !Disabled; } } public override bool CanRead { get { return !Disabled; } } readonly HashSet<Assembly> assemblies; public PolymorphicAssemblyRootConverter(IEnumerable<Assembly> assemblies) { if (assemblies == null) throw new ArgumentNullException(); this.assemblies = new HashSet<Assembly>(assemblies); } public override bool CanConvert(Type objectType) { return assemblies.Contains(objectType.Assembly); } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { using (new PushValue<bool>(true, () => Disabled, val => Disabled = val)) // Prevent infinite recursion of converters using (new PushValue<TypeNameHandling>(TypeNameHandling.Auto, () => serializer.TypeNameHandling, val => serializer.TypeNameHandling = val)) { return serializer.Deserialize(reader, objectType); } } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { using (new PushValue<bool>(true, () => Disabled, val => Disabled = val)) // Prevent infinite recursion of converters using (new PushValue<TypeNameHandling>(TypeNameHandling.Auto, () => serializer.TypeNameHandling, val => serializer.TypeNameHandling = val)) { // Force the $type to be written unconditionally by passing typeof(object) as the type being serialized. serializer.Serialize(writer, value, typeof(object)); } } } public struct PushValue<T> : IDisposable { Action<T> setValue; T oldValue; public PushValue(T value, Func<T> getValue, Action<T> setValue) { if (getValue == null || setValue == null) throw new ArgumentNullException(); this.setValue = setValue; this.oldValue = getValue(); setValue(value); } #region IDisposable Members // By using a disposable struct we avoid the overhead of allocating and freeing an instance of a finalizable class. public void Dispose() { if (setValue != null) setValue(oldValue); } #endregion } 

Then at startup, you would add this converter to the default JsonSerializer , passing in the assemblies for which you want to apply "$type" .

Refresh

If for any reason it is inconvenient to pass the list of assemblies at startup, you can enable the objectType.Namespace converter. All types living in your specified namespaces will be automatically serialized using TypeNameHandling.Auto .

Alternatively, you can enter an Attribute that targets the assembly, class, or interface and includes TypeNameHandling.Auto in combination with the corresponding converter:

 public class EnableJsonTypeNameHandlingConverter : JsonConverter { [ThreadStatic] static bool disabled; // Disables the converter in a thread-safe manner. bool Disabled { get { return disabled; } set { disabled = value; } } public override bool CanWrite { get { return !Disabled; } } public override bool CanRead { get { return !Disabled; } } public override bool CanConvert(Type objectType) { if (Disabled) return false; if (objectType.Assembly.GetCustomAttributes<EnableJsonTypeNameHandlingAttribute>().Any()) return true; if (objectType.GetCustomAttributes<EnableJsonTypeNameHandlingAttribute>(true).Any()) return true; foreach (var type in objectType.GetInterfaces()) if (type.GetCustomAttributes<EnableJsonTypeNameHandlingAttribute>(true).Any()) return true; return false; } public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { using (new PushValue<bool>(true, () => Disabled, val => Disabled = val)) // Prevent infinite recursion of converters using (new PushValue<TypeNameHandling>(TypeNameHandling.Auto, () => serializer.TypeNameHandling, val => serializer.TypeNameHandling = val)) { return serializer.Deserialize(reader, objectType); } } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { using (new PushValue<bool>(true, () => Disabled, val => Disabled = val)) // Prevent infinite recursion of converters using (new PushValue<TypeNameHandling>(TypeNameHandling.Auto, () => serializer.TypeNameHandling, val => serializer.TypeNameHandling = val)) { // Force the $type to be written unconditionally by passing typeof(object) as the type being serialized. serializer.Serialize(writer, value, typeof(object)); } } } [System.AttributeUsage(System.AttributeTargets.Assembly | System.AttributeTargets.Class | System.AttributeTargets.Interface)] public class EnableJsonTypeNameHandlingAttribute : System.Attribute { public EnableJsonTypeNameHandlingAttribute() { } } 

Note - it was tested with various test cases, but not with SignalR itself, since I have not installed it at the moment.

TypeNameHandling Warning

When using TypeNameHandling pay attention to this warning from Newtonsoft Docs :

TypeNameHandling should be used with caution when your application deserializes JSON from an external source. Incoming types must be checked with a special SerializationBinder when deserializing with a value other than None.

For a discussion of why this might be necessary, see TypeNameHandling Caution in Newtonsoft Json .

+3
source

I know that this is a pretty old thread and that there is an accepted answer.

However, I had a problem due to which I could not get the server to read the received json correctly, that is, it only read the base classes

However, the solution to the problem was quite simple:

I added this line before the parameter classes:

 [JsonConverter(typeof(PolymorphicAssemblyRootConverter), typeof(ABase))] public class ABase { } public class ADerived : ABase { public AInner[] DifferentObjects { get; set;} } public class AInner { } public class AInnerDerived : AInner { } ... public class PolymorphicAssemblyRootConverter: JsonConverter { public PolymorphicAssemblyRootConverter(Type classType) : this(new Assembly[]{classType.Assembly}) { } // Here comes the rest of PolymorphicAssemblyRootConverter } 

No need to install JsonSerializer on the client proxy connection and add it to GlobalHost.DependencyResolver.

It took me a long time to figure this out, I use SignalR 2.2.1 on both the client and the server.

0
source

All Articles