diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs index 549982fee58..b06624a5d32 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs @@ -75,7 +75,7 @@ protected virtual void ValidateDbFunctions( var methodInfo = dbFunction.MethodInfo; if (dbFunction.TypeMapping == null - && dbFunction.QueryableEntityType == null) + && dbFunction.ReturnEntityType == null) { throw new InvalidOperationException( RelationalStrings.DbFunctionInvalidReturnType( diff --git a/src/EFCore.Relational/Metadata/Conventions/DbFunctionTypeMappingConvention.cs b/src/EFCore.Relational/Metadata/Conventions/DbFunctionTypeMappingConvention.cs index 1016ec643b6..fe478edb9e7 100644 --- a/src/EFCore.Relational/Metadata/Conventions/DbFunctionTypeMappingConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/DbFunctionTypeMappingConvention.cs @@ -41,6 +41,7 @@ public virtual void ProcessModelFinalizing( foreach (var dbFunction in modelBuilder.Metadata.GetDbFunctions()) { + // TODO: This check needs to be updated to skip over enumerable parameter of aggregate. foreach (var parameter in dbFunction.Parameters) { parameter.Builder.HasTypeMapping(!string.IsNullOrEmpty(parameter.StoreType) @@ -48,14 +49,12 @@ public virtual void ProcessModelFinalizing( : _relationalTypeMappingSource.FindMapping(parameter.ClrType)); } - if (dbFunction.IsQueryable) + if (dbFunction.IsScalar) { - continue; + dbFunction.Builder.HasTypeMapping(!string.IsNullOrEmpty(dbFunction.StoreType) + ? _relationalTypeMappingSource.FindMapping(dbFunction.StoreType) + : _relationalTypeMappingSource.FindMapping(dbFunction.ReturnType)); } - - dbFunction.Builder.HasTypeMapping(!string.IsNullOrEmpty(dbFunction.StoreType) - ? _relationalTypeMappingSource.FindMapping(dbFunction.StoreType) - : _relationalTypeMappingSource.FindMapping(dbFunction.ReturnType)); } } } diff --git a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs index 6e785d7a19b..d1ed7791b15 100644 --- a/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs +++ b/src/EFCore.Relational/Metadata/Conventions/Infrastructure/RelationalConventionSetBuilder.cs @@ -83,7 +83,7 @@ public override ConventionSet CreateConventionSet() ReplaceConvention(conventionSet.ForeignKeyRemovedConventions, valueGenerationConvention); conventionSet.PropertyFieldChangedConventions.Add(relationalColumnAttributeConvention); - conventionSet.PropertyFieldChangedConventions.Add(relationalCommentAttributeConvention); + conventionSet.PropertyFieldChangedConventions.Add(relationalCommentAttributeConvention); var storeGenerationConvention = new StoreGenerationConvention(Dependencies, RelationalDependencies); conventionSet.PropertyAnnotationChangedConventions.Add(storeGenerationConvention); @@ -98,10 +98,10 @@ public override ConventionSet CreateConventionSet() conventionSet.ModelFinalizingConventions, new TableSharingConcurrencyTokenConvention(Dependencies, RelationalDependencies), typeof(TypeMappingConvention)); - // ModelCleanupConvention would remove the entity types added by QueryableDbFunctionConvention #15898 + // ModelCleanupConvention would remove the entity types added by TableValuedDbFunctionConvention #15898 ConventionSet.AddAfter( conventionSet.ModelFinalizingConventions, - new QueryableDbFunctionConvention(Dependencies, RelationalDependencies), + new TableValuedDbFunctionConvention(Dependencies, RelationalDependencies), typeof(ModelCleanupConvention)); conventionSet.ModelFinalizingConventions.Add(dbFunctionAttributeConvention); conventionSet.ModelFinalizingConventions.Add(tableNameFromDbSetConvention); diff --git a/src/EFCore.Relational/Metadata/Conventions/QueryableDbFunctionConvention.cs b/src/EFCore.Relational/Metadata/Conventions/QueryableDbFunctionConvention.cs index 1dbf941e386..56a0217743a 100644 --- a/src/EFCore.Relational/Metadata/Conventions/QueryableDbFunctionConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/QueryableDbFunctionConvention.cs @@ -14,14 +14,14 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Conventions /// /// A convention that configures the entity type to which a queryable function is mapped. /// - public class QueryableDbFunctionConvention : IModelFinalizingConvention + public class TableValuedDbFunctionConvention : IModelFinalizingConvention { /// - /// Creates a new instance of . + /// Creates a new instance of . /// /// Parameter object containing dependencies for this convention. /// Parameter object containing relational dependencies for this convention. - public QueryableDbFunctionConvention( + public TableValuedDbFunctionConvention( [NotNull] ProviderConventionSetBuilderDependencies dependencies, [NotNull] RelationalConventionSetBuilderDependencies relationalDependencies) { @@ -51,7 +51,7 @@ private void ProcessDbFunctionAdded( [NotNull] IConventionDbFunctionBuilder dbFunctionBuilder, [NotNull] IConventionContext context) { var function = dbFunctionBuilder.Metadata; - if (!function.IsQueryable) + if (function.IsScalar) { return; } diff --git a/src/EFCore.Relational/Metadata/IDbFunction.cs b/src/EFCore.Relational/Metadata/IDbFunction.cs index 0ed24146fa8..c8d754cb3d8 100644 --- a/src/EFCore.Relational/Metadata/IDbFunction.cs +++ b/src/EFCore.Relational/Metadata/IDbFunction.cs @@ -41,19 +41,24 @@ public interface IDbFunction : IAnnotatable MethodInfo MethodInfo { get; } /// - /// Gets the value indicating whether this method returns IQueryable + /// Gets the value indicating whether this function returns scalar value. /// - bool IsQueryable { get; } + bool IsScalar { get; } /// - /// Gets the entity type returned by this queryable function + /// Gets the value indicating whether this function is an aggregate function. /// - IEntityType QueryableEntityType => IsQueryable - ? Model.FindEntityType(ReturnType.GetGenericArguments()[0]) - : null; + bool IsAggregate { get; } /// - /// Gets the configured store type string + /// Gets the entity type returned by this table valued function. + /// + IEntityType ReturnEntityType => IsScalar + ? null + : Model.FindEntityType(ReturnType.GetGenericArguments()[0]); + + /// + /// Gets the configured store type string. /// string StoreType { get; } @@ -63,12 +68,12 @@ public interface IDbFunction : IAnnotatable Type ReturnType { get; } /// - /// Gets the type mapping for the function's return type + /// Gets the type mapping for the function's return type. /// RelationalTypeMapping TypeMapping { get; } /// - /// Gets the parameters for this function + /// Gets the parameters for this function. /// IReadOnlyList Parameters { get; } diff --git a/src/EFCore.Relational/Metadata/Internal/DbFunction.cs b/src/EFCore.Relational/Metadata/Internal/DbFunction.cs index 63e385722d1..0c0e0444da5 100644 --- a/src/EFCore.Relational/Metadata/Internal/DbFunction.cs +++ b/src/EFCore.Relational/Metadata/Internal/DbFunction.cs @@ -98,11 +98,9 @@ public DbFunction( name, returnType.ShortDisplayName())); } - if (returnType.IsGenericType - && returnType.GetGenericTypeDefinition() == typeof(IQueryable<>)) - { - IsQueryable = true; - } + IsScalar = !returnType.IsGenericType + || returnType.GetGenericTypeDefinition() != typeof(IQueryable<>); + IsAggregate = false; ModelName = name; ReturnType = returnType; @@ -257,7 +255,10 @@ public static DbFunction RemoveDbFunction( public virtual Type ReturnType { get; } /// - public virtual bool IsQueryable { get; } + public virtual bool IsScalar { get; } + + /// + public virtual bool IsAggregate { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -265,11 +266,11 @@ public static DbFunction RemoveDbFunction( /// 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. /// - IEntityType QueryableEntityType + public virtual IEntityType ReturnEntityType { get { - if (!IsQueryable) + if (IsScalar) { return null; } @@ -475,9 +476,15 @@ public virtual Func, SqlExpression> SetTransl ConfigurationSource configurationSource) { if (translation != null - && IsQueryable) + && !IsScalar) + { + throw new InvalidOperationException(RelationalStrings.DbFunctionTableValuedCustomTranslation(MethodInfo.DisplayName())); + } + + if (translation != null + && !IsAggregate) { - throw new InvalidOperationException(RelationalStrings.DbFunctionQueryableCustomTranslation(MethodInfo.DisplayName())); + throw new InvalidOperationException(RelationalStrings.DbFunctionAggregateCustomTranslation(MethodInfo.DisplayName())); } _translation = translation; @@ -527,7 +534,7 @@ IConventionModel IConventionDbFunction.Model } /// - IEntityType IDbFunction.QueryableEntityType => QueryableEntityType; + IEntityType IDbFunction.ReturnEntityType => ReturnEntityType; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionBuilder.cs b/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionBuilder.cs index a04bf81ff29..d9f0fb84df8 100644 --- a/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionBuilder.cs +++ b/src/EFCore.Relational/Metadata/Internal/InternalDbFunctionBuilder.cs @@ -169,7 +169,7 @@ public virtual IConventionDbFunctionBuilder HasTranslation( /// public virtual bool CanSetTranslation( [CanBeNull] Func, SqlExpression> translation, ConfigurationSource configurationSource) - => (!Metadata.IsQueryable || configurationSource == ConfigurationSource.Explicit) + => ((Metadata.IsScalar && !Metadata.IsAggregate) || configurationSource == ConfigurationSource.Explicit) && (configurationSource.Overrides(Metadata.GetTranslationConfigurationSource()) || Metadata.Translation == translation); diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 63cd2e8728e..d9e0d354759 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -300,14 +300,6 @@ public static string DuplicateColumnNameDefaultSqlMismatch([CanBeNull] object en GetString("DuplicateColumnNameDefaultSqlMismatch", nameof(entityType1), nameof(property1), nameof(entityType2), nameof(property2), nameof(columnName), nameof(table), nameof(value1), nameof(value2)), entityType1, property1, entityType2, property2, columnName, table, value1, value2); - /// - /// '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}' but are configured to use different collations ('{collation1}' and '{collation2}'). - /// - public static string DuplicateColumnNameCollationMismatch([CanBeNull] object entityType1, [CanBeNull] object property1, [CanBeNull] object entityType2, [CanBeNull] object property2, [CanBeNull] object columnName, [CanBeNull] object table, [CanBeNull] object collation1, [CanBeNull] object collation2) - => string.Format( - GetString("DuplicateColumnNameCollationMismatch", nameof(entityType1), nameof(property1), nameof(entityType2), nameof(property2), nameof(columnName), nameof(table), nameof(collation1), nameof(collation2)), - entityType1, property1, entityType2, property2, columnName, table, collation1, collation2); - /// /// '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}' but are configured to use different comments ('{comment1}' and '{comment2}'). /// @@ -316,6 +308,14 @@ public static string DuplicateColumnNameCommentMismatch([CanBeNull] object entit GetString("DuplicateColumnNameCommentMismatch", nameof(entityType1), nameof(property1), nameof(entityType2), nameof(property2), nameof(columnName), nameof(table), nameof(comment1), nameof(comment2)), entityType1, property1, entityType2, property2, columnName, table, comment1, comment2); + /// + /// '{entityType1}.{property1}' and '{entityType2}.{property2}' are both mapped to column '{columnName}' in '{table}' but are configured to use different collations ('{collation1}' and '{collation2}'). + /// + public static string DuplicateColumnNameCollationMismatch([CanBeNull] object entityType1, [CanBeNull] object property1, [CanBeNull] object entityType2, [CanBeNull] object property2, [CanBeNull] object columnName, [CanBeNull] object table, [CanBeNull] object collation1, [CanBeNull] object collation2) + => string.Format( + GetString("DuplicateColumnNameCollationMismatch", nameof(entityType1), nameof(property1), nameof(entityType2), nameof(property2), nameof(columnName), nameof(table), nameof(collation1), nameof(collation2)), + entityType1, property1, entityType2, property2, columnName, table, collation1, collation2); + /// /// {conflictingConfiguration} cannot be set for '{property}' at the same time as {existingConfiguration}. Remove one of these values. /// @@ -633,11 +633,11 @@ public static string DatabaseModelMissing => GetString("DatabaseModelMissing"); /// - /// Cannot set custom translation on the DbFunction '{function}' since it returns IQueryable type. + /// Cannot set custom translation on the DbFunction '{function}' since it is a table valued function. /// - public static string DbFunctionQueryableCustomTranslation([CanBeNull] object function) + public static string DbFunctionTableValuedCustomTranslation([CanBeNull] object function) => string.Format( - GetString("DbFunctionQueryableCustomTranslation", nameof(function)), + GetString("DbFunctionTableValuedCustomTranslation", nameof(function)), function); /// @@ -800,6 +800,14 @@ public static string UnsupportedDataOperationStoreType([CanBeNull] object type, GetString("UnsupportedDataOperationStoreType", nameof(type), nameof(column)), type, column); + /// + /// Cannot set custom translation on the DbFunction '{function}' since it is an aggregate function. + /// + public static string DbFunctionAggregateCustomTranslation([CanBeNull] object function) + => string.Format( + GetString("DbFunctionAggregateCustomTranslation", nameof(function)), + function); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index b0fd389ec80..188c82a9fa3 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -518,8 +518,8 @@ The database model hasn't been initialized. The model needs to be finalized before the database model can be accessed. - - Cannot set custom translation on the DbFunction '{function}' since it returns IQueryable type. + + Cannot set custom translation on the DbFunction '{function}' since it is a table valued function. The DbFunction '{function}' has an invalid return type '{type}'. Only functions that return IQueryable of entity type are supported. @@ -581,4 +581,7 @@ The store type '{type}' used for the column '{column}' in a migration data operation is not supported by the current provider. + + Cannot set custom translation on the DbFunction '{function}' since it is an aggregate function. + \ No newline at end of file diff --git a/src/EFCore.Relational/Query/Internal/QueryableFunctionQueryRootExpression.cs b/src/EFCore.Relational/Query/Internal/TableValuedFunctionQueryRootExpression.cs similarity index 86% rename from src/EFCore.Relational/Query/Internal/QueryableFunctionQueryRootExpression.cs rename to src/EFCore.Relational/Query/Internal/TableValuedFunctionQueryRootExpression.cs index 54afd6c5927..133b63f7832 100644 --- a/src/EFCore.Relational/Query/Internal/QueryableFunctionQueryRootExpression.cs +++ b/src/EFCore.Relational/Query/Internal/TableValuedFunctionQueryRootExpression.cs @@ -12,10 +12,10 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal { - public class QueryableFunctionQueryRootExpression : QueryRootExpression + public class TableValuedFunctionQueryRootExpression : QueryRootExpression { //Since this is always generated while compiling there is no query provider associated - public QueryableFunctionQueryRootExpression( + public TableValuedFunctionQueryRootExpression( [NotNull] IEntityType entityType, [NotNull] IDbFunction function, [NotNull] IReadOnlyCollection arguments) : base(entityType) { @@ -41,7 +41,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) } return changed - ? new QueryableFunctionQueryRootExpression(EntityType, Function, arguments) + ? new TableValuedFunctionQueryRootExpression(EntityType, Function, arguments) : this; } @@ -56,10 +56,10 @@ public override void Print(ExpressionPrinter expressionPrinter) public override bool Equals(object obj) => obj != null && (ReferenceEquals(this, obj) - || obj is QueryableFunctionQueryRootExpression queryRootExpression + || obj is TableValuedFunctionQueryRootExpression queryRootExpression && Equals(queryRootExpression)); - private bool Equals(QueryableFunctionQueryRootExpression queryRootExpression) + private bool Equals(TableValuedFunctionQueryRootExpression queryRootExpression) => base.Equals(queryRootExpression) && Equals(Function, queryRootExpression.Function) && Arguments.SequenceEqual(queryRootExpression.Arguments, ExpressionEqualityComparer.Instance); diff --git a/src/EFCore.Relational/Query/Internal/QueryableFunctionToQueryRootConvertingExpressionVisitor.cs b/src/EFCore.Relational/Query/Internal/TableValuedFunctionToQueryRootConvertingExpressionVisitor.cs similarity index 63% rename from src/EFCore.Relational/Query/Internal/QueryableFunctionToQueryRootConvertingExpressionVisitor.cs rename to src/EFCore.Relational/Query/Internal/TableValuedFunctionToQueryRootConvertingExpressionVisitor.cs index e1ba427b890..a16055a8db5 100644 --- a/src/EFCore.Relational/Query/Internal/QueryableFunctionToQueryRootConvertingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/Internal/TableValuedFunctionToQueryRootConvertingExpressionVisitor.cs @@ -9,11 +9,11 @@ namespace Microsoft.EntityFrameworkCore.Query.Internal { - public class QueryableFunctionToQueryRootConvertingExpressionVisitor : ExpressionVisitor + public class TableValuedFunctionToQueryRootConvertingExpressionVisitor : ExpressionVisitor { private readonly IModel _model; - public QueryableFunctionToQueryRootConvertingExpressionVisitor([NotNull] IModel model) + public TableValuedFunctionToQueryRootConvertingExpressionVisitor([NotNull] IModel model) { Check.NotNull(model, nameof(model)); @@ -24,13 +24,13 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp { var function = _model.FindDbFunction(methodCallExpression.Method); - return function?.IsQueryable == true - ? CreateQueryableFunctionQueryRootExpression(function, methodCallExpression.Arguments) + return function?.IsScalar == false + ? CreateTableValuedFunctionQueryRootExpression(function, methodCallExpression.Arguments) : base.VisitMethodCall(methodCallExpression); } - private Expression CreateQueryableFunctionQueryRootExpression( + private Expression CreateTableValuedFunctionQueryRootExpression( IDbFunction function, IReadOnlyCollection arguments) - => new QueryableFunctionQueryRootExpression(function.QueryableEntityType, function, arguments); + => new TableValuedFunctionQueryRootExpression(function.ReturnEntityType, function, arguments); } } diff --git a/src/EFCore.Relational/Query/NullabilityBasedSqlProcessingExpressionVisitor.cs b/src/EFCore.Relational/Query/NullabilityBasedSqlProcessingExpressionVisitor.cs index 907a431c79b..ce530b9a7e7 100644 --- a/src/EFCore.Relational/Query/NullabilityBasedSqlProcessingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/NullabilityBasedSqlProcessingExpressionVisitor.cs @@ -454,12 +454,12 @@ protected override Expression VisitProjection(ProjectionExpression projectionExp VisitInternal(projectionExpression.Expression).ResultExpression); } - protected override Expression VisitQueryableFunction(QueryableFunctionExpression queryableFunctionExpression) + protected override Expression VisitTableValuedFunction(TableValuedFunctionExpression tableValuedFunctionExpression) { - Check.NotNull(queryableFunctionExpression, nameof(queryableFunctionExpression)); + Check.NotNull(tableValuedFunctionExpression, nameof(tableValuedFunctionExpression)); // See issue#20180 - return queryableFunctionExpression; + return tableValuedFunctionExpression; } protected override Expression VisitRowNumber(RowNumberExpression rowNumberExpression) diff --git a/src/EFCore.Relational/Query/QuerySqlGenerator.cs b/src/EFCore.Relational/Query/QuerySqlGenerator.cs index 26d83ec2fe8..6a5ba9752e4 100644 --- a/src/EFCore.Relational/Query/QuerySqlGenerator.cs +++ b/src/EFCore.Relational/Query/QuerySqlGenerator.cs @@ -270,29 +270,29 @@ protected override Expression VisitSqlFunction(SqlFunctionExpression sqlFunction return sqlFunctionExpression; } - protected override Expression VisitQueryableFunction(QueryableFunctionExpression queryableFunctionExpression) + protected override Expression VisitTableValuedFunction(TableValuedFunctionExpression tableValuedFunctionExpression) { - Check.NotNull(queryableFunctionExpression, nameof(queryableFunctionExpression)); + Check.NotNull(tableValuedFunctionExpression, nameof(tableValuedFunctionExpression)); - if (!string.IsNullOrEmpty(queryableFunctionExpression.Schema)) + if (!string.IsNullOrEmpty(tableValuedFunctionExpression.Schema)) { _relationalCommandBuilder - .Append(_sqlGenerationHelper.DelimitIdentifier(queryableFunctionExpression.Schema)) + .Append(_sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.Schema)) .Append("."); } _relationalCommandBuilder - .Append(_sqlGenerationHelper.DelimitIdentifier(queryableFunctionExpression.Name)) + .Append(_sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.Name)) .Append("("); - GenerateList(queryableFunctionExpression.Arguments, e => Visit(e)); + GenerateList(tableValuedFunctionExpression.Arguments, e => Visit(e)); _relationalCommandBuilder .Append(")") .Append(AliasSeparator) - .Append(_sqlGenerationHelper.DelimitIdentifier(queryableFunctionExpression.Alias)); + .Append(_sqlGenerationHelper.DelimitIdentifier(tableValuedFunctionExpression.Alias)); - return queryableFunctionExpression; + return tableValuedFunctionExpression; } protected override Expression VisitColumn(ColumnExpression columnExpression) diff --git a/src/EFCore.Relational/Query/RelationalEvaluatableExpressionFilter.cs b/src/EFCore.Relational/Query/RelationalEvaluatableExpressionFilter.cs index cd2f984f41f..0d488a5a28e 100644 --- a/src/EFCore.Relational/Query/RelationalEvaluatableExpressionFilter.cs +++ b/src/EFCore.Relational/Query/RelationalEvaluatableExpressionFilter.cs @@ -63,7 +63,7 @@ public override bool IsEvaluatableExpression(Expression expression, IModel model { // Never evaluate DbFunction // If it is inside lambda then we will have whole method call - // If it is outside of lambda then it will be evaluated for queryable function already. + // If it is outside of lambda then it will be evaluated for table valued function already. return false; } diff --git a/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs b/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs index a026b076218..9248f1b3bd0 100644 --- a/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryTranslationPreprocessor.cs @@ -26,7 +26,7 @@ public RelationalQueryTranslationPreprocessor( public override Expression NormalizeQueryableMethod(Expression expression) { expression = base.NormalizeQueryableMethod(expression); - expression = new QueryableFunctionToQueryRootConvertingExpressionVisitor(QueryCompilationContext.Model).Visit(expression); + expression = new TableValuedFunctionToQueryRootConvertingExpressionVisitor(QueryCompilationContext.Model).Visit(expression); return expression; } diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index 62b15aa8f4c..4cd2fc2ff52 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -75,10 +75,10 @@ protected override Expression VisitExtension(Expression extensionExpression) fromSqlQueryRootExpression.Sql, fromSqlQueryRootExpression.Argument)); - case QueryableFunctionQueryRootExpression queryableFunctionQueryRootExpression: - var function = queryableFunctionQueryRootExpression.Function; + case TableValuedFunctionQueryRootExpression tableValuedFunctionQueryRootExpression: + var function = tableValuedFunctionQueryRootExpression.Function; var arguments = new List(); - foreach (var arg in queryableFunctionQueryRootExpression.Arguments) + foreach (var arg in tableValuedFunctionQueryRootExpression.Arguments) { var sqlArgument = _sqlTranslator.Translate(arg); if (sqlArgument == null) @@ -86,7 +86,7 @@ protected override Expression VisitExtension(Expression extensionExpression) var methodCall = Expression.Call( Expression.Constant(null, function.MethodInfo.DeclaringType), function.MethodInfo, - queryableFunctionQueryRootExpression.Arguments); + tableValuedFunctionQueryRootExpression.Arguments); throw new InvalidOperationException(CoreStrings.TranslationFailed(methodCall.Print())); } @@ -94,12 +94,11 @@ protected override Expression VisitExtension(Expression extensionExpression) arguments.Add(sqlArgument); } - // TODO: Allow translation to construct the table - var entityType = queryableFunctionQueryRootExpression.EntityType; + var entityType = tableValuedFunctionQueryRootExpression.EntityType; var alias = (entityType.GetViewOrTableMappings().SingleOrDefault()?.Table.Name ?? entityType.ShortName()).Substring(0, 1).ToLower(); - var translation = new QueryableFunctionExpression(function.Schema, function.Name, arguments, alias); + var translation = new TableValuedFunctionExpression(function.Schema, function.Name, arguments, alias); var queryExpression = _sqlExpressionFactory.Select(entityType, translation); return CreateShapedQueryExpression(entityType, queryExpression); diff --git a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs index 1eba46de3ed..7b8351831cb 100644 --- a/src/EFCore.Relational/Query/SqlExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/SqlExpressionVisitor.cs @@ -68,8 +68,8 @@ protected override Expression VisitExtension(Expression extensionExpression) case ProjectionExpression projectionExpression: return VisitProjection(projectionExpression); - case QueryableFunctionExpression queryableFunctionExpression: - return VisitQueryableFunction(queryableFunctionExpression); + case TableValuedFunctionExpression tableValuedFunctionExpression: + return VisitTableValuedFunction(tableValuedFunctionExpression); case RowNumberExpression rowNumberExpression: return VisitRowNumber(rowNumberExpression); @@ -124,7 +124,7 @@ protected override Expression VisitExtension(Expression extensionExpression) protected abstract Expression VisitOrdering([NotNull] OrderingExpression orderingExpression); protected abstract Expression VisitOuterApply([NotNull] OuterApplyExpression outerApplyExpression); protected abstract Expression VisitProjection([NotNull] ProjectionExpression projectionExpression); - protected abstract Expression VisitQueryableFunction([NotNull] QueryableFunctionExpression queryableFunctionExpression); + protected abstract Expression VisitTableValuedFunction([NotNull] TableValuedFunctionExpression queryableFunctionExpression); protected abstract Expression VisitRowNumber([NotNull] RowNumberExpression rowNumberExpression); protected abstract Expression VisitScalarSubquery([NotNull] ScalarSubqueryExpression scalarSubqueryExpression); protected abstract Expression VisitSelect([NotNull] SelectExpression selectExpression); diff --git a/src/EFCore.Relational/Query/SqlExpressions/QueryableFunctionExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs similarity index 84% rename from src/EFCore.Relational/Query/SqlExpressions/QueryableFunctionExpression.cs rename to src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs index d49a9b511c7..d6e770112dd 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/QueryableFunctionExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/TableValuedFunctionExpression.cs @@ -13,9 +13,9 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions /// /// Represents a SQL Table Valued Fuction in the sql generation tree. /// - public class QueryableFunctionExpression : TableExpressionBase + public class TableValuedFunctionExpression : TableExpressionBase { - public QueryableFunctionExpression( + public TableValuedFunctionExpression( [CanBeNull] string schema, [NotNull] string name, [NotNull] IReadOnlyList arguments, [NotNull] string alias) : base(alias) { @@ -45,16 +45,16 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) } return changed - ? new QueryableFunctionExpression(Schema, Name, arguments, Alias) + ? new TableValuedFunctionExpression(Schema, Name, arguments, Alias) : this; } - public virtual QueryableFunctionExpression Update([NotNull] IReadOnlyList arguments) + public virtual TableValuedFunctionExpression Update([NotNull] IReadOnlyList arguments) { Check.NotNull(arguments, nameof(arguments)); return !arguments.SequenceEqual(Arguments) - ? new QueryableFunctionExpression(Schema, Name, arguments, Alias) + ? new TableValuedFunctionExpression(Schema, Name, arguments, Alias) : this; } @@ -75,10 +75,10 @@ public override void Print(ExpressionPrinter expressionPrinter) public override bool Equals(object obj) => obj != null && (ReferenceEquals(this, obj) - || obj is QueryableFunctionExpression queryableExpression + || obj is TableValuedFunctionExpression queryableExpression && Equals(queryableExpression)); - private bool Equals(QueryableFunctionExpression queryableExpression) + private bool Equals(TableValuedFunctionExpression queryableExpression) => base.Equals(queryableExpression) && string.Equals(Name, queryableExpression.Name) && string.Equals(Schema, queryableExpression.Schema) diff --git a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs index ade8952c423..3c62cc0c7b3 100644 --- a/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs +++ b/src/EFCore.SqlServer/Query/Internal/SearchConditionConvertingExpressionVisitor.cs @@ -449,12 +449,12 @@ protected override Expression VisitSqlFunction(SqlFunctionExpression sqlFunction /// 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. /// - protected override Expression VisitQueryableFunction(QueryableFunctionExpression queryableFunctionExpression) + protected override Expression VisitTableValuedFunction(TableValuedFunctionExpression tableValuedFunctionExpression) { - Check.NotNull(queryableFunctionExpression, nameof(queryableFunctionExpression)); + Check.NotNull(tableValuedFunctionExpression, nameof(tableValuedFunctionExpression)); // TODO: See issue#20180 - return queryableFunctionExpression; + return tableValuedFunctionExpression; } /// diff --git a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs index c12d794fef9..9f4474539af 100644 --- a/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs +++ b/src/EFCore.SqlServer/Query/Internal/SqlServerQuerySqlGenerator.cs @@ -112,20 +112,20 @@ protected override Expression VisitSqlFunction(SqlFunctionExpression sqlFunction /// 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. /// - protected override Expression VisitQueryableFunction(QueryableFunctionExpression queryableFunctionExpression) + protected override Expression VisitTableValuedFunction(TableValuedFunctionExpression tableValuedFunctionExpression) { - Check.NotNull(queryableFunctionExpression, nameof(queryableFunctionExpression)); + Check.NotNull(tableValuedFunctionExpression, nameof(tableValuedFunctionExpression)); - if (string.IsNullOrEmpty(queryableFunctionExpression.Schema)) + if (string.IsNullOrEmpty(tableValuedFunctionExpression.Schema)) { - queryableFunctionExpression = new QueryableFunctionExpression( + tableValuedFunctionExpression = new TableValuedFunctionExpression( schema: "dbo", - queryableFunctionExpression.Name, - queryableFunctionExpression.Arguments, - queryableFunctionExpression.Alias); + tableValuedFunctionExpression.Name, + tableValuedFunctionExpression.Arguments, + tableValuedFunctionExpression.Alias); } - return base.VisitQueryableFunction(queryableFunctionExpression); + return base.VisitTableValuedFunction(tableValuedFunctionExpression); } } } diff --git a/src/EFCore/Query/EvaluatableExpressionFilter.cs b/src/EFCore/Query/EvaluatableExpressionFilter.cs index 8e18416824d..0ac8ba8f5a8 100644 --- a/src/EFCore/Query/EvaluatableExpressionFilter.cs +++ b/src/EFCore/Query/EvaluatableExpressionFilter.cs @@ -21,7 +21,7 @@ namespace Microsoft.EntityFrameworkCore.Query /// This service cannot depend on services registered as . /// /// - public class EvaluatableExpressionFilter : EvaluatableExpressionFilterBase + public class EvaluatableExpressionFilter : IEvaluatableExpressionFilter { // This methods are non-deterministic and result varies based on time of running the query. // Hence we don't evaluate them. See issue#2069 @@ -65,17 +65,24 @@ private static readonly MethodInfo _randomNextTwoArgs /// The dependencies to use. public EvaluatableExpressionFilter( [NotNull] EvaluatableExpressionFilterDependencies dependencies) - : base(dependencies) { + Check.NotNull(dependencies, nameof(dependencies)); + + Dependencies = dependencies; } + /// + /// Parameter object containing service dependencies. + /// + protected virtual EvaluatableExpressionFilterDependencies Dependencies { get; } + /// /// Checks whether the given expression can be evaluated. /// /// The expression. /// The model. /// True if the expression can be evaluated; false otherwise. - public override bool IsEvaluatableExpression(Expression expression, IModel model) + public virtual bool IsEvaluatableExpression(Expression expression, IModel model) { Check.NotNull(expression, nameof(expression)); Check.NotNull(model, nameof(model)); @@ -109,7 +116,15 @@ public override bool IsEvaluatableExpression(Expression expression, IModel model break; } - return base.IsEvaluatableExpression(expression, model); + foreach (var plugin in Dependencies.Plugins) + { + if (!plugin.IsEvaluatableExpression(expression)) + { + return false; + } + } + + return true; } } } diff --git a/src/EFCore/Query/EvaluatableExpressionFilterBase.cs b/src/EFCore/Query/EvaluatableExpressionFilterBase.cs deleted file mode 100644 index 242c8968cca..00000000000 --- a/src/EFCore/Query/EvaluatableExpressionFilterBase.cs +++ /dev/null @@ -1,69 +0,0 @@ -// 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.Metadata; -using Microsoft.EntityFrameworkCore.Utilities; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.EntityFrameworkCore.Query -{ - /// - /// - /// Represents a filter for evaluatable expressions. - /// - /// - /// The service lifetime is . This means a single instance - /// is used by many instances. The implementation must be thread-safe. - /// This service cannot depend on services registered as . - /// - /// - public class EvaluatableExpressionFilterBase : IEvaluatableExpressionFilter - { - /// - /// Parameter object containing dependencies for this service. - /// - protected virtual EvaluatableExpressionFilterDependencies Dependencies { get; } - - /// - /// - /// Creates a new instance. - /// - /// - /// This type is typically used by database providers (and other extensions). It is generally - /// not used in application code. - /// - /// - /// The dependencies to use. - public EvaluatableExpressionFilterBase( - [NotNull] EvaluatableExpressionFilterDependencies dependencies) - { - Check.NotNull(dependencies, nameof(dependencies)); - - Dependencies = dependencies; - } - - /// - /// Checks whether the given expression can be evaluated. - /// - /// The expression. - /// The model. - /// True if the expression can be evaluated; false otherwise. - public virtual bool IsEvaluatableExpression(Expression expression, IModel model) - { - Check.NotNull(expression, nameof(expression)); - Check.NotNull(model, nameof(model)); - - foreach (var plugin in Dependencies.Plugins) - { - if (!plugin.IsEvaluatableExpression(expression)) - { - return false; - } - } - - return true; - } - } -} diff --git a/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs b/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs index be75d3f9d79..c1ab90b9ae8 100644 --- a/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs +++ b/test/EFCore.Relational.Tests/Metadata/DbFunctionMetadataTests.cs @@ -634,8 +634,9 @@ var queryableNoParams function = model.FindDbFunction(function.ModelName); var entityType = model.FindEntityType(typeof(Foo)); - Assert.True(function.IsQueryable); - Assert.Same(entityType, function.QueryableEntityType); + Assert.False(function.IsScalar); + Assert.False(function.IsAggregate); + Assert.Same(entityType, function.ReturnEntityType); } [ConditionalFact]