Updating LINQPad.QueryPlanVisualizer for LINQPad6 And Entity Framework Core

LINQPad.QueryPlanVisualizer is a custom visualizer for LINQPad that shows a database query plan inside LINQPad. It also shows missing indexes for the query that you can create directly from LINQPad.

Since then, a new major version, LINQPad 6, was released that targets .NET Core 3 and .NET 5 and uses Entity Framework Core (as well as LINQ-to-SQL) for running Linq queries.

To support these changes, I have just released a new version of LINQPad.QueryPlanVisualizer that is compatible with LINQPad 6, supports Entity Framework Core, and, last but not least, adds support for viewing PostgreSQL query plans in LINQPad. It also can share SQL Server query plan to Brent Ozar’s Paste The Plan and share PostgreSQL query plan explain.dalibo.com.

Using LINQPad.QueryPlanVisualizer

To use LINQPad.QueryPlanVisualizer in your queries, either reference it from NuGet or download the latest release from GitHub and copy QueryPlanVisualizer.LinqPad6.dll to My Documents\LINQPad Plugins\NetCore3 folder.

To view the query plan call DumpPlan method on any IQueryable instance:

Posts.OrderBy(post => post.PostDate.Year > 2020).OrderByDescending(post => post.BlogId).DumpPlan();
LINQPad showing SQL Server Query Execution Plan

If you run the same query on an PostgreSQL database, you will see a similar output:

LINQPad showing PostgreSQL Query Execution Plan

Internals of LINQPad.QueryPlanVisualizer

As the visualizer supports multiple ORMs (EF Core and LINQ-to-SQL) and different databases (SQL Server and PostgreSQL), the project uses Bridge Pattern to avoid creating separate classes for all possible combinations. The main class hierarchies are OrmHelper, which is responsible for extracting DbCommand from IQueryable for a specific ORM; DatabaseProvider is responsible for getting query plan for a specific database; and PlanProcessor that converts raw plan to Html. The OrmHelper also contains a factory method for creating an instance for the ORM that the current query uses:

public static OrmHelper Create<T>(IQueryable<T> queryable, object dataContext)
{
  if (dataContext is DbContext dbContext)
  {
    var efCoreHelper = new EFCoreHelper(dbContext.Database.ProviderName);
    efCoreHelper.Initialize(queryable);
    return efCoreHelper;
  }

  var queryType = queryable.GetType();

  var dataQueryType = queryType.Assembly.GetType("System.Data.Linq.DataQuery`1");
  var tableQueryType = queryType.Assembly.GetType("System.Data.Linq.Table`1");

  var queryGenericType = queryType.GetGenericTypeDefinition();
  if (queryGenericType == dataQueryType || queryGenericType == tableQueryType)
  {
    var contextField = queryType.GetField("context", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.GetField);
    var context = contextField?.GetValue(queryable);

    if (context != null)
    {
      var linqToSqlHelper = new LinqToSqlHelper(context);
      linqToSqlHelper.Initialize(queryable);
      return linqToSqlHelper;
    }
  }

  return null;
}

The dataContext object is the data context that LIQNPad exposes via Util.CurrentDataContext.

The constructor of EFCoreHelper sets DatabaseProvider and PlanProcessor based on the target database:

public EFCoreHelper(string provider) : base(CreateParameters(provider))
{
}

public static (DatabaseProvider provider, PlanProcessor planConvertor) CreateParameters(string provider)
{
  return provider switch
  {
    "Microsoft.EntityFrameworkCore.SqlServer" => (new SqlServerDatabaseProvider(), new SqlServerPlanProcessor()),
    "Npgsql.EntityFrameworkCore.PostgreSQL" => (new PostgresDatabaseProvider(), new PostgresPlanProcessor()),
    _ => (null, null)
  };
}

The LinqToSqlHelper always passes SqlServerDatabaseProvider and SqlServerPlanProcessor to the base class as it only supports SQL Server.

Extracting and Formatting Query Plan from SQL Server

To extract query plan for SQL Server, the SqlServerDatabaseProvider class executes SET STATISTICS XML ON command before running the actual query and captures the plan returned by the database:

protected override string ExtractPlanInternal(DbCommand command)
{
  using var setStatisticsCommand = command.Connection.CreateCommand();
  setStatisticsCommand.CommandText = "SET STATISTICS XML ON";
  setStatisticsCommand.ExecuteNonQuery();

  using var reader = command.ExecuteReader();
  while (reader.NextResult())
  {
    if (reader.GetName(0) == "Microsoft SQL Server 2005 XML Showplan")
    {
      reader.Read();
      return reader.GetString(0);
    }
  }

  return null;
}

Next, SqlServerPlanProcessor uses html-query-plan to display the plan as Html.

Extracting Query Plan from PostgreSQL

For PostgreSQL, the PostgresPlanProcessor class prepends EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS) to the query text to get the plan from a database:

protected override string ExtractPlanInternal(DbCommand command)
{
  command.CommandText = "EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS) " + command.CommandText;

  using var reader = command.ExecuteReader();
  var plan = string.Join(Environment.NewLine, reader.Cast<IDataRecord>().Select(r => r.GetString(0)));

  return plan;
}

The PostgreSQL query plan is then displayed directly as plain text.

Conclusion

LINQPad is a handy tool for prototyping and testing Linq queries. LINQPad.QueryPlanVisualizer takes it further by showing query plan and adding other helpful features. If you have questions or suggestions please leave a comment, create an issue and star the repository.

Avatar
Giorgi Dalakishvili
World-Class Software Engineer

Related