ORMs like Entity Framework have always been shrouded in a bit of controversy from SQL purists who aren't in control of the types of queries that these tools are building behind the scenes. While this is a perfectly valid stance to take, these same tools can often assist in getting things done much faster than tinkering with an endless chain of JOINs, sub-queries, etc.
But what if you want to know exactly what is going on behind the scenes? What SQL is being generated by Entity Framework? And is it efficient enough to suit your needs, or do you need to handle writing some yourself?
This post will cover a quick tip that will allow you to see the SQL behind the scenes and judge for yourself using Entity Framework Core.
How did this work prior to Entity Framework Core?
Previously, you could use Reflection to create an ObjectQuery object and then call the ToTraceString() method to actually store the query results as seen below.
-
- var query = _context.Widgets.Where(w => w.IsReal && w.Id == 42);
-
- var sql = ((System.Data.Objects.ObjectQuery)query).ToTraceString();
And that's really it. The result of the ToTraceString() call will return a string variable containing the entire SQL query being executed.
Options for Entity Framework Core
The previous approach no longer works within the Entity Framework Core (EF7) world, so we have to resort to one of three options, which may vary depending on your needs.
- Using Built-in or Custom Logging
Logging the executing query using your logger of choice or the built-in Logger in .NET Core as mentioned in this tutorial.
- Using a Profiler
Using an SQL Profiler like MiniProfiler to monitor the executing query.
- Using Crazy Reflection Code
You can implement some custom reflection code similar to the older approach to perform the same basic concept.
Since both of the first two options are fairly well documented, we will be focusing on the crazy reflection approach.
Getting Behind the Scenes in Entity Framework Core
Using the following snippets of code, which rely on Reflection to resolve information about the compiler, parser, database, and fields being targeted, we can use those things to reconstruct what is actually going on behind the scenes.
Note
Since this relies on Reflection, it may be subject to breaking in the future, especially with any API changes.
You can thank fellow Microsoft MVP Ricardo Peres for this magic.
- public class IQueryableExtensions
- {
- private static readonly TypeInfo QueryCompilerTypeInfo = typeof(QueryCompiler).GetTypeInfo();
-
- private static readonly FieldInfo QueryCompilerField = typeof(EntityQueryProvider).GetTypeInfo().DeclaredFields.First(x => x.Name == "_queryCompiler");
-
- private static readonly PropertyInfo NodeTypeProviderField = QueryCompilerTypeInfo.DeclaredProperties.Single(x => x.Name == "NodeTypeProvider");
-
- private static readonly MethodInfo CreateQueryParserMethod = QueryCompilerTypeInfo.DeclaredMethods.First(x => x.Name == "CreateQueryParser");
-
- private static readonly FieldInfo DataBaseField = QueryCompilerTypeInfo.DeclaredFields.Single(x => x.Name == "_database");
-
- private static readonly FieldInfo QueryCompilationContextFactoryField = typeof(Database).GetTypeInfo().DeclaredFields.Single(x => x.Name == "_queryCompilationContextFactory");
-
- public static string ToSql<TEntity>(this IQueryable<TEntity> query) where TEntity : class
- {
- if (!(query is EntityQueryable<TEntity>) && !(query is InternalDbSet<TEntity>))
- {
- throw new ArgumentException("Invalid query");
- }
-
- var queryCompiler = (IQueryCompiler)QueryCompilerField.GetValue(query.Provider);
- var nodeTypeProvider = (INodeTypeProvider)NodeTypeProviderField.GetValue(queryCompiler);
- var parser = (IQueryParser)CreateQueryParserMethod.Invoke(queryCompiler, new object[] { nodeTypeProvider });
- var queryModel = parser.GetParsedQuery(query.Expression);
- var database = DataBaseField.GetValue(queryCompiler);
- var queryCompilationContextFactory = (IQueryCompilationContextFactory)QueryCompilationContextFactoryField.GetValue(database);
- var queryCompilationContext = queryCompilationContextFactory.Create(false);
- var modelVisitor = (RelationalQueryModelVisitor)queryCompilationContext.CreateQueryModelVisitor();
- modelVisitor.CreateQueryExecutor<TEntity>(queryModel);
- var sql = modelVisitor.Queries.First().ToString();
-
- return sql;
- }
- }
And as far as actual usage goes, you would simply call the ToSql() method to return your SQL query string.
- // Build a query using Entity Framework
- var query = _context.Widgets.Where(w => w.IsReal && w.Id == 42);
- // Get the generated SQL
- var sql = query.ToSql();
You can find a gist of the above code on GitHub here.