2015-08-31 12 views
6

Sto cercando di far funzionare SignalR con JsonSerializerSettings personalizzato per il suo payload, in particolare sto cercando di impostare TypeNameHandling = TypeNameHandling.Auto.SignalR Typenamehandling

Il problema sembra essere, che SignalR utilizza le impostazioni di hubConnection.JsonSerializer e GlobalHost.DependencyResolver.Resolve<JsonSerializer>() per le sue strutture di dati interni e che poi fa sì che tutti i tipi di caos (crash del server interno quando ho creato TypeNameHandling.All come l'esempio più grossolano, ma con TypeNameHandling.Auto Ottengo anche problemi, in particolare quando sono coinvolti i callback IProgress<>).

C'è qualche soluzione alternativa o sto sbagliando?

codice di esempio per dimostrare:

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

Cliente:

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

condivisa:

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

C'è un motivo particolare per cui non si desidera utilizzare il serializzatore Json predefinito da SignalR? –

+0

@Matei_Radu Perché usa 'TypenameHandling.None' e ho bisogno di' Auto'. – Voo

+0

Non ho SignalR per testare con; è il problema che hai bisogno della proprietà '$ type' sull'oggetto * root * json, o su qualche oggetto nidificato? E se il primo, c'è un modo per rilassare quel requisito, magari restituendo un oggetto wrapper come root? – dbc

risposta

3

Questo può essere fatto sfruttando il fatto che i tuoi tipi e d i tipi SignalR sono in diversi assemblies. L'idea è quella di creare un che si applica a tutti i tipi dagli assembly. Quando si incontra per la prima volta un oggetto di uno degli assiemi nel grafico dell'oggetto (probabilmente come oggetto radice), il convertitore imposta temporaneamente jsonSerializer.TypeNameHandling = TypeNameHandling.Auto, quindi procede con la serializzazione standard per il tipo, disabilitando se stesso per la durata per impedire la ricorsione infinita:

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 
} 

Poi in avvio si dovrebbe aggiungere questo convertitore al default JsonSerializer, passando nelle assemblee per cui si desidera "$type" applicato.

Aggiornamento

Se per qualsiasi motivo è scomodo per passare alla lista di assemblee in fase di avvio, è possibile attivare il convertitore da objectType.Namespace. Tutti i tipi che vivono negli spazi dei nomi specificati verranno automaticamente serializzati con TypeNameHandling.Auto.

In alternativa, si potrebbe introdurre un Attribute che targets un'assemblea, classe o interfaccia e permette TypeNameHandling.Auto quando combinato con il convertitore appropriato:

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

Nota - testato con i vari casi di test, ma non per sé SignalR da quando don Al momento è installato.

+0

@Voo - questa risposta non funzionava con SignalR, o c'era qualche ragione per cui abilitare 'TypeNameHandling' da assembly o namespace non era conveniente? – dbc

+1

Speravo in gran parte per una soluzione potenzialmente più generale, ma questa sembra essere l'opzione migliore al momento. – Voo

+0

SignalR 2.2, nessuna fortuna con JsonSerializerSettings o questa implementazione. Ho un metodo che restituisce JsonSerializer con questo convertitore aggiunto all'elenco dei convertitori. Registro questo metodo con GlobalHost.DependencyResolver. Lo aggiungo anche all'elenco dei convertitori nel client. Stesso comportamento, nessun errore, lo stesso vecchio elenco di tipi di base sul server. – grinder22

0

So che questo è un thread piuttosto vecchio e che esiste una risposta accettata.

Tuttavia, ho avuto il problema che non ho potuto fare il server di leggere il JSON ricevuto correttamente, che è esso ha letto soltanto le classi base

Tuttavia, la soluzione al problema era molto semplice:

ho aggiunto questa linea prima che le classi di parametri:

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

Non c'è bisogno di impostare JsonSerializer sulla connessione proxy del cliente e aggiungerlo alla GlobalHost.DependencyResolver.

Mi ci è voluto molto tempo per capirlo, sto usando SignalR 2.2.1 su client e server.