Skip to content

Commit 092771f

Browse files
committed
Add support for querable functions
1 parent 6e2703f commit 092771f

File tree

42 files changed

+1787
-131
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

+1787
-131
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
@@ -292,6 +292,9 @@ public static bool IsIgnoredByMigrations([NotNull] this IEntityType entityType)
292292
return true;
293293
}
294294

295+
if (entityType.FindAnnotation(RelationalAnnotationNames.QueryableFunctionResultType) != null)
296+
return true;
297+
295298
var viewDefinition = entityType.FindAnnotation(RelationalAnnotationNames.ViewDefinition);
296299
if (viewDefinition == null)
297300
{

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

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

83-
if (dbFunction.TypeMapping == null)
83+
if (dbFunction.IsIQueryable)
84+
{
85+
if(model.FindEntityType(dbFunction.MethodInfo.ReturnType.GetGenericArguments()[0]) == null)
86+
{
87+
throw new InvalidOperationException(
88+
RelationalStrings.DbFunctionInvalidReturnType(
89+
methodInfo.DisplayName(),
90+
methodInfo.ReturnType.ShortDisplayName()));
91+
}
92+
}
93+
else if (dbFunction.TypeMapping == null)
8494
{
8595
throw new InvalidOperationException(
8696
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
@@ -98,5 +98,10 @@ public static class RelationalAnnotationNames
9898
/// The definition of a database view.
9999
/// </summary>
100100
public const string ViewDefinition = Prefix + "ViewDefinition";
101+
102+
/// <summary>
103+
/// The definition of a Queryable Function Result Type.
104+
/// </summary>
105+
public const string QueryableFunctionResultType = Prefix + "QueryableFunctionResultType";
101106
}
102107
}

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
@@ -141,5 +141,6 @@ SqlFunctionExpression Function(
141141
SelectExpression Select([CanBeNull] SqlExpression projection);
142142
SelectExpression Select([NotNull] IEntityType entityType);
143143
SelectExpression Select([NotNull] IEntityType entityType, [NotNull] string sql, [NotNull] Expression sqlArguments);
144+
SelectExpression Select([NotNull] IEntityType entityType, [NotNull] SqlFunctionExpression expression);
144145
}
145146
}

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(QuerableSqlFunctionExpression 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,27 @@
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] QueryCompilationContext queryCompilationContext,
13+
[NotNull] IEvaluatableExpressionFilter evaluatableExpressionFilter)
14+
: base(queryCompilationContext, evaluatableExpressionFilter)
15+
{
16+
}
17+
18+
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
19+
{
20+
var dbFunction = QueryCompilationContext.Model.FindDbFunction(methodCallExpression.Method);
21+
22+
return dbFunction?.IsIQueryable == true
23+
? CreateNavigationExpansionExpression(methodCallExpression, QueryCompilationContext.Model.FindEntityType(dbFunction.MethodInfo.ReturnType.GetGenericArguments()[0]))
24+
: base.VisitMethodCall(methodCallExpression);
25+
}
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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+
QueryCompilationContext queryCompilationContext, IEvaluatableExpressionFilter evaluatableExpressionFilter)
10+
{
11+
return new RelationalNavigationExpandingExpressionVisitor(queryCompilationContext, evaluatableExpressionFilter);
12+
}
13+
}
14+
}

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
@@ -371,6 +371,9 @@ protected override Expression VisitProjection(ProjectionExpression projectionExp
371371
VisitInternal<SqlExpression>(projectionExpression.Expression).ResultExpression);
372372
}
373373

374+
protected override Expression VisitQueryableSqlFunctionExpression(QuerableSqlFunctionExpression queryableFunctionExpression)
375+
=> Check.NotNull(queryableFunctionExpression, nameof(queryableFunctionExpression));
376+
374377
protected override Expression VisitRowNumber(RowNumberExpression rowNumberExpression)
375378
{
376379
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(QuerableSqlFunctionExpression 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)