Skip to content

Commit

Permalink
Add support for querable functions
Browse files Browse the repository at this point in the history
  • Loading branch information
pmiddleton committed Jan 13, 2020
1 parent 6e2703f commit bb7edb6
Show file tree
Hide file tree
Showing 42 changed files with 1,827 additions and 132 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,37 @@ public static bool CanSetSchema(
return entityTypeBuilder.CanSetAnnotation(RelationalAnnotationNames.Schema, schema, fromDataAnnotation);
}

/// <summary>
/// Configures the entity as a result of a queryable function. Prevents a table from being created for this entity.
/// </summary>
/// <param name="entityTypeBuilder"> The builder for the entity type being configured. </param>
/// <returns> The same builder instance so that multiple calls can be chained. </returns>
public static EntityTypeBuilder ToQueryableFunctionResultType(
[NotNull] this EntityTypeBuilder entityTypeBuilder)
{
Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder));

entityTypeBuilder.Metadata.SetAnnotation(RelationalAnnotationNames.QueryableFunctionResultType, null);

return entityTypeBuilder;
}

/// <summary>
/// Configures the entity as a result of a queryable function. Prevents a table from being created for this entity.
/// </summary>
/// <param name="entityTypeBuilder"> The builder for the entity type being configured. </param>
/// <returns> The same builder instance so that multiple calls can be chained. </returns>
public static EntityTypeBuilder<TEntity> ToQueryableFunctionResultType<TEntity>(
[NotNull] this EntityTypeBuilder<TEntity> entityTypeBuilder)
where TEntity : class
{
Check.NotNull(entityTypeBuilder, nameof(entityTypeBuilder));

entityTypeBuilder.Metadata.SetAnnotation(RelationalAnnotationNames.QueryableFunctionResultType, null);

return entityTypeBuilder;
}

/// <summary>
/// Configures the view that the entity type maps to when targeting a relational database.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,9 @@ public static bool IsIgnoredByMigrations([NotNull] this IEntityType entityType)
return true;
}

if (entityType.FindAnnotation(RelationalAnnotationNames.QueryableFunctionResultType) != null)
return true;

var viewDefinition = entityType.FindAnnotation(RelationalAnnotationNames.ViewDefinition);
if (viewDefinition == null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices()
TryAdd<IQueryTranslationPreprocessorFactory, RelationalQueryTranslationPreprocessorFactory>();
TryAdd<IRelationalParameterBasedQueryTranslationPostprocessorFactory, RelationalParameterBasedQueryTranslationPostprocessorFactory>();
TryAdd<IRelationalQueryStringFactory, RelationalQueryStringFactory>();
TryAdd<INavigationExpandingExpressionVisitorFactory, RelationalNavigationExpandingExpressionVisitorFactory>();

ServiceCollectionMap.GetInfrastructure()
.AddDependencySingleton<RelationalSqlGenerationHelperDependencies>()
Expand Down
12 changes: 11 additions & 1 deletion src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,17 @@ protected virtual void ValidateDbFunctions(
RelationalStrings.DbFunctionNameEmpty(methodInfo.DisplayName()));
}

if (dbFunction.TypeMapping == null)
if (dbFunction.IsIQueryable)
{
if(model.FindEntityType(dbFunction.MethodInfo.ReturnType.GetGenericArguments()[0]) == null)
{
throw new InvalidOperationException(
RelationalStrings.DbFunctionInvalidReturnType(
methodInfo.DisplayName(),
methodInfo.ReturnType.ShortDisplayName()));
}
}
else if (dbFunction.TypeMapping == null)
{
throw new InvalidOperationException(
RelationalStrings.DbFunctionInvalidReturnType(
Expand Down
5 changes: 5 additions & 0 deletions src/EFCore.Relational/Metadata/IDbFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ public interface IDbFunction
/// </summary>
MethodInfo MethodInfo { get; }

/// <summary>
/// Whether this method returns IQueryable
/// </summary>
bool IsIQueryable { get; }

/// <summary>
/// The configured store type string
/// </summary>
Expand Down
44 changes: 43 additions & 1 deletion src/EFCore.Relational/Metadata/Internal/DbFunction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Diagnostics;
Expand Down Expand Up @@ -75,6 +76,19 @@ public DbFunction(
methodInfo.DisplayName(), methodInfo.ReturnType.ShortDisplayName()));
}

if (methodInfo.ReturnType.IsGenericType
&& methodInfo.ReturnType.GetGenericTypeDefinition() == typeof(IQueryable<>))
{
IsIQueryable = true;

//todo - if the generic argument is not usuable as an entitytype should we throw here? IE IQueryable<int>
//the built in entitytype will throw is the type is not a class
if (model.FindEntityType(methodInfo.ReturnType.GetGenericArguments()[0]) == null)
{
model.AddEntityType(methodInfo.ReturnType.GetGenericArguments()[0]).SetAnnotation(RelationalAnnotationNames.QueryableFunctionResultType, null);
}
}

MethodInfo = methodInfo;

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

/// <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 bool IsIQueryable { 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
Expand Down Expand Up @@ -345,7 +367,27 @@ private void UpdateTranslationConfigurationSource(ConfigurationSource configurat
public static DbFunction FindDbFunction(
[NotNull] IModel model,
[NotNull] MethodInfo methodInfo)
=> model[BuildAnnotationName(methodInfo)] as DbFunction;
{
var dbFunction = model[BuildAnnotationName(methodInfo)] as DbFunction;

if(dbFunction == null
&& methodInfo.GetParameters().Any(p => p.ParameterType.IsGenericType && p.ParameterType.GetGenericTypeDefinition() == typeof(Expression<>)))
{
var parameters = methodInfo.GetParameters().Select(p => p.ParameterType.IsGenericType
&& p.ParameterType.GetGenericTypeDefinition() == typeof(Expression<>)
&& p.ParameterType.GetGenericArguments()[0].GetGenericTypeDefinition() == typeof(Func<>)
? p.ParameterType.GetGenericArguments()[0].GetGenericArguments()[0]
: p.ParameterType).ToArray();

var nonExpressionMethod = methodInfo.DeclaringType.GetMethod(methodInfo.Name, parameters);

dbFunction = nonExpressionMethod != null
? model[BuildAnnotationName(nonExpressionMethod)] as DbFunction
: null;
}

return dbFunction;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down
5 changes: 5 additions & 0 deletions src/EFCore.Relational/Metadata/RelationalAnnotationNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,5 +98,10 @@ public static class RelationalAnnotationNames
/// The definition of a database view.
/// </summary>
public const string ViewDefinition = Prefix + "ViewDefinition";

/// <summary>
/// The definition of a Queryable Function Result Type.
/// </summary>
public const string QueryableFunctionResultType = Prefix + "QueryableFunctionResultType";
}
}
14 changes: 14 additions & 0 deletions src/EFCore.Relational/Properties/RelationalStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/EFCore.Relational/Properties/RelationalStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,12 @@
<data name="DbFunctionInvalidInstanceType" xml:space="preserve">
<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>
</data>
<data name="DbFunctionCantProjectIQueryable" xml:space="preserve">
<value>Queryable Db Functions used in projections cannot return IQueryable. IQueryable must be converted to a collection type such as List or Array.</value>
</data>
<data name="DbFunctionProjectedCollectionMustHavePK" xml:space="preserve">
<value>Return type of a queryable function '{functionName}' which is used in a projected collection must define a primary key.</value>
</data>
<data name="ConflictingAmbientTransaction" xml:space="preserve">
<value>An ambient transaction has been detected. The ambient transaction needs to be completed before beginning a transaction on this connection.</value>
</data>
Expand Down
1 change: 1 addition & 0 deletions src/EFCore.Relational/Query/ISqlExpressionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -141,5 +141,6 @@ SqlFunctionExpression Function(
SelectExpression Select([CanBeNull] SqlExpression projection);
SelectExpression Select([NotNull] IEntityType entityType);
SelectExpression Select([NotNull] IEntityType entityType, [NotNull] string sql, [NotNull] Expression sqlArguments);
SelectExpression Select([NotNull] IEntityType entityType, [NotNull] SqlFunctionExpression expression);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,13 @@ protected override Expression VisitSqlFunction(SqlFunctionExpression sqlFunction
return sqlFunctionExpression.Update(newInstance, newArguments);
}

protected override Expression VisitQueryableSqlFunctionExpression(QuerableSqlFunctionExpression queryableFunctionExpression)
{
Check.NotNull(queryableFunctionExpression, nameof(queryableFunctionExpression));

return queryableFunctionExpression;
}

protected override Expression VisitSqlParameter(SqlParameterExpression sqlParameterExpression)
{
Check.NotNull(sqlParameterExpression, nameof(sqlParameterExpression));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;

namespace Microsoft.EntityFrameworkCore.Query.Internal
{
public class RelationalNavigationExpandingExpressionVisitor : NavigationExpandingExpressionVisitor
{
public RelationalNavigationExpandingExpressionVisitor(
[NotNull] QueryCompilationContext queryCompilationContext,
[NotNull] IEvaluatableExpressionFilter evaluatableExpressionFilter)
: base(queryCompilationContext, evaluatableExpressionFilter)
{
}

protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
{
var dbFunction = QueryCompilationContext.Model.FindDbFunction(methodCallExpression.Method);

return dbFunction?.IsIQueryable == true
? CreateNavigationExpansionExpression(methodCallExpression, QueryCompilationContext.Model.FindEntityType(dbFunction.MethodInfo.ReturnType.GetGenericArguments()[0]))
: base.VisitMethodCall(methodCallExpression);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 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.

namespace Microsoft.EntityFrameworkCore.Query.Internal
{
public class RelationalNavigationExpandingExpressionVisitorFactory : INavigationExpandingExpressionVisitorFactory
{
public virtual NavigationExpandingExpressionVisitor Create(
QueryCompilationContext queryCompilationContext, IEvaluatableExpressionFilter evaluatableExpressionFilter)
{
return new RelationalNavigationExpandingExpressionVisitor(queryCompilationContext, evaluatableExpressionFilter);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Utilities;
Expand All @@ -22,6 +23,7 @@ public class RelationalProjectionBindingExpressionVisitor : ExpressionVisitor

private SelectExpression _selectExpression;
private bool _clientEval;
private readonly IModel _model;

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

public RelationalProjectionBindingExpressionVisitor(
[NotNull] RelationalQueryableMethodTranslatingExpressionVisitor queryableMethodTranslatingExpressionVisitor,
[NotNull] RelationalSqlTranslatingExpressionVisitor sqlTranslatingExpressionVisitor)
[NotNull] RelationalSqlTranslatingExpressionVisitor sqlTranslatingExpressionVisitor,
[NotNull] IModel model)
{
_queryableMethodTranslatingExpressionVisitor = queryableMethodTranslatingExpressionVisitor;
_sqlTranslator = sqlTranslatingExpressionVisitor;
_model = model;
}

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

if (newExpression.Arguments.Any(arg => arg is MethodCallExpression methodCallExp && _model.FindDbFunction(methodCallExp.Method)?.IsIQueryable == true))
{
throw new InvalidOperationException(RelationalStrings.DbFunctionCantProjectIQueryable());
}

var newArguments = new Expression[newExpression.Arguments.Count];
for (var i = 0; i < newArguments.Length; i++)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,23 @@ public class RelationalQueryTranslationPreprocessorFactory : IQueryTranslationPr
{
private readonly QueryTranslationPreprocessorDependencies _dependencies;
private readonly RelationalQueryTranslationPreprocessorDependencies _relationalDependencies;
private readonly INavigationExpandingExpressionVisitorFactory _navigationExpandingExpressionVisitorFactory;

public RelationalQueryTranslationPreprocessorFactory(
[NotNull] QueryTranslationPreprocessorDependencies dependencies,
[NotNull] RelationalQueryTranslationPreprocessorDependencies relationalDependencies)
[NotNull] RelationalQueryTranslationPreprocessorDependencies relationalDependencies,
[NotNull] INavigationExpandingExpressionVisitorFactory navigationExpandingExpressionVisitorFactory)
{
_dependencies = dependencies;
_relationalDependencies = relationalDependencies;
_navigationExpandingExpressionVisitorFactory = navigationExpandingExpressionVisitorFactory;
}

public virtual QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext)
{
Check.NotNull(queryCompilationContext, nameof(queryCompilationContext));

return new RelationalQueryTranslationPreprocessor(_dependencies, _relationalDependencies, queryCompilationContext);
return new RelationalQueryTranslationPreprocessor(_dependencies, _relationalDependencies, queryCompilationContext, _navigationExpandingExpressionVisitorFactory);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,9 @@ protected override Expression VisitProjection(ProjectionExpression projectionExp
VisitInternal<SqlExpression>(projectionExpression.Expression).ResultExpression);
}

protected override Expression VisitQueryableSqlFunctionExpression(QuerableSqlFunctionExpression queryableFunctionExpression)
=> Check.NotNull(queryableFunctionExpression, nameof(queryableFunctionExpression));

protected override Expression VisitRowNumber(RowNumberExpression rowNumberExpression)
{
Check.NotNull(rowNumberExpression, nameof(rowNumberExpression));
Expand Down
11 changes: 11 additions & 0 deletions src/EFCore.Relational/Query/QuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,17 @@ protected override Expression VisitSqlFunction(SqlFunctionExpression sqlFunction
return sqlFunctionExpression;
}

protected override Expression VisitQueryableSqlFunctionExpression(QuerableSqlFunctionExpression queryableFunctionExpression)
{
Visit(queryableFunctionExpression.SqlFunctionExpression);

_relationalCommandBuilder
.Append(AliasSeparator)
.Append(_sqlGenerationHelper.DelimitIdentifier(queryableFunctionExpression.Alias));

return queryableFunctionExpression;
}

protected override Expression VisitColumn(ColumnExpression columnExpression)
{
Check.NotNull(columnExpression, nameof(columnExpression));
Expand Down
Loading

0 comments on commit bb7edb6

Please sign in to comment.