November 16th, 2009

Spot the Bug! – The Key to using Anonymous Types (Jonathan Aneja)

This one’s going to be long, but for those of you who’ve felt the first 3 in this series were too easy I promise this one’s tougher J.

 

Let’s say you want to list all the customers from a table in a ComboBox, and update the UI based on which one is selected.  To do this we’ll need to bring back two fields from the database – the customer’s name and the customer’s ID.  When a customer is selected we want the ComboBox’s SelectedValue property to equal the customer’s ID.

 

Here’s some quick code that gets us up and running (using Northwind and LINQ to SQL):

 

        Dim db As New NorthwindDataContext

 

        Dim query = From row In db.Customers _

                    Select row.CompanyName, row.CustomerID

 

        ComboBox1.DataSource = query.ToList()

        ComboBox1.DisplayMember = “CompanyName”

        ComboBox1.ValueMember = “CustomerID”

 

But now let’s say your boss looks at the app and says “I want you to add an ‘All Customers’ option as the first item in the list.”  How would you do that?  You’d need to insert an anonymous type into an existing sequence of anonymous types, which is tricky given that you can never actually use the type’s name. 

 

Thankfully there’s a trick that uses generic parameter inference that allows us to do this (it relies on the fact that the compiler will share (or “unify”) the definition of multiple anonymous types when they have the same number of members, in the same order, of the same type, with the same names, and the same mutability):

 

    Dim db As New NorthwindDataContext

 

    Dim query = From row In db.Customers _

                Select row.CompanyName, row.CustomerID

 

    Dim allOption = New With {.CompanyName = “All Customers”, _

                              .CustomerID = “-1”}

 

    ComboBox1.DataSource = AddOptionForAll(query, allOption).ToList()

    ComboBox1.DisplayMember = “CompanyName”

    ComboBox1.ValueMember = “CustomerID”

       

   

 

    Function AddOptionForAll(Of T)(ByVal sequence As IEnumerable(Of T), _

                                   ByVal allOption As T) As IEnumerable(Of T)

 

        ‘wrap individual element in an array and then union the two sequences

        Return (New T() {allOption}).Union(sequence)

    End Function

 

As long as we have an anonymous type that’s compatible with the anonymous type that the query generated, the compiler will determine that sequence and allOption are actually the same type and this should work fine.

 

Ok so let’s get to the bug – well in this case spotting it is pretty simple (it doesn’t compile J), but fixing it is tricky (though I’ve already given two pretty big hints).  Here’s the text of the compiler error (it’s on the line that calls AddOptionForAll):

 

Data type(s) of the type parameter(s) in method ‘Public Function AddOptionForAll(Of T)(sequence As System.Collections.Generic.IEnumerable(Of T), allOption As T) As System.Collections.Generic.IEnumerable(Of T)’ cannot be inferred from these arguments because they do not convert to the same type. Specifying the data type(s) explicitly might correct this error.            

 

What’s wrong and how do we fix it?

.

 

.

 

.

 

.

 

.

 

Answer:  We actually told the compiler to generate two different anonymous type definitions, and thus it can’t unify them because the types really are different.   

 

From earlier: “…the compiler will share (or “unify”) the definition of multiple anonymous types when they have the same number of members, in the same order, with the same names, and the same mutability

 

The anonymous type that the query generates will have ReadOnly properties, whereas the anonymous type that we generated (allOption) will have Read/Write properties.  The fix is to allOption immutable so that its structure will match the result of the query:

 

        Dim allOption = New With {Key .CompanyName = “All Customers”, _

                                  Key .CustomerID = “-1”}

 

The “Key” modifier tells the compiler to make those properties ReadOnly and to override Equals and GetHashCode such that they only consider “Key” properties when deciding if two instances of the same anonymous type are equal.  (the other hint was in the title J).  For the query above, the compiler automatically inserts the “Key” modifier – i.e. what we wrote is exactly equivalent to this:

 

        Dim query = From row In db.Customers _

                    Select New With {Key row.CompanyName, Key row.CustomerID}

 

So with a simple fix to the allOption line we’re now using the same anonymous type definition and everything works fine. 

 

VB’s anonymous type syntax is very flexible and provides three different options: immutable, fully mutable, or partially mutable (i.e. some fields are ReadOnly while others are not).  Even for a simple scenario like adding an option to a list, the better you understand how things work under the covers the easier it’ll be to debug problems later J.

Author

0 comments