Serializing ExpandoObject with ServiceStack.Text

2019-05-01 06:04发布

问题:

I am trying to serialize objects with the library ServiceStack.Text . This works

using System.Dynamic;
using ServiceStack.Text;
var x = new {Value= 10, Product = "Apples"};                
Console.WriteLine(JsonSerializer.SerializeToString(x));

I get, as I expect

{"Value":10,"Product":"Apples"}

However

dynamic x = new ExpandoObject();
x.Value = 100;
x.Product = "Apples";
Console.WriteLine(JsonSerializer.SerializeToString(x));

I get to my surprise

[{"Key":"Value","Value":100},{"Key":"Product","Value":"Apples"}]

Why! What's going on?

Secondly, how can I get what I want?

回答1:

ExpandoObject implements IConnection<KeyValuePair> and IEnumerable<KeyValuePair>:

public sealed class ExpandoObject :
    IDynamicMetaObjectProvider,
    IDictionary<string, object>,
    ICollection<KeyValuePair<string, object>>,
    IEnumerable<KeyValuePair<string, object>>,
    IEnumerable, INotifyPropertyChanged

My guess is that internally, the ServiceStack serializer is treating the ExpandoObject as an IEnumerable<KeyValuePair>, and so it serializes to a JSON array of key/value pairs.


This differs from your first (working) code snippet because .NET actually builds a real (anonymous) class for your data, basically it makes:

public class SomeNameTheCompilerMakesUp {
    internal int Value { get; set; }
    internal string Product { get; set; }
}

for you automatically, so when it is sent to the serializer, it is working with a real class with real properties, whereas the ExpandoObject is really backed by an object[] internally.


On a side-note, Microsoft's System.Web.Helpers.Json behaves the same way. This test passes:

    [TestMethod]
    public void ExpandoObjectSerializesToJsonArray()
    {
        dynamic anonType = new { Value = 10, Product = "Apples" };

        dynamic expando = new ExpandoObject();
        expando.Value = 10;
        expando.Product = "Apples";

        var anonResult = System.Web.Helpers.Json.Encode(anonType);
        var expandoResult = System.Web.Helpers.Json.Encode(expando);

        Assert.AreEqual("{\"Value\":10,\"Product\":\"Apples\"}", anonResult);
        Assert.AreEqual("[{\"Key\":\"Value\",\"Value\":10},{\"Key\":\"Product\",\"Value\":\"Apples\"}]", expandoResult);
    }

One final edit:

You can make this work the way you want by turning your ExpandoObject into a Dictionary<string, object>. The caveat to this code is that it duplicates the data into a dictionary, so you have 2 copies in memory (or slightly less than, since technically the strings might be interned).

    [TestMethod]
    public void TestMethod1()
    {
        dynamic expando = new ExpandoObject();
        expando.Value = 10;
        expando.Product = "Apples";

        // copy expando properties to dictionary
        var dictionary = ((ExpandoObject)expando).ToDictionary(x => x.Key, x => x.Value);

        var expandoResult    = System.Web.Helpers.Json.Encode(expando);
        var dictionaryResult = System.Web.Helpers.Json.Encode(dictionary);

        Assert.AreEqual("[{\"Key\":\"Value\",\"Value\":10},{\"Key\":\"Product\",\"Value\":\"Apples\"}]", expandoResult);
        Assert.AreEqual("{\"Value\":10,\"Product\":\"Apples\"}", dictionaryResult);
    }

Although, for anyone that comes across this later, and is actually using System.Web.Helpers.Json, the better thing to do is just wrap your ExpandoObject in a DynamicJsonObject like this:

    [TestMethod]
    public void TestMethod1()
    {
        dynamic expando = new ExpandoObject();
        expando.Value = 10;
        expando.Product = "Apples";

        var dictionaryResult = System.Web.Helpers.Json.Encode(new DynamicJsonObject(expando));

        Assert.AreEqual("{\"Value\":10,\"Product\":\"Apples\"}", dictionaryResult);
    }

Ans once I worked through that, I found a similar question here: How to flatten an ExpandoObject returned via JsonResult in asp.net mvc?