Как сгладить ExpandoObject, возвращаемый через JsonResult в asp.net mvc?

95

Мне очень нравится ExpandoObjectкомпиляция динамического объекта на стороне сервера во время выполнения, но у меня возникают проблемы с выравниванием этого объекта во время сериализации JSON. Сначала я создаю экземпляр объекта:

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

Все идет нормально. В моем контроллере MVC я хочу отправить это как JsonResult, поэтому я делаю следующее:

return new JsonResult(expando);

Это сериализует JSON в приведенный ниже код, который будет использоваться браузером:

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

НО, мне бы очень хотелось это увидеть:

{SomeProp: SomeValueOrClass}

Я знаю, что могу добиться этого, если использую dynamicвместо ExpandoObject- JsonResultможет сериализовать dynamicсвойства и значения в один объект (без бизнеса Key или Value), но причина, по которой мне нужно использовать, ExpandoObjectзаключается в том, что я не знаю всех свойства, которые мне нужны для объекта до времени выполнения , и, насколько мне известно, я не могу динамически добавлять свойство в объект dynamicбез использования ExpandoObject.

Возможно, мне придется просеять бизнес «Ключ» и «Ценность» в моем javascript, но я надеялся выяснить это перед отправкой клиенту. Спасибо за вашу помощь!

TimDog
источник
9
Почему бы просто не использовать Dictionary <string, object> вместо ExpandoObject? Он автоматически сериализуется в нужный вам формат, и вы все равно используете свой ExpandoObject только как словарь. Если вы хотите сериализовать допустимые объекты ExpandoObject, используя «return new JsonResult (d.ToDictionary (x => x.Key, x => x.Value));» подход, вероятно, лучший компромисс.
BrainSlugs83

Ответы:

37

Вы также можете создать специальный JSONConverter, который работает только для ExpandoObject, а затем зарегистрировать его в экземпляре JavaScriptSerializer. Таким образом, вы можете сериализовать массивы expando, комбинации объектов expando и ... до тех пор, пока вы не найдете другой тип объекта, который не сериализуется правильно («так, как вы хотите»), затем вы создаете другой конвертер или добавляете другой тип в вот этот. Надеюсь это поможет.

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

Использование конвертера

var serializer = new JavaScriptSerializer(); 
serializer.RegisterConverters(new JavaScriptConverter[] { new ExpandoJSONConverter()});
var json = serializer.Serialize(obj);
Пабло Родда Пожертвовать
источник
2
Это отлично сработало для моих нужд. Если кто-то хочет NotImplementedExceptionдобавить какой-то код, чтобы добавить что-то вроде serializer.Deserialize<ExpandoObject>(json);, @theburningmonk предлагает решение, которое сработало для меня.
Patridge
2
Отличная работа @ pablo. Отличный пример подключения пользовательской процедуры сериализации к платформе MVC!
пб.
Я нашел самый простой способ сделать это: new JavaScriptSerializer (). Deserialize <object> (Newtonsoft.Json.JsonConvert.SerializeObject (listOfExpandoObject)); что вы думаете?
kavain
Мой сериализатор вызывается рекурсивно. Если я устанавливаю RecursionLimit, я получаю либо ошибку превышения лимита рекурсии, либо ошибку исключения переполнения стека. Что я должен делать? :(
Dhanashree
72

Используя JSON.NET, вы можете вызвать SerializeObject, чтобы «сгладить» объект expando:

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

var json = JsonConvert.SerializeObject(expando);

Выведет:

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

В контексте ASP.NET MVC Controller результат может быть возвращен с помощью 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?
Ayyash 06
3
newtonsoft.json имеет лучшую обработку для рекурсивных расширений внутри расширений или словарей и внутренних словарей, прямо из коробки
Джоне Полвора
26

Вот что я сделал для достижения описываемого вами поведения:

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

Стоимость состоит в том, что вы делаете копию данных перед их сериализацией.

ajb
источник
Ницца. Вы также можете преобразовать динамику на лету: return new JsonResult (((ExpandoObject) someIncomingDynamicExpando) .ToDictionary (item => item.Key, item => item.Value))
joeriks
"expando.Add" у меня не работает. Я считаю, что в данном случае это «d.Add» (у меня сработало).
Джастин
9
Так что подождите ... вы создаете ExpandoObject, преобразовываете его как словарь, используете его как словарь, а затем, когда этого недостаточно, преобразовываете его в словарь ... ... почему бы просто не использовать словарь в Это дело? ...
о_о
5
An ExpandoObjectдает вам гораздо больше гибкости, чем простой словарь. Хотя приведенный выше пример не демонстрирует этого, вы можете использовать динамические функции, ExpandoObjectчтобы добавить свойства, которые вы хотите иметь в своем JSON. Обычный Dictioanryобъект конвертируется в JSON без каких-либо проблем, поэтому, выполнив преобразование, это простой способ O (n) преобразовать простую в использовании динамику ExpandoObjectв формат, который может быть JSONified. Однако вы правы, приведенный выше пример - это повторное использование ExpandoObject; простой Dictionaryбудет намного лучше.
ajb
1
Мне больше нравится этот подход - создание копии не работает ни в какой среде, но у меня есть только небольшие объекты, а Expando предоставляется (неизменяемой) третьей стороной ....
Себастьян Дж.
12

Я решил это, написав метод расширения, который преобразует ExpandoObject в строку 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();
}

Здесь используется отличная библиотека Newtonsoft .

Тогда JsonResult выглядит так:

return JsonResult(expando.Flatten());

И это возвращается в браузер:

"{SomeProp: SomeValueOrClass}"

И я могу использовать его в javascript, выполнив это (ссылка здесь ):

var obj = JSON.parse(myJsonString);

Надеюсь, это поможет!

TimDog
источник
7
Не оценивайте это! Вы должны использовать десериализатор JSON, чтобы избежать проблем с безопасностью. См json2.js: json.org/js.html вар о = JSON.parse (myJsonString);
Лэнс Фишер,
Однако мне нравится этот метод расширения. Ницца!
Лэнс Фишер,
3
-1: выполнение этого в методе расширения, который возвращает строку, не является правильным способом взаимодействия этого поведения с платформой. Вместо этого вам следует расширить встроенную архитектуру сериализации.
BrainSlugs83
1
Основным недостатком этого метода является отсутствие рекурсии - если вы знаете, что объект верхнего уровня является динамическим, и это так, это работает, но если динамические объекты могут находиться на любом или каждом уровне возвращаемого дерева объектов, это не удается.
Крис Москини
Я сделал некоторые улучшения в этом методе, чтобы сделать его рекурсивным. Вот код: gist.github.com/renanvieira/e26dc34e2de156723f79
MaltMaster
5

Я смог решить эту же проблему с помощью 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);

выход:

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

Гарфилд
источник
1
Вы также можете сделать это с помощью JSON .Net (Newtonsoft), выполнив следующие шаги. var entity = человек как объект; var json = JsonConvert.SerializeObject (объект);
bkorzynski
4

Я продвинулся на шаг вперед в процессе сглаживания и проверил объекты списка, что устраняет бессмыслицу значения ключа. :)

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
источник
3

Это может быть вам не полезно, но у меня было аналогичное требование, но я использовал SerializableDynamicObject

Я изменил имя словаря на «Поля», а затем он сериализуется с помощью Json.Net для создания json, который выглядит так:

{"Поля": {"Свойство1": "Значение1", "Свойство2": "Значение2" и т. Д., Где Свойство1 и Свойство2 - это динамически добавляемые свойства, то есть ключи словаря

Было бы идеально, если бы я мог избавиться от лишнего свойства «Fields», которое инкапсулирует все остальное, но я обошел это ограничение.

Ответ перенесен с этого вопроса по запросу

BonyT
источник
3

Это поздний ответ, но у меня была такая же проблема, и этот вопрос помог мне их решить. Подводя итог, я подумал, что должен опубликовать свои результаты в надежде, что это ускорит реализацию для других.

Сначала ExpandoJsonResult, экземпляр которого вы можете вернуть в своем действии. Или вы можете переопределить метод Json в своем контроллере и вернуть его туда.

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

Затем конвертер (который поддерживает как сериализацию, так и десериализацию. См. Ниже пример того, как десериализовать).

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

Вы можете увидеть в классе ExpandoJsonResult, как использовать его для сериализации. Для десериализации создайте сериализатор и зарегистрируйте конвертер таким же образом, но используйте

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

Большое спасибо всем участникам, которые мне помогли.

Skymt
источник
2

JsonResultиспользует, JavaScriptSerializerкоторый фактически десериализует (бетон), Dictionary<string, object>как вы хотите.

Имеется перегрузка Dictionary<string, object>конструктора, который принимает IDictionary<string, object>.

ExpandoObjectорудия IDictionary<string, object> (я думаю, вы видите, куда я иду ...)

Одноуровневый 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 };

Одна строка кода, использующая все встроенные типы :)

Вложенные объекты ExpandoObjects

Конечно, если вы вкладываете ExpandoObjects, вам нужно будет рекурсивно преобразовать их все в 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;
}

ваш окончательный код становится

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
источник
1

Используя возврат динамического ExpandoObject из WebApi в ASP.Net 4, модуль форматирования JSON по умолчанию, кажется, сглаживает ExpandoObjects в простой объект JSON.

Джозеф Габриэль
источник
-2

Похоже, что сериализатор переводит Expando в словарь, а затем сериализует его (таким образом, бизнес-ключ / значение). Вы пробовали десериализацию как словарь, а затем преобразовать ее обратно в Expando?

Люк Фуст
источник
1
Объект Expando реализует IDictionary <string, object>, поэтому я думаю, что именно поэтому JsonResult сериализует его в массив пар ключ / значение. Боюсь, что преобразование его в IDictionary и обратно не поможет.
TimDog 01
-2

У меня была такая же проблема, и я выяснил кое-что довольно странное. Если я сделаю:

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

Он работает, но только если мой метод использует атрибут HttpPost. Если я использую HttpGet, я получаю ошибку. Итак, мой ответ работает только на HttpPost. В моем случае это был вызов Ajax, поэтому я мог изменить HttpGet на HttpPost.

Родриго Мангиньо
источник
2
-1 Это не очень полезно, так как сводится к stackoverflow.com/a/7042631/11635, и нет смысла делать это динамически, если вы собираетесь развернуться и зависеть от имен статически, как и вы. Проблема AllowGet полностью ортогональна.
Рубен Бартелинк,