4

I have a design where class holds a List<> of Summary objects, and each Summary is a dictionary of semi-dynamic properties. That is, every Summary in a given list will have the same keys in the dictionary. I'm using this design to build a set of dynamic "properties" to keep track of summary values since, per my project specs, these summary values will be configurable at runtime.

The question is: How can I flatten this List so that each item in the list is treated as-if the keys in the dictionary were actual properties?

I've tried different variations on converting the Dictionary to a list, but that seems inherently wrong since I really need to treat the keys as properties. I'm guessing I need to use the new dynamic feature of C# 4.0+ and the ExpandoObject, but I can't quite get it right.

The code below shows the basic setup, with "flattenedSummary" giving me what I want - a new list of a dynamic type whose properties are the keys of the Summary's dictionary. However, that is flawed in that I've hard-coded the property names and I can't do that since I won't really know them until runtime.

The flattenedSummary2 version attempts to flatten the list but falls short as the returned type is still a List and not the List that I want.

    public class Summary : Dictionary<string, object>
    {
    }

    public class ClassA
    {
        public List<Summary> Summaries = new List<Summary>();
    }

    static void Main(string[] args)
    {
        ClassA a = new ClassA();
        var summary = new Summary();
        summary.Add("Year", 2010);
        summary.Add("Income", 1000m);
        summary.Add("Expenses", 500m);
        a.Summaries.Add(summary);

        summary = new Summary();
        summary.Add("Year", 2011);
        summary.Add("Income", 2000m);
        summary.Add("Expenses", 700m);
        a.Summaries.Add(summary);

        summary = new Summary();
        summary.Add("Year", 2012);
        summary.Add("Income", 1000m);
        summary.Add("Expenses", 800m);
        a.Summaries.Add(summary);

        var flattenedSummary = from s in a.Summaries select new { Year = s["Year"], Income = s["Income"], Expenses = s["Expenses"] };
        ObjectDumper.Write(flattenedSummary, 1);

        var flattenedSummary2 = Convert(a);
        ObjectDumper.Write(flattenedSummary2, 1);

        Console.ReadKey();
    }

    public static List<ExpandoObject> Convert(ClassA a)
    {
        var list = new List<ExpandoObject>();
        foreach (Summary summary in a.Summaries)
        {
            IDictionary<string, object> fields = new ExpandoObject();
            foreach (var field in summary)
            {
                fields.Add(field.Key.ToString(), field.Value);
            }
            dynamic s = fields;
            list.Add(s);
        }

        return list;
    }
Daniel Jones
  • 117
  • 1
  • 7
  • 1
    So how exactly do you envision using these properties in your code if you won't know what they are until run time? Is this just for data-binding or something where you never actually use the properties directly yourself? – jmcilhinney Sep 25 '14 at 02:50
  • Two ways of using this flattened view: Data binding to a grid for debugging purposes and serializing to json (or xml) from web api. See comments below on the accepted answer for a subtle change that gives the json solution. – Daniel Jones Sep 25 '14 at 06:22

2 Answers2

1

Daniel,

I found this article that has a solution that might work for you. http://theburningmonk.com/2011/05/idictionarystring-object-to-expandoobject-extension-method/

So you would create the extension method...

  public static ExpandoObject ToExpando(this IDictionary<string, object> dictionary)
        {
            var expando = new ExpandoObject();
            var expandoDic = (IDictionary<string, object>)expando;

            // go through the items in the dictionary and copy over the key value pairs)
            foreach (var kvp in dictionary)
            {
                // if the value can also be turned into an ExpandoObject, then do it!
                if (kvp.Value is IDictionary<string, object>)
                {
                    var expandoValue = ((IDictionary<string, object>)kvp.Value).ToExpando();
                    expandoDic.Add(kvp.Key, expandoValue);
                }
                else if (kvp.Value is ICollection)
                {
                    // iterate through the collection and convert any strin-object dictionaries
                    // along the way into expando objects
                    var itemList = new List<object>();
                    foreach (var item in (ICollection)kvp.Value)
                    {
                        if (item is IDictionary<string, object>)
                        {
                            var expandoItem = ((IDictionary<string, object>)item).ToExpando();
                            itemList.Add(expandoItem);
                        }
                        else
                        {
                            itemList.Add(item);
                        }
                    }

                    expandoDic.Add(kvp.Key, itemList);
                }
                else
                {
                    expandoDic.Add(kvp);
                }
            }

            return expando;
        }

Then from your Main function...

List<ExpandoObject> flattenSummary3 = new List<ExpandoObject>();
foreach ( var s in a.Summaries)
{
    flattenSummary3.Add(s.ToExpando());
}

And now the flattenSummary3 variable will contain a List of ExpandObjects that you can refer by properties.

I hope this helps.

Ed Mendez
  • 1,510
  • 10
  • 14
  • I had to make a few small changes: (1) Change ToExpando to return "dynamic" instead of ExpandoObject (2) Within the ToExpando method, declaring expando as dynamic instead of var. (3) In the calling code, declaring flattenSummary3 as List Those changes allow the flattenSummary3 to be treated as a truly dynamic list (serializing to json gives the expected array of property/values). The solution still doesn't allow Data binding to work on a WinForm DataGridView - but that could be a separate issue. Thank you for the help, Ed! – Daniel Jones Sep 25 '14 at 06:29
1

While I accepted Ed's answer as it was very close, I'm providing the following code in case anyone else finds it useful. The key changes were to ensure that all uses of ExpandoObject were set as dynamic so that the final List was dynamic. Without these changes, examining the types inside the list still returned ExpandoObject (json serializing, for example, gave ExpandoObject instead of the expected property names/values).

First, the ToExpando() method (which should probably be called ToDynamic):

public static dynamic ToExpando(this IDictionary<string, object> dictionary)
{
    dynamic expando = new ExpandoObject();
    var expandoDic = (IDictionary<string, object>)expando;

    // go through the items in the dictionary and copy over the key value pairs)
    foreach (var kvp in dictionary)
    {
        // if the value can also be turned into an ExpandoObject, then do it!
        if (kvp.Value is IDictionary<string, object>)
        {
            var expandoValue = ((IDictionary<string, object>)kvp.Value).ToExpando();
            expandoDic.Add(kvp.Key, expandoValue);
        }
        else if (kvp.Value is ICollection)
        {
            // iterate through the collection and convert any strin-object dictionaries
            // along the way into expando objects
            var itemList = new List<object>();
            foreach (var item in (ICollection)kvp.Value)
            {
                if (item is IDictionary<string, object>)
                {
                    var expandoItem = ((IDictionary<string, object>)item).ToExpando();
                    itemList.Add(expandoItem);
                }
                else
                {
                    itemList.Add(item);
                }
            }

            expandoDic.Add(kvp.Key, itemList);
        }
        else
        {
            expandoDic.Add(kvp);
        }
    }

    return expando;
}

And the calling code would look like this:

    List<dynamic> summaries = new List<dynamic>();
    foreach (var s in a.Summaries)
    {
        summaries.Add(s.DynamicFields.ToExpando());
    }

Or the even more compact version:

    a.Summaries.Select(s => s.DynamicFields.ToExpando())

All of the above provide an object that could be referenced as:

    int year = a.Summaries[0].Year; // Year is a dynamic property of type int
    decimal income = a.Summaries[0].Income; // Income is a dynamic property of type decimal

Of course, the idea is I won't know the properties - but they can be serialized to json or, with some tweaking, be used to bind a grid or other UI element for display purposes.

Daniel Jones
  • 117
  • 1
  • 7