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 24312e4
Show file tree
Hide file tree
Showing 19 changed files with 228 additions and 70 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);
}

private 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);
}
}
63 changes: 63 additions & 0 deletions src/EFCore.Relational/Query/Internal/FromSqlQueryable`.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// 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 JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Utilities;

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(
[NotNull] IAsyncQueryProvider queryProvider, [NotNull] IEntityType entityType,
[NotNull] string sql, [NotNull] Expression argument)
: base(queryProvider, entityType)
{
Check.NotEmpty(sql, nameof(sql));
Check.NotNull(argument, nameof(argument));

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 virtual 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 virtual 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
1 change: 0 additions & 1 deletion src/EFCore/Metadata/Internal/TypeBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading;
using JetBrains.Annotations;
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
33 changes: 33 additions & 0 deletions src/EFCore/Query/Internal/EntityQueryable`.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,5 +156,38 @@ 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()
=> _entityType != null
? (_entityType.IsSharedType
? $"DbSet<{_entityType.ClrType.ShortDisplayName()}>(\"{_entityType.Name}\")"
: $"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 24312e4

Please sign in to comment.