如何在 asp.net mvc 中压平通过 JsonResult 返回的 ExpandObject?

在运行时编译服务器端动态对象时,我非常喜欢使用 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 中筛选“键”、“值”业务,但是我希望在发送给客户端之前弄清楚这一点。谢谢你的帮助!

50527 次浏览

It seems like the serializer is casting the Expando to a Dictionary and then serializing it (thus the Key/Value business). Have you tried Deserializing as a Dictionary and then casting that back to an Expando?

I solved this by writing an extension method that converts the ExpandoObject into a JSON string:

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

This uses the excellent Newtonsoft library.

JsonResult then looks like this:

return JsonResult(expando.Flatten());

And this is returned to the browser:

"{SomeProp: SomeValueOrClass}"

And I can use it in javascript by doing this (referenced here):

var obj = JSON.parse(myJsonString);

I hope this helps!

I took the flattening process one step further and checked for list objects, which removes the key value nonsense. :)

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

You could also, make a special JSONConverter that works only for ExpandoObject and then register it in an instance of JavaScriptSerializer. This way you could serialize arrays of expando,combinations of expando objects and ... until you find another kind of object that is not getting serialized correctly("the way u want"), then you make another Converter, or add another type to this one. Hope this helps.

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

Using converter

var serializer = new JavaScriptSerializer();
serializer.RegisterConverters(new JavaScriptConverter[] { new ExpandoJSONConverter()});
var json = serializer.Serialize(obj);

I was able to solve this same problem using 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);

output:

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

Here's what I did to achieve the behavior you're describing:

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

The cost is that you're making a copy of the data before serializing it.

I just had the same problem and figured out something pretty weird. If I do:

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

It works, but only if my method use HttpPost attribute. If I use HttpGet i get error. So my answer works only on HttpPost. In my case it was an Ajax Call so i could change HttpGet by HttpPost.

This may not be useful to you, but I had a similar requirement, but used a SerializableDynamicObject

I changed the name of the dictionary to "Fields" and then this serializes with Json.Net to produce json which looks like:

{"Fields":{"Property1":"Value1", "Property2":"Value2" etc. where Property1 and Property2 are Dynamically added properties - i.e. Dictionary Keys

It would be perfect if I could get rid of the extra "Fields" property which encapsulates the rest, but I've worked around that limitation.

Answer moved from this question on request

This is a late answer, but I had the same problem, and this question helped me solve them. As a summary, I thought I should post my results, in hopes that it speeds up the implementation for others.

First the ExpandoJsonResult, which you can return an instance of in your action. Or you can override the Json method in your controller and return it there.

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

Then the converter (which supports both serialization and de-serialization. See below for an example of how to de-serialize).

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

You can see in the ExpandoJsonResult class how to use it for serialization. To de-serialize, create the serializer and register the converter in the same way, but use

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

A big thank you, to all participants here that helped me.

Using JSON.NET you can call SerializeObject to "flatten" the expando object:

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


var json = JsonConvert.SerializeObject(expando);

Will output:

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

In the context of an ASP.NET MVC Controller, the result can be returned using the Content-method:

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

Using returning dynamic ExpandoObject from WebApi in ASP.Net 4, the default JSON formatter seems to flatten ExpandoObjects into simple JSON object.

JsonResult uses JavaScriptSerializer which actually deserializes (the concrete) Dictionary<string, object> as you want.

There's an overload of the Dictionary<string, object> constructor which takes IDictionary<string, object>.

ExpandoObject implements IDictionary<string, object> (I think you can see where I am going here...)

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

One line of code, using all built-in types :)

Nested ExpandoObjects

Of course if you are nesting ExpandoObjects then you will need to recursively convert them all into 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;
}

your final code becoming

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