diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs index 1f1a984b226..55c748b0031 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs @@ -27,6 +27,8 @@ public class CosmosQueryableMethodTranslatingExpressionVisitor : QueryableMethod { private readonly IModel _model; private readonly ISqlExpressionFactory _sqlExpressionFactory; + private readonly IMemberTranslatorProvider _memberTranslatorProvider; + private readonly IMethodCallTranslatorProvider _methodCallTranslatorProvider; private readonly CosmosSqlTranslatingExpressionVisitor _sqlTranslator; private readonly CosmosProjectionBindingExpressionVisitor _projectionBindingExpressionVisitor; @@ -46,11 +48,13 @@ public CosmosQueryableMethodTranslatingExpressionVisitor( { _model = queryCompilationContext.Model; _sqlExpressionFactory = sqlExpressionFactory; + _memberTranslatorProvider = memberTranslatorProvider; + _methodCallTranslatorProvider = methodCallTranslatorProvider; _sqlTranslator = new CosmosSqlTranslatingExpressionVisitor( queryCompilationContext, - sqlExpressionFactory, - memberTranslatorProvider, - methodCallTranslatorProvider); + _sqlExpressionFactory, + _memberTranslatorProvider, + _methodCallTranslatorProvider); _projectionBindingExpressionVisitor = new CosmosProjectionBindingExpressionVisitor(_model, _sqlTranslator); } @@ -66,7 +70,11 @@ protected CosmosQueryableMethodTranslatingExpressionVisitor( { _model = parentVisitor._model; _sqlExpressionFactory = parentVisitor._sqlExpressionFactory; - _sqlTranslator = parentVisitor._sqlTranslator; + _sqlTranslator = new CosmosSqlTranslatingExpressionVisitor( + QueryCompilationContext, + _sqlExpressionFactory, + _memberTranslatorProvider, + _methodCallTranslatorProvider); _projectionBindingExpressionVisitor = new CosmosProjectionBindingExpressionVisitor(_model, _sqlTranslator); } @@ -1091,7 +1099,15 @@ when methodCallExpression.TryGetIndexerArguments(_model, out _, out var property } private SqlExpression TranslateExpression(Expression expression) - => _sqlTranslator.Translate(expression); + { + var translation = _sqlTranslator.Translate(expression); + if (translation == null && _sqlTranslator.TranslationErrorDetails != null) + { + ProvideTranslationErrorDetails(_sqlTranslator.TranslationErrorDetails); + } + + return translation; + } private SqlExpression TranslateLambdaExpression( ShapedQueryExpression shapedQueryExpression, LambdaExpression lambdaExpression) diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index cf7fd5fb10b..8f13512ec78 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -35,6 +35,10 @@ public class CosmosSqlTranslatingExpressionVisitor : ExpressionVisitor private static readonly MethodInfo _concatMethodInfo = typeof(string).GetRuntimeMethod(nameof(string.Concat), new[] { typeof(object), typeof(object) }); + private static readonly MethodInfo _stringEqualsWithStringComparison + = typeof(string).GetRuntimeMethod(nameof(string.Equals), new[] { typeof(string), typeof(StringComparison) }); + private static readonly MethodInfo _stringEqualsWithStringComparisonStatic + = typeof(string).GetRuntimeMethod(nameof(string.Equals), new[] { typeof(string), typeof(string), typeof(StringComparison) }); private readonly QueryCompilationContext _queryCompilationContext; private readonly IModel _model; @@ -63,6 +67,34 @@ public CosmosSqlTranslatingExpressionVisitor( _sqlVerifyingExpressionVisitor = new SqlTypeMappingVerifyingExpressionVisitor(); } + /// + /// 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. + /// + public virtual string TranslationErrorDetails { get; private set; } + + /// + /// 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. + /// + protected virtual void ProvideTranslationErrorDetails([NotNull] string details) + { + Check.NotNull(details, nameof(details)); + + if (TranslationErrorDetails == null) + { + TranslationErrorDetails = details; + } + else + { + TranslationErrorDetails += Environment.NewLine + details; + } + } + /// /// 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 @@ -70,6 +102,15 @@ public CosmosSqlTranslatingExpressionVisitor( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual SqlExpression Translate([NotNull] Expression expression) + { + Check.NotNull(expression, nameof(expression)); + + TranslationErrorDetails = null; + + return TranslateInternal(expression); + } + + private SqlExpression TranslateInternal(Expression expression) { var result = Visit(expression); @@ -416,7 +457,16 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp } } - return _methodCallTranslatorProvider.Translate(_model, sqlObject, methodCallExpression.Method, arguments); + var translation = _methodCallTranslatorProvider.Translate(_model, sqlObject, methodCallExpression.Method, arguments); + + if (translation == null + && (methodCallExpression.Method == _stringEqualsWithStringComparison + || methodCallExpression.Method == _stringEqualsWithStringComparisonStatic)) + { + ProvideTranslationErrorDetails(CoreStrings.QueryUnableToTranslateStringEqualsWithStringComparison); + } + + return translation; static Expression RemoveObjectConvert(Expression expression) => expression is UnaryExpression unaryExpression @@ -518,6 +568,14 @@ private Expression TryBindMember(Expression source, MemberIdentity member) ? entityReferenceExpression.ParameterEntity.BindMember(member.MemberInfo, entityReferenceExpression.Type, clientEval: false, out _) : entityReferenceExpression.ParameterEntity.BindMember(member.Name, entityReferenceExpression.Type, clientEval: false, out _); + if (result == null) + { + ProvideTranslationErrorDetails( + CoreStrings.QueryUnableToTranslateMember( + member.Name, + entityReferenceExpression.EntityType.DisplayName())); + } + return result switch { EntityProjectionExpression entityProjectionExpression => new EntityReferenceExpression(entityProjectionExpression), diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs index 58edc24780c..34d0c93a646 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs @@ -79,6 +79,34 @@ public InMemoryExpressionTranslatingExpressionVisitor( _model = queryCompilationContext.Model; } + /// + /// 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. + /// + public virtual string TranslationErrorDetails { get; private set; } + + /// + /// 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. + /// + protected virtual void ProvideTranslationErrorDetails([NotNull] string details) + { + Check.NotNull(details, nameof(details)); + + if (TranslationErrorDetails == null) + { + TranslationErrorDetails = details; + } + else + { + TranslationErrorDetails += Environment.NewLine + details; + } + } + /// /// 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 @@ -86,6 +114,15 @@ public InMemoryExpressionTranslatingExpressionVisitor( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual Expression Translate([NotNull] Expression expression) + { + Check.NotNull(expression, nameof(expression)); + + TranslationErrorDetails = null; + + return TranslateInternal(expression); + } + + private Expression TranslateInternal(Expression expression) { var result = Visit(expression); @@ -366,7 +403,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp case nameof(Enumerable.Min): case nameof(Enumerable.Sum): { - var translation = Translate(GetSelectorOnGrouping(methodCallExpression, groupByShaperExpression)); + var translation = TranslateInternal(GetSelectorOnGrouping(methodCallExpression, groupByShaperExpression)); if (translation == null) { return null; @@ -409,7 +446,7 @@ MethodInfo GetMethod() groupByShaperExpression.GroupingParameter); } - var translation = Translate(predicate); + var translation = TranslateInternal(predicate); if (translation == null) { return null; @@ -829,7 +866,18 @@ private Expression TryBindMember(Expression source, MemberIdentity member, Type ? entityType.FindProperty(member.MemberInfo) : entityType.FindProperty(member.Name); - return property != null ? BindProperty(entityReferenceExpression, property, type) : null; + if (property != null) + { + return BindProperty(entityReferenceExpression, property, type); + + } + + ProvideTranslationErrorDetails( + CoreStrings.QueryUnableToTranslateMember( + member.Name, + entityReferenceExpression.EntityType.DisplayName())); + + return null; } private Expression BindProperty(EntityReferenceExpression entityReferenceExpression, IProperty property, Type type) diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs index f2aa685c340..694de799264 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs @@ -56,8 +56,8 @@ protected InMemoryQueryableMethodTranslatingExpressionVisitor( [NotNull] InMemoryQueryableMethodTranslatingExpressionVisitor parentVisitor) : base(parentVisitor.Dependencies, parentVisitor.QueryCompilationContext, subquery: true) { - _expressionTranslator = parentVisitor._expressionTranslator; - _weakEntityExpandingExpressionVisitor = parentVisitor._weakEntityExpandingExpressionVisitor; + _expressionTranslator = new InMemoryExpressionTranslatingExpressionVisitor(QueryCompilationContext, parentVisitor); + _weakEntityExpandingExpressionVisitor = new WeakEntityExpandingExpressionVisitor(_expressionTranslator); _projectionBindingExpressionVisitor = new InMemoryProjectionBindingExpressionVisitor(this, _expressionTranslator); _model = parentVisitor._model; } @@ -465,7 +465,7 @@ private Expression TranslateGroupingKey(Expression expression) return memberInitExpression.Update(updatedNewExpression, newBindings); default: - var translation = _expressionTranslator.Translate(expression); + var translation = TranslateExpression(expression); if (translation == null) { return null; @@ -1204,19 +1204,23 @@ protected override ShapedQueryExpression TranslateWhere(ShapedQueryExpression so private Expression TranslateExpression(Expression expression, bool preserveType = false) { - var result = _expressionTranslator.Translate(expression); + var translation = _expressionTranslator.Translate(expression); + if (translation == null && _expressionTranslator.TranslationErrorDetails != null) + { + ProvideTranslationErrorDetails(_expressionTranslator.TranslationErrorDetails); + } if (expression != null - && result != null + && translation != null && preserveType - && expression.Type != result.Type) + && expression.Type != translation.Type) { - result = expression.Type == typeof(bool) - ? Expression.Equal(result, Expression.Constant(true, result.Type)) - : (Expression)Expression.Convert(result, expression.Type); + translation = expression.Type == typeof(bool) + ? Expression.Equal(translation, Expression.Constant(true, translation.Type)) + : (Expression)Expression.Convert(translation, expression.Type); } - return result; + return translation; } private LambdaExpression TranslateLambdaExpression( @@ -1254,6 +1258,8 @@ public WeakEntityExpandingExpressionVisitor(InMemoryExpressionTranslatingExpress _expressionTranslator = expressionTranslator; } + public string TranslationErrorDetails => _expressionTranslator.TranslationErrorDetails; + public Expression Expand(InMemoryQueryExpression queryExpression, Expression lambdaBody) { _queryExpression = queryExpression; diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index e71c2dd7adf..d13df00bb16 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -47,6 +47,7 @@ public RelationalQueryableMethodTranslatingExpressionVisitor( RelationalDependencies = relationalDependencies; var sqlExpressionFactory = relationalDependencies.SqlExpressionFactory; + _queryCompilationContext = queryCompilationContext; _model = queryCompilationContext.Model; _sqlTranslator = relationalDependencies.RelationalSqlTranslatingExpressionVisitorFactory.Create(queryCompilationContext, this); _weakEntityExpandingExpressionVisitor = new WeakEntityExpandingExpressionVisitor(_sqlTranslator, sqlExpressionFactory); @@ -70,8 +71,8 @@ protected RelationalQueryableMethodTranslatingExpressionVisitor( { RelationalDependencies = parentVisitor.RelationalDependencies; _queryCompilationContext = parentVisitor._queryCompilationContext; - _sqlTranslator = parentVisitor._sqlTranslator; - _weakEntityExpandingExpressionVisitor = parentVisitor._weakEntityExpandingExpressionVisitor; + _sqlTranslator = RelationalDependencies.RelationalSqlTranslatingExpressionVisitorFactory.Create(parentVisitor._queryCompilationContext, parentVisitor); + _weakEntityExpandingExpressionVisitor = new WeakEntityExpandingExpressionVisitor(_sqlTranslator, parentVisitor._sqlExpressionFactory); _projectionBindingExpressionVisitor = new RelationalProjectionBindingExpressionVisitor(this, _sqlTranslator); _sqlExpressionFactory = parentVisitor._sqlExpressionFactory; _subquery = true; @@ -98,7 +99,7 @@ protected override Expression VisitExtension(Expression extensionExpression) var arguments = new List(); foreach (var arg in tableValuedFunctionQueryRootExpression.Arguments) { - var sqlArgument = _sqlTranslator.Translate(arg); + var sqlArgument = TranslateExpression(arg); if (sqlArgument == null) { var methodCall = Expression.Call( @@ -106,7 +107,10 @@ protected override Expression VisitExtension(Expression extensionExpression) function.MethodInfo, tableValuedFunctionQueryRootExpression.Arguments); - throw new InvalidOperationException(CoreStrings.TranslationFailed(methodCall.Print())); + throw new InvalidOperationException( + TranslationErrorDetails == null + ? CoreStrings.TranslationFailed(methodCall.Print()) + : CoreStrings.TranslationFailedWithDetails(methodCall.Print(), TranslationErrorDetails)); } arguments.Add(sqlArgument); @@ -476,7 +480,7 @@ private Expression TranslateGroupingKey(Expression expression) return memberInitExpression.Update(updatedNewExpression, newBindings); default: - var translation = _sqlTranslator.Translate(expression); + var translation = TranslateExpression(expression); if (translation == null) { return null; @@ -1139,7 +1143,16 @@ protected override ShapedQueryExpression TranslateWhere(ShapedQueryExpression so return source; } - private SqlExpression TranslateExpression(Expression expression) => _sqlTranslator.Translate(expression); + private SqlExpression TranslateExpression(Expression expression) + { + var translation = _sqlTranslator.Translate(expression); + if (translation == null && _sqlTranslator.TranslationErrorDetails != null) + { + ProvideTranslationErrorDetails(_sqlTranslator.TranslationErrorDetails); + } + + return translation; + } private SqlExpression TranslateLambdaExpression( ShapedQueryExpression shapedQueryExpression, LambdaExpression lambdaExpression) @@ -1170,6 +1183,8 @@ public WeakEntityExpandingExpressionVisitor( _sqlExpressionFactory = sqlExpressionFactory; } + public string TranslationErrorDetails => _sqlTranslator.TranslationErrorDetails; + public Expression Expand(SelectExpression selectExpression, Expression lambdaBody) { _selectExpression = selectExpression; diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs index 314339ecd7a..bd676b2c124 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs @@ -35,6 +35,11 @@ public class RelationalSqlTranslatingExpressionVisitor : ExpressionVisitor private static readonly MethodInfo _parameterListValueExtractor = typeof(RelationalSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ParameterListValueExtractor)); + private static readonly MethodInfo _stringEqualsWithStringComparison + = typeof(string).GetRuntimeMethod(nameof(string.Equals), new[] { typeof(string), typeof(StringComparison) }); + private static readonly MethodInfo _stringEqualsWithStringComparisonStatic + = typeof(string).GetRuntimeMethod(nameof(string.Equals), new[] { typeof(string), typeof(string), typeof(StringComparison) }); + private readonly QueryCompilationContext _queryCompilationContext; private readonly IModel _model; private readonly ISqlExpressionFactory _sqlExpressionFactory; @@ -64,6 +69,29 @@ public RelationalSqlTranslatingExpressionVisitor( _sqlTypeMappingVerifyingExpressionVisitor = new SqlTypeMappingVerifyingExpressionVisitor(); } + /// + /// Detailed information about errors encountered during translation. + /// + public virtual string TranslationErrorDetails { get; private set; } + + /// + /// Provides detailed information about error encountered during translation. + /// + /// Detailed information about error encountered during translation. + protected virtual void ProvideTranslationErrorDetails([NotNull] string details) + { + Check.NotNull(details, nameof(details)); + + if (TranslationErrorDetails == null) + { + TranslationErrorDetails = details; + } + else + { + TranslationErrorDetails += Environment.NewLine + details; + } + } + /// /// Parameter object containing service dependencies. /// @@ -78,6 +106,13 @@ public virtual SqlExpression Translate([NotNull] Expression expression) { Check.NotNull(expression, nameof(expression)); + TranslationErrorDetails = null; + + return TranslateInternal(expression); + } + + private SqlExpression TranslateInternal(Expression expression) + { var result = Visit(expression); if (result is SqlExpression translation) @@ -116,12 +151,15 @@ public virtual SqlExpression TranslateAverage([NotNull] Expression expression) if (!(expression is SqlExpression sqlExpression)) { - sqlExpression = Translate(expression); + sqlExpression = TranslateInternal(expression); } if (sqlExpression == null) { - throw new InvalidOperationException(CoreStrings.TranslationFailed(expression.Print())); + throw new InvalidOperationException( + TranslationErrorDetails == null + ? CoreStrings.TranslationFailed(expression.Print()) + : CoreStrings.TranslationFailedWithDetails(expression.Print(), TranslationErrorDetails)); } var inputType = sqlExpression.Type.UnwrapNullableType(); @@ -206,7 +244,7 @@ public virtual SqlExpression TranslateMax([NotNull] Expression expression) if (!(expression is SqlExpression sqlExpression)) { - sqlExpression = Translate(expression); + sqlExpression = TranslateInternal(expression); } return sqlExpression != null @@ -231,7 +269,7 @@ public virtual SqlExpression TranslateMin([NotNull] Expression expression) if (!(expression is SqlExpression sqlExpression)) { - sqlExpression = Translate(expression); + sqlExpression = TranslateInternal(expression); } return sqlExpression != null @@ -256,12 +294,15 @@ public virtual SqlExpression TranslateSum([NotNull] Expression expression) if (!(expression is SqlExpression sqlExpression)) { - sqlExpression = Translate(expression); + sqlExpression = TranslateInternal(expression); } if (sqlExpression == null) { - throw new InvalidOperationException(CoreStrings.TranslationFailed(expression.Print())); + throw new InvalidOperationException( + TranslationErrorDetails == null + ? CoreStrings.TranslationFailed(expression.Print()) + : CoreStrings.TranslationFailedWithDetails(expression.Print(), TranslationErrorDetails)); } var inputType = sqlExpression.Type.UnwrapNullableType(); @@ -438,7 +479,10 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp if (translatedAggregate == null) { - throw new InvalidOperationException(CoreStrings.TranslationFailed(methodCallExpression.Print())); + throw new InvalidOperationException( + TranslationErrorDetails == null + ? CoreStrings.TranslationFailed(methodCallExpression.Print()) + : CoreStrings.TranslationFailedWithDetails(methodCallExpression.Print(), TranslationErrorDetails)); } return translatedAggregate; @@ -618,7 +662,16 @@ static bool IsAggregateResultWithCustomShaper(MethodInfo method) } } - return Dependencies.MethodCallTranslatorProvider.Translate(_model, sqlObject, methodCallExpression.Method, arguments); + var translation = Dependencies.MethodCallTranslatorProvider.Translate(_model, sqlObject, methodCallExpression.Method, arguments); + + if (translation == null + && (methodCallExpression.Method == _stringEqualsWithStringComparison + || methodCallExpression.Method == _stringEqualsWithStringComparisonStatic)) + { + ProvideTranslationErrorDetails(CoreStrings.QueryUnableToTranslateStringEqualsWithStringComparison); + } + + return translation; static Expression RemoveObjectConvert(Expression expression) => expression is UnaryExpression unaryExpression @@ -750,7 +803,17 @@ private Expression TryBindMember(Expression source, MemberIdentity member) ? entityType.FindProperty(member.MemberInfo) : entityType.FindProperty(member.Name); - return property != null ? BindProperty(entityReferenceExpression, property) : null; + if (property != null) + { + return BindProperty(entityReferenceExpression, property); + } + + ProvideTranslationErrorDetails( + CoreStrings.QueryUnableToTranslateMember( + member.Name, + entityReferenceExpression.EntityType.DisplayName())); + + return null; } private SqlExpression BindProperty(EntityReferenceExpression entityReferenceExpression, IProperty property) diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 95935186f11..a2e9c58c9b8 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -40,6 +40,14 @@ public static string TranslationFailed([CanBeNull] object expression) GetString("TranslationFailed", nameof(expression)), expression); + /// + /// The LINQ expression '{expression}' could not be translated. Additional information: {details} Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information. + /// + public static string TranslationFailedWithDetails([CanBeNull] object expression, [CanBeNull] object details) + => string.Format( + GetString("TranslationFailedWithDetails", nameof(expression), nameof(details)), + expression, details); + /// /// Processing of the LINQ expression '{expression}' by '{visitor}' failed. This may indicate either a bug or a limitation in EF Core. See https://go.microsoft.com/fwlink/?linkid=2101433 for more detailed information. /// @@ -2560,6 +2568,20 @@ public static string QueryUnableToTranslateEFProperty([CanBeNull] object express GetString("QueryUnableToTranslateEFProperty", nameof(expression)), expression); + /// + /// Translation of member '{member}' on entity type '{entityType}' failed. Possibly the specified member is not mapped. + /// + public static string QueryUnableToTranslateMember([CanBeNull] object member, [CanBeNull] object entityType) + => string.Format( + GetString("QueryUnableToTranslateMember", nameof(member), nameof(entityType)), + member, entityType); + + /// + /// Translation of 'string.Equals' method which takes 'StringComparison' argument is not supported. See https://go.microsoft.com/fwlink/?linkid=2129535 for more information. + /// + public static string QueryUnableToTranslateStringEqualsWithStringComparison + => GetString("QueryUnableToTranslateStringEqualsWithStringComparison"); + /// /// Invalid {state} encountered. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index e32292d21df..0072be98d56 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -123,6 +123,9 @@ The LINQ expression '{expression}' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information. + + The LINQ expression '{expression}' could not be translated. Additional information: {details} Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information. + Processing of the LINQ expression '{expression}' by '{visitor}' failed. This may indicate either a bug or a limitation in EF Core. See https://go.microsoft.com/fwlink/?linkid=2101433 for more detailed information. @@ -1356,6 +1359,12 @@ Translation of '{expression}' failed. Either source is not an entity type or the specified property does not exist on the entity type. + + Translation of member '{member}' on entity type '{entityType}' failed. Possibly the specified member is not mapped. + + + Translation of 'string.Equals' method which takes 'StringComparison' argument is not supported. See https://go.microsoft.com/fwlink/?linkid=2129535 for more information. + Invalid {state} encountered. diff --git a/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs index e0ecbf9844d..d7c4a640d40 100644 --- a/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore/Query/QueryableMethodTranslatingExpressionVisitor.cs @@ -54,6 +54,29 @@ protected QueryableMethodTranslatingExpressionVisitor( /// protected virtual QueryableMethodTranslatingExpressionVisitorDependencies Dependencies { get; } + /// + /// Additional information about errors encountered during translation. + /// + public virtual string TranslationErrorDetails { get; private set; } + + /// + /// Provides additional information about errors encountered during translation. + /// + /// Error encountered during translation + protected virtual void ProvideTranslationErrorDetails([NotNull] string details) + { + Check.NotNull(details, nameof(details)); + + if (TranslationErrorDetails == null) + { + TranslationErrorDetails = details; + } + else + { + TranslationErrorDetails += Environment.NewLine + details; + } + } + /// /// The query compilation context object for current compilation. /// @@ -79,7 +102,12 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp ShapedQueryExpression CheckTranslated(ShapedQueryExpression translated) { - return translated ?? throw new InvalidOperationException(CoreStrings.TranslationFailed(methodCallExpression.Print())); + return translated ?? throw new InvalidOperationException( + TranslationErrorDetails == null + ? CoreStrings.TranslationFailed(methodCallExpression.Print()) + : CoreStrings.TranslationFailedWithDetails( + methodCallExpression.Print(), + TranslationErrorDetails)); } var method = methodCallExpression.Method; @@ -596,7 +624,14 @@ public virtual ShapedQueryExpression TranslateSubquery([NotNull] Expression expr { Check.NotNull(expression, nameof(expression)); - return (ShapedQueryExpression)CreateSubqueryVisitor().Visit(expression); + var subqueryVisitor = CreateSubqueryVisitor(); + var translation = (ShapedQueryExpression)subqueryVisitor.Visit(expression); + if (translation == null && subqueryVisitor.TranslationErrorDetails != null) + { + ProvideTranslationErrorDetails(subqueryVisitor.TranslationErrorDetails); + } + + return translation; } /// diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs index 361517cf452..110f7ec28b7 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs @@ -1600,6 +1600,12 @@ public override Task Min_after_default_if_empty_does_not_throw(bool isAsync) return base.Min_after_default_if_empty_does_not_throw(isAsync); } + [ConditionalTheory(Skip = "Issue#20677")] + public override Task Average_with_unmapped_property_access_throws_meaningful_exception(bool async) + { + return base.Average_with_unmapped_property_access_throws_meaningful_exception(async); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs index 8d3059fd816..b4bc8bcec64 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs @@ -4125,6 +4125,14 @@ public override Task Perform_identity_resolution_reuses_same_instances_across_jo return base.Perform_identity_resolution_reuses_same_instances_across_joins(async); } + [ConditionalTheory(Skip = "Issue #17246")] + public override Task All_client_and_server_top_level(bool async) + => base.All_client_and_server_top_level(async); + + [ConditionalTheory(Skip = "Issue #17246")] + public override Task All_client_or_server_top_level(bool async) + => base.All_client_or_server_top_level(async); + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs index cdca991be52..19e1110272c 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs @@ -17,82 +17,58 @@ public GearsOfWarQueryInMemoryTest(GearsOfWarQueryInMemoryFixture fixture, ITest [ConditionalTheory(Skip = "issue #17386")] public override Task Correlated_collection_order_by_constant_null_of_non_mapped_type(bool async) - { - return base.Correlated_collection_order_by_constant_null_of_non_mapped_type(async); - } + => base.Correlated_collection_order_by_constant_null_of_non_mapped_type(async); [ConditionalTheory(Skip = "issue #17386")] public override Task Client_side_equality_with_parameter_works_with_optional_navigations(bool async) - { - return base.Client_side_equality_with_parameter_works_with_optional_navigations(async); - } + => base.Client_side_equality_with_parameter_works_with_optional_navigations(async); [ConditionalTheory(Skip = "issue #17386")] public override Task Where_coalesce_with_anonymous_types(bool async) - { - return base.Where_coalesce_with_anonymous_types(async); - } + => base.Where_coalesce_with_anonymous_types(async); [ConditionalTheory(Skip = "issue #17386")] public override Task Where_conditional_with_anonymous_type(bool async) - { - return base.Where_conditional_with_anonymous_type(async); - } + => base.Where_conditional_with_anonymous_type(async); [ConditionalTheory(Skip = "issue #17386")] public override Task GetValueOrDefault_on_DateTimeOffset(bool async) - { - return base.GetValueOrDefault_on_DateTimeOffset(async); - } + => base.GetValueOrDefault_on_DateTimeOffset(async); [ConditionalFact(Skip = "issue #17537")] public override void Include_on_GroupJoin_SelectMany_DefaultIfEmpty_with_coalesce_result1() - { - base.Include_on_GroupJoin_SelectMany_DefaultIfEmpty_with_coalesce_result1(); - } + => base.Include_on_GroupJoin_SelectMany_DefaultIfEmpty_with_coalesce_result1(); [ConditionalFact(Skip = "issue #17537")] public override void Include_on_GroupJoin_SelectMany_DefaultIfEmpty_with_coalesce_result2() - { - base.Include_on_GroupJoin_SelectMany_DefaultIfEmpty_with_coalesce_result2(); - } + => base.Include_on_GroupJoin_SelectMany_DefaultIfEmpty_with_coalesce_result2(); [ConditionalTheory(Skip = "issue #17540")] - public override Task - Null_semantics_is_correctly_applied_for_function_comparisons_that_take_arguments_from_optional_navigation_complex(bool async) - { - return base.Null_semantics_is_correctly_applied_for_function_comparisons_that_take_arguments_from_optional_navigation_complex( - async); - } + public override Task Null_semantics_is_correctly_applied_for_function_comparisons_that_take_arguments_from_optional_navigation_complex(bool async) + => base.Null_semantics_is_correctly_applied_for_function_comparisons_that_take_arguments_from_optional_navigation_complex(async); [ConditionalTheory(Skip = "issue #18284")] public override Task GroupBy_with_boolean_groupin_key_thru_navigation_access(bool async) - { - return GroupBy_with_boolean_groupin_key_thru_navigation_access(async); - } + => GroupBy_with_boolean_groupin_key_thru_navigation_access(async); [ConditionalTheory(Skip = "issue #17620")] public override Task Select_subquery_projecting_single_constant_inside_anonymous(bool async) - { - return base.Select_subquery_projecting_single_constant_inside_anonymous(async); - } + => base.Select_subquery_projecting_single_constant_inside_anonymous(async); [ConditionalTheory(Skip = "issue #19683")] public override Task Group_by_on_StartsWith_with_null_parameter_as_argument(bool async) - { - return base.Group_by_on_StartsWith_with_null_parameter_as_argument(async); - } + => base.Group_by_on_StartsWith_with_null_parameter_as_argument(async); [ConditionalTheory(Skip = "issue #18284")] public override Task Enum_closure_typed_as_underlying_type_generates_correct_parameter_type(bool async) - { - return base.Enum_closure_typed_as_underlying_type_generates_correct_parameter_type(async); - } + => base.Enum_closure_typed_as_underlying_type_generates_correct_parameter_type(async); + + [ConditionalTheory(Skip = "issue #17386")] + public override Task Client_member_and_unsupported_string_Equals_in_the_same_query(bool async) + => base.Client_member_and_unsupported_string_Equals_in_the_same_query(async); [ConditionalTheory(Skip = "issue #17386")] public override Task Client_eval_followed_by_set_operation_throws_meaningful_exception(bool async) - { - return base.Client_eval_followed_by_set_operation_throws_meaningful_exception(async); - } + => base.Client_eval_followed_by_set_operation_throws_meaningful_exception(async); } } diff --git a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindMiscellaneousQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindMiscellaneousQueryInMemoryTest.cs index 38dc9462e88..fb05af7aae8 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindMiscellaneousQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindMiscellaneousQueryInMemoryTest.cs @@ -22,106 +22,84 @@ public NorthwindMiscellaneousQueryInMemoryTest( } public override Task Where_query_composition_entity_equality_one_element_Single(bool async) - { - return Assert.ThrowsAsync( + => Assert.ThrowsAsync( () => base.Where_query_composition_entity_equality_one_element_Single(async)); - } public override Task Where_query_composition_entity_equality_one_element_First(bool async) - { - return Assert.ThrowsAsync( + => Assert.ThrowsAsync( () => base.Where_query_composition_entity_equality_one_element_First(async)); - } public override Task Where_query_composition_entity_equality_no_elements_Single(bool async) - { - return Assert.ThrowsAsync( + => Assert.ThrowsAsync( () => base.Where_query_composition_entity_equality_no_elements_Single(async)); - } public override Task Where_query_composition_entity_equality_no_elements_First(bool async) - { - return Assert.ThrowsAsync( + => Assert.ThrowsAsync( () => base.Where_query_composition_entity_equality_no_elements_First(async)); - } public override Task Where_query_composition_entity_equality_multiple_elements_SingleOrDefault(bool async) - { - return Assert.ThrowsAsync( + => Assert.ThrowsAsync( () => base.Where_query_composition_entity_equality_multiple_elements_SingleOrDefault(async)); - } public override Task Where_query_composition_entity_equality_multiple_elements_Single(bool async) - { - return Assert.ThrowsAsync( + => Assert.ThrowsAsync( () => base.Where_query_composition_entity_equality_multiple_elements_Single(async)); - } // Sending client code to server [ConditionalFact(Skip = "Issue#17050")] public override void Client_code_using_instance_in_anonymous_type() - { - base.Client_code_using_instance_in_anonymous_type(); - } + => base.Client_code_using_instance_in_anonymous_type(); [ConditionalFact(Skip = "Issue#17050")] public override void Client_code_using_instance_in_static_method() - { - base.Client_code_using_instance_in_static_method(); - } + => base.Client_code_using_instance_in_static_method(); [ConditionalFact(Skip = "Issue#17050")] public override void Client_code_using_instance_method_throws() - { - base.Client_code_using_instance_method_throws(); - } + => base.Client_code_using_instance_method_throws(); [ConditionalTheory(Skip = "Issue#17386")] public override Task OrderBy_multiple_queries(bool async) - { - return base.OrderBy_multiple_queries(async); - } + => base.OrderBy_multiple_queries(async); [ConditionalTheory(Skip = "Issue#17386")] public override Task Random_next_is_not_funcletized_1(bool async) - { - return base.Random_next_is_not_funcletized_1(async); - } + => base.Random_next_is_not_funcletized_1(async); [ConditionalTheory(Skip = "Issue#17386")] public override Task Random_next_is_not_funcletized_2(bool async) - { - return base.Random_next_is_not_funcletized_2(async); - } + => base.Random_next_is_not_funcletized_2(async); [ConditionalTheory(Skip = "Issue#17386")] public override Task Random_next_is_not_funcletized_3(bool async) - { - return base.Random_next_is_not_funcletized_3(async); - } + => base.Random_next_is_not_funcletized_3(async); [ConditionalTheory(Skip = "Issue#17386")] public override Task Random_next_is_not_funcletized_4(bool async) - { - return base.Random_next_is_not_funcletized_4(async); - } + => base.Random_next_is_not_funcletized_4(async); [ConditionalTheory(Skip = "Issue#17386")] public override Task Random_next_is_not_funcletized_5(bool async) - { - return base.Random_next_is_not_funcletized_5(async); - } + => base.Random_next_is_not_funcletized_5(async); [ConditionalTheory(Skip = "Issue#17386")] public override Task Random_next_is_not_funcletized_6(bool async) - { - return base.Random_next_is_not_funcletized_6(async); - } + => base.Random_next_is_not_funcletized_6(async); - [ConditionalTheory] - public override Task DefaultIfEmpty_in_subquery_nested(bool async) - { - return base.DefaultIfEmpty_in_subquery_nested(async); - } + [ConditionalTheory(Skip = "issue#17386")] + public override Task Where_query_composition5(bool async) + => base.Where_query_composition5(async); + + [ConditionalTheory(Skip = "issue#17386")] + public override Task Where_query_composition6(bool async) + => base.Where_query_composition6(async); + + [ConditionalTheory(Skip = "issue #17386")] + public override Task Using_string_Equals_with_StringComparison_throws_informative_error(bool async) + => base.Using_string_Equals_with_StringComparison_throws_informative_error(async); + + [ConditionalTheory(Skip = "issue #17386")] + public override Task Using_static_string_Equals_with_StringComparison_throws_informative_error(bool async) + => base.Using_static_string_Equals_with_StringComparison_throws_informative_error(async); } } diff --git a/test/EFCore.Relational.Specification.Tests/Query/QueryNoClientEvalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/QueryNoClientEvalTestBase.cs index 6c10a8500b6..8d7cbfe2a2d 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/QueryNoClientEvalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/QueryNoClientEvalTestBase.cs @@ -23,25 +23,30 @@ public abstract class QueryNoClientEvalTestBase : IClassFixture context.Customers.Where(c => c.IsLondon).ToList()); + AssertTranslationFailedWithDetails( + () => context.Customers.Where(c => c.IsLondon).ToList(), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } [ConditionalFact] public virtual void Throws_when_orderby() { using var context = CreateContext(); - AssertTranslationFailed(() => context.Customers.OrderBy(c => c.IsLondon).ToList()); + AssertTranslationFailedWithDetails( + () => context.Customers.OrderBy(c => c.IsLondon).ToList(), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } [ConditionalFact] public virtual void Throws_when_orderby_multiple() { using var context = CreateContext(); - AssertTranslationFailed( + AssertTranslationFailedWithDetails( () => context.Customers .OrderBy(c => c.IsLondon) .ThenBy(c => ClientMethod(c)) - .ToList()); + .ToList(), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } private static object ClientMethod(object o) => o.GetHashCode(); @@ -50,28 +55,32 @@ public virtual void Throws_when_orderby_multiple() public virtual void Throws_when_where_subquery_correlated() { using var context = CreateContext(); - AssertTranslationFailed( + AssertTranslationFailedWithDetails( () => context.Customers .Where(c1 => context.Customers.Any(c2 => c1.CustomerID == c2.CustomerID && c2.IsLondon)) - .ToList()); + .ToList(), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } [ConditionalFact] public virtual void Throws_when_all() { using var context = CreateContext(); - AssertTranslationFailed(() => context.Customers.All(c => c.IsLondon)); + AssertTranslationFailedWithDetails( + () => context.Customers.All(c => c.IsLondon), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } [ConditionalFact] public virtual void Throws_when_from_sql_composed() { using var context = CreateContext(); - AssertTranslationFailed( + AssertTranslationFailedWithDetails( () => context.Customers .FromSqlRaw(NormalizeDelimitersInRawString("select * from [Customers]")) .Where(c => c.IsLondon) - .ToList()); + .ToList(), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } [ConditionalFact] @@ -90,13 +99,14 @@ var customers public virtual void Throws_when_subquery_main_from_clause() { using var context = CreateContext(); - AssertTranslationFailed( + AssertTranslationFailedWithDetails( () => (from c1 in context.Customers .Where(c => c.IsLondon) .OrderBy(c => c.CustomerID) .Take(5) select c1) - .ToList()); + .ToList(), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } [ConditionalFact] @@ -151,28 +161,36 @@ public virtual void Throws_when_group_by() public virtual void Throws_when_first() { using var context = CreateContext(); - AssertTranslationFailed(() => context.Customers.First(c => c.IsLondon)); + AssertTranslationFailedWithDetails( + () => context.Customers.First(c => c.IsLondon), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } [ConditionalFact] public virtual void Throws_when_single() { using var context = CreateContext(); - AssertTranslationFailed(() => context.Customers.Single(c => c.IsLondon)); + AssertTranslationFailedWithDetails( + () => context.Customers.Single(c => c.IsLondon), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } [ConditionalFact] public virtual void Throws_when_first_or_default() { using var context = CreateContext(); - AssertTranslationFailed(() => context.Customers.FirstOrDefault(c => c.IsLondon)); + AssertTranslationFailedWithDetails( + () => context.Customers.FirstOrDefault(c => c.IsLondon), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } [ConditionalFact] public virtual void Throws_when_single_or_default() { using var context = CreateContext(); - AssertTranslationFailed(() => context.Customers.SingleOrDefault(c => c.IsLondon)); + AssertTranslationFailedWithDetails( + () => context.Customers.SingleOrDefault(c => c.IsLondon), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } private string NormalizeDelimitersInRawString(string sql) @@ -183,6 +201,11 @@ private void AssertTranslationFailed(Action testCode) CoreStrings.TranslationFailed("").Substring(21), Assert.Throws(testCode).Message); + private void AssertTranslationFailedWithDetails(Action testCode, string details) + => Assert.Contains( + CoreStrings.TranslationFailedWithDetails("", details).Substring(21), + Assert.Throws(testCode).Message); + protected NorthwindContext CreateContext() => Fixture.CreateContext(); } } diff --git a/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs b/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs index e99e7d226bc..b9fa3db8fb6 100644 --- a/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs +++ b/test/EFCore.Specification.Tests/CustomConvertersTestBase.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Xunit; @@ -581,7 +582,8 @@ public virtual void Collection_property_as_scalar_Count_member() { using var context = CreateContext(); Assert.Equal( - @"The LINQ expression 'DbSet() .Where(c => c.Tags.Count == 2)' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.", + CoreStrings.TranslationFailed( + @"DbSet() .Where(c => c.Tags.Count == 2)"), Assert.Throws( () => context.Set().Where(e => e.Tags.Count == 2).ToList()) .Message.Replace("\r", "").Replace("\n", "")); diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index 6e68b173a31..331c19634ed 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -1563,12 +1563,13 @@ public virtual Task Select_subquery_distinct_firstordefault(bool async) [MemberData(nameof(IsAsyncData))] public virtual Task Select_Where_Navigation_Client(bool async) { - return AssertTranslationFailed( + return AssertTranslationFailedWithDetails( () => AssertQuery( async, ss => from t in ss.Set() where t.Gear != null && t.Gear.IsMarcus - select t)); + select t), + CoreStrings.QueryUnableToTranslateMember(nameof(Gear.IsMarcus), nameof(Gear))); } [ConditionalTheory] @@ -7506,6 +7507,85 @@ await AssertTranslationFailed( ss => ss.Set().Select(m => m.Duration.Ticks))); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Trying_to_access_unmapped_property_throws_informative_error(bool async) + { + return AssertTranslationFailedWithDetails( + () => AssertQuery( + async, + ss => ss.Set().Where(g => g.IsMarcus)), + CoreStrings.QueryUnableToTranslateMember(nameof(Gear.IsMarcus), nameof(Gear))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Trying_to_access_unmapped_property_in_projection(bool async) + { + return AssertQueryScalar( + async, + ss => ss.Set().Select(g => g.IsMarcus)); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Trying_to_access_unmapped_property_inside_aggregate(bool async) + { + return AssertTranslationFailedWithDetails( + () => AssertQuery( + async, + ss => ss.Set().Where(c => c.BornGears.Count(g => g.IsMarcus) > 0)), + CoreStrings.QueryUnableToTranslateMember(nameof(Gear.IsMarcus), nameof(Gear))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Trying_to_access_unmapped_property_inside_subquery(bool async) + { + return AssertTranslationFailedWithDetails( + () => AssertQuery( + async, + ss => ss.Set().Where(c => ss.Set().Where(g => g.IsMarcus).Select(g => g.Nickname).FirstOrDefault() == "Marcus")), + CoreStrings.QueryUnableToTranslateMember(nameof(Gear.IsMarcus), nameof(Gear))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Trying_to_access_unmapped_property_inside_join_key_selector(bool async) + { + return AssertTranslationFailedWithDetails( + () => AssertQuery( + async, + ss => from w in ss.Set() + join g in ss.Set() on w.IsAutomatic equals g.IsMarcus into grouping + from g in grouping.DefaultIfEmpty() + select new { w, g }), + CoreStrings.QueryUnableToTranslateMember(nameof(Gear.IsMarcus), nameof(Gear))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Client_projection_with_nested_unmapped_property_bubbles_up_translation_failure_info(bool async) + { + return AssertTranslationFailedWithDetails( + () => AssertQuery( + async, + ss => ss.Set().Select(g => new { nested = ss.Set().Where(gg => gg.IsMarcus).ToList() })), + CoreStrings.QueryUnableToTranslateMember(nameof(Gear.IsMarcus), nameof(Gear))); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Client_member_and_unsupported_string_Equals_in_the_same_query(bool async) + { + return AssertTranslationFailedWithDetails( + () => AssertQuery( + async, + ss => ss.Set().Where(g => g.FullName.Equals(g.Nickname, StringComparison.InvariantCulture) || g.IsMarcus)), + CoreStrings.QueryUnableToTranslateStringEqualsWithStringComparison + + Environment.NewLine + CoreStrings.QueryUnableToTranslateMember(nameof(Gear.IsMarcus), nameof(Gear))); + } + protected GearsOfWarContext CreateContext() => Fixture.CreateContext(); protected virtual void ClearLog() diff --git a/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs index 646edc41142..a157786cad4 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindAggregateOperatorsQueryTestBase.cs @@ -6,6 +6,7 @@ using System.Collections.Immutable; using System.Linq; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.TestModels.Northwind; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; @@ -1889,6 +1890,18 @@ await AssertCount( ss => ss.Set().Select(o => new { Id = CodeFormat(o.OrderID) })); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Average_with_unmapped_property_access_throws_meaningful_exception(bool async) + { + return AssertTranslationFailedWithDetails( + () => AssertAverage( + async, + ss => ss.Set(), + selector: c => c.ShipVia), + CoreStrings.QueryUnableToTranslateMember(nameof(Order.ShipVia), nameof(Order))); + } + private static string CodeFormat(int str) => str.ToString(); [ConditionalTheory] diff --git a/test/EFCore.Specification.Tests/Query/NorthwindIncludeAsyncQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindIncludeAsyncQueryTestBase.cs index 605dd6d0be1..12fde7e2aa4 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindIncludeAsyncQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindIncludeAsyncQueryTestBase.cs @@ -567,7 +567,9 @@ public virtual async Task Include_collection_with_client_filter() { using var context = CreateContext(); Assert.Contains( - CoreStrings.TranslationFailed("").Substring(21), + CoreStrings.TranslationFailedWithDetails( + "", + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))).Substring(21), (await Assert.ThrowsAsync( () => context.Set() .Include(c => c.Orders) diff --git a/test/EFCore.Specification.Tests/Query/NorthwindIncludeQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindIncludeQueryTestBase.cs index 01f12db1bbb..312de559ef9 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindIncludeQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindIncludeQueryTestBase.cs @@ -1812,7 +1812,9 @@ public virtual void Include_collection_with_client_filter(bool useString) { using var context = CreateContext(); Assert.Contains( - CoreStrings.TranslationFailed("").Substring(21), + CoreStrings.TranslationFailedWithDetails( + "", + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))).Substring(21), Assert.Throws( () => useString ? context.Set() diff --git a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs index a93cd8a45a3..bb2ed89ab58 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs @@ -559,11 +559,12 @@ public virtual Task Queryable_simple_anonymous_subquery(bool async) [MemberData(nameof(IsAsyncData))] public virtual Task Queryable_reprojection(bool async) { - return AssertTranslationFailed( + return AssertTranslationFailedWithDetails( () => AssertQuery( async, ss => ss.Set().Where(c => c.IsLondon) - .Select(c => new Customer { CustomerID = "Foo", City = c.City }))); + .Select(c => new Customer { CustomerID = "Foo", City = c.City })), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } [ConditionalTheory] @@ -1140,33 +1141,36 @@ public virtual Task All_top_level_subquery_ef_property(bool async) [MemberData(nameof(IsAsyncData))] public virtual Task All_client(bool async) { - return AssertTranslationFailed( + return AssertTranslationFailedWithDetails( () => AssertAll( async, ss => ss.Set(), - predicate: c => c.IsLondon)); + predicate: c => c.IsLondon), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task All_client_and_server_top_level(bool async) { - return AssertTranslationFailed( + return AssertTranslationFailedWithDetails( () => AssertAll( async, ss => ss.Set(), - predicate: c => c.CustomerID != "Foo" && c.IsLondon)); + predicate: c => c.CustomerID != "Foo" && c.IsLondon), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task All_client_or_server_top_level(bool async) { - return AssertTranslationFailed( + return AssertTranslationFailedWithDetails( () => AssertAll( async, ss => ss.Set(), - predicate: c => c.CustomerID != "Foo" || c.IsLondon)); + predicate: c => c.CustomerID != "Foo" || c.IsLondon), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } [ConditionalTheory] @@ -1207,12 +1211,13 @@ public virtual Task Cast_results_to_object(bool async) [MemberData(nameof(IsAsyncData))] public virtual Task First_client_predicate(bool async) { - return AssertTranslationFailed( + return AssertTranslationFailedWithDetails( () => AssertFirst( async, ss => ss.Set().OrderBy(c => c.CustomerID), predicate: c => c.IsLondon, - entryCount: 1)); + entryCount: 1), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } [ConditionalTheory] @@ -1935,20 +1940,21 @@ where e1.FirstName [MemberData(nameof(IsAsyncData))] public virtual Task Where_query_composition3(bool async) { - return AssertTranslationFailed( + return AssertTranslationFailedWithDetails( () => AssertQuery( async, ss => from c1 in ss.Set() where c1.City == ss.Set().OrderBy(c => c.CustomerID).First(c => c.IsLondon).City select c1, - entryCount: 6)); + entryCount: 6), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Where_query_composition4(bool async) { - return AssertTranslationFailed( + return AssertTranslationFailedWithDetails( () => AssertQuery( async, ss => from c1 in ss.Set().OrderBy(c => c.CustomerID).Take(2) @@ -1957,27 +1963,29 @@ where c1.City from c3 in ss.Set().OrderBy(c => c.IsLondon).ThenBy(c => c.CustomerID) select new { c3 }).First().c3.City select c1, - entryCount: 1)); + entryCount: 1), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Where_query_composition5(bool async) { - return AssertTranslationFailed( + return AssertTranslationFailedWithDetails( () => AssertQuery( async, ss => from c1 in ss.Set() where c1.IsLondon == ss.Set().OrderBy(c => c.CustomerID).First().IsLondon select c1, - entryCount: 85)); + entryCount: 85), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Where_query_composition6(bool async) { - return AssertTranslationFailed( + return AssertTranslationFailedWithDetails( () => AssertQuery( async, ss => from c1 in ss.Set() @@ -1986,7 +1994,8 @@ where c1.IsLondon .Select(c => new { Foo = c }) .First().Foo.IsLondon select c1, - entryCount: 85)); + entryCount: 85), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } [ConditionalTheory] @@ -2614,12 +2623,13 @@ public virtual Task OrderBy_anon2(bool async) [MemberData(nameof(IsAsyncData))] public virtual Task OrderBy_client_mixed(bool async) { - return AssertTranslationFailed( + return AssertTranslationFailedWithDetails( () => AssertQuery( async, ss => ss.Set().OrderBy(c => c.IsLondon).ThenBy(c => c.CompanyName), assertOrder: true, - entryCount: 91)); + entryCount: 91), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } [ConditionalTheory] @@ -5919,5 +5929,27 @@ on c.CustomerID equals o.CustomerID Assert.All(arouts, t => Assert.Same(firstArout, t)); Assert.Empty(context.ChangeTracker.Entries()); } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Using_string_Equals_with_StringComparison_throws_informative_error(bool async) + { + return AssertTranslationFailedWithDetails( + () => AssertQuery( + async, + ss => ss.Set().Where(c => c.CustomerID.Equals("ALFKI", StringComparison.InvariantCulture))), + CoreStrings.QueryUnableToTranslateStringEqualsWithStringComparison); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Using_static_string_Equals_with_StringComparison_throws_informative_error(bool async) + { + return AssertTranslationFailedWithDetails( + () => AssertQuery( + async, + ss => ss.Set().Where(c => string.Equals(c.CustomerID, "ALFKI", StringComparison.InvariantCulture))), + CoreStrings.QueryUnableToTranslateStringEqualsWithStringComparison); + } } } diff --git a/test/EFCore.Specification.Tests/Query/NorthwindNavigationsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindNavigationsQueryTestBase.cs index 238270169ff..4ff9e169cf6 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindNavigationsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindNavigationsQueryTestBase.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.TestModels.Northwind; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; @@ -146,13 +147,14 @@ from o2 in ss.Set().Where(o => o.OrderID < 10400) [MemberData(nameof(IsAsyncData))] public virtual Task Select_Where_Navigation_Client(bool async) { - return AssertTranslationFailed( + return AssertTranslationFailedWithDetails( () => AssertQuery( async, ss => from o in ss.Set() where o.Customer.IsLondon select o, - entryCount: 46)); + entryCount: 46), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } [ConditionalTheory] @@ -603,7 +605,7 @@ public virtual Task Collection_select_nav_prop_all(bool async) [MemberData(nameof(IsAsyncData))] public virtual Task Collection_select_nav_prop_all_client(bool async) { - return AssertTranslationFailed( + return AssertTranslationFailedWithDetails( () => AssertQuery( async, ss => from c in ss.Set() @@ -612,7 +614,8 @@ orderby c.CustomerID ss => from c in ss.Set() orderby c.CustomerID select new { All = (c.Orders ?? new List()).All(o => false) }, - assertOrder: true)); + assertOrder: true), + CoreStrings.QueryUnableToTranslateMember(nameof(Order.ShipCity), nameof(Order))); } [ConditionalTheory] @@ -634,13 +637,14 @@ where c.Orders.All(o => o.CustomerID == "ALFKI") [MemberData(nameof(IsAsyncData))] public virtual Task Collection_where_nav_prop_all_client(bool async) { - return AssertTranslationFailed( + return AssertTranslationFailedWithDetails( () => AssertQuery( async, ss => from c in ss.Set() orderby c.CustomerID where c.Orders.All(o => o.ShipCity == "London") - select c)); + select c), + CoreStrings.QueryUnableToTranslateMember(nameof(Order.ShipCity), nameof(Order))); } [ConditionalTheory] diff --git a/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs index 7029721de67..3b33eb28537 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindWhereQueryTestBase.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.TestModels.Northwind; using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; @@ -591,11 +592,12 @@ where EF.Property(e, "Title") [MemberData(nameof(IsAsyncData))] public virtual Task Where_client(bool async) { - return AssertTranslationFailed( + return AssertTranslationFailedWithDetails( () => AssertQuery( async, ss => ss.Set().Where(c => c.IsLondon), - entryCount: 6)); + entryCount: 6), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } [ConditionalTheory] @@ -612,59 +614,64 @@ public virtual Task Where_subquery_correlated(bool async) [MemberData(nameof(IsAsyncData))] public virtual Task Where_subquery_correlated_client_eval(bool async) { - return AssertTranslationFailed( + return AssertTranslationFailedWithDetails( () => AssertQuery( async, ss => ss.Set() .OrderBy(c1 => c1.CustomerID) .Take(5) .Where(c1 => ss.Set().Any(c2 => c1.CustomerID == c2.CustomerID && c2.IsLondon)), - entryCount: 1)); + entryCount: 1), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Where_client_and_server_top_level(bool async) { - return AssertTranslationFailed( + return AssertTranslationFailedWithDetails( () => AssertQuery( async, ss => ss.Set().Where(c => c.IsLondon && c.CustomerID != "AROUT"), - entryCount: 5)); + entryCount: 5), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Where_client_or_server_top_level(bool async) { - return AssertTranslationFailed( + return AssertTranslationFailedWithDetails( () => AssertQuery( async, ss => ss.Set().Where(c => c.IsLondon || c.CustomerID == "ALFKI"), - entryCount: 7)); + entryCount: 7), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Where_client_and_server_non_top_level(bool async) { - return AssertTranslationFailed( + return AssertTranslationFailedWithDetails( () => AssertQuery( async, ss => ss.Set().Where(c => c.CustomerID != "ALFKI" == (c.IsLondon && c.CustomerID != "AROUT")), - entryCount: 6)); + entryCount: 6), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task Where_client_deep_inside_predicate_and_server_top_level(bool async) { - return AssertTranslationFailed( + return AssertTranslationFailedWithDetails( () => AssertQuery( async, ss => ss.Set() .Where(c => c.CustomerID != "ALFKI" && (c.CustomerID == "MAUMAR" || (c.CustomerID != "AROUT" && c.IsLondon))), - entryCount: 5)); + entryCount: 5), + CoreStrings.QueryUnableToTranslateMember(nameof(Customer.IsLondon), nameof(Customer))); } [ConditionalTheory] diff --git a/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs index f117d106de6..a53e4d1cce6 100644 --- a/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs @@ -785,6 +785,17 @@ public virtual async Task NoTracking_Include_with_cycles_does_not_throw_when_per } } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Trying_to_access_non_existent_indexer_property_throws_meaningful_exception(bool async) + { + return AssertTranslationFailedWithDetails( + () => AssertQuery( + async, + ss => ss.Set().Where(op => (bool)op["Foo"])), + CoreStrings.QueryUnableToTranslateMember("Foo", nameof(OwnedPerson))); + } + protected virtual DbContext CreateContext() => Fixture.CreateContext(); public abstract class OwnedQueryFixtureBase : SharedStoreFixtureBase, IQueryFixtureBase diff --git a/test/EFCore.Specification.Tests/Query/QueryTestBase.cs b/test/EFCore.Specification.Tests/Query/QueryTestBase.cs index 5fa2f444eee..fe62972594d 100644 --- a/test/EFCore.Specification.Tests/Query/QueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/QueryTestBase.cs @@ -1153,6 +1153,12 @@ protected static async Task AssertTranslationFailed(Func query) (await Assert.ThrowsAsync(query)) .Message); + protected static async Task AssertTranslationFailedWithDetails(Func query, string details) + => Assert.Contains( + CoreStrings.TranslationFailedWithDetails("", details).Substring(21), + (await Assert.ThrowsAsync(query)) + .Message); + #endregion } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs index 42dd81259b2..22e17cfd533 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs @@ -6723,8 +6723,9 @@ public void Cast_to_non_implemented_interface_is_not_removed_from_expression_tre () => queryBase.Cast().FirstOrDefault(x => x.Id == id)).Message; Assert.Equal( - CoreStrings.TranslationFailed(@"DbSet() .Cast() .Where(e => e.Id == __id_0)"), - message.Replace("\r", "").Replace("\n", "")); + CoreStrings.TranslationFailed( + @"DbSet() .Cast() .Where(e => e.Id == __id_0)"), + message.Replace("\r", "").Replace("\n", "")); } private SqlServerTestStore CreateDatabase18087()