¿Cómo aplanar un ExpandoObject devuelto a través de JsonResult en asp.net mvc?

95

Realmente me gusta al ExpandoObjectcompilar un objeto dinámico del lado del servidor en tiempo de ejecución, pero tengo problemas para aplanar esto durante la serialización JSON. Primero, instancia el objeto:

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

Hasta aquí todo bien. En mi controlador MVC, quiero enviar esto como un JsonResult, así que hago esto:

return new JsonResult(expando);

Esto serializa el JSON en el siguiente, para ser consumido por el navegador:

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

PERO, lo que realmente me gustaría es ver esto:

{SomeProp: SomeValueOrClass}

Sé que puedo lograr esto si uso en dynamiclugar de ExpandoObject: JsonResultes capaz de serializar las dynamicpropiedades y los valores en un solo objeto (sin negocios de clave o valor), pero la razón por la que necesito usar ExpandoObjectes porque no sé todos las propiedades que quiero en el objeto hasta el tiempo de ejecución , y hasta donde yo sé, no puedo agregar dinámicamente una propiedad a dynamicsin usar un ExpandoObject.

Puede que tenga que examinar el negocio "Clave", "Valor" en mi javascript, pero esperaba resolver esto antes de enviarlo al cliente. ¡Gracias por tu ayuda!

TimDog
fuente
9
¿Por qué no usar Dictionary <string, object> en lugar de ExpandoObject? Se serializa automáticamente al formato que desea, y de todos modos solo está usando su ExpandoObject como un diccionario. Si desea serializar ExpandoObject's legítimos, use el "return new JsonResult (d.ToDictionary (x => x.Key, x => x.Value));" El enfoque es probablemente el mejor compromiso.
BrainSlugs83

Respuestas:

36

También puede crear un JSONConverter especial que funcione solo para ExpandoObject y luego registrarlo en una instancia de JavaScriptSerializer. De esta manera, podría serializar matrices de expando, combinaciones de objetos expando y ... hasta que encuentre otro tipo de objeto que no se serialice correctamente ("de la manera que desee"), entonces crea otro convertidor o agrega otro tipo a éste. Espero que esto ayude.

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

Usando convertidor

var serializer = new JavaScriptSerializer(); 
serializer.RegisterConverters(new JavaScriptConverter[] { new ExpandoJSONConverter()});
var json = serializer.Serialize(obj);
Donar Pablo Rodda
fuente
2
Esto funcionó muy bien para mis necesidades. Si alguien quiere conectar algún código para NotImplementedExceptionagregar algo como serializer.Deserialize<ExpandoObject>(json);, @theburningmonk ofrece una solución que funcionó para mí.
patridge
2
Buen trabajo @ pablo. Excelente ejemplo de cómo conectar una rutina de serialización personalizada en el marco MVC.
pb.
La forma más fácil que encontré para hacer esto fue: new JavaScriptSerializer (). Deserialize <object> (Newtonsoft.Json.JsonConvert.SerializeObject (listOfExpandoObject)); ¿Qué piensas?
Kavain
Mi serializador se llama de forma recursiva. Si configuro RecursionLimit, obtengo un error de límite de recursividad excedido o un error de excepción de desbordamiento de pila. ¿Qué tengo que hacer? :(
Dhanashree
71

Usando JSON.NET puede llamar a SerializeObject para "aplanar" el objeto expando:

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

var json = JsonConvert.SerializeObject(expando);

Saldrá:

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

En el contexto de un controlador ASP.NET MVC, el resultado se puede devolver utilizando el método 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
fuente
1
Newtonsoft.Json, ¿te refieres?
Ayyash
3
newtonsoft.json tiene un mejor manejo para expandos recursivos dentro de expandos o diccionarios y diccionarios internos, fuera de la caja
Jone Polvora
26

Esto es lo que hice para lograr el comportamiento que está describiendo:

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

El costo es que está haciendo una copia de los datos antes de serializarlos.

ajb
fuente
Agradable. También puede lanzar la dinámica sobre la marcha: return new JsonResult (((ExpandoObject) someIncomingDynamicExpando) .ToDictionary (item => item.Key, item => item.Value))
joeriks
"expando.Add" no me funciona. Creo que en este caso es "d.Add" (que funcionó para mí).
Justin
9
Así que espera ... estás creando un ExpandoObject, convirtiéndolo en un diccionario, usándolo como un diccionario, y luego, cuando eso no sea lo suficientemente bueno, convirtiéndolo en un diccionario ... ... ¿por qué no usar un diccionario en ¿este caso? ... o_o
BrainSlugs83
5
Un ExpandoObjectle ofrece mucha más flexibilidad que un diccionario simple. Aunque el ejemplo anterior no lo demuestra, puede usar las características dinámicas del ExpandoObjectpara agregar las propiedades que desea tener en su JSON. Un Dictioanryobjeto normal se convertirá a JSON sin ningún problema, por lo que al hacer la conversión, es una forma O (n) simple de poner la dinámica fácil de usar ExpandoObjecten un formato que puede ser JSONificado. Sin embargo, tiene razón, el ejemplo anterior sería un uso rediculus de ExpandoObject; un simple Dictionarysería mucho mejor.
ajb
1
Me gusta más ese enfoque: crear una copia no funciona en ningún entorno, pero solo tengo objetos pequeños y el Expando lo proporciona un tercero (inmutable) ...
Sebastian J.
12

Resolví esto escribiendo un método de extensión que convierte el ExpandoObject en una cadena 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();
}

Esto usa la excelente biblioteca Newtonsoft .

JsonResult se ve así:

return JsonResult(expando.Flatten());

Y esto se devuelve al navegador:

"{SomeProp: SomeValueOrClass}"

Y puedo usarlo en javascript haciendo esto (mencionado aquí ):

var obj = JSON.parse(myJsonString);

¡Espero que esto ayude!

TimDog
fuente
7
¡No lo evalúes! Debe utilizar un deserializador JSON para evitar problemas de seguridad. Consulte json2.js: json.org/js.html var o = JSON.parse (myJsonString);
Lance Fisher
Sin embargo, me gusta ese método de extensión. ¡Agradable!
Lance Fisher
3
-1: Hacer esto en un método de extensión que devuelve una cadena no es la forma correcta de conectar este comportamiento con el marco. En su lugar, debería ampliar la arquitectura de serialización incorporada.
BrainSlugs83
1
El principal inconveniente de este método es la falta de recursividad: si sabe que el objeto de nivel superior es dinámico y eso es todo, esto funciona, pero si los objetos dinámicos podrían estar en cualquiera o en todos los niveles del árbol de objetos devuelto, esto falla.
Chris Moschini
Hice algunas mejoras en este método para que sea recursivo. Aquí está el código: gist.github.com/renanvieira/e26dc34e2de156723f79
MaltMaster
5

Pude resolver este mismo problema usando 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);

salida:

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

Garfield
fuente
1
También puede hacer esto usando JSON .Net (Newtonsoft) completando los siguientes pasos. var entidad = persona como objeto; var json = JsonConvert.SerializeObject (entidad);
bkorzynski
4

Llevé el proceso de aplanamiento un paso más allá y verifiqué los objetos de la lista, lo que elimina las tonterías del valor clave. :)

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
fuente
3

Esto puede no ser útil para usted, pero tenía un requisito similar, pero usé un SerializableDynamicObject

Cambié el nombre del diccionario a "Campos" y luego esto se serializa con Json.Net para producir json que se ve así:

{"Campos": {"Propiedad1": "Valor1", "Propiedad2": "Valor2", etc. donde Propiedad1 y Propiedad2 son propiedades agregadas dinámicamente, es decir, claves de diccionario

Sería perfecto si pudiera deshacerme de la propiedad adicional "Fields" que encapsula el resto, pero he solucionado esa limitación.

Respuesta movida de esta pregunta a pedido

BonyT
fuente
3

Esta es una respuesta tardía, pero tuve el mismo problema y esta pregunta me ayudó a resolverlos. Como resumen, pensé que debería publicar mis resultados, con la esperanza de que acelere la implementación para otros.

Primero, el ExpandoJsonResult, del cual puede devolver una instancia en su acción. O puede anular el método Json en su controlador y devolverlo allí.

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

Luego, el convertidor (que admite tanto la serialización como la deserialización. Consulte a continuación un ejemplo de cómo deserializar).

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

Puede ver en la clase ExpandoJsonResult cómo usarlo para la serialización. Para deserializar, cree el serializador y registre el convertidor de la misma manera, pero use

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

Un gran agradecimiento a todos los participantes aquí que me ayudaron.

Skymt
fuente
1

Al devolver ExpandoObject dinámico de WebApi en ASP.Net 4, el formateador JSON predeterminado parece aplanar ExpandoObjects en un objeto JSON simple.

José Gabriel
fuente
1

JsonResultutiliza JavaScriptSerializerque realmente deserializa (el concreto) Dictionary<string, object>como desee.

Hay una sobrecarga del Dictionary<string, object>constructor que toma IDictionary<string, object>.

ExpandoObjectimplementos IDictionary<string, object> (creo que puedes ver a dónde voy aquí ...)

ExpandoObject de un solo nivel

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 línea de código, usando todos los tipos integrados :)

ExpandoObjects anidados

Por supuesto, si está anidando ExpandoObjectcorreos electrónicos, deberá convertirlos todos de forma recursiva en correos electrónicos Dictionary<string, object>:

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

tu código final se convierte

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
fuente
-2

Parece que el serializador está enviando el Expando a un Diccionario y luego serializándolo (por lo tanto, el negocio Clave / Valor). ¿Ha intentado deserializar como diccionario y luego volver a convertirlo en un Expando?

Luke Foust
fuente
1
El objeto Expando implementa IDictionary <string, object>, así que creo que es por eso que JsonResult lo serializa en una matriz de pares clave / valor. Lanzarlo como un IDiccionario y viceversa realmente no ayudaría a aplanarlo, me temo.
TimDog
-2

Simplemente tuve el mismo problema y descubrí algo bastante extraño. Si lo hago:

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

Funciona, pero solo si mi método usa el atributo HttpPost. Si uso HttpGet, obtengo un error. Entonces mi respuesta solo funciona en HttpPost. En mi caso, fue una llamada Ajax, por lo que pude cambiar HttpGet por HttpPost.

Rodrigo Manguinho
fuente
2
-1 Esto no es realmente útil ya que se reduce a stackoverflow.com/a/7042631/11635 y no tiene sentido hacer esto dinámicamente si vas a dar la vuelta y depender de los nombres estáticamente mientras lo haces. El problema de AllowGet es completamente ortogonal.
Ruben Bartelink