【问题标题】:SignalR TypenamehandlingSignalR 类型名称处理
【发布时间】:2015-08-31 15:21:55
【问题描述】:

我正在尝试让 SignalR 使用自定义 JsonSerializerSettings 为其有效负载,特别是我正在尝试设置 TypeNameHandling = TypeNameHandling.Auto

问题似乎是,SignalR 将hubConnection.JsonSerializerGlobalHost.DependencyResolver.Resolve<JsonSerializer>() 中的设置也用于其内部数据结构,然后导致各种破坏(当我将TypeNameHandling.All 设置为最粗鲁时,内部服务器崩溃例如,但是使用TypeNameHandling.Auto 我也会遇到问题,尤其是在涉及IProgress<> 回调时)。

有什么解决方法还是我做错了?

演示示例代码:

服务器:

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

客户:

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

共享:

public static class ConverterSettings
{
    public static JsonSerializer GetSerializer()
    {
        return JsonSerializer.Create(new JsonSerializerSettings()
        {
            TypeNameHandling = TypeNameHandling.All
        });
    }
}

【问题讨论】:

  • 有什么特别的原因你不想使用 SignalR 的默认 Json 序列化器吗?
  • @Matei_Radu 因为它使用TypenameHandling.None,我需要Auto
  • 我没有 SignalR 来测试;问题是您需要 root json 对象或某些嵌套对象上的 $type 属性吗?如果是前者,有什么办法可以放宽这个要求,也许通过返回一个包装对象作为根?
  • @dbc 一旦我开始将每个参数和返回值打包到它自己的处理(反)序列化的小包装器中,我不妨将 SignalR 完全扔出窗外……或者更多只要没有任何自动化的方法可以做到这一点。要求是我可以将接口反序列化为其确切类型。
  • 您只需要一个通用包装器:public class Data&lt;T&gt; { public T data { get; set; } }。这里的困难在于,即使你设置了TypenameHandling.Auto(我知道如何为你做),它也不适用于根对象,除非 SignalR 在内部调用specific overload of Serialize

标签: c# json json.net signalr .net-4.5


【解决方案1】:

这可以通过利用您的类型和 SignalR 类型在不同的assemblies 中这一事实来完成。这个想法是创建一个JsonConverter,它适用于您的程序集中的所有类型。当对象图中第一次遇到来自您的一个程序集的类型(可能作为根对象)时,转换器将临时设置jsonSerializer.TypeNameHandling = TypeNameHandling.Auto,然后继续对该类型进行标准序列化,在持续时间内禁用自身以防止无限递归:

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
}

然后在启动时,您可以将此转换器添加到默认的JsonSerializer,并传入您希望应用"$type" 的程序集。

更新

如果由于某种原因在启动时传递程序集列表不方便,您可以通过objectType.Namespace 启用转换器。位于您指定命名空间中的所有类型都将自动使用TypeNameHandling.Auto 序列化。

或者,您可以引入Attribute,其中targets 是一个程序集、类或接口,并在与适当的转换器结合使用时启用TypeNameHandling.Auto

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

注意 - 使用各种测试用例进行测试,但不是 SignalR 本身,因为我目前没有安装它。

TypeNameHandling注意

使用TypeNameHandling 时,请注意Newtonsoft docs 中的这一警告:

当您的应用程序从外部源反序列化 JSON 时,应谨慎使用 TypeNameHandling。使用 None 以外的值反序列化时,应使用自定义 SerializationBinder 验证传入类型。

关于为什么这可能是必要的讨论,请参阅 TypeNameHandling caution in Newtonsoft Json

【讨论】:

  • 我主要希望有一个可能更通用的解决方案,但这似乎是目前最好的选择。
  • SignalR 2.2,JsonSerializerSettings 或此实现不走运。我有一个返回 JsonSerializer 的方法,该转换器将此转换器添加到 Converters 列表中。我用 GlobalHost.DependencyResolver 注册了这个方法。我还将它添加到客户端的转换器列表中。相同的行为,没有错误,服务器上的基本类型相同的旧列表。
  • @Thypari - 它位于答案第一部分的代码中,在更新之前。向下滚动过去 PolymorphicAssemblyRootConverter
  • @Thypari - 很抱歉它不适合你。 1) 如果您使用的是EnableJsonTypeNameHandlingConverter,您是否将EnableJsonTypeNameHandlingAttribute 添加到正在序列化的类型或程序集中? 2)您是否在客户端和服务器端都设置了转换器,如原始问题所示?出于安全原因,TypeNameHandling 必须在客户端和服务器中手动启用。
  • @MuhKuh - 这个答案已经很老了,所以可能 SignalR 在过去 6 年里发生了变化。你试过Casperahanswer that modifies this somewhat吗?
【解决方案2】:

我知道这是一个相当古老的线程并且有一个公认的答案。

但是,我遇到的问题是我无法让服务器正确读取接收到的 json,即它只读取了基类

不过,问题的解决方法很简单:

我在参数类之前添加了这一行:

[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
}

无需在客户端的代理连接上设置JsonSerializer,添加到GlobalHost.DependencyResolver中。

我花了很长时间才弄明白,我在客户端和服务器上都使用 SignalR 2.2.1。

【讨论】:

  • 如果您使用[JsonConverter(typeof(PolymorphicAssemblyRootConverter))] 将其直接应用于基本类型,您可以完全消除HashSet&lt;Assembly&gt; assemblies; 并从CanConvert 抛出异常,因为通过属性直接应用时不会调用它. OP 有一个特定要求,即能够完全通过设置启用TypeNameHandling,但如果放宽该要求,那么这应该可以正常工作。
  • 谢谢,这是一个较旧的答案,但也适用于 SignalR Core!
  • 服务器没有调用 ReadJson 时遇到了同样的问题。但这行得通!
【解决方案3】:

你的想法更容易。我遇到了同样的问题,试图序列化派生类,但是没有发送派生类型的属性。

正如微软所说:https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-how-to?pivots=dotnet-5-0#serialize-properties-of-derived-classes

如果您指定类型为“对象”而不是强类型“基本类型”的模型,它将被序列化,然后发送属性。 如果你有一个很大的对象图,你需要一直向下。它违反了强类型(类型安全),但它允许技术在不更改代码的情况下将数据发送回,而只是发送到您的模型。

举个例子:

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

如果您使用此类型的主要模型类似于:

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

如果你尝试

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

JsonSerializer.Serialize(model); 

您将从派生类型发送所有属性。

该解决方案并不完美,因为您丢失了强类型模型,但如果这是一个被传递给 JavaScript 的 ViewModel,在使用 SignalR 的情况下它工作得很好。

【讨论】:

  • 与已经发布的答案相比,这确实没有任何优势。如果您不想检查命名空间(诚然,这确实是 hack)但可以很好地操作每个接口,您可以为它编写一个通用包装器和一个简单的 Json 转换器,这样就可以了。只需将所有参数包装在包装器中即可。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-03-27
  • 1970-01-01
  • 2016-12-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多