Friday, February 27, 2009

Generic Lists of Anonymous Type

[cross-posted to StormId blog]

Anonymous types can be very useful when you need a few transient classes for use in the middle of a process.

Of course you could just write a class in the usual way but this can quickly clutter up your domain with class definitions that have little meaning beyond the scope of their transient use as part of another process.

For example, I often use anonymous types when I am generating reports from my domain. The snippet below shows me using an anonymous type to store data values that I have collected from my domain.

for (var k = 0; k < optionCount; k++)
{
    var option = options[k];
    var optionTotal = results[option.Id];
    var percent = (questionTotal > 0) ? ((optionTotal/(float)questionTotal) * 100): 0;
    reportList.Add(new
        {
            Diagnostic = diagnostic.Name, 
            Question = question.Text, 
            Option = option.Text, 
            Count = optionTotal, 
            Percent = percent
        });
}

Here I am generating a report on the use of diagnostics (a type of survey). It shows how often each option of each question in each diagnostic has been selected by a user, both count and percent.

You can see that the new anonymous type instance is being added to a list called reportList. This list is strongly typed as can been seen by this next bit of code where I order the list using LINQ.

reportList = reportList
    .OrderBy(x => x.Diagnostic)
    .ThenBy (x => x.Question)
    .ThenBy (x => x.Percent)
    .ToList();

This is where the problem comes in, how is it possible to create a strongly typed (generic) list for an anonymous type? The answer is to use a generics trick, as the following code snippet shows.

public static List<T> MakeList<T>(T example)
{
    return new List<T>();
}

The MakeList method takes in a parameter of type <T> and returns a generic list of the same type. Since this method will accept any type then we can pass an anonymous type instance with no problems. The next snippet shows this happening.

var exampleReportItem = new
    {
        Diagnostic = string.Empty, 
        Question = string.Empty, 
        Option = string.Empty, 
        Count = 0, 
        Percent = 0f
    };
var reportList = MakeList(exampleReportItem);

So here is the context for all these snippets. The following code gathers my report data and stores it in a strongly typed list containing a transient anonymous type.

var exampleReportItem = new
    {
        Diagnostic = string.Empty, 
        Question = string.Empty, 
        Option = string.Empty, 
        Count = 0, 
        Percent = 0f
    };
var reportList = MakeList(exampleReportItem);
for (var i = 0; i < count; i++)
{
    var diagnostic = diagnostics[i];
    var questionCount = diagnostic.Questions.Count;
    for (var j = 0; j < questionCount; j++)
    {
        var question = diagnostic.Questions[j];
        var questionTotal = results[question.Id];
        var options = question.Options;
        var optionCount = options.Count;
        for (var k = 0; k < optionCount; k++)
        {
            var option = options[k];
            var optionTotal = results[option.Id];
            var percent = (questionTotal > 0) ? ((optionTotal/(float)questionTotal) * 100): 0;
            reportList.Add(new
                {
                    Diagnostic = diagnostic.Name, 
                    Question = question.Text, 
                    Option = option.Text, 
                    Count = optionTotal, 
                    Percent = percent
                });
        }
    }
}

Perhaps you are wondering how the type of the anonymous exampleReportItem is the same as the type of the anonymous object I add to the reportList?

This works because of the way the type identities are assigned for anonymous types. If two anonymous types share the same public signature, that is if their property names and types are the same (you can't have methods on anonymous types) then the compiler treats them as the same type.

This is how the MakeList method can do its job. The exampleReportItem instance sent to the MakeList function has exactly the same properties as the anonymous type added to the generic reportList. Because they have the same signatures then they are recognised as the same anonymous type and all is well.