8

I am trying to get SignalR to work with custom JsonSerializerSettings for its payload, specifically I'm trying to set TypeNameHandling = TypeNameHandling.Auto.

The problem seems to be, that SignalR uses the settings in hubConnection.JsonSerializer and GlobalHost.DependencyResolver.Resolve<JsonSerializer>() for its internal data structures as well which then causes all kinds of havoc (internal server crashes when I set TypeNameHandling.All as the most crass example, but with TypeNameHandling.Auto I also get problems, particularly when IProgress<> callbacks are involved).

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

Sample code to demonstrate:

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

Client:

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());
    }

Shared:

public static class ConverterSettings
{
    public static JsonSerializer GetSerializer()
    {
        return JsonSerializer.Create(new JsonSerializerSettings()
        {
            TypeNameHandling = TypeNameHandling.All
        });
    }
}
Voo
  • 29,040
  • 11
  • 82
  • 156
  • Is there a particular reason why you don't want to use the default Json Serializer from SignalR? – radu-matei Aug 31 '15 at 15:24
  • @Matei_Radu Because it uses `TypenameHandling.None` and I need `Auto`. – Voo Aug 31 '15 at 16:16
  • I don't have SignalR to test with; is the problem that you need the `$type` property on the *root* json object, or on some nested object? And if the former, is there any way you could relax that requirement, perhaps by returning a wrapper object as the root? – dbc Sep 04 '15 at 16:25
  • @dbc As soon as I start packing every parameter and return value into its own little wrapper that handles (de)serialising, I might as well throw SignalR completely out of the window.. or to be more exact as long as there's not any automated way to do that. The requirement is that I can deserialize an interface to its exact type. – Voo Sep 04 '15 at 16:34
  • All you need is a single generic wrapper: `public class Data { public T data { get; set; } }`. The difficulty here is that even if you set `TypenameHandling.Auto` (which I know how to do for you), it doesn't apply to the root object unless SignalR is internally calling a [specific overload of `Serialize`](http://www.newtonsoft.com/json/help/html/M_Newtonsoft_Json_JsonSerializer_Serialize_1.htm). – dbc Sep 04 '15 at 16:47
  • @dbc That alone wouldn't help. I would have to take the `T` and then (de)serialize it myself in the custom serializer that `Data` would need to have - not particularly pretty but doable (my current workaround actually). But if I apply this on a global scale all interfaces would be cluttered with `Data foo(Data arg)` which makes it unreadable and a maintenance bother. A really bad design decision of SignalR to use the same serializer settings for itself and for application data - even worse if they expose documented settings for it. – Voo Sep 04 '15 at 16:51

3 Answers3

9

This can be done by taking advantage of the fact that your types and the SignalR types are in different assemblies. The idea is to create a JsonConverter that applies to all types from your assemblies. When a type from one of your assemblies is first encountered in the object graph (possibly as the root object), the converter would temporarily set jsonSerializer.TypeNameHandling = TypeNameHandling.Auto, then proceed with the standard serialization for the type, disabling itself for the duration 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 in startup you would add this converter to the default JsonSerializer, passing in the assemblies for which you want "$type" applied.

Update

If for whatever reason it's inconvenient to pass the list of assemblies in at startup, you could enable the converter by objectType.Namespace. All types living in your specified namespaces would automatically get serialized with TypeNameHandling.Auto.

Alternatively, you could introduce an Attribute which targets an assembly, class or interface and enables TypeNameHandling.Auto when combined with the appropriate 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 - tested with various test cases but not SignalR itself since I don't currently have it installed.

TypeNameHandling Caution

When using TypeNameHandling, do take note of this caution from the Newtonsoft docs:

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

For a discussion of why this may be necessary, see TypeNameHandling caution in Newtonsoft Json.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • @Voo - did this answer not work with SignalR, or was there some reason that enabling `TypeNameHandling` by assembly or namespace was not convenient? – dbc Sep 10 '15 at 04:52
  • 1
    I was mostly hoping for a potentially more general solution, but this seems to be the best option at the moment. – Voo Sep 10 '15 at 06:46
  • 1
    SignalR 2.2, no luck with JsonSerializerSettings or this implementation. I have a method that returns JsonSerializer with this converter added to the Converters list. I register this method with GlobalHost.DependencyResolver. I also add it to the converter list in the client. Same behavior, no errors, same old list of base types on the server. – grinder22 Mar 17 '16 at 16:29
  • Also I have verified this works nicely in isolation. And I'm fairly confident I a ported it to my SignalR solution correctly. I'm thinking this might be a SignalR bug. – grinder22 Mar 17 '16 at 18:17
  • @dbc I know I am quite late at the party. But I can't resolve "PushValue". Where is that from? – Thypari Apr 11 '18 at 17:04
  • 1
    @Thypari - it's in the code from the first part of the answer, before the **Update**. Scroll down past `PolymorphicAssemblyRootConverter`. – dbc Apr 11 '18 at 17:05
  • @dbc I am dumb and maybe too sleep-deprived. Thanks! – Thypari Apr 11 '18 at 17:11
  • @dbc Too bad it's not working for me. I always only get the base class back and can't cast it to the derived class (error: not of type). Tried using TypeNameHandling.All. I added the converter as follows: `_hubConnection = new HubConnection(url); var converter = new EnableJsonTypeNameHandlingConverter(); _hubConnection.JsonSerializer.Converters.Add(converter);` – Thypari Apr 12 '18 at 09:36
  • 1
    @Thypari - I'm sorry it's not working for you. 1) If you are using `EnableJsonTypeNameHandlingConverter` did you add `EnableJsonTypeNameHandlingAttribute ` to the type(s) or assemblies being serialized? 2) Did you set up the converter on both the client and server sides as shown in the original question? For security reasons `TypeNameHandling` must be manually enabled in both the client and server. – dbc Apr 12 '18 at 17:48
  • Thank you for this solution! Unfortunately there seems to be something odd going on when I implement it. The client sends an object to the server that has a list of derived objects which is then also returned back to the client. The client calls the custom `WriteJson` but the server never calls the `ReadJson`. Then the object is sent back and the server calls the `WriteJson` and the client calls the `ReadJson`. No idea why the server doesn't use the custom `ReadJson`... – MuhKuh Aug 19 '21 at 20:21
  • I want to add that the `CanConvert` is correctly called and returns true for all calls. Just the server doesn't call the `ReadJson` after. – MuhKuh Aug 19 '21 at 20:50
  • 1
    @MuhKuh - this answer is quite old so perhaps SignalR has changed in the past 6 years. Have you tried the [answer that modifies this somewhat](https://stackoverflow.com/a/40279480/3744182) by [Casperah](https://stackoverflow.com/users/686421/casperah)? – dbc Aug 19 '21 at 20:50
  • @dbc Somehow when using Casperah's answer, it works fine. Still kind of strange that ReadJson is not executed. Looks like I have to set the extra attribute on all base classes. – MuhKuh Aug 19 '21 at 20:58
  • 1
    @MuhKuh - what was `CanRead` returning at that time? If returning `true` all I can think is that SignalR has some other builtin converter that supersedes the converter above. But Json.NET's [converter precendence](https://www.newtonsoft.com/json/help/html/SerializationAttributes.htm#JsonConverterAttribute) puts converters applied by attribute ahead of converters applied by settings, so applying the converter directly still works. – dbc Aug 19 '21 at 21:06
  • @dbc that must be it! Because it returned true. I wonder if this is a bug in SignalR or by design. – MuhKuh Aug 19 '21 at 21:14
4

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

However, I had the problem that I could not make the Server read the received json correctly, that is it did 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 set JsonSerializer on the proxy connection of the client and add it to the GlobalHost.DependencyResolver.

It took me a long time to figure it out, I am using SignalR 2.2.1 on both client and server.

Casperah
  • 4,504
  • 1
  • 19
  • 13
  • 1
    If you're applying this directly to the base type with `[JsonConverter(typeof(PolymorphicAssemblyRootConverter))]`, you can eliminate the `HashSet assemblies;` completely and just throw an exception from `CanConvert` because it's not called when applied directly via attributes. OP had a specific requirement to be able to enable `TypeNameHandling` entirely through settings but if one relaxes that requirement then this should work well. – dbc Apr 12 '18 at 22:36
  • Thanks, it's an older answer but works for SignalR Core aswell! – Devator Jan 11 '19 at 22:18
  • Had the same issue with the server not calling the ReadJson. But this works! – MuhKuh Aug 19 '21 at 21:02
-1

It's easier that your thought. I came across the same issue, trying to serialize derived classes however no properties from derived types are sent.

As Microsoft says here: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-how-to?pivots=dotnet-5-0#serialize-properties-of-derived-classes

If you specify your model of type "Object" instead of the strongly typed "Base type" it will be serialized as such and then properties will be sent. If you have a big graph of objects you need to to it all the way down. It violates the strongly typed (type safety) but it allows the technology to send the data back with no changes to your code, just to your model.

as an example:

public class NotificationItem
{
   public string CreatedAt { get; set; }
}

public class NotificationEventLive : NotificationItem
{
    public string Activity { get; set; }
    public string ActivityType { get; set;}
    public DateTime Date { get; set;}
}

And if your main model that uses this Type is something like:

public class UserModel
{
    public string Name { get; set; }
    
    public IEnumerable<object> Notifications { get; set; } // note the "object"
    
    ..
}

if you try

var model = new UserModel() { ... }

JsonSerializer.Serialize(model); 

you will sent all your properties from your derived types.

The solution is not perfect because you lose the strongly typed model, but if this is a ViewModel being passed to javascript which is in case of SignalR usage it works just fine.

gds03
  • 1,349
  • 3
  • 18
  • 39
  • There's really no advantage to this over the already posted answers. If you don't want to check namespaces (which admittedly is quite the hack) but are fine manipulating every interface you can write a single generic wrapper and a trivial Json converter for it and that will do the trick just fine. Just wrap all arguments in the wrapper. – Voo Nov 27 '20 at 20:45