diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs
index 527c56ca5d4..67c08640c24 100644
--- a/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs
+++ b/src/EFCore.Cosmos/Properties/CosmosStrings.Designer.cs
@@ -37,6 +37,12 @@ public static string AnalyticalTTLMismatch(object? ttl1, object? entityType1, ob
public static string CanConnectNotSupported
=> GetString("CanConnectNotSupported");
+ ///
+ /// Complex projections in subqueries are currently unsupported.
+ ///
+ public static string ComplexProjectionInSubqueryNotSupported
+ => GetString("ComplexProjectionInSubqueryNotSupported");
+
///
/// None of connection string, CredentialToken, account key or account endpoint were specified. Specify a set of connection details.
///
diff --git a/src/EFCore.Cosmos/Properties/CosmosStrings.resx b/src/EFCore.Cosmos/Properties/CosmosStrings.resx
index d15bd153984..10552b282e4 100644
--- a/src/EFCore.Cosmos/Properties/CosmosStrings.resx
+++ b/src/EFCore.Cosmos/Properties/CosmosStrings.resx
@@ -123,6 +123,9 @@
The Cosmos database does not support 'CanConnect' or 'CanConnectAsync'.
+
+ Complex projections in subqueries are currently unsupported.
+
None of connection string, CredentialToken, account key or account endpoint were specified. Specify a set of connection details.
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs
index b4fe51bb07c..92a985d7b23 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs
@@ -283,16 +283,11 @@ protected override Expression VisitSelect(SelectExpression selectExpression)
{
// If the SELECT projects a single value out, we just project that with the Cosmos VALUE keyword (without VALUE,
// Cosmos projects a JSON object containing the value).
- if (selectExpression.UsesSingleValueProjection)
+ // TODO: Ideally, just always use VALUE for all single-projection SELECTs - but this like requires shaper changes.
+ if (selectExpression.UsesSingleValueProjection && projection is [var singleProjection])
{
_sqlBuilder.Append("VALUE ");
- if (projection is not [var singleProjection])
- {
- throw new UnreachableException(
- $"Encountered SelectExpression with UsesValueProject=true and Projection.Count={projection.Count}.");
- }
-
Visit(singleProjection.Expression);
}
// Otherwise, we'll project a JSON object; Cosmos has two syntaxes for doing so:
@@ -319,16 +314,19 @@ protected override Expression VisitSelect(SelectExpression selectExpression)
_sqlBuilder.Append('1');
}
- if (selectExpression.Sources.Count > 0)
+ var sources = selectExpression.Sources;
+ if (sources.Count > 0)
{
- if (selectExpression.Sources.Count > 1)
- {
- throw new NotImplementedException("JOINs not yet supported");
- }
-
_sqlBuilder.AppendLine().Append("FROM ");
- Visit(selectExpression.Sources[0]);
+ Visit(sources[0]);
+
+ for (var i = 1; i < sources.Count; i++)
+ {
+ _sqlBuilder.AppendLine().Append("JOIN ");
+
+ Visit(sources[i]);
+ }
}
if (selectExpression.Predicate != null)
@@ -752,11 +750,11 @@ protected sealed override Expression VisitSource(SourceExpression sourceExpressi
.Append(" IN ");
- VisitContainerExpression(sourceExpression.ContainerExpression);
+ VisitContainerExpression(sourceExpression.Expression);
}
else
{
- VisitContainerExpression(sourceExpression.ContainerExpression);
+ VisitContainerExpression(sourceExpression.Expression);
if (sourceExpression.Alias is not null)
{
@@ -795,7 +793,7 @@ void VisitContainerExpression(Expression containerExpression)
}
}
- Visit(sourceExpression.ContainerExpression);
+ Visit(sourceExpression.Expression);
if (subquery)
{
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryUtils.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryUtils.cs
index 8d56fcb8de1..da9deb08c5b 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryUtils.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryUtils.cs
@@ -191,7 +191,7 @@ public static bool TryExtractBareArray(
{
WithIn: true,
Alias: var sourceAlias,
- ContainerExpression: SelectExpression
+ Expression: SelectExpression
{
Sources: [],
Predicate: null,
@@ -212,7 +212,7 @@ public static bool TryExtractBareArray(
// For properties: SELECT i FROM i IN c.SomeArray
// So just match any SelectExpression with IN.
- case { Sources: [{ WithIn: true, ContainerExpression: var a, Alias: var sourceAlias }] }
+ case { Sources: [{ WithIn: true, Expression: var a, Alias: var sourceAlias }] }
when projectedReferenceName == sourceAlias:
{
array = a;
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs
index 42f6403f082..1aba18a409f 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs
@@ -22,7 +22,7 @@ public class CosmosQueryableMethodTranslatingExpressionVisitor : QueryableMethod
private readonly IMethodCallTranslatorProvider _methodCallTranslatorProvider;
private readonly CosmosSqlTranslatingExpressionVisitor _sqlTranslator;
private readonly CosmosProjectionBindingExpressionVisitor _projectionBindingExpressionVisitor;
- private readonly bool _subquery;
+ private bool _subquery;
private ReadItemInfo? _readItemExpression;
///
@@ -237,7 +237,7 @@ static bool ExtractPartitionKeyFromPredicate(
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
{
var method = methodCallExpression.Method;
- if (method.DeclaringType == typeof(Queryable))
+ if (method.DeclaringType == typeof(Queryable) && method.IsGenericMethod)
{
switch (methodCallExpression.Method.Name)
{
@@ -370,7 +370,7 @@ private ShapedQueryExpression CreateShapedQueryExpression(IEntityType entityType
new StructuralTypeShaperExpression(
entityType,
new ProjectionBindingExpression(queryExpression, new ProjectionMember(), typeof(ValueBuffer)),
- false));
+ nullable: false));
}
private ShapedQueryExpression CreateShapedQueryExpression(SelectExpression select, Type elementClrType)
@@ -994,7 +994,7 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s
return null!;
}
- var newSelectorBody = ReplacingExpressionVisitor.Replace(selector.Parameters.Single(), source.ShaperExpression, selector.Body);
+ var newSelectorBody = RemapLambdaBody(source, selector);
return source.UpdateShaperExpression(_projectionBindingExpressionVisitor.Translate(selectExpression, newSelectorBody));
}
@@ -1009,16 +1009,53 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s
ShapedQueryExpression source,
LambdaExpression collectionSelector,
LambdaExpression resultSelector)
- => null;
+ {
+ var collectionSelectorBody = RemapLambdaBody(source, collectionSelector);
- ///
- /// 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.
- ///
+ // The collection selector gets translated in subquery context; specifically, if an uncorrelated SelectMany() is attempted
+ // (from b in context.Blogs from p in context.Posts...), we want to detect that and fail translation as an uncorrelated query
+ // (see VisitExtension visitation for EntityQueryRootExpression)
+ var previousSubquery = _subquery;
+ _subquery = true;
+ try
+ {
+ if (Visit(collectionSelectorBody) is ShapedQueryExpression inner)
+ {
+ var select = (SelectExpression)source.QueryExpression;
+ var shaper = select.AddJoin(inner, source.ShaperExpression);
+
+ return TranslateTwoParameterSelector(source.UpdateShaperExpression(shaper), resultSelector);
+ }
+
+ return null;
+ }
+ finally
+ {
+ _subquery = previousSubquery;
+ }
+ }
+
+ ///
protected override ShapedQueryExpression? TranslateSelectMany(ShapedQueryExpression source, LambdaExpression selector)
- => null;
+ {
+ // TODO: Note that we currently never actually seem to get SelectMany without a result selector, because nav expansion rewrites
+ // that to a more complex variant with a result selector (see https://github.com/dotnet/efcore/issues/32957#issuecomment-2170950767)
+ // blogs.SelectMany(c => c.Ints) becomes:
+ // blogs
+ // .SelectMany(p => Property(p, "Ints").AsQueryable(), (p, c) => new TransparentIdentifier`2(Outer = p, Inner = c))
+ // .Select(ti => ti.Inner)
+
+ // TODO: In Cosmos, we currently always add a predicate for the discriminator (unless HasNoDiscriminator is explicitly specified),
+ // so the source is almost never a bare array.
+ // If we stop doing that (see #34005, #20268), and we remove the result selector problem (see just above), we should check if the
+ // source is a bare array, and simply return the ShapedQueryExpression returned from visiting the collection selector. This would
+ // remove the extra unneeded JOIN we'd currently generate.
+ var innerParameter = Expression.Parameter(selector.ReturnType.GetSequenceType(), "i");
+ var resultSelector = Expression.Lambda(
+ innerParameter, Expression.Parameter(source.Type.GetSequenceType()), innerParameter);
+
+ return TranslateSelectMany(source, selector, resultSelector);
+ }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -1691,14 +1728,11 @@ when methodCallExpression.TryGetIndexerArguments(_queryCompilationContext.Model,
private SqlExpression? TranslateLambdaExpression(
ShapedQueryExpression shapedQueryExpression,
LambdaExpression lambdaExpression)
- {
- var lambdaBody = RemapLambdaBody(shapedQueryExpression.ShaperExpression, lambdaExpression);
-
- return TranslateExpression(lambdaBody);
- }
+ => TranslateExpression(RemapLambdaBody(shapedQueryExpression, lambdaExpression));
- private static Expression RemapLambdaBody(Expression shaperBody, LambdaExpression lambdaExpression)
- => ReplacingExpressionVisitor.Replace(lambdaExpression.Parameters.Single(), shaperBody, lambdaExpression.Body);
+ private Expression RemapLambdaBody(ShapedQueryExpression shapedQueryExpression, LambdaExpression lambdaExpression)
+ => ReplacingExpressionVisitor.Replace(
+ lambdaExpression.Parameters.Single(), shapedQueryExpression.ShaperExpression, lambdaExpression.Body);
private static ShapedQueryExpression AggregateResultShaper(
ShapedQueryExpression source,
@@ -1743,4 +1777,28 @@ private static ShapedQueryExpression AggregateResultShaper(
return source.UpdateShaperExpression(shaper);
}
+
+ private ShapedQueryExpression TranslateTwoParameterSelector(ShapedQueryExpression source, LambdaExpression resultSelector)
+ {
+ var transparentIdentifierType = source.ShaperExpression.Type;
+ var transparentIdentifierParameter = Expression.Parameter(transparentIdentifierType);
+
+ Expression original1 = resultSelector.Parameters[0];
+ var replacement1 = AccessField(transparentIdentifierType, transparentIdentifierParameter, "Outer");
+ Expression original2 = resultSelector.Parameters[1];
+ var replacement2 = AccessField(transparentIdentifierType, transparentIdentifierParameter, "Inner");
+ var newResultSelector = Expression.Lambda(
+ new ReplacingExpressionVisitor(
+ new[] { original1, original2 }, new[] { replacement1, replacement2 })
+ .Visit(resultSelector.Body),
+ transparentIdentifierParameter);
+
+ return TranslateSelect(source, newResultSelector);
+ }
+
+ private static Expression AccessField(
+ Type transparentIdentifierType,
+ Expression targetExpression,
+ string fieldName)
+ => Expression.Field(targetExpression, transparentIdentifierType.GetTypeInfo().GetDeclaredField(fieldName)!);
}
diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs
index b2a829ea311..cf593cb0531 100644
--- a/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs
+++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs
@@ -119,7 +119,7 @@ private SelectExpression()
/// 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 static SelectExpression CreateForCollection(Expression containerExpression, string sourceAlias, Expression projection)
+ public static SelectExpression CreateForCollection(Expression sourceExpression, string sourceAlias, Expression projection)
{
// SelectExpressions representing bare arrays are of the form SELECT VALUE i FROM i IN x.
// Unfortunately, Cosmos doesn't support x being anything but a root container or a property access
@@ -127,25 +127,18 @@ public static SelectExpression CreateForCollection(Expression containerExpressio
// For example, x cannot be a function invocation (SELECT VALUE i FROM i IN SetUnion(...)) or an array constant
// (SELECT VALUE i FROM i IN [1,2,3]).
// So we wrap any non-property in a subquery as follows: SELECT i FROM i IN (SELECT VALUE [1,2,3])
- switch (containerExpression)
- {
- case ObjectReferenceExpression:
- case ScalarReferenceExpression:
- case ObjectArrayAccessExpression:
- case ScalarAccessExpression:
- break;
- default:
- containerExpression = new SelectExpression(
- [new ProjectionExpression(containerExpression, null!)],
- sources: [],
- orderings: [])
- {
- UsesSingleValueProjection = true
- };
- break;
+ if (!SourceExpression.IsCompatible(sourceExpression))
+ {
+ sourceExpression = new SelectExpression(
+ [new ProjectionExpression(sourceExpression, null!)],
+ sources: [],
+ orderings: [])
+ {
+ UsesSingleValueProjection = true
+ };
}
- var source = new SourceExpression(containerExpression, sourceAlias, withIn: true);
+ var source = new SourceExpression(sourceExpression, sourceAlias, withIn: true);
return new SelectExpression
{
@@ -517,6 +510,86 @@ public virtual void ReverseOrderings()
}
}
+ ///
+ /// 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 Expression AddJoin(ShapedQueryExpression inner, Expression outerShaper)
+ {
+ var (innerSelect, innerShaper) = ((SelectExpression)inner.QueryExpression, inner.ShaperExpression);
+
+ // TODO: Proper alias management (#33894).
+ // Create a new source (JOIN) for the server side of the query; if the inner query represents a bare array, unwrap it and
+ // add the JOIN directly
+ var joinSource = CosmosQueryUtils.TryExtractBareArray(inner, out var bareArray) && SourceExpression.IsCompatible(bareArray)
+ ? new SourceExpression(bareArray, "a", withIn: true)
+ : new SourceExpression(innerSelect, "a");
+
+ // Make the necessary modifications to the shaper side, projecting out a TransparentIdentifier (outer/inner)
+ var transparentIdentifierType = TransparentIdentifierFactory.Create(outerShaper.Type, innerShaper.Type);
+ var outerMemberInfo = transparentIdentifierType.GetTypeInfo().GetDeclaredField("Outer")!;
+ var innerMemberInfo = transparentIdentifierType.GetTypeInfo().GetDeclaredField("Inner")!;
+
+ var projectionMapping = new Dictionary();
+ var mapping = new Dictionary();
+
+ foreach (var (projectionMember, expression) in _projectionMapping)
+ {
+ var remappedProjectionMember = projectionMember.Prepend(outerMemberInfo);
+ mapping[projectionMember] = remappedProjectionMember;
+ projectionMapping[remappedProjectionMember] = expression;
+ }
+
+ outerShaper = new ProjectionMemberRemappingExpressionVisitor(this, mapping).Visit(outerShaper);
+ mapping.Clear();
+
+ foreach (var (projectionMember, expression) in innerSelect._projectionMapping)
+ {
+ var remappedProjectionMember = projectionMember.Prepend(innerMemberInfo);
+ mapping[projectionMember] = remappedProjectionMember;
+
+ Expression projectionToAdd;
+ if (projectionMember.Last is null)
+ {
+ projectionToAdd = expression switch
+ {
+ SqlExpression e => new ScalarReferenceExpression(joinSource.Alias, e.Type, e.TypeMapping),
+ EntityProjectionExpression e => e.Update(new ObjectReferenceExpression(e.EntityType, joinSource.Alias)),
+
+ _ => throw new UnreachableException(
+ $"Unexpected expression type in projection when adding join: {expression.GetType().Name}")
+ };
+ }
+ else
+ {
+ // TODO: #34004
+ // The subquery is projecting out a JSON object; for the projection mapping of the outer query, we need to generate
+ // property accesses over that object: Scalar/ObjectAccessExpressions over the ObjectReferenceExpression that references
+ // the JOIN source.
+ // However, the JSON object being projected out of the subquery doesn't correspond to any entity type, and there's currently
+ // no way for us to represent a reference to that - ObjectReferenceExpression requires an IEntityType. Changing that
+ // requires shaper-side changes (see comment in ObjectReferenceExpression); if we can remove that requirement, we can
+ // possibly also merge ScalarReferenceExpression and ObjectReferenceExpression to a single SourceReferenceExpression.
+ throw new InvalidOperationException(CosmosStrings.ComplexProjectionInSubqueryNotSupported);
+ }
+
+ projectionMapping[remappedProjectionMember] = projectionToAdd;
+ }
+
+ innerSelect.ApplyProjection();
+ _sources.Add(joinSource);
+
+ innerShaper = new ProjectionMemberRemappingExpressionVisitor(this, mapping).Visit(innerShaper);
+ _projectionMapping = projectionMapping;
+ innerSelect._projectionMapping.Clear();
+
+ return New(
+ transparentIdentifierType.GetTypeInfo().DeclaredConstructors.Single(),
+ new[] { outerShaper, innerShaper }, outerMemberInfo, innerMemberInfo);
+ }
+
///
/// 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
@@ -732,11 +805,16 @@ private void PrintSql(ExpressionPrinter expressionPrinter, bool withTags = true)
expressionPrinter.Append("1");
}
- if (Sources.Any())
+ if (Sources.Count > 0)
{
expressionPrinter.AppendLine().Append("FROM ");
+ expressionPrinter.Visit(Sources[0]);
- expressionPrinter.VisitCollection(Sources, p => p.AppendLine());
+ for (var i = 1; i < Sources.Count; i++)
+ {
+ expressionPrinter.AppendLine().Append("JOIN ");
+ expressionPrinter.Visit(Sources[i]);
+ }
}
if (Predicate != null)
@@ -787,4 +865,27 @@ public virtual string DebugView
=> this.Print();
#endregion Print
+
+ private sealed class ProjectionMemberRemappingExpressionVisitor(
+ SelectExpression queryExpression,
+ Dictionary projectionMemberMappings)
+ : ExpressionVisitor
+ {
+ protected override Expression VisitExtension(Expression expression)
+ {
+ if (expression is ProjectionBindingExpression projectionBindingExpression)
+ {
+ Check.DebugAssert(
+ projectionBindingExpression.ProjectionMember is not null,
+ "ProjectionBindingExpression must have projection member.");
+
+ return new ProjectionBindingExpression(
+ queryExpression,
+ projectionMemberMappings[projectionBindingExpression.ProjectionMember],
+ projectionBindingExpression.Type);
+ }
+
+ return base.VisitExtension(expression);
+ }
+ }
}
diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SourceExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SourceExpression.cs
index f51e9e5f471..8c0a8a716af 100644
--- a/src/EFCore.Cosmos/Query/Internal/Expressions/SourceExpression.cs
+++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SourceExpression.cs
@@ -12,9 +12,26 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
///
/// FROM clause (NoSQL query)
[DebuggerDisplay("{Microsoft.EntityFrameworkCore.Query.ExpressionPrinter.Print(this), nq}")]
-public class SourceExpression(Expression containerExpression, string alias, bool withIn = false)
- : Expression, IAccessExpression, IPrintableExpression
+public class SourceExpression : Expression, IAccessExpression, IPrintableExpression
{
+ ///
+ /// 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 SourceExpression(Expression expression, string alias, bool withIn = false)
+ {
+ if (!IsCompatible(expression))
+ {
+ throw new ArgumentException($"Expression type '{expression.GetType().Name}' cannot appear directly in a source expression");
+ }
+
+ Expression = expression;
+ Alias = alias;
+ WithIn = withIn;
+ }
+
///
/// 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
@@ -31,7 +48,7 @@ public sealed override ExpressionType NodeType
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public override Type Type
- => ContainerExpression.Type;
+ => Expression.Type;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -39,7 +56,7 @@ public override Type Type
/// 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 Expression ContainerExpression { get; } = containerExpression;
+ public virtual Expression Expression { get; }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -47,7 +64,7 @@ public override Type Type
/// 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 Alias { get; } = alias;
+ public virtual string Alias { get; }
///
/// Specifies that the source uses IN, and will be generated as FROM x IN c.Tags
@@ -58,7 +75,7 @@ public override Type Type
/// 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 bool WithIn { get; } = withIn;
+ public virtual bool WithIn { get; }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -76,7 +93,7 @@ string IAccessExpression.PropertyName
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
protected override Expression VisitChildren(ExpressionVisitor visitor)
- => Update(visitor.Visit(ContainerExpression));
+ => Update(visitor.Visit(Expression));
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -85,10 +102,33 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public virtual SourceExpression Update(Expression containerExpression)
- => ReferenceEquals(containerExpression, ContainerExpression)
+ => ReferenceEquals(containerExpression, Expression)
? this
: new SourceExpression(containerExpression, Alias);
+ ///
+ /// Returns whether the given expression type can appear directly within a source expression.
+ ///
+ ///
+ /// 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 static bool IsCompatible(Expression expression)
+ => expression switch
+ {
+ ObjectReferenceExpression => true,
+ ScalarReferenceExpression => true,
+ ObjectArrayAccessExpression => true,
+ ScalarAccessExpression => true,
+
+ SelectExpression => true,
+ FromSqlExpression => true,
+
+ _ => false
+ };
+
///
/// 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
@@ -109,11 +149,11 @@ public void Print(ExpressionPrinter expressionPrinter)
expressionPrinter
.Append(Alias)
.Append(" IN ");
- expressionPrinter.Visit(ContainerExpression);
+ expressionPrinter.Visit(Expression);
}
else
{
- expressionPrinter.Visit(ContainerExpression);
+ expressionPrinter.Visit(Expression);
expressionPrinter
.Append(" AS ")
.Append(Alias);
@@ -134,7 +174,7 @@ private bool Equals(SourceExpression? other)
|| (other is not null
&& Alias == other.Alias
&& WithIn == other.WithIn
- && ContainerExpression.Equals(other.ContainerExpression));
+ && Expression.Equals(other.Expression));
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -145,7 +185,7 @@ private bool Equals(SourceExpression? other)
public override int GetHashCode()
{
var hashCode = new HashCode();
- hashCode.Add(ContainerExpression);
+ hashCode.Add(Expression);
hashCode.Add(Alias);
hashCode.Add(WithIn);
return hashCode.ToHashCode();
diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.Helper.cs b/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.Helper.cs
index 912b08fccbe..bef8d85ee42 100644
--- a/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.Helper.cs
+++ b/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.Helper.cs
@@ -61,35 +61,26 @@ void IDisposable.Dispose()
}
}
- private sealed class ProjectionMemberRemappingExpressionVisitor : ExpressionVisitor
+ private sealed class ProjectionMemberRemappingExpressionVisitor(
+ Expression queryExpression,
+ Dictionary projectionMemberMappings)
+ : ExpressionVisitor
{
- private readonly Expression _queryExpression;
- private readonly Dictionary _projectionMemberMappings;
-
- public ProjectionMemberRemappingExpressionVisitor(
- Expression queryExpression,
- Dictionary projectionMemberMappings)
- {
- _queryExpression = queryExpression;
- _projectionMemberMappings = projectionMemberMappings;
- }
-
- [return: NotNullIfNotNull(nameof(expression))]
- public override Expression? Visit(Expression? expression)
+ protected override Expression VisitExtension(Expression expression)
{
if (expression is ProjectionBindingExpression projectionBindingExpression)
{
Check.DebugAssert(
- projectionBindingExpression.ProjectionMember != null,
+ projectionBindingExpression.ProjectionMember is not null,
"ProjectionBindingExpression must have projection member.");
return new ProjectionBindingExpression(
- _queryExpression,
- _projectionMemberMappings[projectionBindingExpression.ProjectionMember],
+ queryExpression,
+ projectionMemberMappings[projectionBindingExpression.ProjectionMember],
projectionBindingExpression.Type);
}
- return base.Visit(expression);
+ return base.VisitExtension(expression);
}
}
diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
index a52124103fd..20115136354 100644
--- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs
@@ -1094,6 +1094,7 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s
LambdaExpression collectionSelector,
LambdaExpression resultSelector)
{
+ var select = (SelectExpression)source.QueryExpression;
var (newCollectionSelector, correlated, defaultIfEmpty)
= new CorrelationFindingExpressionVisitor().IsCorrelated(collectionSelector);
if (correlated)
@@ -1101,10 +1102,9 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s
var collectionSelectorBody = RemapLambdaBody(source, newCollectionSelector);
if (Visit(collectionSelectorBody) is ShapedQueryExpression inner)
{
- var innerSelectExpression = (SelectExpression)source.QueryExpression;
var shaper = defaultIfEmpty
- ? innerSelectExpression.AddOuterApply(inner, source.ShaperExpression)
- : innerSelectExpression.AddCrossApply(inner, source.ShaperExpression);
+ ? select.AddOuterApply(inner, source.ShaperExpression)
+ : select.AddCrossApply(inner, source.ShaperExpression);
return TranslateTwoParameterSelector(source.UpdateShaperExpression(shaper), resultSelector);
}
@@ -1124,8 +1124,7 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s
inner = translatedInner;
}
- var innerSelectExpression = (SelectExpression)source.QueryExpression;
- var shaper = innerSelectExpression.AddCrossJoin(inner, source.ShaperExpression);
+ var shaper = select.AddCrossJoin(inner, source.ShaperExpression);
return TranslateTwoParameterSelector(source.UpdateShaperExpression(shaper), resultSelector);
}
diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs
index 16c8390cef6..40e1c668e36 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs
@@ -55,35 +55,26 @@ public bool ContainsOuterReference(SelectExpression selectExpression)
}
}
- private sealed class ProjectionMemberRemappingExpressionVisitor : ExpressionVisitor
+ private sealed class ProjectionMemberRemappingExpressionVisitor(
+ SelectExpression queryExpression,
+ Dictionary projectionMemberMappings)
+ : ExpressionVisitor
{
- private readonly SelectExpression _queryExpression;
- private readonly Dictionary _projectionMemberMappings;
-
- public ProjectionMemberRemappingExpressionVisitor(
- SelectExpression queryExpression,
- Dictionary projectionMemberMappings)
- {
- _queryExpression = queryExpression;
- _projectionMemberMappings = projectionMemberMappings;
- }
-
- [return: NotNullIfNotNull(nameof(expression))]
- public override Expression? Visit(Expression? expression)
+ protected override Expression VisitExtension(Expression expression)
{
if (expression is ProjectionBindingExpression projectionBindingExpression)
{
Check.DebugAssert(
- projectionBindingExpression.ProjectionMember != null,
+ projectionBindingExpression.ProjectionMember is not null,
"ProjectionBindingExpression must have projection member.");
return new ProjectionBindingExpression(
- _queryExpression,
- _projectionMemberMappings[projectionBindingExpression.ProjectionMember],
+ queryExpression,
+ projectionMemberMappings[projectionBindingExpression.ProjectionMember],
projectionBindingExpression.Type);
}
- return base.Visit(expression);
+ return base.VisitExtension(expression);
}
}
diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
index fbbf01b9b5a..3754f1030fe 100644
--- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
+++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
@@ -2728,18 +2728,18 @@ private enum JoinType
private Expression AddJoin(
JoinType joinType,
- SelectExpression innerSelectExpression,
+ SelectExpression innerSelect,
Expression outerShaper,
Expression innerShaper,
SqlExpression? joinPredicate = null)
{
- AddJoin(joinType, ref innerSelectExpression, out _, joinPredicate);
+ AddJoin(joinType, ref innerSelect, out _, joinPredicate);
var transparentIdentifierType = TransparentIdentifierFactory.Create(outerShaper.Type, innerShaper.Type);
var outerMemberInfo = transparentIdentifierType.GetTypeInfo().GetDeclaredField("Outer")!;
var innerMemberInfo = transparentIdentifierType.GetTypeInfo().GetDeclaredField("Inner")!;
var outerClientEval = _clientProjections.Count > 0;
- var innerClientEval = innerSelectExpression._clientProjections.Count > 0;
+ var innerClientEval = innerSelect._clientProjections.Count > 0;
var innerNullable = joinType is JoinType.LeftJoin or JoinType.OuterApply;
if (outerClientEval)
@@ -2748,25 +2748,25 @@ private Expression AddJoin(
if (innerClientEval)
{
// Add inner to projection and update indexes
- var indexMap = new int[innerSelectExpression._clientProjections.Count];
- for (var i = 0; i < innerSelectExpression._clientProjections.Count; i++)
+ var indexMap = new int[innerSelect._clientProjections.Count];
+ for (var i = 0; i < innerSelect._clientProjections.Count; i++)
{
- var projectionToAdd = innerSelectExpression._clientProjections[i];
+ var projectionToAdd = innerSelect._clientProjections[i];
projectionToAdd = MakeNullable(projectionToAdd, innerNullable);
_clientProjections.Add(projectionToAdd);
- _aliasForClientProjections.Add(innerSelectExpression._aliasForClientProjections[i]);
+ _aliasForClientProjections.Add(innerSelect._aliasForClientProjections[i]);
indexMap[i] = _clientProjections.Count - 1;
}
- innerSelectExpression._clientProjections.Clear();
- innerSelectExpression._aliasForClientProjections.Clear();
+ innerSelect._clientProjections.Clear();
+ innerSelect._aliasForClientProjections.Clear();
- innerShaper = new ProjectionIndexRemappingExpressionVisitor(innerSelectExpression, this, indexMap).Visit(innerShaper);
+ innerShaper = new ProjectionIndexRemappingExpressionVisitor(innerSelect, this, indexMap).Visit(innerShaper);
}
else
{
// Apply inner projection mapping and convert projection member binding to indexes
- var mapping = ConvertProjectionMappingToClientProjections(innerSelectExpression._projectionMapping, innerNullable);
+ var mapping = ConvertProjectionMappingToClientProjections(innerSelect._projectionMapping, innerNullable);
innerShaper = new ProjectionMemberToIndexConvertingExpressionVisitor(this, mapping).Visit(innerShaper);
}
}
@@ -2779,20 +2779,20 @@ private Expression AddJoin(
var mapping = ConvertProjectionMappingToClientProjections(_projectionMapping);
outerShaper = new ProjectionMemberToIndexConvertingExpressionVisitor(this, mapping).Visit(outerShaper);
- var indexMap = new int[innerSelectExpression._clientProjections.Count];
- for (var i = 0; i < innerSelectExpression._clientProjections.Count; i++)
+ var indexMap = new int[innerSelect._clientProjections.Count];
+ for (var i = 0; i < innerSelect._clientProjections.Count; i++)
{
- var projectionToAdd = innerSelectExpression._clientProjections[i];
+ var projectionToAdd = innerSelect._clientProjections[i];
projectionToAdd = MakeNullable(projectionToAdd, innerNullable);
_clientProjections.Add(projectionToAdd);
- _aliasForClientProjections.Add(innerSelectExpression._aliasForClientProjections[i]);
+ _aliasForClientProjections.Add(innerSelect._aliasForClientProjections[i]);
indexMap[i] = _clientProjections.Count - 1;
}
- innerSelectExpression._clientProjections.Clear();
- innerSelectExpression._aliasForClientProjections.Clear();
+ innerSelect._clientProjections.Clear();
+ innerSelect._aliasForClientProjections.Clear();
- innerShaper = new ProjectionIndexRemappingExpressionVisitor(innerSelectExpression, this, indexMap).Visit(innerShaper);
+ innerShaper = new ProjectionIndexRemappingExpressionVisitor(innerSelect, this, indexMap).Visit(innerShaper);
}
else
{
@@ -2809,19 +2809,18 @@ private Expression AddJoin(
outerShaper = new ProjectionMemberRemappingExpressionVisitor(this, mapping).Visit(outerShaper);
mapping.Clear();
- foreach (var projection in innerSelectExpression._projectionMapping)
+ foreach (var (projectionMember, expression) in innerSelect._projectionMapping)
{
- var projectionMember = projection.Key;
- var remappedProjectionMember = projection.Key.Prepend(innerMemberInfo);
+ var remappedProjectionMember = projectionMember.Prepend(innerMemberInfo);
mapping[projectionMember] = remappedProjectionMember;
- var projectionToAdd = projection.Value;
+ var projectionToAdd = expression;
projectionToAdd = MakeNullable(projectionToAdd, innerNullable);
projectionMapping[remappedProjectionMember] = projectionToAdd;
}
innerShaper = new ProjectionMemberRemappingExpressionVisitor(this, mapping).Visit(innerShaper);
_projectionMapping = projectionMapping;
- innerSelectExpression._projectionMapping.Clear();
+ innerSelect._projectionMapping.Clear();
}
}
@@ -2837,7 +2836,7 @@ private Expression AddJoin(
private void AddJoin(
JoinType joinType,
- ref SelectExpression innerSelectExpression,
+ ref SelectExpression innerSelect,
out bool innerPushdownOccurred,
SqlExpression? joinPredicate = null)
{
@@ -2845,43 +2844,43 @@ private void AddJoin(
// Try to convert Apply to normal join
if (joinType is JoinType.CrossApply or JoinType.OuterApply)
{
- var limit = innerSelectExpression.Limit;
- var offset = innerSelectExpression.Offset;
- if (!innerSelectExpression.IsDistinct
+ var limit = innerSelect.Limit;
+ var offset = innerSelect.Offset;
+ if (!innerSelect.IsDistinct
|| (limit == null && offset == null))
{
- innerSelectExpression.Limit = null;
- innerSelectExpression.Offset = null;
+ innerSelect.Limit = null;
+ innerSelect.Offset = null;
- var originalInnerSelectPredicate = innerSelectExpression.GroupBy.Count > 0
- ? innerSelectExpression.Having
- : innerSelectExpression.Predicate;
+ var originalInnerSelectPredicate = innerSelect.GroupBy.Count > 0
+ ? innerSelect.Having
+ : innerSelect.Predicate;
- joinPredicate = TryExtractJoinKey(this, innerSelectExpression, allowNonEquality: limit == null && offset == null);
+ joinPredicate = TryExtractJoinKey(this, innerSelect, allowNonEquality: limit == null && offset == null);
if (joinPredicate != null)
{
var containsOuterReference = new SelectExpressionCorrelationFindingExpressionVisitor(this)
- .ContainsOuterReference(innerSelectExpression);
+ .ContainsOuterReference(innerSelect);
if (!containsOuterReference)
{
if (limit != null || offset != null)
{
var partitions = new List();
- GetPartitions(innerSelectExpression, joinPredicate, partitions);
- var orderings = innerSelectExpression.Orderings.Count > 0
- ? innerSelectExpression.Orderings
- : innerSelectExpression._identifier.Count > 0
- ? innerSelectExpression._identifier.Select(e => new OrderingExpression(e.Column, true))
+ GetPartitions(innerSelect, joinPredicate, partitions);
+ var orderings = innerSelect.Orderings.Count > 0
+ ? innerSelect.Orderings
+ : innerSelect._identifier.Count > 0
+ ? innerSelect._identifier.Select(e => new OrderingExpression(e.Column, true))
: new[] { new OrderingExpression(new SqlFragmentExpression("(SELECT 1)"), true) };
var rowNumberExpression = new RowNumberExpression(
partitions, orderings.ToList(), (limit ?? offset)!.TypeMapping);
- innerSelectExpression.ClearOrdering();
+ innerSelect.ClearOrdering();
- joinPredicate = innerSelectExpression.PushdownIntoSubqueryInternal().Remap(joinPredicate);
+ joinPredicate = innerSelect.PushdownIntoSubqueryInternal().Remap(joinPredicate);
- var outerColumn = ((SelectExpression)innerSelectExpression.Tables[0]).GenerateOuterColumn(
- innerSelectExpression.Tables[0].Alias!, rowNumberExpression, "row");
+ var outerColumn = ((SelectExpression)innerSelect.Tables[0]).GenerateOuterColumn(
+ innerSelect.Tables[0].Alias!, rowNumberExpression, "row");
SqlExpression? offsetPredicate = null;
SqlExpression? limitPredicate = null;
if (offset != null)
@@ -2913,12 +2912,12 @@ private void AddJoin(
joinPredicate.TypeMapping)
: offsetPredicate
: limitPredicate;
- innerSelectExpression.ApplyPredicate(predicate!);
+ innerSelect.ApplyPredicate(predicate!);
}
AddJoin(
joinType == JoinType.CrossApply ? JoinType.InnerJoin : JoinType.LeftJoin,
- ref innerSelectExpression,
+ ref innerSelect,
out innerPushdownOccurred,
joinPredicate);
@@ -2927,13 +2926,13 @@ private void AddJoin(
if (originalInnerSelectPredicate != null)
{
- if (innerSelectExpression.GroupBy.Count > 0)
+ if (innerSelect.GroupBy.Count > 0)
{
- innerSelectExpression.Having = originalInnerSelectPredicate;
+ innerSelect.Having = originalInnerSelectPredicate;
}
else
{
- innerSelectExpression.Predicate = originalInnerSelectPredicate;
+ innerSelect.Predicate = originalInnerSelectPredicate;
}
}
@@ -2943,12 +2942,12 @@ private void AddJoin(
// Order matters Apply Offset before Limit
if (offset != null)
{
- innerSelectExpression.ApplyOffset(offset);
+ innerSelect.ApplyOffset(offset);
}
if (limit != null)
{
- innerSelectExpression.ApplyLimit(limit);
+ innerSelect.ApplyLimit(limit);
}
}
}
@@ -2959,31 +2958,31 @@ private void AddJoin(
|| GroupBy.Count > 0)
{
var sqlRemappingVisitor = PushdownIntoSubqueryInternal();
- innerSelectExpression = sqlRemappingVisitor.Remap(innerSelectExpression);
+ innerSelect = sqlRemappingVisitor.Remap(innerSelect);
joinPredicate = sqlRemappingVisitor.Remap(joinPredicate);
}
- if (innerSelectExpression.Limit != null
- || innerSelectExpression.Offset != null
- || innerSelectExpression.IsDistinct
- || innerSelectExpression.Predicate != null
- || innerSelectExpression.Tables.Count > 1
- || innerSelectExpression.GroupBy.Count > 0)
+ if (innerSelect.Limit != null
+ || innerSelect.Offset != null
+ || innerSelect.IsDistinct
+ || innerSelect.Predicate != null
+ || innerSelect.Tables.Count > 1
+ || innerSelect.GroupBy.Count > 0)
{
- joinPredicate = innerSelectExpression.PushdownIntoSubqueryInternal().Remap(joinPredicate);
+ joinPredicate = innerSelect.PushdownIntoSubqueryInternal().Remap(joinPredicate);
innerPushdownOccurred = true;
}
if (_identifier.Count > 0
- && innerSelectExpression._identifier.Count > 0)
+ && innerSelect._identifier.Count > 0)
{
if (joinType is JoinType.LeftJoin or JoinType.OuterApply)
{
- _identifier.AddRange(innerSelectExpression._identifier.Select(e => (e.Column.MakeNullable(), e.Comparer)));
+ _identifier.AddRange(innerSelect._identifier.Select(e => (e.Column.MakeNullable(), e.Comparer)));
}
else
{
- _identifier.AddRange(innerSelectExpression._identifier);
+ _identifier.AddRange(innerSelect._identifier);
}
}
else
@@ -2991,10 +2990,10 @@ private void AddJoin(
// if the subquery that is joined to can't be uniquely identified
// then the entire join should also not be marked as non-identifiable
_identifier.Clear();
- innerSelectExpression._identifier.Clear();
+ innerSelect._identifier.Clear();
}
- var innerTable = innerSelectExpression.Tables.Single();
+ var innerTable = innerSelect.Tables.Single();
var joinTable = joinType switch
{
JoinType.InnerJoin => new InnerJoinExpression(innerTable, joinPredicate!),
@@ -3007,26 +3006,26 @@ private void AddJoin(
_tables.Add(joinTable);
- static void GetPartitions(SelectExpression selectExpression, SqlExpression sqlExpression, List partitions)
+ static void GetPartitions(SelectExpression select, SqlExpression sqlExpression, List partitions)
{
- if (sqlExpression is SqlBinaryExpression sqlBinaryExpression)
+ switch (sqlExpression)
{
- if (sqlBinaryExpression.OperatorType == ExpressionType.Equal)
+ case SqlBinaryExpression { OperatorType: ExpressionType.Equal } binary:
{
- if (sqlBinaryExpression.Left is ColumnExpression columnExpression
- && selectExpression.ContainsReferencedTable(columnExpression))
- {
- partitions.Add(sqlBinaryExpression.Left);
- }
- else
- {
- partitions.Add(sqlBinaryExpression.Right);
- }
+ partitions.Add(
+ binary.Left is ColumnExpression column && select.ContainsReferencedTable(column)
+ ? binary.Left
+ : binary.Right);
+
+ break;
}
- else if (sqlBinaryExpression.OperatorType == ExpressionType.AndAlso)
+
+ case SqlBinaryExpression { OperatorType: ExpressionType.AndAlso } binary:
{
- GetPartitions(selectExpression, sqlBinaryExpression.Left, partitions);
- GetPartitions(selectExpression, sqlBinaryExpression.Right, partitions);
+ GetPartitions(select, binary.Left, partitions);
+ GetPartitions(select, binary.Right, partitions);
+
+ break;
}
}
}
diff --git a/src/EFCore/Query/QueryCompilationContext.cs b/src/EFCore/Query/QueryCompilationContext.cs
index 2e05c759414..286dc70c201 100644
--- a/src/EFCore/Query/QueryCompilationContext.cs
+++ b/src/EFCore/Query/QueryCompilationContext.cs
@@ -225,23 +225,20 @@ public virtual Func CreateQueryExecutor(Expressi
public virtual Expression> CreateQueryExecutorExpression(Expression query)
{
var queryAndEventData = Logger.QueryCompilationStarting(Dependencies.Context, _expressionPrinter, query);
- query = queryAndEventData.Query;
+ var interceptedQuery = queryAndEventData.Query;
- query = _queryTranslationPreprocessorFactory.Create(this).Process(query);
- // Convert EntityQueryable to ShapedQueryExpression
- query = _queryableMethodTranslatingExpressionVisitorFactory.Create(this).Translate(query);
- query = _queryTranslationPostprocessorFactory.Create(this).Process(query);
+ var preprocessedQuery = _queryTranslationPreprocessorFactory.Create(this).Process(interceptedQuery);
+ var translatedQuery = _queryableMethodTranslatingExpressionVisitorFactory.Create(this).Translate(preprocessedQuery);
+ var postprocessedQuery = _queryTranslationPostprocessorFactory.Create(this).Process(translatedQuery);
- // Inject actual entity materializer
- // Inject tracking
- query = _shapedQueryCompilingExpressionVisitorFactory.Create(this).Visit(query);
+ var compiledQuery = _shapedQueryCompilingExpressionVisitorFactory.Create(this).Visit(postprocessedQuery);
// If any additional parameters were added during the compilation phase (e.g. entity equality ID expression),
// wrap the query with code adding those parameters to the query context
- query = InsertRuntimeParameters(query);
+ var compiledQueryWithRuntimeParameters = InsertRuntimeParameters(compiledQuery);
return Expression.Lambda>(
- query,
+ compiledQueryWithRuntimeParameters,
QueryContextParameter);
}
diff --git a/src/EFCore/Query/TransparentIdentifierFactory.cs b/src/EFCore/Query/TransparentIdentifierFactory.cs
index 6bbdf329929..15c0417906d 100644
--- a/src/EFCore/Query/TransparentIdentifierFactory.cs
+++ b/src/EFCore/Query/TransparentIdentifierFactory.cs
@@ -34,19 +34,24 @@ public static class TransparentIdentifierFactory
public static Type Create(Type outerType, Type innerType)
=> typeof(TransparentIdentifier<,>).MakeGenericType(outerType, innerType);
- private readonly struct TransparentIdentifier
+ private readonly struct TransparentIdentifier(TOuter outer, TInner inner)
{
+ ///
+ /// 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.
+ ///
[UsedImplicitly]
- public TransparentIdentifier(TOuter outer, TInner inner)
- {
- Outer = outer;
- Inner = inner;
- }
+ public readonly TOuter Outer = outer;
+ ///
+ /// 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.
+ ///
[UsedImplicitly]
- public readonly TOuter Outer;
-
- [UsedImplicitly]
- public readonly TInner Inner;
+ public readonly TInner Inner = inner;
}
}
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs
index 4f035765ff5..d65433ac39b 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/OwnedQueryCosmosTest.cs
@@ -21,7 +21,7 @@ public OwnedQueryCosmosTest(OwnedQueryCosmosFixture fixture, ITestOutputHelper t
public override Task Query_loads_reference_nav_automatically_in_projection(bool async)
=> AssertTranslationFailed(() => base.Query_loads_reference_nav_automatically_in_projection(async));
- // TODO: SelectMany, #17246
+ // Non-correlated queries not supported by Cosmos
public override Task Query_with_owned_entity_equality_operator(bool async)
=> AssertTranslationFailed(() => base.Query_with_owned_entity_equality_operator(async));
@@ -223,23 +223,36 @@ public override Task Project_multiple_owned_navigations_with_expansion_on_owned_
=> AssertTranslationFailed(
() => base.Project_multiple_owned_navigations_with_expansion_on_owned_collections(async));
- // TODO: SelectMany, #17246
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
public override Task SelectMany_on_owned_collection(bool async)
- => AssertTranslationFailed(() => base.SelectMany_on_owned_collection(async));
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.SelectMany_on_owned_collection(a);
- // TODO: SelectMany, #17246
+ AssertSql(
+ """
+SELECT a
+FROM root c
+JOIN a IN c["Orders"]
+WHERE c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA")
+""");
+ });
+
+ // TODO: Fake LeftJoin, #33969
public override Task SelectMany_on_owned_reference_followed_by_regular_entity_and_collection(bool async)
=> AssertTranslationFailed(() => base.SelectMany_on_owned_reference_followed_by_regular_entity_and_collection(async));
- // TODO: SelectMany, #17246
+ // TODO: Fake LeftJoin, #33969
public override Task SelectMany_on_owned_reference_with_entity_in_between_ending_in_owned_collection(bool async)
=> AssertTranslationFailed(() => base.SelectMany_on_owned_reference_with_entity_in_between_ending_in_owned_collection(async));
- // TODO: SelectMany, #17246
+ // Non-correlated queries not supported by Cosmos
public override Task Query_with_owned_entity_equality_method(bool async)
=> AssertTranslationFailed(() => base.Query_with_owned_entity_equality_method(async));
- // TODO: SelectMany, #17246
+ // Non-correlated queries not supported by Cosmos
public override Task Query_with_owned_entity_equality_object_method(bool async)
=> AssertTranslationFailed(() => base.Query_with_owned_entity_equality_object_method(async));
@@ -290,25 +303,140 @@ public override async Task Client_method_skip_loads_owned_navigations_variation_
Assert.Equal(CosmosStrings.OffsetRequiresLimit, exception.Message);
}
- // TODO: SelectMany, #17246
public override Task Where_owned_collection_navigation_ToList_Count(bool async)
- => AssertTranslationFailed(() => base.Where_owned_collection_navigation_ToList_Count(async));
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ // TODO: #34011
+ // We override this test because the test data for this class gets saved incorrectly - the Order.Details collection gets persisted
+ // as null instead of [].
+ await AssertQuery(
+ a,
+ ss => ss.Set()
+ .OrderBy(p => p.Id)
+ .SelectMany(p => p.Orders)
+ .Select(p => p.Details.ToList())
+ .Where(e => e.Count() == 1),
+ assertOrder: true,
+ elementAsserter: (e, a) => AssertCollection(e, a));
+
+ AssertSql(
+ """
+SELECT a
+FROM root c
+JOIN a IN c["Orders"]
+WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (ARRAY_LENGTH(a["Details"]) = 1))
+ORDER BY c["Id"]
+""");
+ });
- // TODO: SelectMany, #17246
public override Task Where_collection_navigation_ToArray_Count(bool async)
- => AssertTranslationFailed(() => base.Where_collection_navigation_ToArray_Count(async));
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ // TODO: #34011
+ // We override this test because the test data for this class gets saved incorrectly - the Order.Details collection gets persisted
+ // as null instead of [].
+ await AssertQuery(
+ a,
+ ss => ss.Set()
+ .OrderBy(p => p.Id)
+ .SelectMany(p => p.Orders)
+ .Select(p => p.Details.AsEnumerable().ToArray())
+ .Where(e => e.Count() == 1),
+ assertOrder: true,
+ elementAsserter: (e, a) => AssertCollection(e, a));
+
+ AssertSql(
+ """
+SELECT a
+FROM root c
+JOIN a IN c["Orders"]
+WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (ARRAY_LENGTH(a["Details"]) = 1))
+ORDER BY c["Id"]
+""");
+ });
- // TODO: SelectMany, #17246
public override Task Where_collection_navigation_AsEnumerable_Count(bool async)
- => AssertTranslationFailed(() => base.Where_collection_navigation_AsEnumerable_Count(async));
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ // TODO: #34011
+ // We override this test because the test data for this class gets saved incorrectly - the Order.Details collection gets persisted
+ // as null instead of [].
+ await AssertQuery(
+ a,
+ ss => ss.Set()
+ .OrderBy(p => p.Id)
+ .SelectMany(p => p.Orders)
+ .Select(p => p.Details.AsEnumerable())
+ .Where(e => e.Count() == 1),
+ assertOrder: true,
+ elementAsserter: (e, a) => AssertCollection(e, a));
+
+ AssertSql(
+ """
+SELECT a
+FROM root c
+JOIN a IN c["Orders"]
+WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (ARRAY_LENGTH(a["Details"]) = 1))
+ORDER BY c["Id"]
+""");
+ });
- // TODO: SelectMany, #17246
public override Task Where_collection_navigation_ToList_Count_member(bool async)
- => AssertTranslationFailed(() => base.Where_collection_navigation_ToList_Count_member(async));
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ // TODO: #34011
+ // We override this test because the test data for this class gets saved incorrectly - the Order.Details collection gets persisted
+ // as null instead of [].
+ await AssertQuery(
+ a,
+ ss => ss.Set()
+ .OrderBy(p => p.Id)
+ .SelectMany(p => p.Orders)
+ .Select(p => p.Details.ToList())
+ .Where(e => e.Count == 1),
+ assertOrder: true,
+ elementAsserter: (e, a) => AssertCollection(e, a));
+
+ AssertSql(
+ """
+SELECT a
+FROM root c
+JOIN a IN c["Orders"]
+WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (ARRAY_LENGTH(a["Details"]) = 1))
+ORDER BY c["Id"]
+""");
+ });
- // TODO: SelectMany, #17246
public override Task Where_collection_navigation_ToArray_Length_member(bool async)
- => AssertTranslationFailed(() => base.Where_collection_navigation_ToArray_Length_member(async));
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ // TODO: #34011
+ // We override this test because the test data for this class gets saved incorrectly - the Order.Details collection gets persisted
+ // as null instead of [].
+ await AssertQuery(
+ a,
+ ss => ss.Set()
+ .OrderBy(p => p.Id)
+ .SelectMany(p => p.Orders)
+ .Select(p => p.Details.AsEnumerable().ToArray())
+ .Where(e => e.Length == 1),
+ assertOrder: true,
+ elementAsserter: (e, a) => AssertCollection(e, a));
+
+ AssertSql(
+ """
+SELECT a
+FROM root c
+JOIN a IN c["Orders"]
+WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (ARRAY_LENGTH(a["Details"]) = 1))
+ORDER BY c["Id"]
+""");
+ });
// TODO: GroupBy, #17313
public override Task GroupBy_with_multiple_aggregates_on_owned_navigation_properties(bool async)
@@ -559,7 +687,7 @@ public override async Task NoTracking_Include_with_cycles_throws(bool async)
AssertSql();
}
- // TODO: SelectMany, #17246
+ // TODO: Fake LeftJoin, #33969
public override Task NoTracking_Include_with_cycles_does_not_throw_when_performing_identity_resolution(
bool async,
bool useAsTracking)
@@ -594,9 +722,29 @@ FROM root c
}
}
- // TODO: SelectMany, #17246
public override Task Query_on_collection_entry_works_for_owned_collection(bool async)
- => AssertTranslationFailed(() => base.Query_on_collection_entry_works_for_owned_collection(async));
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Query_on_collection_entry_works_for_owned_collection(a);
+
+ AssertSql(
+ """
+SELECT c
+FROM root c
+WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (c["Id"] = 1))
+OFFSET 0 LIMIT 2
+""",
+ //
+ """
+@__p_0='1'
+
+SELECT a
+FROM root c
+JOIN a IN c["Orders"]
+WHERE (c["Discriminator"] IN ("OwnedPerson", "Branch", "LeafB", "LeafA") AND (a["ClientId"] = @__p_0))
+""");
+ });
// Non-correlated queries not supported by Cosmos
public override Task Projecting_collection_correlated_with_keyless_entity_after_navigation_works_using_parent_identifiers(
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs
index 1e9a610f5f6..9e2864cb8a7 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/PrimitiveCollectionsQueryCosmosTest.cs
@@ -1299,12 +1299,50 @@ public override async Task Column_collection_Distinct(bool async)
AssertSql();
}
- public override async Task Column_collection_SelectMany(bool async)
+ public override Task Column_collection_SelectMany(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_SelectMany(a);
+
+ AssertSql(
+ """
+SELECT a
+FROM root c
+JOIN a IN c["Ints"]
+WHERE (c["Discriminator"] = "PrimitiveCollectionsEntity")
+""");
+ });
+
+ public override Task Column_collection_SelectMany_with_filter(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(
+ async, async a =>
+ {
+ await base.Column_collection_SelectMany_with_filter(a);
+
+ AssertSql(
+ """
+SELECT a
+FROM root c
+JOIN (
+ SELECT VALUE i
+ FROM i IN c["Ints"]
+ WHERE (i > 1)) a
+WHERE (c["Discriminator"] = "PrimitiveCollectionsEntity")
+""");
+ });
+
+ public override async Task Column_collection_SelectMany_with_Select_to_anonymous_type(bool async)
{
- // TODO: SelectMany
- await AssertTranslationFailed(() => base.Column_collection_SelectMany(async));
+ // Always throws for sync.
+ if (async)
+ {
+ // TODO: #34004
+ var exception = await Assert.ThrowsAsync(
+ () => base.Column_collection_SelectMany_with_Select_to_anonymous_type(async));
- AssertSql();
+ Assert.Equal(CosmosStrings.ComplexProjectionInSubqueryNotSupported, exception.Message);
+ }
}
public override Task Column_collection_projection_from_top_level(bool async)
diff --git a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs
index e7bf0d05611..45490365abd 100644
--- a/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs
+++ b/test/EFCore.Specification.Tests/Query/PrimitiveCollectionsQueryTestBase.cs
@@ -738,6 +738,20 @@ public virtual Task Column_collection_SelectMany(bool async)
async,
ss => ss.Set().SelectMany(c => c.Ints));
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_SelectMany_with_filter(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().SelectMany(c => c.Ints.Where(i => i > 1)));
+
+ [ConditionalTheory]
+ [MemberData(nameof(IsAsyncData))]
+ public virtual Task Column_collection_SelectMany_with_Select_to_anonymous_type(bool async)
+ => AssertQuery(
+ async,
+ ss => ss.Set().SelectMany(c => c.Ints.Select(i => new { Original = i, Incremented = i + 1 })));
+
[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Column_collection_projection_from_top_level(bool async)
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs
index 7ec617c8858..655aef626db 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQueryOldSqlServerTest.cs
@@ -751,6 +751,12 @@ public override Task Column_collection_Distinct(bool async)
public override Task Column_collection_SelectMany(bool async)
=> AssertTranslationFailed(() => base.Column_collection_SelectMany(async));
+ public override Task Column_collection_SelectMany_with_filter(bool async)
+ => AssertTranslationFailed(() => base.Column_collection_SelectMany_with_filter(async));
+
+ public override Task Column_collection_SelectMany_with_Select_to_anonymous_type(bool async)
+ => AssertTranslationFailed(() => base.Column_collection_SelectMany_with_Select_to_anonymous_type(async));
+
public override async Task Column_collection_projection_from_top_level(bool async)
{
await base.Column_collection_projection_from_top_level(async);
diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs
index 7e352ef43b8..37725332129 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Query/PrimitiveCollectionsQuerySqlServerTest.cs
@@ -1273,6 +1273,34 @@ CROSS APPLY OPENJSON([p].[Ints]) WITH ([value] int '$') AS [i]
""");
}
+ public override async Task Column_collection_SelectMany_with_filter(bool async)
+ {
+ await base.Column_collection_SelectMany_with_filter(async);
+
+ AssertSql(
+ """
+SELECT [i0].[value]
+FROM [PrimitiveCollectionsEntity] AS [p]
+CROSS APPLY (
+ SELECT [i].[value]
+ FROM OPENJSON([p].[Ints]) WITH ([value] int '$') AS [i]
+ WHERE [i].[value] > 1
+) AS [i0]
+""");
+ }
+
+ public override async Task Column_collection_SelectMany_with_Select_to_anonymous_type(bool async)
+ {
+ await base.Column_collection_SelectMany_with_Select_to_anonymous_type(async);
+
+ AssertSql(
+ """
+SELECT [i].[value] AS [Original], [i].[value] + 1 AS [Incremented]
+FROM [PrimitiveCollectionsEntity] AS [p]
+CROSS APPLY OPENJSON([p].[Ints]) WITH ([value] int '$') AS [i]
+""");
+ }
+
public override async Task Column_collection_projection_from_top_level(bool async)
{
await base.Column_collection_projection_from_top_level(async);
diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs
index 90be08ddd74..a0641afdf81 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Query/PrimitiveCollectionsQuerySqliteTest.cs
@@ -1253,6 +1253,18 @@ public override async Task Column_collection_SelectMany(bool async)
(await Assert.ThrowsAsync(
() => base.Column_collection_SelectMany(async))).Message);
+ public override async Task Column_collection_SelectMany_with_filter(bool async)
+ => Assert.Equal(
+ SqliteStrings.ApplyNotSupported,
+ (await Assert.ThrowsAsync(
+ () => base.Column_collection_SelectMany_with_filter(async))).Message);
+
+ public override async Task Column_collection_SelectMany_with_Select_to_anonymous_type(bool async)
+ => Assert.Equal(
+ SqliteStrings.ApplyNotSupported,
+ (await Assert.ThrowsAsync(
+ () => base.Column_collection_SelectMany_with_Select_to_anonymous_type(async))).Message);
+
public override async Task Column_collection_projection_from_top_level(bool async)
{
await base.Column_collection_projection_from_top_level(async);