如何在ASP.NET MVC中通过JsonResult返回的ExpandoObject对象进行扁平化处理?

100

我很喜欢在运行时编译服务器端动态对象时使用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属性和值序列化为单个对象(没有键或值的问题),但我需要使用ExpandoObject的原因是在运行时之前我不知道想要在对象上有哪些属性,据我所知,我不能在dynamic中动态添加属性,除非使用ExpandoObject

我可能需要在我的JavaScript中筛选“Key”、“Value”,但我希望在向客户端发送数据之前就解决这个问题。感谢您的帮助!


10
为何不直接使用Dictionary<string, object>而使用ExpandoObject呢?因为它能自动序列化成你想要的格式,而且你只是像使用字典一样使用ExpandoObject。如果你想要序列化合法的ExpandoObject,那么使用"return new JsonResult(d.ToDictionary(x => x.Key, x => x.Value));"方法可能是最好的折衷方案。 - BrainSlugs83
12个回答

72
使用JSON.NET,您可以调用SerializeObject来“展平”动态对象:
dynamic expando = new ExpandoObject();
expando.name = "John Smith";
expando.age = 30;

var json = JsonConvert.SerializeObject(expando);

将输出:

{"name":"John Smith","age":30}
在ASP.NET MVC控制器的上下文中,可以使用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
3
Newtonsoft.Json可以更好地处理展开式或字典内部以及内部字典中的递归expandos,无需额外操作。 - Jone Polvora

37
您还可以创建一个特殊的JSONConverter,仅适用于ExpandoObject,然后将其注册在JavaScriptSerializer的实例中。这样,您就可以序列化expando数组、expando对象的组合等,直到找到另一种未能以您期望的方式正确序列化的对象,然后再创建另一个Converter或将另一种类型添加到此Converter中。希望这可以帮助到您。
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提供了一个解决方案,对我很有效。链接在此:http://theburningmonk.com/2011/05/idictionarystring-object-to-expandoobject-extension-method/ - patridge
2
@pablo,干得好!很棒的例子,展示了如何将自定义序列化程序插入MVC框架中! - pb.
我发现最简单的方法是:new JavaScriptSerializer().Deserialize<object>(Newtonsoft.Json.JsonConvert.SerializeObject(listOfExpandoObject)); 你觉得呢? - kavain
我的序列化程序被递归调用了。如果我设置 RecursionLimit,我要么得到递归限制超出错误,要么得到堆栈溢出异常错误。我该怎么办?:( - Dhanashree

27

下面是我为实现你所描述的行为所做的操作:

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

代价在于在序列化之前要复制数据。


1
很好。您还可以动态转换:返回一个新的JsonResult(((ExpandoObject)someIncomingDynamicExpando).ToDictionary(item => item.Key, item => item.Value)) - joeriks
"expando.Add" 对我无效。在这种情况下,我相信应该使用 "d.Add"(这个对我有效)。 - Justin
9
等等……你正在创建一个ExpandoObject,将其强制转换为字典,像使用字典一样使用它,当这还不够好时,又把它转换回字典……为什么不直接在这种情况下使用一个字典呢?…o_o - BrainSlugs83
5
使用ExpandoObject比使用简单的字典更加灵活。虽然上面的例子没有展示这一点,但你可以利用ExpandoObject的动态特性来添加你想要在JSON中拥有的属性。普通的Dictionary对象可以无问题地转换为JSON,因此通过进行转换,将易于使用的动态ExpandoObject转换为可JSON化的格式是一种简单的O(n)方法。不过,你是正确的,上面的例子是对ExpandoObject荒谬的运用;使用简单的Dictionary会更好。 - ajb
1
我更喜欢这种方法——在任何环境下都不能创建副本,但我的对象很小,并且Expando由(不可更改的)第三方提供... - Sebastian J.

11

我通过编写一个扩展方法将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);

希望这有所帮助!


7
不要使用eval函数!为了避免安全问题,你应该使用JSON反序列化器。请参考json2.js:http://www.json.org/js.html var o = JSON.parse(myJsonString); - Lance Fisher
我喜欢那个扩展方法。不错! - Lance Fisher
3
在一个返回字符串的扩展方法中完成这个操作并不是正确将此行为与框架进行接口交互的方式。相反,你应该扩展内置的序列化架构。 - BrainSlugs83
1
这种方法的主要缺点是缺乏递归 - 如果您知道顶级对象是动态的,那么它就可以工作,但如果返回的对象树中的动态对象可能在任何或每个级别上,那么它将失败。 - Chris Moschini
我对这个方法进行了一些改进,使其变成了递归的。以下是代码:https://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": "约翰", "LastName": "多伊", "Address": "1234 Home St", "City": "家乡", "State": "加利福尼亚州", "Zip": "12345" }


1
您也可以通过完成以下步骤,使用JSON .Net(Newtonsoft)来实现此操作。var entity = person as object; var json = JsonConvert.SerializeObject(entity); - 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();
    }

4
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

当然,如果你在嵌套ExpandoObject,那么你需要递归地将它们全部转换成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;
}

你的最终代码变成了:
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);

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

非常感谢所有在这里帮助我的参与者。


3
这可能对你没什么用,但我有一个类似的需求,但是使用了SerializableDynamicObject。我将字典的名称更改为“Fields”,然后使用Json.Net进行序列化,生成的json如下所示:{"Fields":{"Property1":"Value1", "Property2":"Value2"等,其中Property1和Property2是动态添加的属性,即字典键。如果我能够摆脱封装其余内容的额外“Fields”属性,那就太完美了,但我已经绕过了这个限制。答案来自于此问题的请求。

1

在ASP.Net 4中,当从WebApi返回动态ExpandoObject时,默认的JSON格式化程序似乎会将ExpandoObject扁平化为简单的JSON对象。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接