SignalR Typenamehandling

后端 未结 3 1054
栀梦
栀梦 2020-12-03 22:43

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

相关标签:
3条回答
  • 2020-12-03 22:55

    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.

    0 讨论(0)
  • 2020-12-03 22:56

    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.

    0 讨论(0)
  • 2020-12-03 23:15

    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://docs.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.

    0 讨论(0)
提交回复
热议问题