Skip to content

Commit 1ef5b54

Browse files
committed
Support any expression type inside inline primitive collections
Closes #30732 Closes #30734
1 parent f2a3da4 commit 1ef5b54

File tree

43 files changed

+1373
-641
lines changed

Some content is hidden

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

43 files changed

+1373
-641
lines changed

src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EFCore.Cosmos/Properties/CosmosStrings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@
123123
<data name="CanConnectNotSupported" xml:space="preserve">
124124
<value>The Cosmos database does not support 'CanConnect' or 'CanConnectAsync'.</value>
125125
</data>
126+
<data name="CannotTranslateNonConstantNewArrayExpression" xml:space="preserve">
127+
<value>The query contained a new array expression containing non-constant elements, which could not be translated: '{newArrayExpression}'.</value>
128+
</data>
126129
<data name="ConnectionStringConflictingConfiguration" xml:space="preserve">
127130
<value>Both the connection string and CredentialToken, account key or account endpoint were specified. Specify only one set of connection details.</value>
128131
</data>

src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs

Lines changed: 36 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#nullable disable
55

66
using System.Collections;
7+
using System.Diagnostics.CodeAnalysis;
78
using Microsoft.EntityFrameworkCore.Cosmos.Internal;
89

910
namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
@@ -429,7 +430,9 @@ protected override Expression VisitMember(MemberExpression memberExpression)
429430
/// doing so can result in application failures when updating to a new Entity Framework Core release.
430431
/// </summary>
431432
protected override Expression VisitMemberInit(MemberInitExpression memberInitExpression)
432-
=> GetConstantOrNull(memberInitExpression);
433+
=> TryEvaluateToConstant(memberInitExpression, out var sqlConstantExpression)
434+
? sqlConstantExpression
435+
: QueryCompilationContext.NotTranslatedExpression;
433436

434437
/// <summary>
435438
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -603,7 +606,9 @@ static Expression RemoveObjectConvert(Expression expression)
603606
/// doing so can result in application failures when updating to a new Entity Framework Core release.
604607
/// </summary>
605608
protected override Expression VisitNew(NewExpression newExpression)
606-
=> GetConstantOrNull(newExpression);
609+
=> TryEvaluateToConstant(newExpression, out var sqlConstantExpression)
610+
? sqlConstantExpression
611+
: QueryCompilationContext.NotTranslatedExpression;
607612

608613
/// <summary>
609614
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -612,7 +617,15 @@ protected override Expression VisitNew(NewExpression newExpression)
612617
/// doing so can result in application failures when updating to a new Entity Framework Core release.
613618
/// </summary>
614619
protected override Expression VisitNewArray(NewArrayExpression newArrayExpression)
615-
=> null;
620+
{
621+
if (TryEvaluateToConstant(newArrayExpression, out var sqlConstantExpression))
622+
{
623+
return sqlConstantExpression;
624+
}
625+
626+
AddTranslationErrorDetails(CosmosStrings.CannotTranslateNonConstantNewArrayExpression(newArrayExpression.Print()));
627+
return QueryCompilationContext.NotTranslatedExpression;
628+
}
616629

617630
/// <summary>
618631
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -990,38 +1003,34 @@ private static List<TProperty> ParameterListValueExtractor<TEntity, TProperty>(
9901003
private static bool IsNullSqlConstantExpression(Expression expression)
9911004
=> expression is SqlConstantExpression sqlConstant && sqlConstant.Value == null;
9921005

993-
private static SqlConstantExpression GetConstantOrNull(Expression expression)
994-
=> CanEvaluate(expression)
995-
? new SqlConstantExpression(
1006+
private static bool TryEvaluateToConstant(Expression expression, out SqlConstantExpression sqlConstantExpression)
1007+
{
1008+
if (CanEvaluate(expression))
1009+
{
1010+
sqlConstantExpression = new SqlConstantExpression(
9961011
Expression.Constant(
9971012
Expression.Lambda<Func<object>>(Expression.Convert(expression, typeof(object)))
9981013
.Compile(preferInterpretation: true)
9991014
.Invoke(),
10001015
expression.Type),
1001-
null)
1002-
: null;
1016+
null);
1017+
return true;
1018+
}
1019+
1020+
sqlConstantExpression = null;
1021+
return false;
1022+
}
10031023

10041024
private static bool CanEvaluate(Expression expression)
1005-
{
1006-
#pragma warning disable IDE0066 // Convert switch statement to expression
1007-
switch (expression)
1008-
#pragma warning restore IDE0066 // Convert switch statement to expression
1025+
=> expression switch
10091026
{
1010-
case ConstantExpression:
1011-
return true;
1012-
1013-
case NewExpression newExpression:
1014-
return newExpression.Arguments.All(e => CanEvaluate(e));
1015-
1016-
case MemberInitExpression memberInitExpression:
1017-
return CanEvaluate(memberInitExpression.NewExpression)
1018-
&& memberInitExpression.Bindings.All(
1019-
mb => mb is MemberAssignment memberAssignment && CanEvaluate(memberAssignment.Expression));
1020-
1021-
default:
1022-
return false;
1023-
}
1024-
}
1027+
ConstantExpression => true,
1028+
NewExpression e => e.Arguments.All(CanEvaluate),
1029+
NewArrayExpression e => e.Expressions.All(CanEvaluate),
1030+
MemberInitExpression e => CanEvaluate(e.NewExpression)
1031+
&& e.Bindings.All(mb => mb is MemberAssignment memberAssignment && CanEvaluate(memberAssignment.Expression)),
1032+
_ => false
1033+
};
10251034

10261035
[DebuggerStepThrough]
10271036
private static bool TranslationFailed(Expression original, Expression translation, out SqlExpression castTranslation)

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

Lines changed: 15 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EFCore.Relational/Properties/RelationalStrings.resx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@
130130
<data name="CannotChangeWhenOpen" xml:space="preserve">
131131
<value>The instance of DbConnection is currently in use. The connection can only be changed when the existing connection is not being used.</value>
132132
</data>
133+
<data name="CannotTranslateNonConstantNewArrayExpression" xml:space="preserve">
134+
<value>The query contained a new array expression containing non-constant elements, which could not be translated: '{newArrayExpression}'.</value>
135+
</data>
133136
<data name="CannotConfigureTriggerNonRootTphEntity" xml:space="preserve">
134137
<value>Can't configure a trigger on entity type '{entityType}', which is in a TPH hierarchy and isn't the root. Configure the trigger on the TPH root entity type '{rootEntityType}' instead.</value>
135138
</data>
@@ -346,8 +349,8 @@
346349
<data name="DuplicateSeedDataSensitive" xml:space="preserve">
347350
<value>A seed entity for entity type '{entityType}' has the same key value {keyValue} as another seed entity mapped to the same table '{table}'. Key values should be unique across seed entities.</value>
348351
</data>
349-
<data name="EitherOfTwoValuesMustBeNull" xml:space="preserve">
350-
<value>Either {param1} or {param2} must be null.</value>
352+
<data name="OneOfThreeValuesMustBeSet" xml:space="preserve">
353+
<value>Exactly one of '{param1}', '{param2}' or '{param3}' must be set.</value>
351354
</data>
352355
<data name="EmptyCollectionNotSupportedAsInlineQueryRoot" xml:space="preserve">
353356
<value>Empty collections are not supported as inline query roots.</value>
@@ -911,8 +914,8 @@
911914
<data name="NoDbCommand" xml:space="preserve">
912915
<value>Cannot create a DbCommand for a non-relational query.</value>
913916
</data>
914-
<data name="NonConstantOrParameterAsInExpressionValues" xml:space="preserve">
915-
<value>Expression of type '{type}' isn't supported as the Values of an InExpression; only constants and parameters are supported.</value>
917+
<data name="NonConstantOrParameterAsInExpressionValue" xml:space="preserve">
918+
<value>Expression of type '{type}' isn't supported in the values of an InExpression; only constants and parameters are supported.</value>
916919
</data>
917920
<data name="NoneRelationalTypeMappingOnARelationalTypeMappingSource" xml:space="preserve">
918921
<value>'FindMapping' was called on a 'RelationalTypeMappingSource' with a non-relational 'TypeMappingInfo'.</value>

src/EFCore.Relational/Query/ISqlExpressionFactory.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -402,21 +402,29 @@ SqlFunctionExpression NiladicFunction(
402402
/// <returns>An expression representing an EXISTS operation in a SQL tree.</returns>
403403
ExistsExpression Exists(SelectExpression subquery);
404404

405+
/// <summary>
406+
/// Creates a new <see cref="InExpression" /> which represents an IN operation in a SQL tree.
407+
/// </summary>
408+
/// <param name="item">An item to look into values.</param>
409+
/// <param name="subquery">A subquery in which item is searched.</param>
410+
/// <returns>An expression representing an IN operation in a SQL tree.</returns>
411+
InExpression In(SqlExpression item, SelectExpression subquery);
412+
405413
/// <summary>
406414
/// Creates a new <see cref="InExpression" /> which represents an IN operation in a SQL tree.
407415
/// </summary>
408416
/// <param name="item">An item to look into values.</param>
409417
/// <param name="values">A list of values in which item is searched.</param>
410418
/// <returns>An expression representing an IN operation in a SQL tree.</returns>
411-
InExpression In(SqlExpression item, SqlExpression values);
419+
InExpression In(SqlExpression item, IReadOnlyList<SqlExpression> values);
412420

413421
/// <summary>
414422
/// Creates a new <see cref="InExpression" /> which represents an IN operation in a SQL tree.
415423
/// </summary>
416424
/// <param name="item">An item to look into values.</param>
417-
/// <param name="subquery">A subquery in which item is searched.</param>
425+
/// <param name="valuesParameter">A parameterized list of values in which the item is searched.</param>
418426
/// <returns>An expression representing an IN operation in a SQL tree.</returns>
419-
InExpression In(SqlExpression item, SelectExpression subquery);
427+
InExpression In(SqlExpression item, SqlParameterExpression valuesParameter);
420428

421429
/// <summary>
422430
/// Creates a new <see cref="InExpression" /> which represents a LIKE in a SQL tree.

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

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Collections;
45
using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
56

67
namespace Microsoft.EntityFrameworkCore.Query.Internal;
@@ -38,26 +39,48 @@ public ContainsTranslator(ISqlExpressionFactory sqlExpressionFactory)
3839
IReadOnlyList<SqlExpression> arguments,
3940
IDiagnosticsLogger<DbLoggerCategory.Query> logger)
4041
{
42+
SqlExpression? itemExpression = null, valuesExpression = null;
43+
44+
// Identify static Enumerable.Contains and instance List.Contains
4145
if (method.IsGenericMethod
42-
&& method.GetGenericMethodDefinition().Equals(EnumerableMethods.Contains)
46+
&& method.GetGenericMethodDefinition() == EnumerableMethods.Contains
4347
&& ValidateValues(arguments[0]))
4448
{
45-
return _sqlExpressionFactory.In(RemoveObjectConvert(arguments[1]), arguments[0]);
49+
(itemExpression, valuesExpression) = (RemoveObjectConvert(arguments[1]), arguments[0]);
4650
}
4751

4852
if (arguments.Count == 1
4953
&& method.IsContainsMethod()
5054
&& instance != null
5155
&& ValidateValues(instance))
5256
{
53-
return _sqlExpressionFactory.In(RemoveObjectConvert(arguments[0]), instance);
57+
(itemExpression, valuesExpression) = (RemoveObjectConvert(arguments[0]), instance);
58+
}
59+
60+
if (itemExpression is not null && valuesExpression is not null)
61+
{
62+
switch (valuesExpression)
63+
{
64+
case SqlParameterExpression parameter:
65+
return _sqlExpressionFactory.In(itemExpression, parameter);
66+
67+
case SqlConstantExpression { Value: IEnumerable values }:
68+
var valuesExpressions = new List<SqlExpression>();
69+
70+
foreach (var value in values)
71+
{
72+
valuesExpressions.Add(_sqlExpressionFactory.Constant(value));
73+
}
74+
75+
return _sqlExpressionFactory.In(itemExpression, valuesExpressions);
76+
}
5477
}
5578

5679
return null;
5780
}
5881

5982
private static bool ValidateValues(SqlExpression values)
60-
=> values is SqlConstantExpression || values is SqlParameterExpression;
83+
=> values is SqlConstantExpression or SqlParameterExpression;
6184

6285
private static SqlExpression RemoveObjectConvert(SqlExpression expression)
6386
=> expression is SqlUnaryExpression sqlUnaryExpression

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

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -130,38 +130,58 @@ public virtual Expression Expand(
130130
return _visitedFromSqlExpressions[fromSql] = fromSql.Update(
131131
Expression.Constant(new CompositeRelationalParameter(parameterExpression.Name!, subParameters)));
132132

133-
case ConstantExpression constantExpression:
134-
var existingValues = constantExpression.GetConstantValue<object?[]>();
133+
case ConstantExpression { Value: object?[] existingValues }:
134+
{
135135
var constantValues = new object?[existingValues.Length];
136136
for (var i = 0; i < existingValues.Length; i++)
137137
{
138-
var value = existingValues[i];
139-
if (value is DbParameter dbParameter)
140-
{
141-
var parameterName = _parameterNameGenerator.GenerateNext();
142-
if (string.IsNullOrEmpty(dbParameter.ParameterName))
143-
{
144-
dbParameter.ParameterName = parameterName;
145-
}
146-
else
147-
{
148-
parameterName = dbParameter.ParameterName;
149-
}
138+
constantValues[i] = ProcessConstantValue(existingValues[i]);
139+
}
150140

151-
constantValues[i] = new RawRelationalParameter(parameterName, dbParameter);
152-
}
153-
else
141+
return _visitedFromSqlExpressions[fromSql] = fromSql.Update(Expression.Constant(constantValues, typeof(object[])));
142+
}
143+
144+
case NewArrayExpression { Expressions: var expressions }:
145+
{
146+
var constantValues = new object?[expressions.Count];
147+
for (var i = 0; i < constantValues.Length; i++)
148+
{
149+
if (expressions[i] is not SqlConstantExpression { Value: var existingValue })
154150
{
155-
constantValues[i] = _sqlExpressionFactory.Constant(
156-
value, _typeMappingSource.GetMappingForValue(value));
151+
Check.DebugFail("FromSql.Arguments must be Constant/ParameterExpression");
152+
throw new InvalidOperationException();
157153
}
154+
155+
constantValues[i] = ProcessConstantValue(existingValue);
158156
}
159157

160158
return _visitedFromSqlExpressions[fromSql] = fromSql.Update(Expression.Constant(constantValues, typeof(object[])));
159+
}
161160

162161
default:
163162
Check.DebugFail("FromSql.Arguments must be Constant/ParameterExpression");
164163
return null;
165164
}
165+
166+
object ProcessConstantValue(object? existingConstantValue)
167+
{
168+
if (existingConstantValue is DbParameter dbParameter)
169+
{
170+
var parameterName = _parameterNameGenerator.GenerateNext();
171+
if (string.IsNullOrEmpty(dbParameter.ParameterName))
172+
{
173+
dbParameter.ParameterName = parameterName;
174+
}
175+
else
176+
{
177+
parameterName = dbParameter.ParameterName;
178+
}
179+
180+
return new RawRelationalParameter(parameterName, dbParameter);
181+
}
182+
183+
return _sqlExpressionFactory.Constant(
184+
existingConstantValue, _typeMappingSource.GetMappingForValue(existingConstantValue));
185+
}
166186
}
167187
}

0 commit comments

Comments
 (0)