Comment aplatir un ExpandoObject retourné via JsonResult dans asp.net mvc?

95

J'aime vraiment la ExpandoObjectcompilation d'un objet dynamique côté serveur au moment de l'exécution, mais j'ai du mal à aplatir cette chose lors de la sérialisation JSON. Tout d'abord, j'instancie l'objet:

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

Jusqu'ici tout va bien. Dans mon contrôleur MVC, je veux ensuite envoyer ceci en tant que JsonResult, donc je fais ceci:

return new JsonResult(expando);

Cela sérialise le JSON dans le ci-dessous, pour être consommé par le navigateur:

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

MAIS, ce que j'aimerais vraiment, c'est voir ceci:

{SomeProp: SomeValueOrClass}

Je sais que je peux y parvenir si j'utilise à la dynamicplace de ExpandoObject- JsonResultest capable de sérialiser les dynamicpropriétés et les valeurs en un seul objet (sans clé ni valeur), mais la raison pour laquelle je dois utiliser ExpandoObjectest que je ne sais pas tout les propriétés que je veux sur l'objet jusqu'à l'exécution , et pour autant que je sache, je ne peux pas ajouter dynamiquement une propriété à un dynamicsans utiliser un ExpandoObject.

Je devrais peut-être passer au crible les affaires "Clé", "Valeur" dans mon javascript, mais j'espérais comprendre cela avant de l'envoyer au client. Merci de votre aide!

TimDog
la source
9
Pourquoi ne pas simplement utiliser Dictionary <string, object> au lieu de ExpandoObject? Il sérialise automatiquement au format souhaité et vous n'utilisez de toute façon votre ExpandoObject que comme un dictionnaire. Si vous souhaitez sérialiser des ExpandoObject légitimes, en utilisant le "return new JsonResult (d.ToDictionary (x => x.Key, x => x.Value));" l'approche est probablement le meilleur compromis.
BrainSlugs83

Réponses:

36

Vous pouvez également créer un JSONConverter spécial qui ne fonctionne que pour ExpandoObject, puis l'enregistrer dans une instance de JavaScriptSerializer. De cette façon, vous pouvez sérialiser des tableaux d'expansion, des combinaisons d'objets expando et ... jusqu'à ce que vous trouviez un autre type d'objet qui ne soit pas sérialisé correctement ("comme vous le souhaitez"), puis vous créez un autre convertisseur, ou ajoutez un autre type à celui-là. J'espère que cela t'aides.

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

Utilisation du convertisseur

var serializer = new JavaScriptSerializer(); 
serializer.RegisterConverters(new JavaScriptConverter[] { new ExpandoJSONConverter()});
var json = serializer.Serialize(obj);
Pablo Rodda Faire un don
la source
2
Cela a très bien fonctionné pour mes besoins. Si quelqu'un veut brancher du code pour NotImplementedExceptionajouter quelque chose comme serializer.Deserialize<ExpandoObject>(json);, @theburningmonk propose une solution qui a fonctionné pour moi.
patridge
2
Excellent travail @ pablo.Excellent exemple de connexion d'une routine de sérialisation personnalisée dans le framework MVC!
pb.
Le moyen le plus simple que j'ai trouvé pour le faire était: new JavaScriptSerializer (). Deserialize <object> (Newtonsoft.Json.JsonConvert.SerializeObject (listOfExpandoObject)); Qu'est-ce que tu penses?
kavain
Mon sérialiseur est appelé récursivement. Si je définis RecursionLimit, j'obtiens une erreur de dépassement de la limite de récursivité ou une erreur d'exception de dépassement de capacité de pile. Que devrais-je faire? :(
Dhanashree
71

En utilisant JSON.NET, vous pouvez appeler SerializeObject pour "aplatir" l'objet expando:

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

var json = JsonConvert.SerializeObject(expando);

Sortira:

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

Dans le contexte d'un contrôleur ASP.NET MVC, le résultat peut être renvoyé à l'aide de la méthode 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");
    }
}
Mikael Koskinen
la source
1
Newtonsoft.Json vous voulez dire?
Ayyash
3
newtonsoft.json a une meilleure gestion des expandos récursifs dans des expandos ou des dictionnaires et des dictionnaires internes, prêt à l'emploi
Jone Polvora
26

Voici ce que j'ai fait pour obtenir le comportement que vous décrivez:

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

Le coût est que vous faites une copie des données avant de les sérialiser.

ajb
la source
Agréable. Vous pouvez également lancer la dynamique à la volée: return new JsonResult (((ExpandoObject) someIncomingDynamicExpando) .ToDictionary (item => item.Key, item => item.Value))
joeriks
"expando.Add" ne fonctionne pas pour moi. Je crois que dans ce cas c'est "d.Add" (qui a fonctionné pour moi).
Justin
9
Alors attendez ... vous créez un ExpandoObject, vous le convertissez en dictionnaire, vous l'utilisez comme un dictionnaire, puis quand ce n'est pas assez bon, vous le convertissez en dictionnaire ... ... pourquoi ne pas simplement utiliser un dictionnaire dans ce cas? ... o_o
BrainSlugs83
5
Un ExpandoObjectvous offre beaucoup plus de flexibilité qu'un simple dictionnaire. Bien que l'exemple ci-dessus ne le démontre pas, vous pouvez utiliser les fonctionnalités dynamiques de ExpandoObjectpour ajouter les propriétés que vous souhaitez avoir dans votre JSON. Un Dictioanryobjet normal sera converti en JSON sans aucun problème, donc en effectuant la conversion, c'est un moyen simple O (n) de mettre la dynamique facile à utiliser ExpandoObjectdans un format qui peut être JSONifié. Vous avez raison cependant, l'exemple ci-dessus serait une utilisation simplifiée du ExpandoObject; un simple Dictionaryserait beaucoup mieux.
ajb
1
Comme cette approche davantage - créer une copie ne fonctionne dans aucun environnement mais je n'ai que de petits objets et l'Expando est fourni par une tierce partie (non modifiable) ....
Sebastian J.
12

J'ai résolu ce problème en écrivant une méthode d'extension qui convertit ExpandoObject en une chaîne 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();
}

Cela utilise l'excellente bibliothèque Newtonsoft .

JsonResult ressemble alors à ceci:

return JsonResult(expando.Flatten());

Et ceci est renvoyé au navigateur:

"{SomeProp: SomeValueOrClass}"

Et je peux l'utiliser en javascript en faisant ceci (référencé ici ):

var obj = JSON.parse(myJsonString);

J'espère que ça aide!

TimDog
la source
7
Ne l'évaluez pas! Vous devez utiliser un désérialiseur JSON pour éviter les problèmes de sécurité. Voir json2.js: json.org/js.html var o = JSON.parse (myJsonString);
Lance Fisher
J'aime cette méthode d'extension cependant. Agréable!
Lance Fisher
3
-1: Faire cela dans une méthode d'extension qui renvoie une chaîne n'est pas la bonne façon d'interfacer ce comportement avec le framework. Vous devriez plutôt étendre l'architecture de sérialisation intégrée.
BrainSlugs83
1
L'inconvénient majeur de cette méthode est un manque de récursivité - si vous savez que l'objet de niveau supérieur est dynamique et c'est tout, cela fonctionne, mais si les objets dynamiques peuvent être à n'importe quel niveau ou à tous les niveaux de l'arborescence d'objets renvoyés, cela échoue.
Chris Moschini
J'ai apporté quelques améliorations à cette méthode pour la rendre récursive. Voici le code: gist.github.com/renanvieira/e26dc34e2de156723f79
MaltMaster
5

J'ai pu résoudre ce même problème en utilisant 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);

production:

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

Garfield
la source
1
Vous pouvez également le faire à l'aide de JSON .Net (Newtonsoft) en procédant comme suit. var entité = personne comme objet; var json = JsonConvert.SerializeObject (entité);
bkorzynski
4

J'ai poussé le processus d'aplatissement un peu plus loin et j'ai vérifié les objets de liste, ce qui supprime le non-sens de la valeur clé. :)

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();
    }
JustEngland
la source
3

Cela peut ne pas vous être utile, mais j'avais une exigence similaire, mais j'ai utilisé un SerializableDynamicObject

J'ai changé le nom du dictionnaire en "Fields", puis cela se sérialise avec Json.Net pour produire json qui ressemble à:

{"Fields": {"Property1": "Value1", "Property2": "Value2" etc. où Property1 et Property2 sont des propriétés ajoutées dynamiquement - c'est-à-dire des clés de dictionnaire

Ce serait parfait si je pouvais me débarrasser de la propriété supplémentaire "Fields" qui encapsule le reste, mais j'ai contourné cette limitation.

Réponse déplacée de cette question sur demande

BonyT
la source
3

C'est une réponse tardive, mais j'ai eu le même problème, et cette question m'a aidé à les résoudre. En résumé, j'ai pensé que je devrais poster mes résultats, dans l'espoir que cela accélère la mise en œuvre pour les autres.

Tout d'abord, ExpandoJsonResult, dont vous pouvez renvoyer une instance dans votre action. Ou vous pouvez remplacer la méthode Json dans votre contrôleur et la renvoyer là-bas.

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

Ensuite, le convertisseur (qui prend en charge à la fois la sérialisation et la désérialisation. Voir ci-dessous pour un exemple de dé-sérialisation).

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

Vous pouvez voir dans la classe ExpandoJsonResult comment l'utiliser pour la sérialisation. Pour désérialiser, créez le sérialiseur et enregistrez le convertisseur de la même manière, mais utilisez

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

Un grand merci à tous les participants ici qui m'ont aidé.

Skymt
la source
1

En utilisant le retour de ExpandoObject dynamique de WebApi dans ASP.Net 4, le formateur JSON par défaut semble aplatir ExpandoObjects en un simple objet JSON.

Joseph Gabriel
la source
1

JsonResultutilise JavaScriptSerializerqui désérialise réellement (le béton) Dictionary<string, object>comme vous le souhaitez.

Il y a une surcharge du Dictionary<string, object>constructeur qui prend IDictionary<string, object>.

ExpandoObjectoutils IDictionary<string, object> (je pense que vous pouvez voir où je vais ici ...)

ExpandoObject à un niveau

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

Une ligne de code, utilisant tous les types intégrés :)

ExpandoObjects imbriqués

Bien sûr, si vous imbriquez des ExpandoObjects, vous devrez tous les convertir récursivement enDictionary<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;
}

votre code final devenant

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);
dav_i
la source
-2

Il semble que le sérialiseur transforme l'Expando dans un dictionnaire, puis le sérialise (donc l'activité Clé / Valeur). Avez-vous essayé de désérialiser en tant que dictionnaire, puis de le renvoyer dans un Expando?

Luke Foust
la source
1
L'objet Expando implémente l'IDictionary <string, object>, donc je pense que c'est pourquoi JsonResult le sérialise dans un tableau de paires clé / valeur. Le lancer comme un IDictionary et inversement n'aiderait pas vraiment à l'aplatir, j'en ai peur.
TimDog
-2

J'ai juste eu le même problème et j'ai découvert quelque chose d'assez bizarre. Si je fais:

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

Cela fonctionne, mais seulement si ma méthode utilise l'attribut HttpPost. Si j'utilise HttpGet, j'obtiens une erreur. Donc ma réponse ne fonctionne que sur HttpPost. Dans mon cas, c'était un appel Ajax afin que je puisse changer HttpGet par HttpPost.

Rodrigo Manguinho
la source
2
-1 Ce n'est pas vraiment utile car cela se résume à stackoverflow.com/a/7042631/11635 et il ne sert à rien de faire ça dynamiquement si vous allez vous retourner et dépendre statiquement des noms comme vous le faites. Le problème AllowGet est complètement orthogonal.
Ruben Bartelink