Skip to content

Commit ffe29af

Browse files
pmiddletonsmitpatel
authored andcommitted
Query: Add support for queryable functions
1 parent b6ddad1 commit ffe29af

File tree

42 files changed

+2117
-132
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+2117
-132
lines changed

src/EFCore.Relational/Extensions/RelationalEntityTypeBuilderExtensions.cs

+31
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,37 @@ public static bool CanSetSchema(
264264
return entityTypeBuilder.CanSetAnnotation(RelationalAnnotationNames.Schema, schema, fromDataAnnotation);
265265
}
266266

267+
/// <summary>
268+
/// Configures the entity as a result of a queryable function. Prevents a table from being created for this entity.
269+
/// </summary>
270+
/// <param name="entityTypeBuilder"> The builder for the entity type being configured. </param>
271+
/// <returns> The same builder instance so that multiple calls can be chained. </returns>
272+
public static EntityTypeBuilder ToQueryableFunctionResultType(
273+
[NotNull] this EntityTypeBuilder entityTypeBuilder)
274+
{
275+
Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder));
276+
277+
entityTypeBuilder.Metadata.SetAnnotation(RelationalAnnotationNames.QueryableFunctionResultType, null);
278+
279+
return entityTypeBuilder;
280+
}
281+
282+
/// <summary>
283+
/// Configures the entity as a result of a queryable function. Prevents a table from being created for this entity.
284+
/// </summary>
285+
/// <param name="entityTypeBuilder"> The builder for the entity type being configured. </param>
286+
/// <returns> The same builder instance so that multiple calls can be chained. </returns>
287+
public static EntityTypeBuilder<TEntity> ToQueryableFunctionResultType<TEntity>(
288+
[NotNull] this EntityTypeBuilder<TEntity> entityTypeBuilder)
289+
where TEntity : class
290+
{
291+
Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder));
292+
293+
entityTypeBuilder.Metadata.SetAnnotation(RelationalAnnotationNames.QueryableFunctionResultType, null);
294+
295+
return entityTypeBuilder;
296+
}
297+
267298
/// <summary>
268299
/// Configures the view that the entity type maps to when targeting a relational database.
269300
/// </summary>

src/EFCore.Relational/Extensions/RelationalEntityTypeExtensions.cs

+3
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,9 @@ public static bool IsIgnoredByMigrations([NotNull] this IEntityType entityType)
454454
return true;
455455
}
456456

457+
if (entityType.FindAnnotation(RelationalAnnotationNames.QueryableFunctionResultType) != null)
458+
return true;
459+
457460
var viewDefinition = entityType.FindAnnotation(RelationalAnnotationNames.ViewDefinition);
458461
if (viewDefinition == null)
459462
{

src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs

+1
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices()
169169
TryAdd<IQueryTranslationPreprocessorFactory, RelationalQueryTranslationPreprocessorFactory>();
170170
TryAdd<IRelationalParameterBasedQueryTranslationPostprocessorFactory, RelationalParameterBasedQueryTranslationPostprocessorFactory>();
171171
TryAdd<IRelationalQueryStringFactory, RelationalQueryStringFactory>();
172+
TryAdd<INavigationExpandingExpressionVisitorFactory, RelationalNavigationExpandingExpressionVisitorFactory>();
172173

173174
ServiceCollectionMap.GetInfrastructure()
174175
.AddDependencySingleton<RelationalSqlGenerationHelperDependencies>()

src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ protected virtual void ValidateDbFunctions(
8080
RelationalStrings.DbFunctionNameEmpty(methodInfo.DisplayName()));
8181
}
8282

83-
if (dbFunction.TypeMapping == null)
83+
if (dbFunction.TypeMapping == null &&
84+
!(dbFunction.IsIQueryable && model.FindEntityType(dbFunction.MethodInfo.ReturnType.GetGenericArguments()[0]) != null))
8485
{
8586
throw new InvalidOperationException(
8687
RelationalStrings.DbFunctionInvalidReturnType(

src/EFCore.Relational/Metadata/IDbFunction.cs

+5
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ public interface IDbFunction
3434
/// </summary>
3535
MethodInfo MethodInfo { get; }
3636

37+
/// <summary>
38+
/// Whether this method returns IQueryable
39+
/// </summary>
40+
bool IsIQueryable { get; }
41+
3742
/// <summary>
3843
/// The configured store type string
3944
/// </summary>

src/EFCore.Relational/Metadata/Internal/DbFunction.cs

+43-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections.Generic;
66
using System.Linq;
7+
using System.Linq.Expressions;
78
using System.Reflection;
89
using JetBrains.Annotations;
910
using Microsoft.EntityFrameworkCore.Diagnostics;
@@ -75,6 +76,19 @@ public DbFunction(
7576
methodInfo.DisplayName(), methodInfo.ReturnType.ShortDisplayName()));
7677
}
7778

79+
if (methodInfo.ReturnType.IsGenericType
80+
&& methodInfo.ReturnType.GetGenericTypeDefinition() == typeof(IQueryable<>))
81+
{
82+
IsIQueryable = true;
83+
84+
//todo - if the generic argument is not usuable as an entitytype should we throw here? IE IQueryable<int>
85+
//the built in entitytype will throw is the type is not a class
86+
if (model.FindEntityType(methodInfo.ReturnType.GetGenericArguments()[0]) == null)
87+
{
88+
model.AddEntityType(methodInfo.ReturnType.GetGenericArguments()[0]).SetAnnotation(RelationalAnnotationNames.QueryableFunctionResultType, null);
89+
}
90+
}
91+
7892
MethodInfo = methodInfo;
7993

8094
_model = model;
@@ -310,6 +324,14 @@ public virtual Func<IReadOnlyCollection<SqlExpression>, SqlExpression> Translati
310324
set => SetTranslation(value, ConfigurationSource.Explicit);
311325
}
312326

327+
/// <summary>
328+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
329+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
330+
/// any release. You should only use it directly in your code with extreme caution and knowing that
331+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
332+
/// </summary>
333+
public virtual bool IsIQueryable { get; }
334+
313335
/// <summary>
314336
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
315337
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -345,7 +367,27 @@ private void UpdateTranslationConfigurationSource(ConfigurationSource configurat
345367
public static DbFunction FindDbFunction(
346368
[NotNull] IModel model,
347369
[NotNull] MethodInfo methodInfo)
348-
=> model[BuildAnnotationName(methodInfo)] as DbFunction;
370+
{
371+
var dbFunction = model[BuildAnnotationName(methodInfo)] as DbFunction;
372+
373+
if (dbFunction == null
374+
&& methodInfo.GetParameters().Any(p => p.ParameterType.IsGenericType && p.ParameterType.GetGenericTypeDefinition() == typeof(Expression<>)))
375+
{
376+
var parameters = methodInfo.GetParameters().Select(p => p.ParameterType.IsGenericType
377+
&& p.ParameterType.GetGenericTypeDefinition() == typeof(Expression<>)
378+
&& p.ParameterType.GetGenericArguments()[0].GetGenericTypeDefinition() == typeof(Func<>)
379+
? p.ParameterType.GetGenericArguments()[0].GetGenericArguments()[0]
380+
: p.ParameterType).ToArray();
381+
382+
var nonExpressionMethod = methodInfo.DeclaringType.GetMethod(methodInfo.Name, parameters);
383+
384+
dbFunction = nonExpressionMethod != null
385+
? model[BuildAnnotationName(nonExpressionMethod)] as DbFunction
386+
: null;
387+
}
388+
389+
return dbFunction;
390+
}
349391

350392
/// <summary>
351393
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to

src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs

+5
Original file line numberDiff line numberDiff line change
@@ -143,5 +143,10 @@ public static class RelationalAnnotationNames
143143
/// The name for column mappings annotations.
144144
/// </summary>
145145
public const string ViewColumnMappings = Prefix + "ViewColumnMappings";
146+
147+
/// <summary>
148+
/// The definition of a Queryable Function Result Type.
149+
/// </summary>
150+
public const string QueryableFunctionResultType = Prefix + "QueryableFunctionResultType";
146151
}
147152
}

src/EFCore.Relational/Properties/RelationalStrings.Designer.cs

+14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EFCore.Relational/Properties/RelationalStrings.resx

+6
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,12 @@
435435
<data name="DbFunctionInvalidInstanceType" xml:space="preserve">
436436
<value>The DbFunction '{function}' defined on type '{type}' must be either a static method or an instance method defined on a DbContext subclass. Instance methods on other types are not supported.</value>
437437
</data>
438+
<data name="DbFunctionCantProjectIQueryable" xml:space="preserve">
439+
<value>Queryable Db Functions used in projections cannot return IQueryable. IQueryable must be converted to a collection type such as List or Array.</value>
440+
</data>
441+
<data name="DbFunctionProjectedCollectionMustHavePK" xml:space="preserve">
442+
<value>Return type of a queryable function '{functionName}' which is used in a projected collection must define a primary key.</value>
443+
</data>
438444
<data name="ConflictingAmbientTransaction" xml:space="preserve">
439445
<value>An ambient transaction has been detected. The ambient transaction needs to be completed before beginning a transaction on this connection.</value>
440446
</data>

src/EFCore.Relational/Query/ISqlExpressionFactory.cs

+1
Original file line numberDiff line numberDiff line change
@@ -193,5 +193,6 @@ SqlFunctionExpression Function(
193193
SelectExpression Select([CanBeNull] SqlExpression projection);
194194
SelectExpression Select([NotNull] IEntityType entityType);
195195
SelectExpression Select([NotNull] IEntityType entityType, [NotNull] string sql, [NotNull] Expression sqlArguments);
196+
SelectExpression Select([NotNull] IEntityType entityType, [NotNull] SqlFunctionExpression expression);
196197
}
197198
}

src/EFCore.Relational/Query/Internal/NullSemanticsRewritingExpressionVisitor.cs

+7
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,13 @@ protected override Expression VisitSqlFunction(SqlFunctionExpression sqlFunction
656656
return sqlFunctionExpression.Update(newInstance, newArguments);
657657
}
658658

659+
protected override Expression VisitQueryableSqlFunctionExpression(QueryableSqlFunctionExpression queryableFunctionExpression)
660+
{
661+
Check.NotNull(queryableFunctionExpression, nameof(queryableFunctionExpression));
662+
663+
return queryableFunctionExpression;
664+
}
665+
659666
protected override Expression VisitSqlParameter(SqlParameterExpression sqlParameterExpression)
660667
{
661668
Check.NotNull(sqlParameterExpression, nameof(sqlParameterExpression));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Diagnostics.CodeAnalysis;
5+
using System.Linq.Expressions;
6+
7+
namespace Microsoft.EntityFrameworkCore.Query.Internal
8+
{
9+
public class RelationalNavigationExpandingExpressionVisitor : NavigationExpandingExpressionVisitor
10+
{
11+
public RelationalNavigationExpandingExpressionVisitor(
12+
[NotNull] QueryTranslationPreprocessor queryTranslationPreprocessor,
13+
[NotNull] QueryCompilationContext queryCompilationContext,
14+
[NotNull] IEvaluatableExpressionFilter evaluatableExpressionFilter)
15+
: base(queryTranslationPreprocessor, queryCompilationContext, evaluatableExpressionFilter)
16+
{
17+
}
18+
19+
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
20+
{
21+
var dbFunction = QueryCompilationContext.Model.FindDbFunction(methodCallExpression.Method);
22+
23+
return dbFunction?.IsIQueryable == true
24+
? CreateNavigationExpansionExpression(methodCallExpression, QueryCompilationContext.Model.FindEntityType(dbFunction.MethodInfo.ReturnType.GetGenericArguments()[0]))
25+
: base.VisitMethodCall(methodCallExpression);
26+
}
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.EntityFrameworkCore.Query.Internal
5+
{
6+
public class RelationalNavigationExpandingExpressionVisitorFactory : INavigationExpandingExpressionVisitorFactory
7+
{
8+
public virtual NavigationExpandingExpressionVisitor Create(
9+
QueryTranslationPreprocessor queryTranslationPreprocessor,
10+
QueryCompilationContext queryCompilationContext, IEvaluatableExpressionFilter evaluatableExpressionFilter)
11+
{
12+
return new RelationalNavigationExpandingExpressionVisitor(queryTranslationPreprocessor, queryCompilationContext, evaluatableExpressionFilter);
13+
}
14+
}
15+
}

src/EFCore.Relational/Query/Internal/RelationalProjectionBindingExpressionVisitor.cs

+10-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using JetBrains.Annotations;
1010
using Microsoft.EntityFrameworkCore.Diagnostics;
1111
using Microsoft.EntityFrameworkCore.Infrastructure;
12+
using Microsoft.EntityFrameworkCore.Metadata;
1213
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
1314
using Microsoft.EntityFrameworkCore.Storage;
1415
using Microsoft.EntityFrameworkCore.Utilities;
@@ -22,6 +23,7 @@ public class RelationalProjectionBindingExpressionVisitor : ExpressionVisitor
2223

2324
private SelectExpression _selectExpression;
2425
private bool _clientEval;
26+
private readonly IModel _model;
2527

2628
private readonly IDictionary<ProjectionMember, Expression> _projectionMapping
2729
= new Dictionary<ProjectionMember, Expression>();
@@ -30,10 +32,12 @@ private readonly IDictionary<ProjectionMember, Expression> _projectionMapping
3032

3133
public RelationalProjectionBindingExpressionVisitor(
3234
[NotNull] RelationalQueryableMethodTranslatingExpressionVisitor queryableMethodTranslatingExpressionVisitor,
33-
[NotNull] RelationalSqlTranslatingExpressionVisitor sqlTranslatingExpressionVisitor)
35+
[NotNull] RelationalSqlTranslatingExpressionVisitor sqlTranslatingExpressionVisitor,
36+
[NotNull] IModel model)
3437
{
3538
_queryableMethodTranslatingExpressionVisitor = queryableMethodTranslatingExpressionVisitor;
3639
_sqlTranslator = sqlTranslatingExpressionVisitor;
40+
_model = model;
3741
}
3842

3943
public virtual Expression Translate([NotNull] SelectExpression selectExpression, [NotNull] Expression expression)
@@ -242,6 +246,11 @@ protected override Expression VisitNew(NewExpression newExpression)
242246
return null;
243247
}
244248

249+
if (newExpression.Arguments.Any(arg => arg is MethodCallExpression methodCallExp && _model.FindDbFunction(methodCallExp.Method)?.IsIQueryable == true))
250+
{
251+
throw new InvalidOperationException(RelationalStrings.DbFunctionCantProjectIQueryable());
252+
}
253+
245254
var newArguments = new Expression[newExpression.Arguments.Count];
246255
for (var i = 0; i < newArguments.Length; i++)
247256
{

src/EFCore.Relational/Query/Internal/RelationalQueryTranslationPreprocessorFactory.cs

+5-2
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,23 @@ public class RelationalQueryTranslationPreprocessorFactory : IQueryTranslationPr
2121
{
2222
private readonly QueryTranslationPreprocessorDependencies _dependencies;
2323
private readonly RelationalQueryTranslationPreprocessorDependencies _relationalDependencies;
24+
private readonly INavigationExpandingExpressionVisitorFactory _navigationExpandingExpressionVisitorFactory;
2425

2526
public RelationalQueryTranslationPreprocessorFactory(
2627
[NotNull] QueryTranslationPreprocessorDependencies dependencies,
27-
[NotNull] RelationalQueryTranslationPreprocessorDependencies relationalDependencies)
28+
[NotNull] RelationalQueryTranslationPreprocessorDependencies relationalDependencies,
29+
[NotNull] INavigationExpandingExpressionVisitorFactory navigationExpandingExpressionVisitorFactory)
2830
{
2931
_dependencies = dependencies;
3032
_relationalDependencies = relationalDependencies;
33+
_navigationExpandingExpressionVisitorFactory = navigationExpandingExpressionVisitorFactory;
3134
}
3235

3336
public virtual QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext)
3437
{
3538
Check.NotNull(queryCompilationContext, nameof(queryCompilationContext));
3639

37-
return new RelationalQueryTranslationPreprocessor(_dependencies, _relationalDependencies, queryCompilationContext);
40+
return new RelationalQueryTranslationPreprocessor(_dependencies, _relationalDependencies, queryCompilationContext, _navigationExpandingExpressionVisitorFactory);
3841
}
3942
}
4043
}

src/EFCore.Relational/Query/NullabilityBasedSqlProcessingExpressionVisitor.cs

+3
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,9 @@ protected override Expression VisitProjection(ProjectionExpression projectionExp
421421
VisitInternal<SqlExpression>(projectionExpression.Expression).ResultExpression);
422422
}
423423

424+
protected override Expression VisitQueryableSqlFunctionExpression(QueryableSqlFunctionExpression queryableFunctionExpression)
425+
=> Check.NotNull(queryableFunctionExpression, nameof(queryableFunctionExpression));
426+
424427
protected override Expression VisitRowNumber(RowNumberExpression rowNumberExpression)
425428
{
426429
Check.NotNull(rowNumberExpression, nameof(rowNumberExpression));

src/EFCore.Relational/Query/QuerySqlGenerator.cs

+11
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,17 @@ protected override Expression VisitSqlFunction(SqlFunctionExpression sqlFunction
270270
return sqlFunctionExpression;
271271
}
272272

273+
protected override Expression VisitQueryableSqlFunctionExpression(QueryableSqlFunctionExpression queryableFunctionExpression)
274+
{
275+
Visit(queryableFunctionExpression.SqlFunctionExpression);
276+
277+
_relationalCommandBuilder
278+
.Append(AliasSeparator)
279+
.Append(_sqlGenerationHelper.DelimitIdentifier(queryableFunctionExpression.Alias));
280+
281+
return queryableFunctionExpression;
282+
}
283+
273284
protected override Expression VisitColumn(ColumnExpression columnExpression)
274285
{
275286
Check.NotNull(columnExpression, nameof(columnExpression));

0 commit comments

Comments
 (0)