Add Extension Methods in LinqPad by ThinqLinq

Add Extension Methods in LinqPad

As we already announced, the samples for chapters 1-8 of our LINQ in Action book are available through LINQPad. This includes the LINQ to Objects and LINQ to SQL. I've been working on the LINQ to XML chapters (9-11) and hope that we will add them to the download soon. In the process, I've needed to learn a bit about how LINQPad works under the covers in order to add specialized classes.

By default, LINQPad offers three options: Expressions, Statements and Programs. With the Expressions, you can include a single statement and have the result output. Typically here you would include a LINQ query as follows:

From cust in Customers _
Where cust.Country = "USA" _
Order By cust.CompanyName _
Select cust.CompanyName, cust.ContactName

Notice here that we don't include the Context as we typically would inside Visual Studio. That's the first clue as to what's happening under the covers. Keep this in mind as we'll come back to this in a bit.

If you need more than just a single statement, for example when demonstrating deferred execution, you can use the Statements option to include multiple statements that would otherwise appear inside a single method:

Dim books = dataContext.GetTable(Of Book)()
Dim query = From book In books _
            Skip 2 _
            Take 2 _
            Select book.Title, book.Pricequery.Dump()

If you need to refer to external methods or add other classes, choose the Program option. This will add a Sub Main method and allow you to add additional methods. Here's the sample we used for the compiled query option:


Sub Main
  ExpensiveBooks(Me, 30).Dump()
End Sub

''' 
''' Precompiled version of the Expensive Books query
''' 
Public Shared ExpensiveBooks As Func(Of TypedDataContext, Decimal, _
                                     IQueryable(Of Book)) = _
  CompiledQuery.Compile(Function(context As TypedDataContext, minimumPrice As Decimal) _
  From book In context.Books() _
  Where (book.Price >= minimumPrice) _
  Select book)

Notice here, when we pass the context in the Sub Main, that we are referring to "Me" (in C#, "this"). So what is this "Me" class that we are referring to and how were we able to refer to the Customers in the first query without including the Context? In a nutshell, LINQPad wraps your code inside of a class that is generated when you run the snippet. This class inherits from DataContext and includes the typical generated code for the objects in the database similar to the definitions generated by SqlMetal. (There are subtle differences which can cause some unexpected results, particularly when looking at the concurrency SQL on generated update statements.) Thus when using the Program option, your code is inserted into a class using the following Pseudo-code:


Public Class TypedDataContext
  Inherits DataContext
  'Generated constructors, tables, functions, views, etc
  'LINQPad user entered code
  Sub Main
    'Your functionality goes here
  End Sub
  'Other LINQPad user entered code
End Class

In the area of the other LINQPad user entered code, you are not limited to methods, fields, etc., but can also include full class/module/type definitions. Since we can include full classes, we should be able to add extension method definitions. We can't add extension methods to the generated TypedDataContext class because it doesn't fit the required signature for extension method classes (Module in VB or Shared Class in C#). Thus we need a separate class.

To create an extension method that uppercases each word, it would be nice if we could do the following:

Sub Main
  Console.WriteLine("this is a test".ToTitleCase())
End Sub

' Define other methods and classes here
Public Module Extensions
  <System.Runtime.CompilerServices.Extension()> _
  Public Function ToTitleCase(ByVal input As String) As String
    Return New System.Globalization.CultureInfo("en-US") _
                 .TextInfo.ToTitleCase(input)
  End Function
End Module

At first glance, this would seem to work. However remember that this extension module is actually nested inside of the TypedDataContext class. Here's a snapshot of the class relationships:


Public Class TypedDataContext
  Inherits DataContext
  ' SQL Metal generated classes
  ' LINQPad user entered code
  Sub Main
  End Sub
  Public Module Extensions
  End Module
End Class

If we try to run this, we get the message indicating that the extension method can't be found. By definition, nested classes can't contain extension methods. They have to be root level classes. The trick here is to trick our code to close off the generated TypedDataContext and then inject the start of a new dummy class definition at the end which will be closed off when we insert our code into the code generated by LINQPad as follows:


Sub Main
  Console.WriteLine("this is a test".ToTitleCase())
End Sub
' Close off the TypedDataContext Class
End Class

Public Module Extensions
  <System.Runtime.CompilerServices.Extension()> _
  Public Function ToTitleCase(ByVal input As String) As String
    Return New System.Globalization.CultureInfo("en-US") _
                 .TextInfo.ToTitleCase(input)
  End Function
End Module

' Create a new dummy class which will be closed by LINQPad
Class Foo

Notice here, we don't explicitly close the Foo class, but rather let LINQPad add that End Class for us. (For those of you familiar with SQL Injection, this is a technique that is typically used there as well).

Realizing the relationship between your code and the TypedDataContext that LINQPad generates allows you to use LINQPad in a host of interesting ways. Try playing with it, and while you're at it, check out the LINQ in Action samples.

Posted on - Comment
Categories: LINQ) - VB) - LinqPad) -
comments powered by Disqus