left-icon

Getting the Most from LINQPad Succinctly®
by José Roberto Olivas Mendoza

Previous
Chapter

of
A
A
A

CHAPTER 5

Custom Data Context Drivers

Custom Data Context Drivers


What is a data context driver?

A data context driver is a mechanism that allows programmers to extend LINQPad in order to support several data sources. In other words, it is the way to add new drivers to the following dialog.

The Choose Data Context Dialog

Figure 32: The Choose Data Context Dialog

Why is writing custom data context drivers useful?

LINQPad can query any data source without a custom data context driver, but in this case, the user must manually reference libraries, import custom namespaces, and formulate all queries like the following example.

Code Listing 41: Querying Data without a Custom Data Context Driver

var dataSource = new ItemsData();

(from i in dataSource.Items

   where i.Name.StartsWith ("D")

   select new { i.Name, i.UnitPrice }).Dump();

Instead, by using a custom data context driver, the query from the previous example turns into the following code.

Code Listing 42: Querying Data with a Custom Data Context Driver

(from i in dataSource.Items

   where i.Name.StartsWith ("D")

   select new { i.Name, i.UnitPrice }).Dump();

If we compare Code Listing 41 and Code Listing 42, the difference between them is the instantiation of the ItemsData() class. It might not seem like a big deal for small queries, but in large queries, the use of a data context driver helps you avoid writing many extra lines of code.

Data context drivers from the user’s perspective

A data context driver is employed every time a user adds a connection to LINQPad. Those connections appear on the Connection’s tree view area of the user interface.

To add a connection, click the Add Connection hyperlink located at the top of the Connection’s tree view area. The dialog shown in Figure 32 pops up on the screen. If you click the View More Drivers button (which is highlighted in the same figure), the dialog displayed in the following figure appears.

The Choose a Driver Dialog

Figure 33: The Choose a Driver Dialog

The Drivers Gallery is the first item shown in the dialog from Figure 33. It’s assumed that you’re online—if you’re not, this gallery is not displayed. You can click the Download hyperlink of any driver from the gallery to download and install it onto your computer. Clicking the Browse button located near the bottom of the dialog allows you to choose a driver previously deployed onto your computer. LINQPad asks for the location of the driver file in the following dialog.

The Browse LINQPad Data Context Driver Dialog

Figure 34: The Browse LINQPad Data Context Driver Dialog

Once the driver is located, you can click the file name, and then click Open. If you don’t want to install a driver, you can abort the process by clicking Cancel.

Note: As shown in Figure 34, the extension for a data context driver file is .lpx (LINQPad extension).

When the driver is properly installed, it will become visible in the dialog shown in Figure 33.

Basic steps for writing a data context driver

Writing a data context driver is not trivial, but the process isn’t as complex as you might guess. The basic steps for building a driver are:

  1. Choose between writing a dynamic or a static driver (discussed later in this book).
  2. Build a class project in Visual Studio that references LINQPad.
  3. Create classes derived from DynamicDataContextDriver or StaticDataContextDriver.
  4. Implement a few abstract methods (and some virtual methods, optionally).
  5. Zip the files generated by the class project (and all their dependencies) and change the extension from .zip to .lpx
  6. Place the file in any place on your computer, so it can be found by using the Browse button shown in Figure 33.

LINQPad’s extensibility model has been designed in a way that makes it quick to write a data context driver with basic functionality.

Special terms and conditions

There are no special terms or conditions for writing a data context driver, unless you plan to submit the driver to the LINQPad Driver Gallery, which is outside the scope of this book.

Basic concepts about data context drivers

There are some basics to cover before building a data context driver, to give you some perspective on how LINQPad treats data in the background. These basics are discussed in the following sections.

Connection

A connection relates to what you’ll enter after clicking Add Connection. This is wider than the classic concept of a database connection; a LINQPad connection can refer to other types of data sources, such a web service URI. In addition, a LINQPad connection can include data context-specific details, such as pluralization and capitalization options.

Note: A LINQPad connection is represented by the IConnectionInfo interface.

Typed data contexts

A typed data context is a class with properties, fields, and methods that can be queried by the user. A typical example is a typed LINQ to SQL DataContext or a typed ObjectContext in Entity Framework, both of which are shown in the following code listing.

Code Listing 43: A Typical Data Context Example

public class TypedDataContext : DataContext

   {

      public IQueryable<Product> Products

      { get { return this.GetTable<Product>(); } }

      public IQueryable<InventoryEntry> InventoryEntries

      { get { return this.GetTable<InventoryEntry>();   } }

   }

We can define a DataContext without using a base class, as shown in the following code example.

Code Listing 44: A Typed Data Context with No Base Class

public class TypedDataContext

   {

      public IEnumerable<string> ProductNames

      public int[] Numbers;

      public void DoYourThings() { … }

   }

When writing data context drivers, it’s mandatory to define a typed data context. There are two techniques for getting a typed data context:

  • The driver can build one on the fly (dynamic driver).
  • The driver can consume a typed data context already defined (static driver).

Dynamic and static drivers

When you click the Add Connection hyperlink in LINQPad, the dialog that appears shows two lists from which you can choose a driver:

  • Build Data Context Automatically: This list shows the dynamic data context drivers currently installed in LINQPad.
  • Use a typed data context from your own assembly: This list shows the static data context drivers installed in LINQPad.

So, why the separate lists? This is because different kinds of drivers work in different ways. The following list explains how dynamic and static drivers work:

  • A dynamic driver builds the data context on the fly, either by generating code and compiling it after, or by using Reflection.Emit.
  • A static driver requires the data context to be supplied by the user. So, when the user attempts to use a static driver, the connection dialog will prompt for a path where a custom assembly containing the typed data context is located, and after that, for the name of the type.

The advantage of using a dynamic driver is that it allows the user to query data without having to first write classes in a Visual Studio project. The advantage of using a static driver is that it gives the user a finer degree of control, and compatibility with typed data contexts written in previous projects.

The Choose Data Context Dialog Showing the Two Kinds of Data Context Drivers

Figure 35: The Choose Data Context Dialog Showing the Two Kinds of Data Context Drivers

Note: Both kinds of drivers can be implemented in the same project.

How LINQPad works with queries

Every time the user writes a query, it doesn’t explicitly refer to a particular data context. Instead, the user writes queries as shown in the following code example.

Code Listing 45: A Typical Query

from p in Products

   where p.Name.Contains ("CARD")

   select new { p.Name, p.UnitPrice }

When the user runs this query, LINQPad subclasses the typed data context by transforming the user’s query into a method, as seen in the following code example.

Code Listing 46: A Query After Being Transformed by LINQPad

   public class UserQuery : TypedDataContext

   {

      public UserQuery (parameters...) : base (parameters...) { }

      void RunUserAuthoredQuery()

      {

         (

            from p in Products

             where p.Name.Contains ("CARD")

            select new { p.Name, p.UnitPrice }  

        )

         .Dump();

      }

   }

After that, LINQPad calls the C# or VB compiler service (depending on the language selected in the user interface), compiles the code into a temporary assembly, creates an instance of the class, and finally, calls RunUserAuthoredQuery. This principle applies to both dynamic and static drivers.

Note: The TypedDataContext class of a data context driver must not be a sealed class and must have a public constructor.

Writing a data context driver

All the concepts discussed previously will be combined in the following sections in order to create a custom data context driver. Let’s have some fun!

Setting up a project

The first step is to set up a Visual Studio project with the following properties:

  • Project type: Class library (.NET Framework)
  • Name: MyDataContextDriver
  • Location: D:\LINQPad Samples
  • Framework: .NET Framework 4.6.1
  • Create directory for solution: Unchecked

The following figure shows the project configuration.

The Data Context Driver Project Properties

Figure 36: The Data Context Driver Project Properties

After configuring the project as shown in Figure 36, click OK to create the project files, which will be located at D:\LINQPad Samples\MyDataContextDriver.

The header file

Every time LINQPad tries to install a data context driver, it looks for a file named header.xml. If this file is not present in the deployment package, the installation process will fail. So, the first thing to do is add the corresponding header.xml file to the project. This file can be created in a text editor (like Notepad), according to the structure shown in the following code example.

Code Listing 47: The header.xml File

<?xml version="1.0" encoding="utf-8" ?>

<DataContextDriver>

   <MainAssembly> MyDataContextDriver.dll</MainAssembly>

   <SupportUri>http://YourURIHere</SupportUri>

</DataContextDriver>

The file should be saved at the project’s root directory (in this case D:\LINQPad Samples\MyDataContextDriver). Next, it should be added to the project by right-clicking on the project’s name node in the Solution Explorer, and then clicking the Add > Existing Item option from the context menu.

Adding an Existing Item to The Project

Figure 37: Adding an Existing Item to The Project

The Add Existing Item dialog will appear. Select All Files (*.*) in the file types list, and then go to the D:\LINQPad Samples\MyDataContextDriver folder to select the header.xml file. Finally, click the Add button to complete the process and make the file visible in the solution items tree.

As mentioned previously, the header.xml file is necessary for deploying the data context driver to ensure that the file will be copied to the project’s output directory, along with the assembly files and dependencies. To accomplish this, change the file’s properties by right-clicking the file name and going to the Properties window located at the bottom of the Solution Explorer. Set the Build Action property to Content and the Copy to Output Directory property to Copy if newer.

These steps are shown in the following figures.

The Add Existing Item Dialog

Figure 38: The Add Existing Item Dialog

The header.xml File in the Project

Figure 39: The header.xml File in the Project

The header.xml File Properties

Figure 40: The header.xml File Properties

Making room for dynamic and static context drivers

As mentioned previously, a context driver project can hold both dynamic and static context drivers. To accomplish this, we’ll create a pair of folders named mydynamicdriver and mystaticdriver within the project.

The Dynamic and Static Context Drivers Folders

Figure 41: The Dynamic and Static Context Drivers Folders

Adding necessary references

At this point, the only reference needed is the one that points to LINQPad.exe. Let’s add this reference to the project.

The Reference to LINQPad Added into the Project

Figure 42: The Reference to LINQPad Added into the Project

Writing the driver

There are two base classes for context drivers: DynamicDataContextDriver and StaticDataContextDriver. The name of each type tells us the kind of driver we can build with each one of them. Both types are derived from the base class DataContextDriver, which is defined in the LINQPad.Extensibility.DataContext namespace. This class defines some abstract members that will be implemented during project creation.

The first thing to do is add a code file for each kind of data context driver. These files will be named MyDynamicDataContextDriver.cs and MyStaticDataContextDriver.cs, and will define the MyDynamicDataContextDriver and MyStaticDataContextDriver classes, respectively. Since both classes are derived from the DataContextDriver class, Visual Studio automatically asks us to implement the abstract members defined in DataContextDriver. We simply let Visual Studio generate the implementation code automatically. Now, the code for both classes looks like the following examples.

Code Listing 48: MyDynamicDataContextDriver.cs

using System.Collections.Generic;

using System.Reflection;

using LINQPad.Extensibility.DataContext;

namespace MyDataContextDriver.mydynamicdriver

{

    public class MyDynamicDataContextDriver : DynamicDataContextDriver

    {

        public override string GetConnectionDescription(IConnectionInfo cxInfo)

        {

            throw new System.NotImplementedException();

        }

        public override bool ShowConnectionDialog(IConnectionInfo cxInfo, bool isNewConnection)

        {

            throw new System.NotImplementedException();

        }

        public override string Name => "My Dynamic Data Context Driver (Demo)";

        public override string Author => "Getting the Most from LINQPad Succinctly";

        public override List<ExplorerItem> GetSchemaAndBuildAssembly(IConnectionInfo cxInfo, AssemblyName assemblyToBuild, ref string nameSpace,ref string typeName)

        {

            throw new System.NotImplementedException();

        }

    }

}

Code Listing 49: MyStaticDataContextDriver.cs

using System;

using System.Collections.Generic;

using LINQPad.Extensibility.DataContext;

namespace MyDataContextDriver.mystaticdriver

{

    public class MyStaticDataContextDriver : StaticDataContextDriver

    {

        public override string GetConnectionDescription(IConnectionInfo cxInfo)

        {

            throw new NotImplementedException();

        }

        public override bool ShowConnectionDialog(IConnectionInfo cxInfo, bool isNewConnection)

        {

            throw new NotImplementedException();

        }

        public override string Name => "My Static Data Context Driver (Demo)";

        public override string Author => "Getting the Most from LINQPad Succinctly";

        public override List<ExplorerItem> GetSchema(IConnectionInfo cxInfo, Type customType)

        {

            throw new NotImplementedException();

        }

    }

}

The code shown in the previous examples has already implemented the Name and Author abstract members. The Name member returns the name for the driver that is displayed in the LINQPad Driver column of the Choose Data Context dialog. The Author member, as it suggests, displays the name of the driver’s author in the corresponding column of the Choose Data Context dialog.

Writing the static driver

At this point, we will focus on building the static data context driver defined in MyStaticDataContextDriver.cs. To do so, we’re going to follow the series of steps explained in the following sections.

The GetSchema method

The first step is implementing the GetSchema abstract method shown in Code Listing 48. This method is used to return a hierarchy of objects that will be displayed in the Schema Explorer (which is the Connection’s tree view area of the user interface). This implementation is presented in the following code listing.

Code Listing 50: GetSchema Method Implementation

        public override List<ExplorerItem> GetSchema(IConnectionInfo cxInfo, Type customType)

        {

            //First, we iterate throught all top level properties of custumType

            var topLevelProps =

            (

                from prop in customType.GetProperties()

                where prop.PropertyType != typeof (string)

                // Get and display all properties of IEnumerable<T>

                let ienumerableOfT = prop.PropertyType.GetInterface ("System.Collections.Generic.IEnumerable`1")

                where ienumerableOfT != null

                orderby prop.Name

                select new ExplorerItem (prop.Name, ExplorerItemKind.QueryableObject, ExplorerIcon.Table)

                {

                    IsEnumerable = true,

                    ToolTipText = FormatTypeName (prop.PropertyType, false),

                    // Store entity type to the Tag property. This will be used later.

                    Tag = ienumerableOfT.GetGenericArguments()[0]

                }

            ).ToList ();

            // Create a lookup element, associating each element type to the properties of that type.

            // This will allow to build hyperlinks which let the user click between relationships.

            var elementTypeLookup = topLevelProps.ToLookup (tp => (Type)tp.Tag);

            // Populate the properties of each entity

            foreach (var table in topLevelProps)

                table.Children = ((Type)table.Tag)

                    .GetProperties()

                    .Select (childProp => GetChildItem (elementTypeLookup, childProp))

                    .OrderBy (childItem => childItem.Kind)

                    .ToList ();

            return topLevelProps;

        }

        private ExplorerItem GetChildItem (ILookup<Type, ExplorerItem> elementTypeLookup, PropertyInfo childProp)

        {

            //if the property's type is in the list of entities, then we're going to assume that it's a Many:1 or

            //1:1 referefence. It's not reliable to identify 1:1s relationships purely from reflection.

            if (elementTypeLookup.Contains (childProp.PropertyType))

                return new ExplorerItem (childProp.Name, ExplorerItemKind.ReferenceLink, ExplorerIcon.ManyToOne)

                {

                    HyperlinkTarget = elementTypeLookup [childProp.PropertyType].First (),

                    ToolTipText = FormatTypeName (childProp.PropertyType, true) // FormatTypeName is a LINQPad's helper method that returns a nicely formatted type name.

                };

            //We're going to check if the property's type is a collection of entities

            var ienumerableOfT = childProp.PropertyType.GetInterface ("System.Collections.Generic.IEnumerable`1");

            // If it isn't we return the Name and Type of the property as an ExplorerItem

            if (ienumerableOfT == null) return new ExplorerItem(childProp.Name + " (" + FormatTypeName(childProp.PropertyType, false) + ")",ExplorerItemKind.Property, ExplorerIcon.Column);

            //Now, we're going to check if it is a 1:Many relationship

            var elementType = ienumerableOfT.GetGenericArguments()[0];

            if (elementTypeLookup.Contains(elementType))

                return new ExplorerItem (childProp.Name, ExplorerItemKind.CollectionLink, ExplorerIcon.OneToMany)

                {

                    HyperlinkTarget = elementTypeLookup [elementType].First (),

                    ToolTipText = FormatTypeName (elementType, true)

                };

            //If it isn't, this is an ordinary property.

            return new ExplorerItem (childProp.Name + " (" + FormatTypeName (childProp.PropertyType, false) + ")",

                ExplorerItemKind.Property, ExplorerIcon.Column);

        }

The first thing this code does is get a list of all top-level properties of the Type passed to the method. In the case of a database type, the entities (tables) of the database will be the top-level properties.

The next step to complete is the association of the elements for each top-level type (again, in the case of a database type, the elements will be the columns of each entity) into the Children property of each top-level element. The GetChildItem private method formats each descendant element in order to create a hyperlink when one of these descendants is involved in a relationship with another entity. The result of this implementation can be seen in the following figure.

The GetSchema Method in Action

Figure 43: The GetSchema Method in Action

The ShowConnectionDialog method

This method must display a modal WPF dialog that will prompt the user for connection information. Since we’re going to use WPF in the project, a reference to System.Xaml must be added to this. After that, we’re going to right-click the mystaticdriver tree view’s node and select the Add > New Item option from the context menu. Now, in the Add New Item dialog, look for the WPF section. The dialog should look like the following figure.

The WPF Section in the Add New Item Dialog

Figure 44: The WPF Section in the Add New Item Dialog

We can see in Figure 44 that only one option is available in the WPF section, and it’s not a form, but a WPF User Control. To solve this problem, we’re going to make a little tweak to the MyDataContextDriver.csproj file. Using a text editor, we’ll add the following line to the global <PropertyGroup> section in the file.

Code Listing 51: Code Line for Tweaking the MyDataContextDriver.csproj File

<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>

The <ProjectTypeGuids> tag tells Visual Studio which kind of project it’s dealing with. In this case, {60dc8134-eba5-43b8-bcc9-bb4bc16c2548} stands for WPF, and {FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} stands for a C# project. Now, the global <PropertyGroup> section of MyDataContextDriver.csproj should look like the following code example.

Code Listing 52: The Global <PropertyGroup> Section Tweaked

  <PropertyGroup>

    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>

    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>

    <ProjectGuid>{59F6858D-5BAC-4A9F-9D84-40E012AFC36C}</ProjectGuid>

    <ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids> 

    <OutputType>Library</OutputType>

    <AppDesignerFolder>Properties</AppDesignerFolder>

    <RootNamespace>MyDataContextDriver</RootNamespace>

    <AssemblyName>MyDataContextDriver</AssemblyName>

    <TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>

    <FileAlignment>512</FileAlignment>

    <Deterministic>true</Deterministic>

  </PropertyGroup>

Now, when we select Add > New Item from the context menu, the dialog will look like the following figure.

The WPF Section After Tweaking MyDataContextDriver.csproj

Figure 45: The WPF Section After Tweaking MyDataContextDriver.csproj

To add the WPF form, we’re going to type ConnectionDialog into the Name text box, and then click the Add button. Now, a ConnectionDialog.xaml file entry will be displayed in the Solution Explorer. To add the necessary controls to enter connection information, we’re going to right-click the entry and select View Designer from the context menu. Once the designer is displayed on the screen, we’ll place those controls using the designer’s toolbox, dragging each one of them into the form. The final result of these actions will be a form that will look like the following figure.

The Connection Dialog in the WPF Form’s Designer

Figure 46: The Connection Dialog in the WPF Form’s Designer

Now, we will create the event handlers for every control in the form (except labels). The resulting code is shown in the following example.

Code Listing 53: Interaction Logic for ConnectionDialog

using System;

using System.Diagnostics.CodeAnalysis;

using System.IO;

using System.Windows;

using LINQPad.Extensibility.DataContext;

namespace MyDataContextDriver.mystaticdriver

{

    /// <inheritdoc cref="Window" />

    /// <summary>

    /// Interaction logic for ConnectionDialog.xaml

    /// </summary>

    public partial class ConnectionDialog

    {

        readonly IConnectionInfo _cxInfo;

        public ConnectionDialog(IConnectionInfo cxInfo)

        {

            _cxInfo = cxInfo;

            DataContext = cxInfo.CustomTypeInfo;

            InitializeComponent();

        }

        private void BrowseAssembly(object sender, RoutedEventArgs e)

        {

            var dialog = new Microsoft.Win32.OpenFileDialog()

            {

                Title = "Choose custom assembly",

                DefaultExt = ".dll",

            };

            if (dialog.ShowDialog() == true) _cxInfo.CustomTypeInfo.CustomAssemblyPath = dialog.FileName;

        }

        [SuppressMessage("ReSharper", "CoVariantArrayConversion")]

        private void ChooseType(object sender, RoutedEventArgs e)

        {

            var assemPath = _cxInfo.CustomTypeInfo.CustomAssemblyPath;

            if (assemPath.Length == 0)

            {

                MessageBox.Show("First enter a path to an assembly.");

                return;

            }

            if (!File.Exists(assemPath))

            {

                MessageBox.Show("File '" + assemPath + "' does not exist.");

                return;

            }

            string[] customTypes;

            try

            {

                customTypes = _cxInfo.CustomTypeInfo.GetCustomTypesInAssembly();

            }

            catch (Exception ex)

            {

                MessageBox.Show("Error obtaining custom types: " + ex.Message);

                return;

            }

            if (customTypes.Length == 0)

            {

                MessageBox.Show("There are no public types in that assembly.");

                return;

            }

            var result = (string)LINQPad.Extensibility.DataContext.UI.Dialogs.PickFromList("Choose Custom Type", customTypes);

            if (result != null) _cxInfo.CustomTypeInfo.CustomTypeName = result;

        }

        private void BrowseAppConfig(object sender, RoutedEventArgs e)

        {

            var dialog = new Microsoft.Win32.OpenFileDialog()

            {

                Title = "Choose application config file",

                DefaultExt = ".config",

            };

            if (dialog.ShowDialog() == true) _cxInfo.AppConfigPath = dialog.FileName;

        }

        private void BtnOK_Click(object sender, RoutedEventArgs e) => DialogResult = true;

    }

}

The most significant member of this code is _cxInfo, which is an IConnectionInfo type. This object will hold the connection information using the attributes detailed in the following table.

Table 3: IConnectionInfo Attributes

Attribute

Description

CustomTypeInfo.CustomAssemblyPath

Path to the custom assembly file containing the entity types that will be displayed in the Schema Explorer.

CustomTypeInfo.GetCustomTypesInAssembly()

Retrieves a list of all public custom types exposed by the custom assembly.

CustomTypeInfo.CustomTypeName

Name of the custom type that will be used to populate the Schema Explorer.

AppConfigPath

Path to the configuration file associated with the custom assembly being used (.config file extension).

Once the form’s design is finished, we’ll use it in the ShowConnectionDialog method, as shown in the following code example.

Code Listing 54: ShowConnectionDialog Implemented

public override bool ShowConnectionDialog(IConnectionInfo cxInfo, bool isNewConnection) => new ConnectionDialog(cxInfo).ShowDialog() == true;

Now, if you add a connection using our driver, the connection dialog will appear, asking for the information needed to create it. If you click Cancel, the ShowDialog method will return false, and the process will be terminated. Otherwise, LINQPad will populate the Schema Explorer with the custom type’s entities and their attributes.

The GetConnectionDescription method

Finally, we need to implement the GetConnectionDescription method in order to display the selected custom type’s name in the Schema Explorer. This will be accomplished using the code in the following example.

Code Listing 55: GetConnectionDescription Method Implemented

// We'll use the description of the custom type and its
// assembly for static drivers

public override string GetConnectionDescription(IConnectionInfo cxInfo) => cxInfo.CustomTypeInfo.GetCustomTypeDescription();

We can see the use of CustomTypeInfo.GetCustomTypeDescription, which is the method that retrieves the name of the custom type selected for populating the Schema Explorer.

The completed static data context driver

The static data context driver will be obtained as a result of completing all the previous steps. The final code is shown in the following example.

Code Listing 56: Completed MyStaticDataContextDriver

using System;

using System.Collections.Generic;

using System.Linq;

using System.Reflection;

using LINQPad.Extensibility.DataContext;

namespace MyDataContextDriver.mystaticdriver

{

    public class MyStaticDataContextDriver : StaticDataContextDriver

    {

        // We'll use the description of the custom type and its assembly for Static Drivers

        public override string GetConnectionDescription(IConnectionInfo cxInfo) => cxInfo.CustomTypeInfo.GetCustomTypeDescription();

        public override bool ShowConnectionDialog(IConnectionInfo cxInfo, bool isNewConnection) => new ConnectionDialog(cxInfo).ShowDialog() == true;

        public override string Name => "My Static Data Context Driver (Demo)";

        public override string Author => "Getting the Most from LINQPad Succinctly";

        public override List<ExplorerItem> GetSchema(IConnectionInfo cxInfo, Type customType)

        {

            // First, we iterate through all top-level properties of customType

            var topLevelProps =

            (

                from prop in customType.GetProperties()

                where prop.PropertyType != typeof (string)

                // Get and display all properties of IEnumerable<T>

                let ienumerableOfT = prop.PropertyType.GetInterface ("System.Collections.Generic.IEnumerable`1")

                where ienumerableOfT != null

                orderby prop.Name

                select new ExplorerItem (prop.Name, ExplorerItemKind.QueryableObject, ExplorerIcon.Table)

                {

                    IsEnumerable = true,

                    ToolTipText = FormatTypeName (prop.PropertyType, false),

                    // Store entity type to the Tag property. This will be used later.

                    Tag = ienumerableOfT.GetGenericArguments()[0]

                }

            ).ToList ();

            // Create a lookup element, associating each element type to the properties of that type.

            // This will allow us to build hyperlinks that let the user click between relationships.

            var elementTypeLookup = topLevelProps.ToLookup (tp => (Type)tp.Tag);

            // Populate the properties of each entity

            foreach (var table in topLevelProps)

                table.Children = ((Type)table.Tag)

                    .GetProperties()

                    .Select (childProp => GetChildItem (elementTypeLookup, childProp))

                    .OrderBy (childItem => childItem.Kind)

                    .ToList ();

            return topLevelProps;

        }

        private ExplorerItem GetChildItem (ILookup<Type, ExplorerItem> elementTypeLookup, PropertyInfo childProp)

        {

            // If the property's type is in the list of entities, then we're going to assume that it's a Many:1 or

            // 1:1 reference. It's not reliable to identify 1:1 relationships purely from reflection.

            if (elementTypeLookup.Contains (childProp.PropertyType))

                return new ExplorerItem (childProp.Name, ExplorerItemKind.ReferenceLink, ExplorerIcon.ManyToOne)

                {

                    HyperlinkTarget = elementTypeLookup [childProp.PropertyType].First (),

                    ToolTipText = FormatTypeName (childProp.PropertyType, true) // FormatTypeName is LINQPad's helper method that returns a nicely formatted type name.

                };

            // We're going to check if the property's type is a collection of entities

            var ienumerableOfT = childProp.PropertyType.GetInterface ("System.Collections.Generic.IEnumerable`1");

            // If it isn't, we return the Name and Type of the property as an ExplorerItem

            if (ienumerableOfT == null) return new ExplorerItem(childProp.Name + " (" + FormatTypeName(childProp.PropertyType, false) + ")",ExplorerItemKind.Property, ExplorerIcon.Column);

            // Now, we're going to check if it is a 1:Many relationship

            var elementType = ienumerableOfT.GetGenericArguments()[0];

            if (elementTypeLookup.Contains(elementType))

                return new ExplorerItem (childProp.Name, ExplorerItemKind.CollectionLink, ExplorerIcon.OneToMany)

                {

                    HyperlinkTarget = elementTypeLookup [elementType].First (),

                    ToolTipText = FormatTypeName (elementType, true)

                };

            // If it isn't, this is an ordinary property.

            return new ExplorerItem (childProp.Name + " (" + FormatTypeName (childProp.PropertyType, false) + ")",

                ExplorerItemKind.Property, ExplorerIcon.Column);

        }

    }

}

Deploying the data context driver

To deploy the data context driver, we need to perform the following steps:

  1. Build the assembly by selecting Build > Build Solution or Rebuild Solution from the Visual Studio menu bar.
  2. Zip MyDataContextDriver.dll and header.xml files into a single file named mycontextdatadriver.zip.
  3. Rename mycontextdatadriver.zip to mycontextdatadriver.lpx.
  4. Copy mycontextdatadriver.lpx to any desired folder in the file system.

Installing the data context driver

Now, it’s time to install our developed driver into LINQPad. To complete this action, it’s mandatory to try adding a new connection by clicking the Add Connection hyperlink in the user interface. As seen at the beginning of this chapter, LINQPad will show the Choose Data Context dialog. In order to work with custom drivers, we need to click the View More Drivers button. This will bring up the Choose a Driver dialog, as seen in the “Data context drivers from the user’s perspective” section. Once the dialog is displayed, we’ll click the Browse button, and the Browse LINQPad Data Context Driver dialog will be shown.

Browse LINQPad Data Context Driver Dialog Displaying Our Custom Context Data Driver

Figure 47: Browse LINQPad Data Context Driver Dialog Displaying Our Custom Context Data Driver

Next, we need to locate the file for our custom driver and double-click its name. This will open the file and install the driver. When finished, LINQPad will show the following dialog.

Context Data Driver Successfully Installed

Figure 48: Context Data Driver Successfully Installed

Now, the Choose Data Context dialog will display our custom data context drivers as available options.

Our Custom Context Data Drivers in the Choose Data Context Dialog

Figure 49: Our Custom Context Data Drivers in the Choose Data Context Dialog

Testing our static data context driver

To test our driver, select the Use a typed data context from your own assembly option from the Choose Data Context dialog, and choose My Static Data Context Driver (Demo) from the drivers list. Click Next to continue, and the connection dialog will be displayed.

The Connection Dialog of Our Custom Context Data Driver

Figure 50: The Connection Dialog of Our Custom Context Data Driver

Selecting a custom assembly

We’re going to use the Entity Data Model assembly created in Chapter 2 to test our driver. So, after we browse the file system and select the uspostalcodes.dll assembly, the connection dialog should look like the following figure.

The uspostalcodes.dll Assembly Selected

Figure 51: The uspostalcodes.dll Assembly Selected

Now, we’re going to browse the file system and locate the uspostalcodes.dll.config file, which is the application configuration file required by the connection dialog. Finally, we’re going to choose the uspostalcodes.uspostalcodesEntities custom type. This type will be used to populate the Schema Explorer.

The Choose Custom Type Dialog

Figure 52: The Choose Custom Type Dialog

After you complete the previous steps, the connection dialog will look like this.

The Connection Dialog Filled In

Figure 53: The Connection Dialog Filled In

Creating the connection and viewing the entities

After you click OK, LINQPad will create the connection and display the entities in the user interface.

The uspostalcodesEntities Displayed Using Our Custom Driver

Figure 54: The uspostalcodesEntities Displayed Using Our Custom Driver

Displaying the custom driver description

To display the description of our custom driver, right-click the uspostalcodesEntities node. A context menu will be displayed, and the description for our driver will be shown at the top of this menu. The following figure shows an example.

LINQPad Using Our Custom Driver

Figure 55: LINQPad Using Our Custom Driver

Executing a query

We will execute a query of one of the displayed entities to finish our testing process. In this case, we’ll show the elements stored in the states entity using a data grid. To accomplish this, right-click the states node in the user interface, and then select the View top 100 rows in grid option.

Executing a Query with The Driver

Figure 56: Executing a Query with The Driver

LINQPad will automatically generate the appropriate code for executing the query, and the results will be displayed in the output panel.

Displaying the Results of the Query

Figure 57: Displaying the Results of the Query

Debugging the driver

We can debug our custom driver by following these steps:

  1. Start LINQPad.
  2. Open the MyDataContextDriver solution in Visual Studio.
  3. Once the solution is opened, go to Debug > Attach to Process. Then, locate LINQPad.exe in the dialog. After selecting LINQPad.exe from the list, click the Attach button.
  4. Set the desired breakpoints in the solution.

The Attach to Process Dialog

Figure 58: The Attach to Process Dialog

Exception logging

When a custom driver throws an exception, LINQPad writes the exception stack trace and details in a log file. This file resides in the %localappdata%\linqpad\logs\ folder, which is usually located at C:\Users\UserName\AppData\Local\LINQPad\logs.

Code Listing 57: LINQPad log.txt File Example

5.36.03 2018-12-19T09:54:55.8803299-07:00 NotImplementedException: The method or operation is not implemented.

   at MyDataContextDriver.mystaticdriver.MyStaticDataContextDriver.GetConnectionDescription(IConnectionInfo cxInfo)

   at LINQPad.Extensibility.DataContext.DataContextDriver.GetConnectionDescription(IConnectionInfo cxInfo)

   at LINQPad.Repository.GetFriendlyName(FriendlyNameMode mode)

First chance data: LINQPad.Extensibility.DataContext.DataContextDriver.GetConnectionDescription(cxInfo) offset=0xFFFFFFFF

   -LINQPad.Repository.GetFriendlyName(mode) offset=0x7C

   -LINQPad.Repository.GetFriendlyName() offset=0x1

   -LINQPad.UI.SchemaTreeInternal.RepositoryNode..ctor(r) offset=0x22

   -LINQPad.UI.SchemaTreeInternal.StaticSchemaNode..ctor(r) offset=0x7

   -LINQPad.UI.SchemaTree.AddCx(repos,child,selectNode,expandNode,massPopulation,updateAllNodeText) offset=0x35

   -LINQPad.UI.SchemaTree.AddCx(r,selectNode,expandNode) offset=0xD

   -LINQPad.UI.SchemaTree.AddCx() offset=0x2C

   -LINQPad.UI.SchemaTree.ProcessLeftMouseDown(node) offset=0x250

   -LINQPad.UI.SchemaTree.WndProc(m) offset=0x27

   -LINQPad.Program.Run(queryToLoad,runQuery,activationCode,activateAll,deactivate,noForward,noUpdate,caller) offset=0x27C

   -LINQPad.Program.Go(args) offset=0x763

   -LINQPad.Program.Start(args) offset=0xA3

   -LINQPad.ProgramStarter.Run(args) offset=0x12

   -LINQPad.Loader.Main(args) offset=0x2B4

5.36.03 2018-12-19T09:54:55.9595444-07:00 NotImplementedException: The method or operation is not implemented.

   at MyDataContextDriver.mystaticdriver.MyStaticDataContextDriver.GetConnectionDescription(IConnectionInfo cxInfo)

   at LINQPad.Extensibility.DataContext.DataContextDriver.GetConnectionDescription(IConnectionInfo cxInfo)

   at LINQPad.Repository.GetFriendlyName(FriendlyNameMode mode)

First chance data: LINQPad.Extensibility.DataContext.DataContextDriver.GetConnectionDescription(cxInfo) offset=0xFFFFFFFF

   -LINQPad.Repository.GetFriendlyName(mode) offset=0x7C

   -LINQPad.UI.QueryControl.UpdateFocusedRepository() offset=0x3E

   -LINQPad.UI.QueryControl._schemaTree_AfterSelect(sender,e) offset=0x7

   -LINQPad.UI.SchemaTree.WndProc(m) offset=0x3C

   -LINQPad.UI.SchemaTree.WndProc(m) offset=0x3C

   -LINQPad.UI.SchemaTree.AddCx(repos,child,selectNode,expandNode,massPopulation,updateAllNodeText) offset=0xED

   -LINQPad.UI.SchemaTree.AddCx(r,selectNode,expandNode) offset=0xD

   -LINQPad.UI.SchemaTree.AddCx() offset=0x2C

   -LINQPad.UI.SchemaTree.ProcessLeftMouseDown(node) offset=0x250

   -LINQPad.UI.SchemaTree.WndProc(m) offset=0x27

   -LINQPad.Program.Run(queryToLoad,runQuery,activationCode,activateAll,deactivate,noForward,noUpdate,caller) offset=0x27C

   -LINQPad.Program.Go(args) offset=0x763

   -LINQPad.Program.Start(args) offset=0xA3

   -LINQPad.ProgramStarter.Run(args) offset=0x12

   -LINQPad.Loader.Main(args) offset=0x2B4

5.36.03 2018-12-19T09:54:55.9675620-07:00 NotImplementedException: The method or operation is not implemented.

   at MyDataContextDriver.mystaticdriver.MyStaticDataContextDriver.GetConnectionDescription(IConnectionInfo cxInfo)

   at LINQPad.Extensibility.DataContext.DataContextDriver.GetConnectionDescription(IConnectionInfo cxInfo)

   at LINQPad.Repository.GetFriendlyName(FriendlyNameMode mode)

First chance data: LINQPad.Extensibility.DataContext.DataContextDriver.GetConnectionDescription(cxInfo) offset=0xFFFFFFFF

   -LINQPad.Repository.GetFriendlyName(mode) offset=0x7C

   -LINQPad.UI.QueryControl.UpdateFocusedRepository() offset=0x3E

   -LINQPad.UI.QueryControl._schemaTree_AfterSelect(sender,e) offset=0x7

   -LINQPad.UI.SchemaTree.WndProc(m) offset=0x3C

   -LINQPad.UI.SchemaTree.WndProc(m) offset=0x3C

   -LINQPad.UI.SchemaTree.AddCx(repos,child,selectNode,expandNode,massPopulation,updateAllNodeText) offset=0xED

   -LINQPad.UI.SchemaTree.AddCx(r,selectNode,expandNode) offset=0xD

   -LINQPad.UI.SchemaTree.AddCx() offset=0x2C

   -LINQPad.UI.SchemaTree.ProcessLeftMouseDown(node) offset=0x250

   -LINQPad.UI.SchemaTree.WndProc(m) offset=0x27

   -LINQPad.Program.Run(queryToLoad,runQuery,activationCode,activateAll,deactivate,noForward,noUpdate,caller) offset=0x27C

   -LINQPad.Program.Go(args) offset=0x763

   -LINQPad.Program.Start(args) offset=0xA3

   -LINQPad.ProgramStarter.Run(args) offset=0x12

   -LINQPad.Loader.Main(args) offset=0x2B4

5.36.03 2018-12-19T09:54:56.0106750-07:00 NotImplementedException: The method or operation is not implemented.

   at MyDataContextDriver.mystaticdriver.MyStaticDataContextDriver.GetConnectionDescription(IConnectionInfo cxInfo)

   at LINQPad.Extensibility.DataContext.DataContextDriver.GetConnectionDescription(IConnectionInfo cxInfo)

   at LINQPad.Repository.GetFriendlyName(FriendlyNameMode mode)

First chance data: LINQPad.Extensibility.DataContext.DataContextDriver.GetConnectionDescription(cxInfo) offset=0xFFFFFFFF

   -LINQPad.Repository.GetFriendlyName(mode) offset=0x7C

   -LINQPad.UI.QueryControl.UpdateFocusedRepository() offset=0x3E

   -LINQPad.UI.QueryControl._schemaTree_AfterSelect(sender,e) offset=0x7

   -LINQPad.UI.SchemaTree.WndProc(m) offset=0x3C

   -LINQPad.UI.SchemaTree.WndProc(m) offset=0x3C

   -LINQPad.UI.SchemaTree.AddCx(repos,child,selectNode,expandNode,massPopulation,updateAllNodeText) offset=0xED

   -LINQPad.UI.SchemaTree.AddCx(r,selectNode,expandNode) offset=0xD

   -LINQPad.UI.SchemaTree.AddCx() offset=0x2C

   -LINQPad.UI.SchemaTree.ProcessLeftMouseDown(node) offset=0x250

   -LINQPad.UI.SchemaTree.WndProc(m) offset=0x27

   -LINQPad.Program.Run(queryToLoad,runQuery,activationCode,activateAll,deactivate,noForward,noUpdate,caller) offset=0x27C

   -LINQPad.Program.Go(args) offset=0x763

   -LINQPad.Program.Start(args) offset=0xA3

   -LINQPad.ProgramStarter.Run(args) offset=0x12

   -LINQPad.Loader.Main(args) offset=0x2B4

5.36.03 2018-12-19T09:54:56.0157221-07:00 NotImplementedException: The method or operation is not implemented.

   at MyDataContextDriver.mystaticdriver.MyStaticDataContextDriver.GetConnectionDescription(IConnectionInfo cxInfo)

   at LINQPad.Extensibility.DataContext.DataContextDriver.GetConnectionDescription(IConnectionInfo cxInfo)

   at LINQPad.Repository.GetFriendlyName(FriendlyNameMode mode)

First chance data: LINQPad.Extensibility.DataContext.DataContextDriver.GetConnectionDescription(cxInfo) offset=0xFFFFFFFF

   -LINQPad.Repository.GetFriendlyName(mode) offset=0x7C

   -LINQPad.UI.QueryControl.UpdateFocusedRepository() offset=0x3E

   -LINQPad.UI.QueryControl._schemaTree_AfterSelect(sender,e) offset=0x7

   -LINQPad.UI.SchemaTree.WndProc(m) offset=0x3C

   -LINQPad.UI.SchemaTree.WndProc(m) offset=0x3C

   -LINQPad.UI.SchemaTree.AddCx(repos,child,selectNode,expandNode,massPopulation,updateAllNodeText) offset=0xED

   -LINQPad.UI.SchemaTree.AddCx(r,selectNode,expandNode) offset=0xD

   -LINQPad.UI.SchemaTree.AddCx() offset=0x2C

   -LINQPad.UI.SchemaTree.ProcessLeftMouseDown(node) offset=0x250

   -LINQPad.UI.SchemaTree.WndProc(m) offset=0x27

   -LINQPad.Program.Run(queryToLoad,runQuery,activationCode,activateAll,deactivate,noForward,noUpdate,caller) offset=0x27C

   -LINQPad.Program.Go(args) offset=0x763

   -LINQPad.Program.Start(args) offset=0xA3

   -LINQPad.ProgramStarter.Run(args) offset=0x12

   -LINQPad.Loader.Main(args) offset=0x2B4

5.36.03 2018-12-19T09:54:56.0307559-07:00 NotImplementedException: The method or operation is not implemented.

   at MyDataContextDriver.mystaticdriver.MyStaticDataContextDriver.GetConnectionDescription(IConnectionInfo cxInfo)

   at LINQPad.Extensibility.DataContext.DataContextDriver.GetConnectionDescription(IConnectionInfo cxInfo)

   at LINQPad.Repository.GetFriendlyName(FriendlyNameMode mode)

First chance data: LINQPad.Extensibility.DataContext.DataContextDriver.GetConnectionDescription(cxInfo) offset=0xFFFFFFFF

   -LINQPad.Repository.GetFriendlyName(mode) offset=0x7C

   -LINQPad.Repository.GetFriendlyName() offset=0x1

   -LINQPad.UI.SchemaTreeInternal.RepositoryNode.UpdateText() offset=0x2

   -LINQPad.UI.SchemaTreeInternal.StaticSchemaNode.UpdateText() offset=0x7

   -LINQPad.UI.SchemaTree.UpdateAllNodeText() offset=0x31

   -LINQPad.UI.SchemaTree.AddCx(repos,child,selectNode,expandNode,massPopulation,updateAllNodeText) offset=0xFC

   -LINQPad.UI.SchemaTree.AddCx(r,selectNode,expandNode) offset=0xD

   -LINQPad.UI.SchemaTree.AddCx() offset=0x2C

   -LINQPad.UI.SchemaTree.ProcessLeftMouseDown(node) offset=0x250

   -LINQPad.UI.SchemaTree.WndProc(m) offset=0x27

   -LINQPad.Program.Run(queryToLoad,runQuery,activationCode,activateAll,deactivate,noForward,noUpdate,caller) offset=0x27C

   -LINQPad.Program.Go(args) offset=0x763

   -LINQPad.Program.Start(args) offset=0xA3

   -LINQPad.ProgramStarter.Run(args) offset=0x12

   -LINQPad.Loader.Main(args) offset=0x2B4

Credits

Part of the code of this chapter is based on information and examples provided by Joseph Albahari. This information and the code associated with it are public and can be downloaded from https://www.linqpad.net/DataContextDrivers.docx and https://www.linqpad.net/DataContextDriverDemo.zip.

Chapter summary

A data context driver is a mechanism that allows programmers to extend LINQPad in order to support several data sources. LINQPad can query any data source without a custom data context driver, but in this case, the user must manually reference libraries, import custom namespaces, and formulate all queries.

A data context driver is employed every time a user adds a connection to LINQPad. Those connections appear on the Connection’s tree view area of the user interface. To add a connection, click the Add Connection hyperlink located at the top of the Connection’s tree view area.

The basic steps for building a driver are:

  1. Choose between writing a dynamic or static driver.
  2. Build a class project in Visual Studio that references LINQPad.
  3. Create classes derived from DynamicDataContextDriver or StaticDataContextDriver.
  4. Implement a few abstract methods (and some virtual methods, optionally).
  5. Zip the files generated by the class project (and all their dependencies) and change the extension from .zip to .lpx.
  6. Save the file in any place in the computer, so it can be found by using the Browse button.

There are no special terms or conditions for writing a data context driver, unless you plan to submit the driver to LINQPad’s Driver Gallery, which is beyond the scope of this book.

The following are basic concepts about data context drivers:

  • Connection: Relates to what you enter when you click Add Connection. This is broader than the classic concept of a database connection because a LINQPad connection can refer to other types of data sources, such as a web service URI.
  • Typed data contexts: A typed data context is a class with properties, fields, and methods that can be queried by the user. A typical example is a typed LINQ to SQL DataContext or a typed ObjectContext in Entity Framework.
  • Dynamic driver: A driver that builds the data context on the fly, either by generating code and compiling it after, or by using Reflection.Emit.
  • Static driver: A driver that requires a data context supplied by the user. The connection dialog will prompt for a path where a custom assembly containing the typed data context is located, and for the name of the type.

For the purposes of this book, writing a data context driver requires you to set up a Visual Studio project with the following properties:

  • Project type: Class library (.NET Framework)
  • Framework: .NET Framework 4.6.1
  • Name: MyDataContextDriver
  • Location: D:\LINQPad Samples
  • Create directory for solution: Unchecked

Every time LINQPad tries to install a context data driver, it looks for a file named header.xml. If this file is not present in the deployment package, the installation process will fail. So, the first thing to do is to add the corresponding header.xml file to the project. This file can be created in a text editor (like Notepad).

There are two base classes for context drivers: DynamicDataContextDriver and StaticDataContextDriver. The name of each type tells us the kind of driver we can build with each one of them. Both types are derived from the base class DataContextDriver, which is defined in the LINQPad.Extensibility.DataContext namespace. This class defines some abstract members that will be implemented during project creation. The files named MyDynamicDataContextDriver.cs and MyStaticDataContextDriver.cs will define the MyDynamicDataContextDriver and MyStaticDataContextDriver classes, to implement both kind of drivers. Since both classes are derived from the DataContextDriver class, Visual Studio automatically asks us to implement the abstract members defined in it. We simply let Visual Studio generate the implementation code automatically.

We focused on building the static data context driver defined in MyStaticDataContextDriver.cs by implementing the GetSchema abstract method. This method is employed to return a hierarchy of objects that will be displayed in the Schema Explorer (which is in the Connection’s tree view area of the user interface). Then, we implemented the ShowConnectionDialog method, which displays a modal WPF dialog to prompt the user for connection information.

To deploy the data context driver, we complete the following steps:

  1. Build the assembly by selecting Build > Build Solution or Rebuild Solution from the Visual Studio menu bar.
  2. Zip the MyDataContextDriver.dll and header.xml files into a single file named mycontextdatadriver.zip.
  3. Rename mycontextdatadriver.zip to mycontextdatadriver.lpx.
  4. Copy mycontextdatadriver.lpx to any desired folder in the file system.

To install the custom driver in LINQPad, it’s mandatory that you try adding a new connection by clicking the Add Connection hyperlink in the user interface. Then, LINQPad will show the Choose Data Context dialog. Next, click the View More Drivers button. This will bring up the Choose a Driver dialog. Once the dialog is displayed, click Browse, and the Browse LINQPad Data Context Driver dialog will be shown. After that, locate the file for our custom driver and double-click its name. When installation is finished, LINQPad will show the Installation Succeeded dialog.

Finally, we tested our custom driver using the Entity Data Model assembly created in Chapter 2.

Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.