2011-11-24 23 views
19

Ho circa la seguente immagine:Come passare argomenti a un costruttore non predefinito?

public class Foo 
{ 
    public Foo(Bar bar, String x, String y) 
    { 
     this.Bar = bar; 
     this.X = x; 
     this.Y = y; 
    } 

    [JsonIgnore] 
    public Bar Bar { get; private set; } 

    public String X { get; private set; } 
    public String Y { get; private set; } 
} 

public class Bar 
{ 
    public Bar(String z) 
    { 
     this.Z = z; 
    } 

    public String Z { get; private set; } 
} 

voglio in qualche modo di passare un oggetto di tipo Bar per un costruttore di tipo Foo durante la deserializzazione, vale a dire:

var bar = new Bar("Hello world"); 
var x = JsonConvert.DeserializeObject<Foo>(fooJsonString, bar); 

risposta

15

qui sono i miei pensieri per quanto riguarda la soluzione del problema:

Il problema:

JSON. L'api di deserializzazione personalizzata di Net non è trasparente, cioè influenza la mia gerarchia di classi.

In realtà non è un problema nel caso in cui tu abbia 10-20 classi nel tuo progetto, ma se hai un progetto enorme con migliaia di classi, non sei particolarmente felice del fatto che devi conformare il tuo design OOP con Json Netti requisiti.

Json.Net è valido per gli oggetti POCO che vengono popolati (inizializzati) dopo la creazione. Ma non è la verità in tutti i casi, a volte i tuoi oggetti vengono inizializzati all'interno del costruttore. E per rendere possibile l'inizializzazione è necessario passare argomenti 'corretti'.Questi argomenti 'corretti' possono essere o all'interno di testo serializzato o possono essere già creati e inizializzati qualche tempo prima. Sfortunatamente Json.Net durante la deserializzazione passa valori predefiniti ad argomenti che non capisce, e nel mio caso causa sempre ArgumentNullException.

La soluzione:

Ecco approccio che permette la creazione oggetto reale personalizzato durante la deserializzazione utilizzando qualsiasi insieme di argomenti sia serializzati o non serializzati, il problema principale è che l'approccio sub-ottimale, richiede 2 fasi di deserializzazione per oggetto che richiede deserializzazione personalizzato, ma funziona e permette di deserializzazione oggetti il ​​modo in cui avete bisogno, quindi ecco qui:

In primo luogo abbiamo rimontare la classe CustomCreationConverter seguente modo:

public class FactoryConverter<T> : Newtonsoft.Json.JsonConverter 
{ 
    /// <summary> 
    /// Writes the JSON representation of the object. 
    /// </summary> 
    /// <param name="writer">The <see cref="JsonWriter"/> to write to.</param> 
    /// <param name="value">The value.</param> 
    /// <param name="serializer">The calling serializer.</param> 
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) 
    { 
     throw new NotSupportedException("CustomCreationConverter should only be used while deserializing."); 
    } 

    /// <summary> 
    /// Reads the JSON representation of the object. 
    /// </summary> 
    /// <param name="reader">The <see cref="JsonReader"/> to read from.</param> 
    /// <param name="objectType">Type of the object.</param> 
    /// <param name="existingValue">The existing value of object being read.</param> 
    /// <param name="serializer">The calling serializer.</param> 
    /// <returns>The object value.</returns> 
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) 
    { 
     if (reader.TokenType == JsonToken.Null) 
      return null; 

     T value = CreateAndPopulate(objectType, serializer.Deserialize<Dictionary<String, String>>(reader)); 

     if (value == null) 
      throw new JsonSerializationException("No object created."); 

     return value; 
    } 

    /// <summary> 
    /// Creates an object which will then be populated by the serializer. 
    /// </summary> 
    /// <param name="objectType">Type of the object.</param> 
    /// <returns></returns> 
    public abstract T CreateAndPopulate(Type objectType, Dictionary<String, String> jsonFields); 

    /// <summary> 
    /// Determines whether this instance can convert the specified object type. 
    /// </summary> 
    /// <param name="objectType">Type of the object.</param> 
    /// <returns> 
    ///  <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>. 
    /// </returns> 
    public override bool CanConvert(Type objectType) 
    { 
     return typeof(T).IsAssignableFrom(objectType); 
    } 

    /// <summary> 
    /// Gets a value indicating whether this <see cref="JsonConverter"/> can write JSON. 
    /// </summary> 
    /// <value> 
    ///  <c>true</c> if this <see cref="JsonConverter"/> can write JSON; otherwise, <c>false</c>. 
    /// </value> 
    public override bool CanWrite 
    { 
     get 
     { 
      return false; 
     } 
    } 
} 
.210

Poi creiamo la classe factory che creerà il nostro Foo:

public class FooFactory : FactoryConverter<Foo> 
{ 
    public FooFactory(Bar bar) 
    { 
     this.Bar = bar; 
    } 

    public Bar Bar { get; private set; } 

    public override Foo Create(Type objectType, Dictionary<string, string> arguments) 
    { 
     return new Foo(Bar, arguments["X"], arguments["Y"]); 
    } 
} 

Ecco il codice di esempio:

var bar = new Bar("BarObject"); 

var fooSrc = new Foo 
(
    bar, 
    "A", "B" 
); 

var str = JsonConvert.SerializeObject(fooSrc); 

var foo = JsonConvert.DeserializeObject<Foo>(str, new FooFactory(bar)); 

Console.WriteLine(str); 

In questo caso foo contiene un argomento che avevamo bisogno di passare a un costruttore Foo durante deserializzazione.

9

Io non sono un esperto su Json.NET, ma AFAIK che semplicemente non è possibile. Se fossi in te, vorrei esaminare le opzioni per correggere questo dopo la deserializzazione.

Pochissime API di serializzazione consentono di controllare la costruzione in quel grado; quattro approcci più tipiche sono (più comuni prima):

  • invocare il costruttore senza parametri
  • saltare costruttore completamente
  • utilizzare un costruttore che ha un ovvio 1: 1 mapping ai membri essendo serializzato
  • utilizzare un metodo factory fornito dall'utente

suona come si desidera che l'ultima, che è piuttosto raro. Potrebbe essere necessario accontentarsi di fare questo all'esterno del costruttore.

Alcune API di serializzazione offrono "callback di serializzazione/deserializzazione", che consentono di eseguire un metodo sull'oggetto in vari punti (in genere prima e dopo la serializzazione e la deserializzazione), passando anche alcune informazioni di contesto nel callback. IF Json.NET supporta i callback di deserializzazione, che potrebbero essere la cosa da guardare. This question suggerisce che il pattern di callback [OnDeserialized] possa essere effettivamente supportato; il context proviene dalla proprietà .ContextJsonSerializerSettings che è possibile opzionalmente fornire al metodo deserialize.

Altrimenti, è sufficiente eseguirlo manualmente dopo la deserializzazione.

mio pseudo-codice di massima (completamente non testata):

// inside type: Foo 
[OnDeserialized] 
public void OnDeserialized(StreamingContext ctx) { 
    if(ctx != null) { 
     Bar bar = ctx.Context as Bar; 
     if(bar != null) this.Bar = bar; 
    } 
} 

e

var ctx = new StreamingContext(StreamingContextStates.Other, bar); 
var settings = new JsonSerializerSettings { Context = ctx }; 
var obj = JsonConvert.DeserializeObject<Foo>(fooJsonString, settings); 
+10

Non ho idea del perché gli scrittori di serializzatori considerino questa bassa priorità. Ho sempre considerato di essere in grado di costruire oggetti immutabili senza hack di basso livello, una delle funzionalità veramente basilari che un serializzatore basato su un'interfaccia pubblica dovrebbe avere. – CodesInChaos

+1

@CodeInChaos protobuf-net supporta tutte e 4 le opzioni elencate, * e * almeno un'altra (trasformazioni surrogate) ... basta dire ' –

+0

@CodeInChaos come per il motivo: semplicemente, è davvero piuttosto difficile –

0

Se si dispone di un costruttore i cui soli parametri sono valori non serializzati, creare prima l'istanza e quindi compilare l'oggetto anziché deserializzare. La classe JsonConvert ha un metodo PopulateObject, definiti come segue:

public static void PopulateObject(
    string value,      // JSON string 
    object target)      // already-created instance 

Se si dispone di particolari impostazioni di serializzazione, c'è un sovraccarico che comprende anche un parametro JsonSerializerSettings.

Aggiungere un costruttore Foo che ha un solo parametro da bar, si potrebbe fare qualcosa di simile:

var bar = new Bar("Hello World"); 
var foo = new Foo(bar); 
JsonConvert.PopulateObject(fooJsonString, foo); 

Potrebbe essere necessario regolare la classe di utilizzare i campi per la mappatura, o effettuare regolazioni per NHibernate per consentire la scrittura setter privati ​​(uso di una classe personalizzata IProxyValidator).

Problemi correlati