Skip to content

Commit 93e05c7

Browse files
committed
Support any expression type inside inline primitive collections
Closes #30732 Closes #30734
1 parent fc5be30 commit 93e05c7

File tree

39 files changed

+1321
-606
lines changed

39 files changed

+1321
-606
lines changed

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)