Como nivelar um ExpandoObject retornado via JsonResult em asp.net mvc?

95

Eu realmente gosto da ExpandoObjectcompilação de um objeto dinâmico do lado do servidor em tempo de execução, mas estou tendo problemas para nivelar isso durante a serialização JSON. Primeiro, eu instancio o objeto:

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

Por enquanto, tudo bem. Em meu controlador MVC, quero enviar isso como um JsonResult, então faço o seguinte:

return new JsonResult(expando);

Isso serializa o JSON abaixo, para ser consumido pelo navegador:

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

MAS, o que eu realmente gostaria é de ver isso:

{SomeProp: SomeValueOrClass}

Sei que posso conseguir isso se usar em dynamicvez de ExpandoObject- JsonResultfor capaz de serializar as dynamicpropriedades e valores em um único objeto (sem negócios de chave ou valor), mas o motivo pelo qual preciso usar ExpandoObjecté porque não conheço todos as propriedades que desejo no objeto até o tempo de execução e, até onde sei, não posso adicionar dinamicamente uma propriedade a a dynamicsem usar um ExpandoObject.

Posso ter que vasculhar o negócio "Chave", "Valor" em meu javascript, mas esperava descobrir isso antes de enviá-lo ao cliente. Obrigado pela ajuda!

TimDog
fonte
9
Por que não usar apenas Dicionário <string, objeto> em vez de ExpandoObject? Ele serializa automaticamente no formato que você deseja e, de qualquer forma, você só está usando o ExpandoObject como um dicionário. Se você deseja serializar ExpandoObject's legítimos, usando o "return new JsonResult (d.ToDictionary (x => x.Key, x => x.Value));" abordagem é provavelmente o melhor meio-termo.
BrainSlugs83,

Respostas:

36

Você também pode fazer um JSONConverter especial que funcione apenas para ExpandoObject e registrá-lo em uma instância de JavaScriptSerializer. Desta forma você poderia serializar arrays de expando, combinações de objetos expando e ... até encontrar outro tipo de objeto que não está sendo serializado corretamente ("do jeito que você quer"), então você faz outro Conversor, ou adiciona outro tipo a este. Espero que isto ajude.

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 conversor

var serializer = new JavaScriptSerializer(); 
serializer.RegisterConverters(new JavaScriptConverter[] { new ExpandoJSONConverter()});
var json = serializer.Serialize(obj);
Pablo Rodda Doe
fonte
2
Isso funcionou muito bem para minhas necessidades. Se alguém quiser NotImplementedExceptioninserir algum código para adicionar algo como serializer.Deserialize<ExpandoObject>(json);, @theburningmonk oferece uma solução que funcionou para mim.
patridge de
2
Bom trabalho @ pablo. Excelente exemplo de como conectar uma rotina de serialização customizada no framework MVC!
pb.
A maneira mais fácil que descobri de fazer isso foi: new JavaScriptSerializer (). Deserialize <object> (Newtonsoft.Json.JsonConvert.SerializeObject (listOfExpandoObject)); O que você acha?
kavain de
Meu serializador é chamado recursivamente. Se eu definir RecursionLimit, recebo o erro de limite de recursão excedido ou erro de exceção de estouro de pilha. O que devo fazer? :(
Dhanashree
71

Usando JSON.NET, você pode chamar SerializeObject para "nivelar" o objeto expando:

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

var json = JsonConvert.SerializeObject(expando);

Irá produzir:

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

No contexto de um controlador ASP.NET MVC, o resultado pode ser retornado usando o 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
fonte
1
Newtonsoft.Json você quer dizer?
Ayyash
3
newtonsoft.json tem melhor manuseio para expandos recursivos dentro de expandos ou dicionários e dicionários internos, fora da caixa
Jone Polvora
26

Aqui está o que fiz para atingir o comportamento que você está descrevendo:

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

O custo é que você está fazendo uma cópia dos dados antes de serializá-los.

ajb
fonte
Agradável. Você também pode lançar a dinâmica na hora: return new JsonResult (((ExpandoObject) someIncomingDynamicExpando) .ToDictionary (item => item.Key, item => item.Value))
joeriks
"expando.Add" não funciona para mim. Eu acredito que neste caso é "d.Add" (que funcionou para mim).
Justin de
9
Então espere ... você está criando um ExpandoObject, lançando-o como um dicionário, usando-o como um dicionário, e então quando isso não é bom o suficiente, convertendo-o em um dicionário ... ... porque não usar um dicionário em este caso? ... o_o
BrainSlugs83
5
Um ExpandoObjectoferece muito mais flexibilidade do que um simples Dicionário. Embora o exemplo acima não demonstre isso, você pode usar os recursos dinâmicos do ExpandoObjectpara adicionar as propriedades que deseja ter em seu JSON. Um Dictioanryobjeto normal será convertido para JSON sem problemas, portanto, ao fazer a conversão, é uma maneira simples de colocar a dinâmica fácil de usar ExpandoObjectem um formato que pode ser JSONified. Você está correto, porém, o exemplo acima seria um uso ridículo do ExpandoObject; um simples Dictionaryseria muito melhor.
ajb
1
Gosto mais dessa abordagem - criar uma cópia não funciona em nenhum ambiente, mas tenho apenas pequenos objetos e o Expando é fornecido por um terceiro (imutável) ....
Sebastian J.
12

Resolvi isso escrevendo um método de extensão que converte o ExpandoObject em uma string 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();
}

Ele usa a excelente biblioteca Newtonsoft .

JsonResult então se parece com isto:

return JsonResult(expando.Flatten());

E isso é retornado ao navegador:

"{SomeProp: SomeValueOrClass}"

E posso usá-lo em javascript fazendo isso (referenciado aqui ):

var obj = JSON.parse(myJsonString);

Eu espero que isso ajude!

TimDog
fonte
7
Não avalie! Você deve usar um desserializador JSON para evitar problemas de segurança. Consulte json2.js: json.org/js.html var o = JSON.parse (myJsonString);
Lance Fisher
Eu gosto desse método de extensão embora. Agradável!
Lance Fisher
3
-1: Fazer isso em um método de extensão que retorna uma string não é a maneira correta de fazer a interface desse comportamento com a estrutura. Em vez disso, você deve estender a arquitetura de serialização embutida.
BrainSlugs83,
1
A principal desvantagem desse método é a falta de recursão - se você souber que o objeto de nível superior é dinâmico e pronto, isso funciona, mas se os objetos dinâmicos puderem estar em qualquer ou todos os níveis da árvore de objetos retornada, isso falhará.
Chris Moschini
Fiz algumas melhorias neste método para torná-lo recursivo. Aqui está o código: gist.github.com/renanvieira/e26dc34e2de156723f79
MaltMaster
5

Consegui resolver esse mesmo 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);

resultado:

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

Garfield
fonte
1
Você também pode fazer isso usando JSON .Net (Newtonsoft), concluindo as etapas a seguir. var entidade = pessoa como objeto; var json = JsonConvert.SerializeObject (entidade);
bkorzynski
4

Eu levei o processo de nivelamento um passo adiante e verifiquei os objetos de lista, o que remove o absurdo do valor da chave. :)

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

Isso pode não ser útil para você, mas eu tinha um requisito semelhante, mas usei um SerializableDynamicObject

Mudei o nome do dicionário para "Campos" e então ele serializa com Json.Net para produzir json que se parece com:

{"Campos": {"Propriedade1": "Valor1", "Propriedade2": "Valor2" etc. onde Propriedade1 e Propriedade2 são propriedades adicionadas dinamicamente - isto é, chaves de dicionário

Seria perfeito se eu pudesse me livrar da propriedade extra "Fields" que encapsula o resto, mas eu contornei essa limitação.

Resposta movida desta pergunta a pedido

BonyT
fonte
3

Esta é uma resposta tardia, mas eu tive o mesmo problema, e essa pergunta me ajudou a resolvê-los. Resumindo, achei que deveria postar meus resultados, na esperança de que isso acelere a implementação para outras pessoas.

Primeiro o ExpandoJsonResult, do qual você pode retornar uma instância em sua ação. Ou você pode substituir o método Json em seu controlador e retorná-lo 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));
        }
    }
}

Em seguida, o conversor (que suporta serialização e desserialização. Veja abaixo um exemplo de como desserializar).

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

Você pode ver na classe ExpandoJsonResult como usá-lo para serialização. Para desserializar, crie o serializador e registre o conversor da mesma forma, mas use

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

Um grande obrigado, a todos os participantes aqui que me ajudaram.

Skymt
fonte
1

Usando o retorno de ExpandoObject dinâmico de WebApi no ASP.Net 4, o formatador JSON padrão parece achatar ExpandoObjects em um objeto JSON simples.

Joseph gabriel
fonte
1

JsonResultusa JavaScriptSerializerque realmente desserializa (o concreto) Dictionary<string, object>como você deseja.

Há uma sobrecarga do Dictionary<string, object>construtor que leva IDictionary<string, object>.

ExpandoObjectimplementos IDictionary<string, object> (acho que você pode ver onde estou indo aqui ...)

ExpandoObject de nível único

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

Uma linha de código, usando todos os tipos integrados :)

ExpandoObjects aninhados

Claro, se você estiver aninhando ExpandoObjects, será necessário convertê-los recursivamente em 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;
}

seu código final se tornando

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

Parece que o serializador está convertendo o Expando em um Dicionário e, em seguida, serializando-o (portanto, o negócio de Chave / Valor). Você já tentou desserializar como um dicionário e depois lançar isso de volta para um Expando?

Luke Foust
fonte
1
O objeto Expando implementa o IDictionary <string, object>, então acho que é por isso que JsonResult o serializa em uma matriz de pares de chave / valor. Lançar um IDictionary e voltar não ajudaria muito a achatá-lo, infelizmente.
TimDog
-2

Eu simplesmente tive o mesmo problema e descobri algo muito estranho. Se eu fizer:

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

Funciona, mas apenas se meu método usar o atributo HttpPost. Se eu usar o HttpGet, recebo um erro. Portanto, minha resposta funciona apenas no HttpPost. No meu caso, foi uma chamada Ajax para que eu pudesse alterar HttpGet por HttpPost.

Rodrigo Manguinho
fonte
2
-1 Isso não é realmente útil, pois se resume a stackoverflow.com/a/7042631/11635 e não há nenhum ponto fazer isso dinamicamente se você vai mudar e depender dos nomes estaticamente como você faz. O problema AllowGet é completamente ortogonal.
Ruben Bartelink