As you probably know… yesterday I posted the samples and some screen shots from my LINQ on Mobile Devices webcast. A short time later I received feedback asking that I post VB versions of the samples.
The feedback author made a very good point that these examples (especially the last one) are very difficult to translate from C# to VB if you're not already familiar with C#. Certainly a reasonable request so I figured a take an hour or two today and convert them to VB…
Well so much for an "hour or two" … I started working on them at 10:30 AM and didn't finish until after 5:00 PM … Let's just say that converting the last demo to VB was a little more than just difficult. J
BTW: This isn't going to be a VB bash-fest where I keep touting the amazing power of the C# compiler. I will draw parallels between the two compilers just to provide a reference to an alternate implementation. In the interest of full disclosure, I will say that I do find the C# compiler's handling a of LINQ queries easier to follow than that of the VB compiler.
Sample Downloads
Before I go into the explanation, let me give you the links to the VB Samples first (remember the first demo was already VB)
- Second demo – Shows the time and resource cost of using a DataSet as compared to a SqlCeResultSet
- Third demo – Shows the benefit of using a custom extension methods and enumerators to query the SSC database tables directly (Be sure to read the below explanation)
For C# versions of the demos, see yesterday's post.
Some Unexpected Behavior in VB's Processing of LINQ Queries
Some of you will remember earlier this month when I ran into what appeared to be a bug when performing LINQ queries against DataTables in VB – as we later learned, it was due to subtle differences between the C# and VB compilers combined with a subtle difference between Extension Method support in .NET CF and the full framework. With that in mind, I'm going to be a little more cautious with the issue I encountered today. J
I'm not prepared to call what I've run into a bug but I can honestly say that I encountered what I found to be very unexpected behavior when attempting to use custom Extension Methods in VB LINQ queries. Let me explain…
As you'll recall from the webcast, we defined a custom Where extension method for SqlCeResultSet. This method allows us to provide a "smart" enumerator which we can use to expedite our queries. In VB, that extension method is declared as follows.
<Extension()> _
Public Function Where(ByVal resultSet As SqlCeResultSet, ByVal theFunc As Func(Of SqlCeUpdatableRecord, Boolean)) As IEnumerable(Of SqlCeUpdatableRecord)
Debug.WriteLine("Where(resultSet As SqlCeResultSet")
Return New PrepAwareEnumerableWrapper(resultSet, theFunc)
End Function
When we did this in C#, our custom Where extension method immediately worked – there were really no significant caveats. I didn't have the same experience in VB J. Here's what I initially found interesting when doing this in VB … using the method syntax to query the SqlCeResultSet works as expected but the query syntax won't even compile!
resultSet.Where(Function(order) order("Ship Country") = "UK") ' Works
Dim records = From order In resultSet _
Where order("Ship Country") = "UK" ' Won't compile?!?!
The problem as reported by the compiler is that the query syntax is trying to use late binding – the compiler believes that "order" is of type Object rather than inferring that it's a SqlCeUpdatableRecord as expected.
In order to get the method to compile, I tried declaring the data type of "order" explicitly [ From order as SqlCeUpdatableRecord in resultSet ] – I also tried letting the compiler infer the type and then using a cast in the Where clause [Where DirectCast(order, SqlCeUpdatableRecord)("Ship Country") = "UK" ]. But when I run the code, the program uses one of the built-in Where extension methods rather than our custom Where extension method.
Ugh! – not what I was hoping for.
Getting a Handle on the VB Compiler's Type Inference within LINQ Query Syntax
Obviously these difficulties with the query syntax were a surprise. As I understand LINQ handling, the query syntax should be transformed into the method syntax – in fact, I know that's how it works in C#. After several hours of research, I now understand that the direct query-syntax-to-method-syntax translation is sort of what happens in VB … but not exactly. I think the best way to explain how things work is with some examples.
Let's assume that I have a class named BizObjectManager that represents a business object that manages lists of other subordinate business objects. The subordinate business object class is called ChildBizObject whose definition looks like the following.
Public Class ChildBizObject
Public ReadOnly Property SalesDivision() As String
'...
End Property
'...
'...
End Class
We'll query the BizObjectManager object for a list of ChildBizObject objects using both the method and query syntax.
Dim manager As New BizObjectManager
Dim records1 = manager.Where(Function(child) child.SalesDivision = "SouthEast") ' method syntax
Dim records2 = From child In manager _
Where child.SalesDivision = "SouthEast" ' query syntax
And we have the following Where extension method that we hope will be used when doing the LINQ queries.
<Extension()>_
Public Function Where(ByVal manager As BizObjectManager, ByVal theFunc As Func(Of ChildBizObject, Boolean)) As IEnumerable(Of ChildBizObject)
'...
End Function
Now let's look at 3 possible ways that we might declare the BizObjectManager class. The method syntax works with all 3 of the declarations but I think you'll be surprised (I certainly was) at the way the VB compiler's handling of the query syntax changes based on that declaration.
Example 1 – Class implements no interfaces
Public Class BizObjectManager
'...
End Class
In this case the VB compiler reports that the class BizObjectManager cannot be queried. This isn't quite true. As I mentioned a few lines back, the method syntax compiles and works fine. Also, this same situation compiles and works fine in C#. The bottom-line is that there is a Where extension method returning an IEnumerable(of T) available so the class is query compatible.
It appears that the VB compiler will only allow classes that implement specific interfaces to appear in the query syntax. Those interfaces include IEnumerable, IEnumerable(of T), and possibly others.
Example 2 – Class implements the strongly-typed IEnumerable(of T) interface
Public Class BizObjectManager
Implements IEnumerable(Of ChildBizObject)
'...
End Class
So to satisfy the compiler, we have our class implement IEnumerable (of ChildBizObject). This code now compiles fine. But what will happen when we run the code?
Since the VB compiler is requiring us to implement this interface, which Where extension method will it choose: the built-in Where extension method that accepts an IEnumerable(of T) or our Where extension method that accepts the BizObjectManager?
The standard rules for method overloading dictate that the method that most closely matches a type be used; in this case the closest match is our custom Where extension method. The good news is, the compiler does choose the correct Where extension method … ours (closest type match). What that means though, is that to get past the VB compiler I had to implement IEnumerable(of T) even though the compiler won't actually use it. The IEnumerable(of T) method implementations can literally return the value "Nothing".
Example 3 – Class implements the loosely-typed IEnumerable interface
Public Class BizObjectManager
Implements IEnumerable
'...
End Class
This is exactly the case with SqlCeResultSet – it implements only IEnumerable not IEnumerable(of T). So just like SqlCeResultSet, the BizObjectManager query won't compile unless we explicitly declare the type of "child" in our query or cast "child" in the Where clause – but once it compiles, when it runs it uses a built-in Where extension method instead of our custom Where extension method.
What's happening in this case is that the VB compiler appears to want to bias the type towards being some version of IEnumerable(of T). Let's assume that in order to get the code to compile that I've modified the query so that there's a cast in the Where clause – like the following…
Dim records = From child In manager _
Where DirectCast(child, ChildBizObject).SalesDivision = "SouthEast"
The compiler then generates code similar to the following (I've simplified the code for clarity).
Dim records = Manager.Cast(Of Object)().Where(DirectCast(child, ChildBizObject).SalesDivision = "SouthEast")
Basically it appears that once the VB compiler sees that the class implements the loosely-typed IEnumerable [members are returned as object references] the compiler then calls the Cast method which generates a new collection containing the members of the original collection but this time the collection is typed as IEnumerable(of Object) [members are returned as … object references J]. I'm not sure why the compiler does this [I'd guess it's related to the same issue that causes the compiler to require that objects being queried implement IEnumerable/IEnumerable(of T)] but the net-effect is that the type being queried is now IEnumerable(of T) and therefore the built-in Where extension method gets used.
OK … So How'd You Get the SqlCeResultSet-based Extension Method to Work in VB
I don't mean to leave you hanging but this post is already so much longer than I wanted it to be – I'm going to call it a night here and post the details of how I worked around the issue tomorrow.
If you just can't wait, you can download the VB version of Demo three and checkout the code directly.
Oh … I wanted to mention that the webcast that goes with all of this is now available On-Demand.
See ya tomorrow.
Posted
Feb 28 2008, 08:57 PM
by
jim-wilson