Jak spłaszczyć ExpandoObject zwrócony przez JsonResult w asp.net mvc?

95

Bardzo podoba mi się ExpandoObjectpodczas kompilowania obiektu dynamicznego po stronie serwera w czasie wykonywania, ale mam problem ze spłaszczeniem tego elementu podczas serializacji JSON. Najpierw tworzę instancję obiektu:

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

Na razie w porządku. W moim kontrolerze MVC chcę następnie wysłać to jako JsonResult, więc robię to:

return new JsonResult(expando);

Spowoduje to serializację JSON do poniższego, do wykorzystania przez przeglądarkę:

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

ALE, naprawdę chciałbym to zobaczyć:

{SomeProp: SomeValueOrClass}

Wiem, że mogę to osiągnąć, jeśli dynamiczamiast tego używam ExpandoObject- JsonResultjest w stanie serializować dynamicwłaściwości i wartości w jednym obiekcie (bez biznesu kluczowego lub wartościowego), ale powodem, dla którego muszę użyć, ExpandoObjectjest to, że nie znam wszystkich właściwości, które chcę na obiekcie do czasu wykonania , i o ile wiem, nie mogę dynamicznie dodawać właściwości do obiektu dynamicbez użycia pliku ExpandoObject.

Być może będę musiał przejrzeć biznes „Klucz”, „Wartość” w moim javascript, ale miałem nadzieję, że uda mi się to rozgryźć przed wysłaniem go do klienta. Dzięki za pomoc!

TimDog
źródło
9
Dlaczego po prostu nie użyć Dictionary <string, object> zamiast ExpandoObject? Automatycznie serializuje się do żądanego formatu, a i tak używasz tylko ExpandoObject jak słownika. Jeśli chcesz serializować autentyczne ExpandoObject, użyj "return new JsonResult (d.ToDictionary (x => x.Key, x => x.Value));" podejście jest prawdopodobnie najlepszym kompromisem.
BrainSlugs83

Odpowiedzi:

37

Możesz również stworzyć specjalny JSONConverter, który działa tylko dla ExpandoObject, a następnie zarejestrować go w instancji JavaScriptSerializer. W ten sposób możesz serializować tablice expando, kombinacje obiektów expando i ... dopóki nie znajdziesz innego rodzaju obiektu, który nie jest serializowany poprawnie ("tak jak chcesz"), wtedy tworzysz kolejny konwerter lub dodajesz inny typ do ten. Mam nadzieję że to pomoże.

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

Korzystanie z konwertera

var serializer = new JavaScriptSerializer(); 
serializer.RegisterConverters(new JavaScriptConverter[] { new ExpandoJSONConverter()});
var json = serializer.Serialize(obj);
Pablo Rodda Przekaż darowiznę
źródło
2
To działało świetnie na moje potrzeby. Jeśli ktoś chce podłączyć jakiś kod, NotImplementedExceptionaby dodać coś takiego serializer.Deserialize<ExpandoObject>(json);, @theburningmonk oferuje rozwiązanie, które działało dla mnie.
patridge
2
Świetna praca @ pablo. Doskonały przykład podłączenia niestandardowej procedury serializacji do frameworka MVC!
pb.
Najłatwiejszy sposób, w jaki udało mi się to zrobić, to: new JavaScriptSerializer (). Deserialize <object> (Newtonsoft.Json.JsonConvert.SerializeObject (listOfExpandoObject)); co myślisz?
kavain
Mój serializator jest wywoływany rekurencyjnie. Jeśli ustawię RecursionLimit, otrzymuję błąd przekroczenia limitu rekursji lub błąd wyjątku przepełnienia stosu. Co powinienem zrobić? :(
Dhanashree,
71

Używając JSON.NET, możesz wywołać SerializeObject, aby „spłaszczyć” obiekt expando:

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

var json = JsonConvert.SerializeObject(expando);

Wyświetli:

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

W kontekście kontrolera ASP.NET MVC wynik można zwrócić przy użyciu metody 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
źródło
1
Newtonsoft.Json masz na myśli?
Ayyash,
3
newtonsoft.json ma lepszą obsługę rekurencyjnych rozszerzeń w rozszerzeniach lub słownikach i wewnętrznych słownikach, po wyjęciu z pudełka
Jone Polvora
26

Oto, co zrobiłem, aby osiągnąć opisywane przez Ciebie zachowanie:

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

Koszt jest taki, że tworzysz kopię danych przed ich serializacją.

ajb
źródło
Ładny. Możesz także rzucić dynamikę w locie: return new JsonResult (((ExpandoObject) someIncomingDynamicExpando) .ToDictionary (item => item.Key, item => item.Value))
joeriks
„expando.Add” nie działa dla mnie. Uważam, że w tym przypadku jest to „d.Add” (które zadziałało dla mnie).
Justin
9
Więc czekaj ... tworzysz ExpandoObject, przesyłasz go jako słownik, używasz go jak słownika, a kiedy to nie wystarczy, konwertujesz go na słownik ... ... dlaczego nie użyć słownika w ta sprawa? ... o_o
BrainSlugs83
5
An ExpandoObjectzapewnia znacznie większą elastyczność niż prosty słownik. Chociaż powyższy przykład tego nie demonstruje, możesz użyć dynamicznych funkcji, ExpandoObjectaby dodać właściwości, które chcesz mieć w swoim formacie JSON. Normalny Dictioanryobiekt zostanie przekonwertowany na JSON bez żadnych problemów, więc wykonując konwersję, jest to prosty sposób O (n) na umieszczenie łatwej w użyciu dynamiki ExpandoObjectw formacie, który można zweryfikować za pomocą JSON. Masz jednak rację, powyższy przykład byłby wykorzystaniem rediculus ExpandoObject; proste Dictionarybyłoby znacznie lepsze.
ajb
1
Bardziej podobne podejście - tworzenie kopii nie działa w żadnym środowisku, ale mam tylko małe obiekty, a Expando jest dostarczane przez (niezmienną) firmę zewnętrzną ....
Sebastian J.
12

Rozwiązałem to, pisząc metodę rozszerzenia, która konwertuje ExpandoObject na ciąg 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();
}

Wykorzystuje to doskonałą bibliotekę Newtonsoft .

JsonResult wygląda wtedy następująco:

return JsonResult(expando.Flatten());

I to jest zwracane do przeglądarki:

"{SomeProp: SomeValueOrClass}"

I mogę go użyć w javascript, robiąc to (przywołane tutaj ):

var obj = JSON.parse(myJsonString);

Mam nadzieję, że to pomoże!

TimDog
źródło
7
Nie oceniaj tego! Aby uniknąć problemów z zabezpieczeniami, należy użyć deserializatora JSON. Zobacz json2.js: json.org/js.html var o = JSON.parse (myJsonString);
Lance Fisher
Podoba mi się jednak ta metoda rozszerzenia. Ładny!
Lance Fisher
3
-1: zrobienie tego w metodzie rozszerzenia, która zwraca ciąg znaków, nie jest prawidłowym sposobem połączenia tego zachowania ze strukturą. Zamiast tego należy rozszerzyć wbudowaną architekturę serializacji.
BrainSlugs83
1
Główną wadą tej metody jest brak rekursji - jeśli wiesz, że obiekt najwyższego poziomu jest dynamiczny i to wszystko, to działa, ale jeśli obiekty dynamiczne mogą znajdować się na dowolnym lub każdym poziomie zwracanego drzewa obiektów, kończy się to niepowodzeniem.
Chris Moschini,
Wprowadziłem kilka ulepszeń w tej metodzie, aby była rekurencyjna. Oto kod: gist.github.com/renanvieira/e26dc34e2de156723f79
MaltMaster
5

Udało mi się rozwiązać ten sam problem za pomocą 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);

wynik:

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

Garfield
źródło
1
Możesz to również zrobić za pomocą JSON .Net (Newtonsoft), wykonując następujące kroki. zmienna jednostka = osoba jako przedmiot; var json = JsonConvert.SerializeObject (jednostka);
bkorzyński 22.07.13
4

Poszedłem o krok dalej w procesie spłaszczania i sprawdziłem, czy nie ma obiektów na liście, co eliminuje nonsensowne wartości klucza. :)

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
źródło
3

To może ci się nie przydać, ale miałem podobne wymaganie, ale użyłem SerializableDynamicObject

Zmieniłem nazwę słownika na „Fields”, a następnie serializuję go z Json.Net, aby utworzyć plik json, który wygląda następująco:

{"Pola": {"Właściwość1": "Wartość1", "Właściwość2": "Wartość2" itd. Gdzie Właściwość1 i Właściwość2 to właściwości dodawane dynamicznie - tj. Klucze słownika

Byłoby idealnie, gdybym mógł pozbyć się dodatkowej właściwości „Fields”, która obejmuje resztę, ale obejrzałem to ograniczenie.

Odpowiedź przeniesiona z tego pytania na żądanie

BonyT
źródło
3

To późna odpowiedź, ale miałem ten sam problem, a to pytanie pomogło mi je rozwiązać. Podsumowując, pomyślałem, że powinienem opublikować swoje wyniki, mając nadzieję, że przyspieszy to wdrożenie u innych.

Najpierw ExpandoJsonResult, do którego możesz zwrócić instancję w swojej akcji. Lub możesz zastąpić metodę Json w kontrolerze i zwrócić ją tam.

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

Następnie konwerter (który obsługuje zarówno serializację, jak i deserializację. Poniżej znajduje się przykład usuwania serializacji).

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

W klasie ExpandoJsonResult można zobaczyć, jak używać go do serializacji. Aby usunąć serializację, utwórz serializator i zarejestruj konwerter w ten sam sposób, ale użyj

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

Wielkie podziękowania dla wszystkich uczestników, którzy mi pomogli.

Skymt
źródło
2

JsonResultużywa, JavaScriptSerializerktóre faktycznie deserializuje (konkret), Dictionary<string, object>jak chcesz.

Dictionary<string, object>Konstruktor jest przeciążony, który pobiera IDictionary<string, object>.

ExpandoObjectnarzędzia IDictionary<string, object> (chyba widać, dokąd tu idę ...)

Jednopoziomowy ExpandoObject

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

Jedna linia kodu, wykorzystująca wszystkie wbudowane typy :)

Zagnieżdżone ExpandoObjects

Oczywiście, jeśli zagnieżdżasz ExpandoObjects, będziesz musiał rekurencyjnie przekonwertować je wszystkie na 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;
}

Twój ostateczny kod staje się

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
źródło
1

Korzystając z zwracania dynamicznego ExpandoObject z WebApi w ASP.Net 4, domyślny program formatujący JSON wydaje się spłaszczać ExpandoObjects do prostego obiektu JSON.

Joseph Gabriel
źródło
-2

Wygląda na to, że serializator rzutuje Expando do słownika, a następnie serializuje go (czyli biznes klucz / wartość). Czy próbowałeś deserializacji jako słownika, a następnie odesłanie go z powrotem do Expando?

Luke Foust
źródło
1
Obiekt Expando implementuje IDictionary <string, object>, więc myślę, że właśnie dlatego JsonResult serializuje go do tablicy par klucz / wartość. Obawiam się, że przekazanie tego jako słownika i z powrotem tak naprawdę nie pomogłoby go spłaszczyć.
TimDog
-2

Po prostu miałem ten sam problem i wymyśliłem coś dziwnego. Jeśli zrobię:

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

Działa, ale tylko wtedy, gdy moja metoda używa atrybutu HttpPost. Jeśli używam HttpGet, pojawia się błąd. Więc moja odpowiedź działa tylko na HttpPost. W moim przypadku było to połączenie Ajax, więc mogłem zmienić HttpGet przez HttpPost.

Rodrigo Manguinho
źródło
2
-1 Nie jest to zbyt przydatne, ponieważ sprowadza się do stackoverflow.com/a/7042631/11635 i nie ma sensu robić tego dynamicznie, jeśli zamierzasz się odwrócić i polegać na nazwach statycznie, tak jak robisz. Problem AllowGet jest całkowicie ortogonalny.
Ruben Bartelink,