Come appiattire un ExpandoObject restituito tramite JsonResult in asp.net mvc?


95

Mi piace molto la ExpandoObjectcompilazione di un oggetto dinamico lato server in fase di esecuzione, ma ho problemi ad appiattire questa cosa durante la serializzazione JSON. Per prima cosa, istanzio l'oggetto:

dynamic expando = new ExpandoObject();
var d = expando as IDictionary<string, object>;
expando.Add("SomeProp", SomeValueOrClass);

Fin qui tutto bene. Nel mio controller MVC, voglio quindi inviarlo come JsonResult, quindi lo faccio:

return new JsonResult(expando);

Questo serializza il JSON nel seguente, per essere utilizzato dal browser:

[{"Key":"SomeProp", "Value": SomeValueOrClass}]

MA, quello che mi piacerebbe davvero è vedere questo:

{SomeProp: SomeValueOrClass}

So di poter ottenere questo risultato se utilizzo dynamicinvece di ExpandoObject- JsonResultè in grado di serializzare le dynamicproprietà e i valori in un singolo oggetto (senza attività chiave o valore), ma il motivo per cui devo usarlo ExpandoObjectè perché non conosco tutto le proprietà che voglio sull'oggetto fino al runtime e, per quanto ne so, non posso aggiungere dinamicamente una proprietà a a dynamicsenza usare un file ExpandoObject.

Potrei dover setacciare le attività "Chiave", "Valore" nel mio javascript, ma speravo di capirlo prima di inviarlo al cliente. Grazie per l'aiuto!


9
Perché non usare semplicemente Dictionary <string, object> invece di ExpandoObject? Serializza automaticamente nel formato desiderato e comunque stai usando il tuo ExpandoObject come un dizionario. Se si desidera serializzare ExpandoObject legittimi, utilizzando "return new JsonResult (d.ToDictionary (x => x.Key, x => x.Value));" è probabilmente il miglior compromesso.
BrainSlugs83

Risposte:


36

Puoi anche creare un JSONConverter speciale che funzioni solo per ExpandoObject e quindi registrarlo in un'istanza di JavaScriptSerializer. In questo modo potresti serializzare array di expando, combinazioni di oggetti expando e ... finché non trovi un altro tipo di oggetto che non viene serializzato correttamente ("come vuoi"), quindi crei un altro convertitore o aggiungi un altro tipo a questo. Spero che questo ti aiuti.

using System.Web.Script.Serialization;    
public class ExpandoJSONConverter : JavaScriptConverter
{
    public override object Deserialize(IDictionary<string, object> dictionary, Type type, JavaScriptSerializer serializer)
    {
        throw new NotImplementedException();
    }
    public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer)
    {         
        var result = new Dictionary<string, object>();
        var dictionary = obj as IDictionary<string, object>;
        foreach (var item in dictionary)
            result.Add(item.Key, item.Value);
        return result;
    }
    public override IEnumerable<Type> SupportedTypes
    {
        get 
        { 
              return new ReadOnlyCollection<Type>(new Type[] { typeof(System.Dynamic.ExpandoObject) });
        }
    }
}

Utilizzo del convertitore

var serializer = new JavaScriptSerializer(); 
serializer.RegisterConverters(new JavaScriptConverter[] { new ExpandoJSONConverter()});
var json = serializer.Serialize(obj);

2
Ha funzionato benissimo per le mie esigenze. Se qualcuno vuole inserire del codice per NotImplementedExceptionaggiungere qualcosa di simile serializer.Deserialize<ExpandoObject>(json);, @theburningmonk offre una soluzione che ha funzionato per me.
patridge

2
Ottimo lavoro @ pablo.Eccellente esempio di inserimento di una routine di serializzazione personalizzata nel framework MVC!
pb.

Il modo più semplice che ho trovato per farlo è stato: new JavaScriptSerializer (). Deserialize <object> (Newtonsoft.Json.JsonConvert.SerializeObject (listOfExpandoObject)); cosa ne pensi?
kavain

Il mio serializzatore viene chiamato in modo ricorsivo. Se imposto RecursionLimit, ottengo un errore di superamento del limite di ricorsione o un errore di eccezione di overflow dello stack. Cosa dovrei fare? :(
Dhanashree

71

Utilizzando JSON.NET puoi chiamare SerializeObject per "appiattire" l'oggetto expando:

dynamic expando = new ExpandoObject();
expando.name = "John Smith";
expando.age = 30;

var json = JsonConvert.SerializeObject(expando);

Produrrà:

{"name":"John Smith","age":30}

Nel contesto di un controller ASP.NET MVC, il risultato può essere restituito utilizzando il metodo Content:

public class JsonController : Controller
{
    public ActionResult Data()
    {
        dynamic expando = new ExpandoObject();
        expando.name = "John Smith";
        expando.age = 30;

        var json = JsonConvert.SerializeObject(expando);

        return Content(json, "application/json");
    }
}

1
Newtonsoft.Json intendi?
Ayyash

3
newtonsoft.json ha una migliore gestione per le espansioni ricorsive all'interno di espansioni o dizionari e dizionari interni, fuori dagli schemi
Jone Polvora

26

Ecco cosa ho fatto per ottenere il comportamento che stai descrivendo:

dynamic expando = new ExpandoObject();
expando.Blah = 42;
expando.Foo = "test";
...

var d = expando as IDictionary<string, object>;
d.Add("SomeProp", SomeValueOrClass);

// After you've added the properties you would like.
d = d.ToDictionary(x => x.Key, x => x.Value);
return new JsonResult(d);

Il costo è che stai facendo una copia dei dati prima di serializzarli.


Bello. Puoi anche lanciare la dinamica al volo: return new JsonResult (((ExpandoObject) someIncomingDynamicExpando) .ToDictionary (item => item.Key, item => item.Value))
joeriks

"expando.Add" non funziona per me. Credo che in questo caso sia "d.Add" (che ha funzionato per me).
Justin

9
Quindi aspetta ... stai creando un ExpandoObject, trasformandolo in un dizionario, usandolo come un dizionario e poi, quando non è abbastanza buono, convertendolo in un dizionario ... ... perché non usare semplicemente un dizionario in questo caso? ... o_o
BrainSlugs83

5
Un ExpandoObjectti offre molta più flessibilità di un semplice dizionario. Sebbene l'esempio sopra non lo dimostri, puoi utilizzare le funzionalità dinamiche di ExpandoObjectper aggiungere le proprietà che desideri avere nel tuo JSON. Un Dictioanryoggetto normale verrà convertito in JSON senza problemi, quindi eseguendo la conversione, è un modo semplice O (n) per inserire la dinamica di facile utilizzo ExpandoObjectin un formato che può essere JSONificato. Hai ragione, tuttavia, l'esempio sopra sarebbe un uso ridicolo del ExpandoObject; un semplice Dictionarysarebbe molto meglio.
ajb

1
Mi piace di più questo approccio: la creazione di una copia non funziona in nessun ambiente ma ho solo piccoli oggetti e Expando è fornito da una terza parte (immutabile) ....
Sebastian J.

12

Ho risolto questo problema scrivendo un metodo di estensione che converte ExpandoObject in una stringa JSON:

public static string Flatten(this ExpandoObject expando)
{
    StringBuilder sb = new StringBuilder();
    List<string> contents = new List<string>();
    var d = expando as IDictionary<string, object>;
    sb.Append("{");

    foreach (KeyValuePair<string, object> kvp in d) {
        contents.Add(String.Format("{0}: {1}", kvp.Key,
           JsonConvert.SerializeObject(kvp.Value)));
    }
    sb.Append(String.Join(",", contents.ToArray()));

    sb.Append("}");

    return sb.ToString();
}

Questo utilizza l'eccellente libreria Newtonsoft .

JsonResult ha quindi questo aspetto:

return JsonResult(expando.Flatten());

E questo viene restituito al browser:

"{SomeProp: SomeValueOrClass}"

E posso usarlo in javascript facendo questo (referenziato qui ):

var obj = JSON.parse(myJsonString);

Spero che aiuti!


7
Non valutarlo! È necessario utilizzare un deserializzatore JSON per evitare problemi di sicurezza. Vedere json2.js: json.org/js.html var o = JSON.parse (myJsonString);
Lance Fisher

Mi piace quel metodo di estensione però. Bello!
Lance Fisher

3
-1: eseguire questa operazione in un metodo di estensione che restituisce una stringa non è il modo corretto per interfacciare questo comportamento con il framework. Dovresti invece estendere l'architettura di serializzazione incorporata.
BrainSlugs83

1
Il principale svantaggio di questo metodo è la mancanza di ricorsione: se sai che l'oggetto di primo livello è dinamico e basta, funziona, ma se gli oggetti dinamici possono trovarsi a uno o tutti i livelli dell'albero degli oggetti restituiti, non funziona.
Chris Moschini

Ho apportato alcuni miglioramenti a questo metodo per renderlo ricorsivo. Ecco il codice: gist.github.com/renanvieira/e26dc34e2de156723f79
MaltMaster

5

Sono stato in grado di risolvere lo stesso problema utilizzando JsonFx .

        dynamic person = new System.Dynamic.ExpandoObject();
        person.FirstName  = "John";
        person.LastName   = "Doe";
        person.Address    = "1234 Home St";
        person.City       = "Home Town";
        person.State      = "CA";
        person.Zip        = "12345";

        var writer = new JsonFx.Json.JsonWriter();
        return writer.Write(person);

produzione:

{"FirstName": "John", "LastName": "Doe", "Address": "1234 Home St", "City": "Home Town", "State": "CA", "Zip": "12345 "}


1
Puoi anche farlo utilizzando JSON .Net (Newtonsoft) completando i seguenti passaggi. var entity = person as object; var json = JsonConvert.SerializeObject (entità);
bkorzynski

4

Ho portato il processo di appiattimento un ulteriore passo avanti e ho verificato la presenza di oggetti elenco, che rimuove l'assurdità del valore chiave. :)

public string Flatten(ExpandoObject expando)
    {
        StringBuilder sb = new StringBuilder();
        List<string> contents = new List<string>();
        var d = expando as IDictionary<string, object>;
        sb.Append("{ ");

        foreach (KeyValuePair<string, object> kvp in d)
        {       
            if (kvp.Value is ExpandoObject)
            {
                ExpandoObject expandoValue = (ExpandoObject)kvp.Value;
                StringBuilder expandoBuilder = new StringBuilder();
                expandoBuilder.Append(String.Format("\"{0}\":[", kvp.Key));

                String flat = Flatten(expandoValue);
                expandoBuilder.Append(flat);

                string expandoResult = expandoBuilder.ToString();
                // expandoResult = expandoResult.Remove(expandoResult.Length - 1);
                expandoResult += "]";
                contents.Add(expandoResult);
            }
            else if (kvp.Value is List<Object>)
            {
                List<Object> valueList = (List<Object>)kvp.Value;

                StringBuilder listBuilder = new StringBuilder();
                listBuilder.Append(String.Format("\"{0}\":[", kvp.Key));
                foreach (Object item in valueList)
                {
                    if (item is ExpandoObject)
                    {
                        String flat = Flatten(item as ExpandoObject);
                        listBuilder.Append(flat + ",");
                    }
                }

                string listResult = listBuilder.ToString();
                listResult = listResult.Remove(listResult.Length - 1);
                listResult += "]";
                contents.Add(listResult);

            }
            else
            { 
                contents.Add(String.Format("\"{0}\": {1}", kvp.Key,
                   JsonSerializer.Serialize(kvp.Value)));
            }
            //contents.Add("type: " + valueType);
        }
        sb.Append(String.Join(",", contents.ToArray()));

        sb.Append("}");

        return sb.ToString();
    }

3

Potrebbe non esserti utile, ma avevo un requisito simile, ma utilizzavo un SerializableDynamicObject

Ho cambiato il nome del dizionario in "Fields" e poi questo serializza con Json.Net per produrre json che assomiglia a:

{"Fields": {"Property1": "Value1", "Property2": "Value2" ecc. Dove Property1 e Property2 sono proprietà aggiunte dinamicamente, ovvero chiavi del dizionario

Sarebbe perfetto se potessi sbarazzarmi della proprietà extra "Fields" che incapsula il resto, ma ho aggirato questa limitazione.

Risposta spostata da questa domanda su richiesta


3

Questa è una risposta tardiva, ma ho avuto lo stesso problema e questa domanda mi ha aiutato a risolverli. In sintesi, ho pensato di pubblicare i miei risultati, nella speranza che acceleri l'implementazione per altri.

Innanzitutto ExpandoJsonResult, di cui puoi restituire un'istanza nella tua azione. Oppure puoi sovrascrivere il metodo Json nel tuo controller e restituirlo lì.

public class ExpandoJsonResult : JsonResult
{
    public override void ExecuteResult(ControllerContext context)
    {
        HttpResponseBase response = context.HttpContext.Response;
        response.ContentType = !string.IsNullOrEmpty(ContentType) ? ContentType : "application/json";
        response.ContentEncoding = ContentEncoding ?? response.ContentEncoding;

        if (Data != null)
        {
            JavaScriptSerializer serializer = new JavaScriptSerializer();
            serializer.RegisterConverters(new JavaScriptConverter[] { new ExpandoConverter() });
            response.Write(serializer.Serialize(Data));
        }
    }
}

Quindi il convertitore (che supporta sia la serializzazione che la de-serializzazione. Vedi sotto per un esempio di come de-serializzare).

public class ExpandoConverter : JavaScriptConverter
{
    public override object Deserialize(IDictionary<string, object> dictionary, Type type, JavaScriptSerializer serializer)
    { return DictionaryToExpando(dictionary); }

    public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer)
    { return ((ExpandoObject)obj).ToDictionary(x => x.Key, x => x.Value); }

    public override IEnumerable<Type> SupportedTypes
    { get { return new ReadOnlyCollection<Type>(new Type[] { typeof(System.Dynamic.ExpandoObject) }); } }

    private ExpandoObject DictionaryToExpando(IDictionary<string, object> source)
    {
        var expandoObject = new ExpandoObject();
        var expandoDictionary = (IDictionary<string, object>)expandoObject;
        foreach (var kvp in source)
        {
            if (kvp.Value is IDictionary<string, object>) expandoDictionary.Add(kvp.Key, DictionaryToExpando((IDictionary<string, object>)kvp.Value));
            else if (kvp.Value is ICollection)
            {
                var valueList = new List<object>();
                foreach (var value in (ICollection)kvp.Value)
                {
                    if (value is IDictionary<string, object>) valueList.Add(DictionaryToExpando((IDictionary<string, object>)value));
                    else valueList.Add(value);
                }
                expandoDictionary.Add(kvp.Key, valueList);
            }
            else expandoDictionary.Add(kvp.Key, kvp.Value);
        }
        return expandoObject;
    }
}

Puoi vedere nella classe ExpandoJsonResult come usarlo per la serializzazione. Per de-serializzare, crea il serializzatore e registra il convertitore allo stesso modo, ma usa

dynamic _data = serializer.Deserialize<ExpandoObject>("Your JSON string");

Un grande grazie, a tutti i partecipanti qui che mi hanno aiutato.


1

Utilizzando la restituzione di ExpandoObject dinamico da WebApi in ASP.Net 4, il formattatore JSON predefinito sembra appiattire ExpandoObjects in un semplice oggetto JSON.


1

JsonResultutilizza JavaScriptSerializerche effettivamente deserializza (il cemento) Dictionary<string, object>come vuoi.

C'è un sovraccarico del Dictionary<string, object>costruttore che accetta IDictionary<string, object>.

ExpandoObjectattrezzi IDictionary<string, object> (penso che tu possa vedere dove sto andando qui ...)

ExpandoObject a livello singolo

dynamic expando = new ExpandoObject();

expando.hello = "hi";
expando.goodbye = "cya";

var dictionary = new Dictionary<string, object>(expando);

return this.Json(dictionary); // or new JsonResult { Data = dictionary };

Una riga di codice, utilizzando tutti i tipi incorporati :)

ExpandoObjects annidati

Ovviamente se stai annidando ExpandoObjects, dovrai convertirli ricorsivamente tutti in Dictionary<string, object>s:

public static Dictionary<string, object> RecursivelyDictionary(
    IDictionary<string, object> dictionary)
{
    var concrete = new Dictionary<string, object>();

    foreach (var element in dictionary)
    {
        var cast = element.Value as IDictionary<string, object>;
        var value = cast == null ? element.Value : RecursivelyDictionary(cast);
        concrete.Add(element.Key, value);
    }

    return concrete;
}

il tuo codice finale che diventa

dynamic expando = new ExpandoObject();
expando.hello = "hi";
expando.goodbye = "cya";
expando.world = new ExpandoObject();
expando.world.hello = "hello world";

var dictionary = RecursivelyDictionary(expando);

return this.Json(dictionary);

-2

Sembra che il serializzatore stia trasmettendo Expando a un dizionario e quindi serializzandolo (quindi l'attività chiave / valore). Hai provato a deserializzare come dizionario e poi a trasmetterlo a un Expando?


1
L'oggetto Expando implementa IDictionary <string, object>, quindi penso che sia per questo che JsonResult lo serializza in un array di coppie chiave / valore. Lanciarlo come un IDictionary e tornare indietro non aiuterebbe davvero ad appiattirlo, temo.
TimDog

-2

Ho appena avuto lo stesso problema e ho scoperto qualcosa di piuttosto strano. Se lo faccio:

dynamic x = new ExpandoObject();
x.Prop1 = "xxx";
x.Prop2 = "yyy";
return Json
(
    new
    {
        x.Prop1,
        x.Prop2
    }
);

Funziona, ma solo se il mio metodo utilizza l'attributo HttpPost. Se utilizzo HttpGet ottengo un errore. Quindi la mia risposta funziona solo su HttpPost. Nel mio caso si trattava di una chiamata Ajax, quindi ho potuto cambiare HttpGet con HttpPost.


2
-1 Questo non è molto utile in quanto si riduce a stackoverflow.com/a/7042631/11635 e non ha senso fare queste cose dinamicamente se hai intenzione di voltarti e dipendere dai nomi staticamente come fai. Il problema di AllowGet è completamente ortogonale.
Ruben Bartelink
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.