Skip to content

Commit

Permalink
Query: Introduce custom query roots
Browse files Browse the repository at this point in the history
This change introduces derived types of EntityQueryable<T> to be processed as query root in core pipeline.
Currently FromSql and Queryable functions generate a method call in query pipeline which would be mapped to a query root. This means that any visitor which need to detect query root, they have to have special code to handle this.
With this change, any provider bringing custom query roots can inject custom query root for relevant subtree in the query and it would be processed transparently through the stack. Only during translation, once the custom query root needs to be intercepted to generate correct SQL.

Converted FromSql in this PR.
Will submit another PR to convert queryable functions.

Custom query roots can be generated during query construction itself (before it reaches EF), such query root requires
- Overriden Equals/GetHashCode methods so we differentiate them during query caching.
- Optional ToString method for debugging print out.
- Conversion to such custom roots in QueryFilter if needed.
- Components of custom root cannot be parameterized in ParameterExtractingExpressionVisitor

Part of #18923
  • Loading branch information
smitpatel committed Feb 27, 2020
1 parent 233856f commit 7ecadd7
Show file tree
Hide file tree
Showing 16 changed files with 216 additions and 65 deletions.
23 changes: 23 additions & 0 deletions src/EFCore.Relational/Query/IFromSqlQueryable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Linq.Expressions;

namespace Microsoft.EntityFrameworkCore.Query
{
/// <summary>
/// An interface to identify FromSql query roots in LINQ.
/// </summary>
public interface IFromSqlQueryable : IEntityQueryable
{
/// <summary>
/// Return Sql used to get data for this query root.
/// </summary>
string Sql { get; }

/// <summary>
/// Return arguments for the Sql.
/// </summary>
Expression Argument { get; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Linq.Expressions;
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Metadata;

namespace Microsoft.EntityFrameworkCore.Query.Internal
{
public class CustomQueryableInjectingExpressionVisitor : ExpressionVisitor
{
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
{
if (methodCallExpression.Method.DeclaringType == typeof(RelationalQueryableExtensions)
&& methodCallExpression.Method.Name == nameof(RelationalQueryableExtensions.FromSqlOnQueryable))
{
var sql = (string)((ConstantExpression)methodCallExpression.Arguments[1]).Value;
var entityType = ((IEntityQueryable)((ConstantExpression)methodCallExpression.Arguments[0]).Value).EntityType;

return CreateFromSqlQueryableExpression(entityType, sql, methodCallExpression.Arguments[2]);
}

return base.VisitMethodCall(methodCallExpression);
}

public static ConstantExpression CreateFromSqlQueryableExpression(IEntityType entityType, string sql, Expression argument)
{
return Expression.Constant(
_createFromSqlQueryableMethod
.MakeGenericMethod(entityType.ClrType)
.Invoke(
null, new object[] { NullAsyncQueryProvider.Instance, entityType, sql, argument }));
}

private static readonly MethodInfo _createFromSqlQueryableMethod
= typeof(CustomQueryableInjectingExpressionVisitor)
.GetTypeInfo().GetDeclaredMethod(nameof(CreateFromSqlQueryable));

[UsedImplicitly]
private static FromSqlQueryable<TResult> CreateFromSqlQueryable<TResult>(
IAsyncQueryProvider entityQueryProvider, IEntityType entityType, string sql, Expression argument)
=> new FromSqlQueryable<TResult>(entityQueryProvider, entityType, sql, argument);
}
}
56 changes: 56 additions & 0 deletions src/EFCore.Relational/Query/Internal/FromSqlQueryable`.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;

namespace Microsoft.EntityFrameworkCore.Query.Internal
{
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public class FromSqlQueryable<TResult> : EntityQueryable<TResult>, IFromSqlQueryable
{
/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public FromSqlQueryable(IAsyncQueryProvider queryProvider, IEntityType entityType, string sql, Expression argument)
: base(queryProvider, entityType)
{
Sql = sql;
Argument = argument;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public string Sql { get; }

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public Expression Argument { get; }

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public override string ToString() => $"{base.ToString()}.FromSql(\"{Sql}\", {Argument.Print()}";
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Linq.Expressions;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.Utilities;

namespace Microsoft.EntityFrameworkCore.Query
Expand All @@ -21,5 +23,12 @@ public RelationalQueryTranslationPreprocessor(
}

protected virtual RelationalQueryTranslationPreprocessorDependencies RelationalDependencies { get; }

public override Expression NormalizeQueryableMethodCall(Expression expression)
{
expression = new CustomQueryableInjectingExpressionVisitor().Visit(expression);

return base.NormalizeQueryableMethodCall(expression);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,21 +63,18 @@ protected RelationalQueryableMethodTranslatingExpressionVisitor(
_subquery = true;
}

protected override Expression VisitConstant(ConstantExpression constantExpression)
{
return constantExpression.Value is IFromSqlQueryable fromSqlQueryable
? CreateShapedQueryExpression(fromSqlQueryable)
: base.VisitConstant(constantExpression);
}

protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
{
Check.NotNull(methodCallExpression, nameof(methodCallExpression));

if (methodCallExpression.Method.DeclaringType == typeof(RelationalQueryableExtensions)
&& methodCallExpression.Method.Name == nameof(RelationalQueryableExtensions.FromSqlOnQueryable))
{
var sql = (string)((ConstantExpression)methodCallExpression.Arguments[1]).Value;
var queryable = (IEntityQueryable)((ConstantExpression)methodCallExpression.Arguments[0]).Value;

return CreateShapedQueryExpression(
queryable.EntityType, _sqlExpressionFactory.Select(queryable.EntityType, sql, methodCallExpression.Arguments[2]));
}

var dbFunction = this._model.FindDbFunction(methodCallExpression.Method);
var dbFunction = _model.FindDbFunction(methodCallExpression.Method);
if (dbFunction != null && dbFunction.IsIQueryable)
{
return CreateShapedQueryExpression(methodCallExpression);
Expand All @@ -94,13 +91,13 @@ protected virtual ShapedQueryExpression CreateShapedQueryExpression([NotNull] Me
var sqlFuncExpression = _sqlTranslator.TranslateMethodCall(methodCallExpression) as SqlFunctionExpression;

var elementType = methodCallExpression.Method.ReturnType.GetGenericArguments()[0];
var entityType =_model.FindEntityType(elementType);
var entityType = _model.FindEntityType(elementType);
var queryExpression = _sqlExpressionFactory.Select(entityType, sqlFuncExpression);

return CreateShapedQueryExpression(entityType, queryExpression);
}

[Obsolete("Use overload which takes IEntityType.")]
[Obsolete("Use overload which takes IEntityType.")]
protected override ShapedQueryExpression CreateShapedQueryExpression(Type elementType)
{
Check.NotNull(elementType, nameof(elementType));
Expand All @@ -118,6 +115,15 @@ protected override ShapedQueryExpression CreateShapedQueryExpression(IEntityType
return CreateShapedQueryExpression(entityType, _sqlExpressionFactory.Select(entityType));
}

private ShapedQueryExpression CreateShapedQueryExpression(IFromSqlQueryable fromSqlQueryable)
{
Check.NotNull(fromSqlQueryable, nameof(fromSqlQueryable));

return CreateShapedQueryExpression(
fromSqlQueryable.EntityType,
_sqlExpressionFactory.Select(fromSqlQueryable.EntityType, fromSqlQueryable.Sql, fromSqlQueryable.Argument));
}

private static ShapedQueryExpression CreateShapedQueryExpression(IEntityType entityType, SelectExpression selectExpression)
=> new ShapedQueryExpression(
selectExpression,
Expand Down
5 changes: 3 additions & 2 deletions src/EFCore/Extensions/Internal/ExpressionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Linq.Expressions;
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Query;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.Utilities;

Expand Down Expand Up @@ -138,9 +139,9 @@ public static bool IsLogicalOperation([NotNull] this Expression expression)
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
[Obsolete("Use constantExpression.Value is IEntityQueryable instead.")]
public static bool IsEntityQueryable([NotNull] this ConstantExpression constantExpression)
=> constantExpression.Type.IsGenericType
&& constantExpression.Type.GetGenericTypeDefinition() == typeof(EntityQueryable<>);
=> constantExpression.Value is IEntityQueryable;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
4 changes: 2 additions & 2 deletions src/EFCore/Query/ExpressionPrinter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -376,9 +376,9 @@ protected override Expression VisitConstant(ConstantExpression constantExpressio
{
printable.Print(this);
}
else if (constantExpression.IsEntityQueryable())
else if (constantExpression.Value is IEntityQueryable entityQueryable)
{
_stringBuilder.Append($"DbSet<{constantExpression.Type.GenericTypeArguments.First().ShortDisplayName()}>");
_stringBuilder.Append(entityQueryable.ToString());
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,8 @@ protected override Expression VisitConstant(ConstantExpression constantExpressio
{
Check.NotNull(constantExpression, nameof(constantExpression));

return constantExpression.IsEntityQueryable()
? new EntityReferenceExpression(
constantExpression,
((IEntityQueryable)constantExpression.Value).EntityType)
return constantExpression.Value is IEntityQueryable entityQueryable
? new EntityReferenceExpression(constantExpression, entityQueryable.EntityType)
: (Expression)constantExpression;
}

Expand Down
32 changes: 32 additions & 0 deletions src/EFCore/Query/Internal/EntityQueryable`.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,5 +156,37 @@ IList IListSource.GetList()
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual QueryDebugView DebugView => new QueryDebugView(() => Expression.Print(), this.ToQueryString);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public override bool Equals(object obj)
=> obj != null
&& (ReferenceEquals(this, obj)
|| obj is IEntityQueryable entityQueryable
&& entityQueryable.EntityType == _entityType);

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public override int GetHashCode() => _entityType?.GetHashCode() ?? 0;

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public override string ToString()
=> Expression is ConstantExpression constantExpression
&& ReferenceEquals(constantExpression.Value, this)
? $"DbSet<{_entityType.ClrType.ShortDisplayName()}>()"
: base.ToString();
}
}
3 changes: 1 addition & 2 deletions src/EFCore/Query/Internal/ExpressionEqualityComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -421,8 +421,7 @@ private static bool CompareConstant(ConstantExpression a, ConstantExpression b)
=> a.Value == b.Value
|| (a.Value != null
&& b.Value != null
&& (a.IsEntityQueryable() && b.IsEntityQueryable() && a.Value.GetType() == b.Value.GetType()
|| Equals(a.Value, b.Value)));
&& (Equals(a.Value, b.Value)));

private bool CompareGoto(GotoExpression a, GotoExpression b)
=> a.Kind == b.Kind
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -682,12 +682,11 @@ protected override Expression VisitConstant(ConstantExpression constantExpressio
{
Check.NotNull(constantExpression, nameof(constantExpression));

if (constantExpression.IsEntityQueryable())
if (constantExpression.Value is IEntityQueryable entityQueryable)
{
var entityType = ((IEntityQueryable)constantExpression.Value).EntityType;
if (entityType == _entityType)
if (entityQueryable.EntityType == _entityType)
{
return _navigationExpandingExpressionVisitor.CreateNavigationExpansionExpression(constantExpression, entityType);
return _navigationExpandingExpressionVisitor.CreateNavigationExpansionExpression(constantExpression, _entityType);
}
}

Expand Down
Loading

0 comments on commit 7ecadd7

Please sign in to comment.