diff --git a/src/EFCore.Cosmos/Extensions/Internal/CosmosShapedQueryExpressionExtensions.cs b/src/EFCore.Cosmos/Extensions/Internal/CosmosShapedQueryExpressionExtensions.cs
index be295279ee4..74116f14320 100644
--- a/src/EFCore.Cosmos/Extensions/Internal/CosmosShapedQueryExpressionExtensions.cs
+++ b/src/EFCore.Cosmos/Extensions/Internal/CosmosShapedQueryExpressionExtensions.cs
@@ -167,9 +167,9 @@ public static bool TryExtractArray(
projectedStructuralTypeShaper = shaper;
projection = shaper.ValueBufferExpression;
if (projection is ProjectionBindingExpression { ProjectionMember: { } projectionMember }
- && select.GetMappedProjection(projectionMember) is EntityProjectionExpression entityProjection)
+ && select.GetMappedProjection(projectionMember) is StructuralTypeProjectionExpression structuralTypeProjection)
{
- projection = entityProjection.Object;
+ projection = structuralTypeProjection.Object;
}
}
else
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosAliasManager.cs b/src/EFCore.Cosmos/Query/Internal/CosmosAliasManager.cs
index 6227029eae3..15328419326 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosAliasManager.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosAliasManager.cs
@@ -226,7 +226,7 @@ protected override Expression VisitExtension(Expression node)
ScalarReferenceExpression reference when aliasRewritingMap.TryGetValue(reference.Name, out var newAlias)
=> new ScalarReferenceExpression(newAlias, reference.Type, reference.TypeMapping),
ObjectReferenceExpression reference when aliasRewritingMap.TryGetValue(reference.Name, out var newAlias)
- => new ObjectReferenceExpression(reference.EntityType, newAlias),
+ => new ObjectReferenceExpression(reference.StructuralType, newAlias),
_ => base.VisitExtension(node)
};
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs
index 8c53bec67ee..c1f5033a0a8 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs
@@ -133,6 +133,7 @@ public virtual Expression Translate(SelectExpression selectExpression, Expressio
var translation = _sqlTranslator.Translate(expression);
if (translation == null)
{
+ _selectExpression.IndicateClientProjection();
return base.Visit(expression);
}
@@ -214,11 +215,11 @@ protected override Expression VisitExtension(Expression extensionExpression)
if (_clientEval)
{
- var entityProjection = (EntityProjectionExpression)projection;
+ var structuralTypeProjection = (StructuralTypeProjectionExpression)projection;
return entityShaperExpression.Update(
new ProjectionBindingExpression(
- _selectExpression, _selectExpression.AddToProjection(entityProjection), typeof(ValueBuffer)));
+ _selectExpression, _selectExpression.AddToProjection(structuralTypeProjection), typeof(ValueBuffer)));
}
_projectionMapping[_projectionMembers.Peek()] = projection;
@@ -303,19 +304,19 @@ protected override Expression VisitMember(MemberExpression memberExpression)
return NullSafeUpdate(innerExpression);
}
- var innerEntityProjection = shaperExpression.ValueBufferExpression switch
+ var innerStructuralTypeProjection = shaperExpression.ValueBufferExpression switch
{
ProjectionBindingExpression innerProjectionBindingExpression
- => (EntityProjectionExpression)_selectExpression.Projection[innerProjectionBindingExpression.Index!.Value].Expression,
+ => (StructuralTypeProjectionExpression)_selectExpression.Projection[innerProjectionBindingExpression.Index!.Value].Expression,
- // Unwrap EntityProjectionExpression when the root entity is not projected
+ // Unwrap StructuralTypeProjectionExpression when the root entity is not projected
UnaryExpression unaryExpression
- => (EntityProjectionExpression)((UnaryExpression)unaryExpression.Operand).Operand,
+ => (StructuralTypeProjectionExpression)((UnaryExpression)unaryExpression.Operand).Operand,
_ => throw new InvalidOperationException(CoreStrings.TranslationFailed(memberExpression.Print()))
};
- var navigationProjection = innerEntityProjection.BindMember(
+ var navigationProjection = innerStructuralTypeProjection.BindMember(
memberExpression.Member, innerExpression.Type, clientEval: true, out var propertyBase);
if (propertyBase is not INavigation navigation
@@ -326,10 +327,10 @@ UnaryExpression unaryExpression
switch (navigationProjection)
{
- case EntityProjectionExpression entityProjection:
+ case StructuralTypeProjectionExpression structuralTypeProjection:
return new StructuralTypeShaperExpression(
navigation.TargetEntityType,
- Expression.Convert(Expression.Convert(entityProjection, typeof(object)), typeof(ValueBuffer)),
+ Expression.Convert(Expression.Convert(structuralTypeProjection, typeof(object)), typeof(ValueBuffer)),
nullable: true);
case ObjectArrayAccessExpression objectArrayProjectionExpression:
@@ -525,16 +526,16 @@ when _collectionShaperMapping.TryGetValue(parameterExpression, out var collectio
return QueryCompilationContext.NotTranslatedExpression;
}
- var innerEntityProjection = shaperExpression.ValueBufferExpression switch
+ var innerStructuralTypeProjection = shaperExpression.ValueBufferExpression switch
{
- EntityProjectionExpression entityProjection
- => entityProjection,
+ StructuralTypeProjectionExpression structuralTypeProjection
+ => structuralTypeProjection,
ProjectionBindingExpression innerProjectionBindingExpression
- => (EntityProjectionExpression)_selectExpression.Projection[innerProjectionBindingExpression.Index!.Value].Expression,
+ => (StructuralTypeProjectionExpression)_selectExpression.Projection[innerProjectionBindingExpression.Index!.Value].Expression,
UnaryExpression unaryExpression
- => (EntityProjectionExpression)((UnaryExpression)unaryExpression.Operand).Operand,
+ => (StructuralTypeProjectionExpression)((UnaryExpression)unaryExpression.Operand).Operand,
_ => throw new InvalidOperationException(CoreStrings.TranslationFailed(methodCallExpression.Print()))
};
@@ -543,7 +544,7 @@ UnaryExpression unaryExpression
var navigation = _includedNavigations.FirstOrDefault(n => n.Name == memberName);
if (navigation == null)
{
- navigationProjection = innerEntityProjection.BindMember(
+ navigationProjection = innerStructuralTypeProjection.BindMember(
memberName, visitedSource.Type, clientEval: true, out var propertyBase);
if (propertyBase is not INavigation projectedNavigation || !projectedNavigation.IsEmbedded())
@@ -555,7 +556,7 @@ UnaryExpression unaryExpression
}
else
{
- navigationProjection = innerEntityProjection.BindNavigation(navigation, clientEval: true);
+ navigationProjection = innerStructuralTypeProjection.BindNavigation(navigation, clientEval: true);
}
switch (navigationProjection)
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs
index 8ac1a8dcaa9..4542629ff12 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs
@@ -62,11 +62,11 @@ public virtual CosmosSqlQuery GetSqlQuery(
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
- protected override Expression VisitEntityProjection(EntityProjectionExpression entityProjectionExpression)
+ protected override Expression VisitStructuralTypeProjection(StructuralTypeProjectionExpression structuralTypeProjectionExpression)
{
- Visit(entityProjectionExpression.Object);
+ Visit(structuralTypeProjectionExpression.Object);
- return entityProjectionExpression;
+ return structuralTypeProjectionExpression;
}
///
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs
index fe5de54ca5f..be02822bdca 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs
@@ -252,7 +252,7 @@ protected override Expression VisitExtension(Expression extensionExpression)
var alias = _aliasManager.GenerateSourceAlias(fromSql);
var selectExpression = new SelectExpression(
new SourceExpression(fromSql, alias),
- new EntityProjectionExpression(new ObjectReferenceExpression(entityType, alias), entityType));
+ new StructuralTypeProjectionExpression(new ObjectReferenceExpression(entityType, alias), entityType));
return CreateShapedQueryExpression(entityType, selectExpression) ?? QueryCompilationContext.NotTranslatedExpression;
default:
@@ -300,7 +300,7 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis
var alias = _aliasManager.GenerateSourceAlias("c");
var selectExpression = new SelectExpression(
new SourceExpression(new ObjectReferenceExpression(entityType, "root"), alias),
- new EntityProjectionExpression(new ObjectReferenceExpression(entityType, alias), entityType));
+ new StructuralTypeProjectionExpression(new ObjectReferenceExpression(entityType, alias), entityType));
// Add discriminator predicate
var concreteEntityTypes = entityType.GetConcreteDerivedTypesInclusive().ToList();
@@ -323,7 +323,7 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis
"Missing discriminator property in hierarchy");
if (discriminatorProperty is not null)
{
- var discriminatorColumn = ((EntityProjectionExpression)selectExpression.GetMappedProjection(new ProjectionMember()))
+ var discriminatorColumn = ((StructuralTypeProjectionExpression)selectExpression.GetMappedProjection(new ProjectionMember()))
.BindProperty(discriminatorProperty, clientEval: false);
var success = TryApplyPredicate(
@@ -340,9 +340,9 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis
return CreateShapedQueryExpression(entityType, selectExpression);
}
- private ShapedQueryExpression? CreateShapedQueryExpression(IEntityType entityType, SelectExpression queryExpression)
+ private ShapedQueryExpression? CreateShapedQueryExpression(ITypeBase structuralType, SelectExpression queryExpression)
{
- if (!entityType.IsOwned())
+ if (structuralType is IEntityType entityType && !entityType.IsOwned())
{
var existingEntityType = _queryCompilationContext.RootEntityType;
if (existingEntityType is not null && existingEntityType != entityType)
@@ -358,7 +358,7 @@ protected override QueryableMethodTranslatingExpressionVisitor CreateSubqueryVis
return new ShapedQueryExpression(
queryExpression,
new StructuralTypeShaperExpression(
- entityType,
+ structuralType,
new ProjectionBindingExpression(queryExpression, new ProjectionMember(), typeof(ValueBuffer)),
nullable: false));
}
@@ -532,6 +532,14 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou
return null;
}
+ // We can not apply distinct because SQL DISTINCT operates on the full
+ // structural type, but the shaper extracts only a subset of that data.
+ // Cosmos: Projecting out nested documents retrieves the entire document #34067
+ if (select.UsesClientProjection)
+ {
+ return null;
+ }
+
select.ApplyDistinct();
return source;
@@ -607,7 +615,7 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou
var translatedSelect =
new SelectExpression(
- new EntityProjectionExpression(translation, (IEntityType)projectedStructuralTypeShaper.StructuralType));
+ new StructuralTypeProjectionExpression(translation, projectedStructuralTypeShaper.StructuralType));
return source.Update(
translatedSelect,
new StructuralTypeShaperExpression(
@@ -859,51 +867,46 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou
///
protected override ShapedQueryExpression? TranslateOfType(ShapedQueryExpression source, Type resultType)
{
- if (source.ShaperExpression is not StructuralTypeShaperExpression entityShaperExpression)
+ if (source.ShaperExpression is StructuralTypeShaperExpression { StructuralType: IEntityType entityType } shaper)
{
- return null;
- }
-
- if (entityShaperExpression.StructuralType is not IEntityType entityType)
- {
- throw new UnreachableException("Complex types not supported in Cosmos");
- }
+ if (entityType.ClrType == resultType)
+ {
+ return source;
+ }
- if (entityType.ClrType == resultType)
- {
- return source;
- }
+ var select = (SelectExpression)source.QueryExpression;
- var select = (SelectExpression)source.QueryExpression;
+ var parameterExpression = Expression.Parameter(shaper.Type);
+ var predicate = Expression.Lambda(Expression.TypeIs(parameterExpression, resultType), parameterExpression);
- var parameterExpression = Expression.Parameter(entityShaperExpression.Type);
- var predicate = Expression.Lambda(Expression.TypeIs(parameterExpression, resultType), parameterExpression);
+ if (!TryApplyPredicate(source, predicate))
+ {
+ return null;
+ }
- if (!TryApplyPredicate(source, predicate))
- {
- return null;
- }
+ var baseType = entityType.GetAllBaseTypes().SingleOrDefault(et => et.ClrType == resultType);
+ if (baseType != null)
+ {
+ return source.UpdateShaperExpression(shaper.WithType(baseType));
+ }
- var baseType = entityType.GetAllBaseTypes().SingleOrDefault(et => et.ClrType == resultType);
- if (baseType != null)
- {
- return source.UpdateShaperExpression(entityShaperExpression.WithType(baseType));
- }
+ var derivedType = entityType.GetDerivedTypes().Single(et => et.ClrType == resultType);
+ var projectionBindingExpression = (ProjectionBindingExpression)shaper.ValueBufferExpression;
- var derivedType = entityType.GetDerivedTypes().Single(et => et.ClrType == resultType);
- var projectionBindingExpression = (ProjectionBindingExpression)entityShaperExpression.ValueBufferExpression;
+ var projectionMember = projectionBindingExpression.ProjectionMember;
+ Check.DebugAssert(new ProjectionMember().Equals(projectionMember), "Invalid ProjectionMember when processing OfType");
- var projectionMember = projectionBindingExpression.ProjectionMember;
- Check.DebugAssert(new ProjectionMember().Equals(projectionMember), "Invalid ProjectionMember when processing OfType");
+ var structuralTypeProjectionExpression = (StructuralTypeProjectionExpression)select.GetMappedProjection(projectionMember);
+ select.ReplaceProjectionMapping(
+ new Dictionary
+ {
+ { projectionMember, structuralTypeProjectionExpression.UpdateEntityType(derivedType) }
+ });
- var entityProjectionExpression = (EntityProjectionExpression)select.GetMappedProjection(projectionMember);
- select.ReplaceProjectionMapping(
- new Dictionary
- {
- { projectionMember, entityProjectionExpression.UpdateEntityType(derivedType) }
- });
+ return source.UpdateShaperExpression(shaper.WithType(derivedType));
+ }
- return source.UpdateShaperExpression(entityShaperExpression.WithType(derivedType));
+ return null;
}
///
@@ -1131,9 +1134,9 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s
var translatedSelect = SelectExpression.CreateForCollection(
slice,
alias,
- new EntityProjectionExpression(
- new ObjectReferenceExpression((IEntityType)projectedStructuralTypeShaper.StructuralType, alias),
- (IEntityType)projectedStructuralTypeShaper.StructuralType));
+ new StructuralTypeProjectionExpression(
+ new ObjectReferenceExpression(projectedStructuralTypeShaper.StructuralType, alias),
+ projectedStructuralTypeShaper.StructuralType));
return source.Update(
translatedSelect,
new StructuralTypeShaperExpression(
@@ -1270,9 +1273,9 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s
var translatedSelect = SelectExpression.CreateForCollection(
slice,
alias,
- new EntityProjectionExpression(
- new ObjectReferenceExpression((IEntityType)projectedStructuralTypeShaper.StructuralType, alias),
- (IEntityType)projectedStructuralTypeShaper.StructuralType));
+ new StructuralTypeProjectionExpression(
+ new ObjectReferenceExpression(projectedStructuralTypeShaper.StructuralType, alias),
+ projectedStructuralTypeShaper.StructuralType));
return source.Update(
translatedSelect,
new StructuralTypeShaperExpression(
@@ -1361,7 +1364,7 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s
///
protected override ShapedQueryExpression? TranslateMemberAccess(Expression source, MemberIdentity member)
{
- // Attempt to translate access into a primitive collection property
+ // Attempt to translate access into a primitive, complex or embedded navigation collection property
if (_sqlTranslator.TryBindMember(
_sqlTranslator.Visit(source),
member,
@@ -1380,17 +1383,28 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s
{
case StructuralTypeShaperExpression shaper when property is INavigation { IsCollection: true }:
{
- var targetEntityType = (IEntityType)shaper.StructuralType;
- var projection = new EntityProjectionExpression(
- new ObjectReferenceExpression(targetEntityType, sourceAlias), targetEntityType);
+ var targetStructuralType = shaper.StructuralType;
+ var projection = new StructuralTypeProjectionExpression(
+ new ObjectReferenceExpression(targetStructuralType, sourceAlias), targetStructuralType);
var select = SelectExpression.CreateForCollection(
shaper.ValueBufferExpression,
sourceAlias,
projection);
- return CreateShapedQueryExpression(targetEntityType, select);
+ return CreateShapedQueryExpression(targetStructuralType, select);
}
- // TODO: Collection of complex type (#31253)
+ case CollectionResultExpression collectionResult:
+ {
+ var query = collectionResult.QueryExpression;
+ var targetStructuralType = collectionResult.ComplexProperty.ComplexType;
+ var projection = new StructuralTypeProjectionExpression(
+ new ObjectReferenceExpression(targetStructuralType, sourceAlias), targetStructuralType);
+ var select = SelectExpression.CreateForCollection(
+ query,
+ sourceAlias,
+ projection);
+ return CreateShapedQueryExpression(targetStructuralType, select);
+ }
// Note that non-collection navigations/complex types are handled in CosmosSqlTranslatingExpressionVisitor
// (no collection -> no queryable operators)
@@ -1666,7 +1680,7 @@ private bool TryPushdownIntoSubquery(SelectExpression select)
var translation = new ObjectFunctionExpression(functionName, [array1, array2], arrayType);
var alias = _aliasManager.GenerateSourceAlias(translation);
var select = SelectExpression.CreateForCollection(
- translation, alias, new ObjectReferenceExpression((IEntityType)structuralType1, alias));
+ translation, alias, new ObjectReferenceExpression(structuralType1, alias));
return CreateShapedQueryExpression(select, structuralType1.ClrType);
}
}
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSerializationUtilities.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSerializationUtilities.cs
new file mode 100644
index 00000000000..41e34876613
--- /dev/null
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosSerializationUtilities.cs
@@ -0,0 +1,96 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections;
+using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
+using Microsoft.EntityFrameworkCore.Update.Internal;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
+
+///
+/// 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 class CosmosSerializationUtilities
+{
+ ///
+ /// 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 readonly MethodInfo SerializeObjectToComplexPropertyMethod
+ = typeof(CosmosSerializationUtilities).GetMethod(nameof(SerializeObjectToComplexProperty)) ?? throw new UnreachableException();
+
+ ///
+ /// 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 JToken SerializeObjectToComplexProperty(IComplexType type, object? value, bool collection) // #34567
+ {
+ if (value == null)
+ {
+ return JValue.CreateNull();
+ }
+
+ if (collection)
+ {
+ var array = new JArray();
+ foreach (var element in (IEnumerable)value)
+ {
+ array.Add(SerializeObjectToComplexProperty(type, element, false));
+ }
+ return array;
+ }
+
+ var obj = new JObject();
+ foreach (var property in type.GetProperties())
+ {
+ var jsonPropertyName = property.GetJsonPropertyName();
+
+ var propertyValue = property.GetGetter().GetClrValue(value);
+#pragma warning disable EF1001 // Internal EF Core API usage.
+ var providerValue = property.ConvertToProviderValue(propertyValue);
+#pragma warning restore EF1001 // Internal EF Core API usage.
+ if (providerValue is null)
+ {
+ if (!property.IsNullable)
+ {
+ throw new InvalidOperationException(CoreStrings.PropertyConceptualNull(property.Name, type.DisplayName()));
+ }
+
+ obj[jsonPropertyName] = null;
+ }
+ else
+ {
+ obj[jsonPropertyName] = JToken.FromObject(providerValue, CosmosClientWrapper.Serializer);
+ }
+ }
+
+ foreach (var complexProperty in type.GetComplexProperties())
+ {
+ var jsonPropertyName = complexProperty.Name;
+ var propertyValue = complexProperty.GetGetter().GetClrValue(value);
+ if (propertyValue is null)
+ {
+ if (!complexProperty.IsNullable)
+ {
+ throw new InvalidOperationException(CoreStrings.PropertyConceptualNull(complexProperty.Name, type.DisplayName()));
+ }
+
+ obj[jsonPropertyName] = null;
+ }
+ else
+ {
+ obj[jsonPropertyName] = SerializeObjectToComplexProperty(complexProperty.ComplexType, propertyValue, complexProperty.IsCollection);
+ }
+ }
+
+ return obj;
+ }
+}
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs
index aafd01a66e8..12f15f9714e 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs
@@ -94,7 +94,7 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression)
storeName = e.PropertyName;
break;
- case EntityProjectionExpression e:
+ case StructuralTypeProjectionExpression e:
storeName = e.PropertyName;
break;
}
@@ -108,8 +108,8 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression)
objectArrayProjectionExpression.Object, storeName, parameterExpression.Type);
break;
- case EntityProjectionExpression entityProjectionExpression:
- var accessExpression = entityProjectionExpression.Object;
+ case StructuralTypeProjectionExpression structuralTypeProjectionExpression:
+ var accessExpression = structuralTypeProjectionExpression.Object;
_projectionBindings[accessExpression] = parameterExpression;
switch (accessExpression)
@@ -127,7 +127,7 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression)
accessExpression = objectAccessExpression.Object;
storeNames.Add(objectAccessExpression.PropertyName);
_ownerMappings[objectAccessExpression]
- = (objectAccessExpression.Navigation.DeclaringEntityType, accessExpression);
+ = ((IEntityType)objectAccessExpression.StructuralProperty.DeclaringType, accessExpression);
}
valueExpression = CreateGetValueExpression(accessExpression, (string)null, typeof(JObject));
@@ -165,19 +165,19 @@ when jObjectMethodCallExpression.Method.GetGenericMethodDefinition() == ToObject
}
else
{
- EntityProjectionExpression entityProjectionExpression;
+ StructuralTypeProjectionExpression structuralTypeProjectionExpression;
if (newExpression.Arguments[0] is ProjectionBindingExpression projectionBindingExpression)
{
var projection = GetProjection(projectionBindingExpression);
- entityProjectionExpression = (EntityProjectionExpression)projection.Expression;
+ structuralTypeProjectionExpression = (StructuralTypeProjectionExpression)projection.Expression;
}
else
{
var projection = ((UnaryExpression)((UnaryExpression)newExpression.Arguments[0]).Operand).Operand;
- entityProjectionExpression = (EntityProjectionExpression)projection;
+ structuralTypeProjectionExpression = (StructuralTypeProjectionExpression)projection;
}
- _materializationContextBindings[parameterExpression] = entityProjectionExpression.Object;
+ _materializationContextBindings[parameterExpression] = structuralTypeProjectionExpression.Object;
}
var updatedExpression = New(
@@ -288,7 +288,7 @@ protected override Expression VisitExtension(Expression extensionExpression)
var accessExpression = objectArrayAccess.InnerProjection.Object;
_projectionBindings[accessExpression] = jObjectParameter;
_ownerMappings[accessExpression] =
- (objectArrayAccess.Navigation.DeclaringEntityType, objectArrayAccess.Object);
+ ((IEntityType)objectArrayAccess.StructuralProperty.DeclaringType, objectArrayAccess.Object);
_ordinalParameterBindings[accessExpression] = Add(
ordinalParameter, Constant(1, typeof(int)));
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs
new file mode 100644
index 00000000000..8176fbd33f4
--- /dev/null
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs
@@ -0,0 +1,354 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
+
+namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
+
+public partial class CosmosSqlTranslatingExpressionVisitor
+{
+ private const string RuntimeParameterPrefix = "entity_equality_";
+
+ private static readonly MethodInfo ParameterPropertyValueExtractorMethod =
+ typeof(CosmosSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ParameterPropertyValueExtractor))!;
+
+ private static readonly MethodInfo ParameterValueExtractorMethod =
+ typeof(CosmosSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ParameterValueExtractor))!;
+
+ private static readonly MethodInfo ParameterListValueExtractorMethod =
+ typeof(CosmosSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ParameterListValueExtractor))!;
+
+ private bool TryRewriteContainsEntity(Expression source, Expression item, [NotNullWhen(true)] out Expression? result)
+ {
+ result = null;
+
+ if (item is not StructuralTypeReferenceExpression { StructuralType: IEntityType entityType })
+ {
+ return false;
+ }
+
+ var primaryKeyProperties = entityType.FindPrimaryKey()?.Properties;
+
+ switch (primaryKeyProperties)
+ {
+ case null:
+ throw new InvalidOperationException(
+ CoreStrings.EntityEqualityOnKeylessEntityNotSupported(
+ nameof(Queryable.Contains), entityType.DisplayName()));
+
+ case { Count: > 1 }:
+ throw new InvalidOperationException(
+ CoreStrings.EntityEqualityOnCompositeKeyEntitySubqueryNotSupported(
+ nameof(Queryable.Contains), entityType.DisplayName()));
+ }
+
+ var property = primaryKeyProperties[0];
+ Expression rewrittenSource;
+ switch (source)
+ {
+ case SqlConstantExpression sqlConstantExpression:
+ var values = (IEnumerable)sqlConstantExpression.Value!;
+ var propertyValueList =
+ (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(property.ClrType.MakeNullable()))!;
+ var propertyGetter = property.GetGetter();
+ foreach (var value in values)
+ {
+ propertyValueList.Add(propertyGetter.GetClrValue(value));
+ }
+
+ rewrittenSource = Expression.Constant(propertyValueList);
+ break;
+
+ case SqlParameterExpression sqlParameterExpression:
+ var lambda = Expression.Lambda(
+ Expression.Call(
+ ParameterListValueExtractorMethod.MakeGenericMethod(entityType.ClrType, property.ClrType.MakeNullable()),
+ QueryCompilationContext.QueryContextParameter,
+ Expression.Constant(sqlParameterExpression.Name, typeof(string)),
+ Expression.Constant(property, typeof(IProperty))),
+ QueryCompilationContext.QueryContextParameter
+ );
+
+ var newParameterName = $"{RuntimeParameterPrefix}{sqlParameterExpression.Name}_{property.Name}";
+
+ rewrittenSource = queryCompilationContext.RegisterRuntimeParameter(newParameterName, lambda);
+ break;
+
+ default:
+ return false;
+ }
+
+ result = Visit(
+ Expression.Call(
+ EnumerableMethods.Contains.MakeGenericMethod(property.ClrType.MakeNullable()),
+ rewrittenSource,
+ CreatePropertyAccessExpression(item, property)));
+
+ return true;
+ }
+
+ private bool TryRewriteStructuralTypeEquality(
+ ExpressionType nodeType,
+ Expression left,
+ Expression right,
+ bool equalsMethod,
+ [NotNullWhen(true)] out SqlExpression? result)
+ {
+ switch (left, right)
+ {
+ // Null equality always translates to = null in cosmos, no matter the type of structural type.
+ case (StructuralTypeReferenceExpression, SqlConstantExpression { Value: null }):
+ case (SqlConstantExpression { Value: null }, StructuralTypeReferenceExpression):
+ return RewriteNullEquality(out result);
+
+ case (StructuralTypeReferenceExpression { StructuralType: IEntityType }, _):
+ case (_, StructuralTypeReferenceExpression { StructuralType: IEntityType }):
+ return TryRewriteEntityEquality(out result);
+
+ case (StructuralTypeReferenceExpression { StructuralType: IComplexType }, _):
+ case (_, StructuralTypeReferenceExpression { StructuralType: IComplexType }):
+ return TryRewriteComplexTypeEquality(collection: false, out result);
+
+ case (CollectionResultExpression, _):
+ case (_, CollectionResultExpression):
+ return TryRewriteComplexTypeEquality(collection: true, out result);
+
+ default:
+ result = null;
+ return false;
+ }
+
+ bool RewriteNullEquality(out SqlExpression? result)
+ {
+ var reference = left as StructuralTypeReferenceExpression ?? (StructuralTypeReferenceExpression)right;
+ var boolTypeMapping = typeMappingSource.FindMapping(typeof(bool))!;
+
+ var shaper = reference.Parameter ??
+ (StructuralTypeShaperExpression)reference.Subquery!.ShaperExpression;
+ if (!shaper.IsNullable)
+ {
+ result = sqlExpressionFactory.Constant(nodeType != ExpressionType.Equal, boolTypeMapping);
+ return true;
+ }
+
+ var access = Visit(shaper.ValueBufferExpression);
+ result = new SqlBinaryExpression(
+ nodeType,
+ access,
+ sqlExpressionFactory.Constant(null, typeof(object), CosmosTypeMapping.Default)!,
+ typeof(bool),
+ boolTypeMapping)!;
+ return true;
+ }
+
+ bool TryRewriteEntityEquality(out SqlExpression? result)
+ {
+ var leftReference = left as StructuralTypeReferenceExpression;
+ var rightReference = right as StructuralTypeReferenceExpression;
+
+ var leftEntityType = leftReference?.StructuralType as IEntityType;
+ var rightEntityType = rightReference?.StructuralType as IEntityType;
+ var entityType = leftEntityType ?? rightEntityType;
+
+ Check.DebugAssert(entityType != null, "We checked that at least one side is an entity type before calling this function");
+
+ if (leftEntityType != null
+ && rightEntityType != null
+ && leftEntityType.GetRootType() != rightEntityType.GetRootType())
+ {
+ result = sqlExpressionFactory.Constant(false);
+ return true;
+ }
+
+ var primaryKeyProperties = entityType.FindPrimaryKey()?.Properties;
+ if (primaryKeyProperties == null)
+ {
+ throw new InvalidOperationException(
+ CoreStrings.EntityEqualityOnKeylessEntityNotSupported(
+ nodeType == ExpressionType.Equal
+ ? equalsMethod ? nameof(object.Equals) : "=="
+ : equalsMethod
+ ? "!" + nameof(object.Equals)
+ : "!=",
+ entityType.DisplayName()));
+ }
+
+ result = Visit(
+ primaryKeyProperties.Select(p => Expression.MakeBinary(nodeType,
+ CreatePropertyAccessExpression(left, p),
+ CreatePropertyAccessExpression(right, p)))
+ .Aggregate((l, r) => nodeType == ExpressionType.Equal
+ ? Expression.AndAlso(l, r)
+ : Expression.OrElse(l, r))) as SqlExpression;
+
+ return result is not null;
+
+ }
+
+ bool TryRewriteComplexTypeEquality(bool collection, out SqlExpression? result)
+ {
+ var leftComplexType = left switch
+ {
+ StructuralTypeReferenceExpression { StructuralType: IComplexType type } => type,
+ CollectionResultExpression { ComplexProperty: IComplexProperty { ComplexType: var type } } => type,
+
+ _ => null
+ };
+
+ var rightComplexType = right switch
+ {
+ StructuralTypeReferenceExpression { StructuralType: IComplexType type } => type,
+ CollectionResultExpression { ComplexProperty: IComplexProperty { ComplexType: var type } } => type,
+
+ _ => null
+ };
+
+ if (leftComplexType is not null
+ && rightComplexType is not null
+ && leftComplexType.ClrType != rightComplexType.ClrType)
+ {
+ // Currently only support comparing complex types of the same CLR type.
+ // We could allow any case where the complex types have the same properties (some may be shadow).
+ result = null;
+ return false;
+ }
+
+ var boolTypeMapping = typeMappingSource.FindMapping(typeof(bool))!;
+ SqlExpression? comparison = null;
+
+ // Generate an expression that compares by direct object access
+ if (!TryGenerateComparison(leftComplexType, rightComplexType, left, right, ref comparison))
+ {
+ result = null;
+ return false;
+ }
+
+ result = comparison;
+ return true;
+
+ bool TryGenerateComparison(
+ IComplexType? leftComplexType,
+ IComplexType? rightComplexType,
+ Expression left,
+ Expression right,
+ [NotNullWhen(true)] ref SqlExpression? comparison)
+ {
+ var complexType = leftComplexType ?? rightComplexType;
+ Debug.Assert(complexType != null);
+
+ if (!TryProcessComplexAccess(left, out var leftAccess) || !TryProcessComplexAccess(right, out var rightAccess))
+ {
+ comparison = null;
+ return false;
+ }
+
+ comparison = new SqlBinaryExpression(
+ nodeType,
+ leftAccess,
+ rightAccess,
+ typeof(bool),
+ boolTypeMapping)!;
+ return true;
+
+ bool TryProcessComplexAccess(Expression expression, [NotNullWhen(true)] out Expression? result)
+ {
+ result = expression switch
+ {
+ StructuralTypeReferenceExpression { StructuralType: IComplexType } reference
+ => Visit((reference.Parameter ?? (StructuralTypeShaperExpression)reference.Subquery!.ShaperExpression).ValueBufferExpression),
+ CollectionResultExpression { ComplexProperty: IComplexProperty } collectionResult
+ => collectionResult.QueryExpression,
+ SqlParameterExpression sqlParameterExpression
+ => CreateJsonQueryParameter(sqlParameterExpression),
+ SqlConstantExpression constant
+ => sqlExpressionFactory.Constant(
+ CosmosSerializationUtilities.SerializeObjectToComplexProperty(complexType, constant.Value, collection),
+ CosmosTypeMapping.Default),
+
+ _ => null
+ };
+
+ return result != null;
+ }
+
+ SqlExpression CreateJsonQueryParameter(SqlParameterExpression sqlParameterExpression)
+ {
+ var lambda = Expression.Lambda(
+ Expression.Call(
+ CosmosSerializationUtilities.SerializeObjectToComplexPropertyMethod,
+ Expression.Constant(complexType, typeof(IComplexType)),
+ Expression.Convert(
+ Expression.Call(
+ ParameterValueExtractorMethod.MakeGenericMethod(sqlParameterExpression.Type.MakeNullable()),
+ QueryCompilationContext.QueryContextParameter,
+ Expression.Constant(sqlParameterExpression.Name, typeof(string))),
+ typeof(object)),
+ Expression.Constant(collection)),
+ QueryCompilationContext.QueryContextParameter);
+
+ var param = queryCompilationContext.RegisterRuntimeParameter($"{RuntimeParameterPrefix}{sqlParameterExpression.Name}", lambda);
+ return new SqlParameterExpression(param.Name, param.Type, CosmosTypeMapping.Default);
+ }
+ }
+ }
+ }
+
+ private Expression CreatePropertyAccessExpression(Expression target, IPropertyBase property)
+ {
+ switch (target)
+ {
+ case SqlConstantExpression sqlConstantExpression:
+ return Expression.Constant(
+ property.GetGetter().GetClrValue(sqlConstantExpression.Value!), property.ClrType.MakeNullable());
+
+ case SqlParameterExpression sqlParameterExpression:
+ var lambda = Expression.Lambda(
+ Expression.Call(
+ ParameterPropertyValueExtractorMethod.MakeGenericMethod(property.ClrType.MakeNullable()),
+ QueryCompilationContext.QueryContextParameter,
+ Expression.Constant(sqlParameterExpression.Name, typeof(string)),
+ Expression.Constant(property, typeof(IPropertyBase))),
+ QueryCompilationContext.QueryContextParameter);
+
+ var newParameterName = $"{RuntimeParameterPrefix}{sqlParameterExpression.Name}_{property.Name}";
+
+ return queryCompilationContext.RegisterRuntimeParameter(newParameterName, lambda);
+
+ case MemberInitExpression memberInitExpression
+ when memberInitExpression.Bindings.SingleOrDefault(mb => mb.Member.Name == property.Name) is MemberAssignment
+ memberAssignment:
+ return memberAssignment.Expression;
+
+ default:
+ return target.CreateEFPropertyExpression(property);
+ }
+ }
+
+ private static T? ParameterPropertyValueExtractor(QueryContext context, string baseParameterName, IPropertyBase property)
+ {
+ var baseParameter = context.Parameters[baseParameterName];
+ return baseParameter == null ? (T?)(object?)null : (T?)property.GetGetter().GetClrValue(baseParameter);
+ }
+
+ private static T? ParameterValueExtractor(QueryContext context, string baseParameterName)
+ {
+ var baseParameter = context.Parameters[baseParameterName];
+ return (T?)baseParameter;
+ }
+
+ private static List? ParameterListValueExtractor(
+ QueryContext context,
+ string baseParameterName,
+ IProperty property)
+ {
+ if (context.Parameters[baseParameterName] is not IEnumerable baseListParameter)
+ {
+ return null;
+ }
+
+ var getter = property.GetGetter();
+ return baseListParameter.Select(e => e != null ? (TProperty?)getter.GetClrValue(e) : (TProperty?)(object?)null).ToList();
+ }
+}
diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs
index 20992155bf3..e0bbea048b1 100644
--- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs
@@ -4,7 +4,6 @@
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using Microsoft.EntityFrameworkCore.Cosmos.Internal;
-using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
using Microsoft.EntityFrameworkCore.Internal;
using static Microsoft.EntityFrameworkCore.Infrastructure.ExpressionExtensions;
@@ -16,7 +15,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
/// 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 class CosmosSqlTranslatingExpressionVisitor(
+public partial class CosmosSqlTranslatingExpressionVisitor(
QueryCompilationContext queryCompilationContext,
ISqlExpressionFactory sqlExpressionFactory,
ITypeMappingSource typeMappingSource,
@@ -25,14 +24,6 @@ public class CosmosSqlTranslatingExpressionVisitor(
QueryableMethodTranslatingExpressionVisitor queryableMethodTranslatingExpressionVisitor)
: ExpressionVisitor
{
- private const string RuntimeParameterPrefix = "entity_equality_";
-
- private static readonly MethodInfo ParameterValueExtractorMethod =
- typeof(CosmosSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ParameterValueExtractor))!;
-
- private static readonly MethodInfo ParameterListValueExtractorMethod =
- typeof(CosmosSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ParameterListValueExtractor))!;
-
private static readonly MethodInfo ConcatMethodInfo
= typeof(string).GetRuntimeMethod(nameof(string.Concat), [typeof(object), typeof(object)])!;
@@ -137,16 +128,16 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression)
case ExpressionType.Equal:
case ExpressionType.NotEqual when binaryExpression.Left.Type == typeof(Type):
- if (IsGetTypeMethodCall(binaryExpression.Left, out var entityReference1)
+ if (IsGetTypeMethodCall(binaryExpression.Left, out var structuralTypeReference1)
&& IsTypeConstant(binaryExpression.Right, out var type1))
{
- return ProcessGetType(entityReference1!, type1!, binaryExpression.NodeType == ExpressionType.Equal);
+ return ProcessGetType(structuralTypeReference1!, type1!, binaryExpression.NodeType == ExpressionType.Equal);
}
- if (IsGetTypeMethodCall(binaryExpression.Right, out var entityReference2)
+ if (IsGetTypeMethodCall(binaryExpression.Right, out var structuralTypeReference2)
&& IsTypeConstant(binaryExpression.Left, out var type2))
{
- return ProcessGetType(entityReference2!, type2!, binaryExpression.NodeType == ExpressionType.Equal);
+ return ProcessGetType(structuralTypeReference2!, type2!, binaryExpression.NodeType == ExpressionType.Equal);
}
break;
@@ -179,7 +170,7 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression)
{
// Visited expression could be null, We need to pass MemberInitExpression
case { NodeType: ExpressionType.Equal or ExpressionType.NotEqual }
- when TryRewriteEntityEquality(
+ when TryRewriteStructuralTypeEquality(
binaryExpression.NodeType,
visitedLeft == QueryCompilationContext.NotTranslatedExpression ? left : visitedLeft,
visitedRight == QueryCompilationContext.NotTranslatedExpression ? right : visitedRight,
@@ -210,15 +201,14 @@ when TryRewriteEntityEquality(
?? QueryCompilationContext.NotTranslatedExpression;
}
- Expression ProcessGetType(EntityReferenceExpression entityReferenceExpression, Type comparisonType, bool match)
+ Expression ProcessGetType(StructuralTypeReferenceExpression typeReference, Type comparisonType, bool match)
{
- var entityType = entityReferenceExpression.EntityType;
-
- if (entityType.BaseType == null
- && !entityType.GetDirectlyDerivedTypes().Any())
+ if (typeReference.StructuralType is not IEntityType entityType
+ || (entityType.BaseType == null
+ && !entityType.GetDirectlyDerivedTypes().Any()))
{
// No hierarchy
- return sqlExpressionFactory.Constant((entityType.ClrType == comparisonType) == match);
+ return sqlExpressionFactory.Constant((typeReference.StructuralType.ClrType == comparisonType) == match);
}
if (entityType.GetAllBaseTypes().Any(e => e.ClrType == comparisonType))
@@ -240,7 +230,7 @@ Expression ProcessGetType(EntityReferenceExpression entityReferenceExpression, T
// Or add predicate for matching that particular type discriminator value
// All hierarchies have discriminator property
if (TryBindMember(
- entityReferenceExpression,
+ typeReference,
MemberIdentity.Create(entityType.GetDiscriminatorPropertyName()),
out var discriminatorMember,
out _)
@@ -259,17 +249,17 @@ Expression ProcessGetType(EntityReferenceExpression entityReferenceExpression, T
return QueryCompilationContext.NotTranslatedExpression;
}
- bool IsGetTypeMethodCall(Expression expression, [NotNullWhen(true)] out EntityReferenceExpression? entityReferenceExpression)
+ bool IsGetTypeMethodCall(Expression expression, [NotNullWhen(true)] out StructuralTypeReferenceExpression? typeReference)
{
- entityReferenceExpression = null;
+ typeReference = null;
if (expression is not MethodCallExpression methodCallExpression
|| methodCallExpression.Method != GetTypeMethodInfo)
{
return false;
}
- entityReferenceExpression = Visit(methodCallExpression.Object) as EntityReferenceExpression;
- return entityReferenceExpression != null;
+ typeReference = Visit(methodCallExpression.Object) as StructuralTypeReferenceExpression;
+ return typeReference != null;
}
static bool IsTypeConstant(Expression expression, [NotNullWhen(true)] out Type? type)
@@ -340,8 +330,8 @@ protected override Expression VisitExtension(Expression extensionExpression)
{
switch (extensionExpression)
{
- case EntityProjectionExpression:
- case EntityReferenceExpression:
+ case StructuralTypeProjectionExpression:
+ case StructuralTypeReferenceExpression:
case SqlExpression:
return extensionExpression;
@@ -349,7 +339,7 @@ protected override Expression VisitExtension(Expression extensionExpression)
return new SqlParameterExpression(queryParameter.Name, queryParameter.Type, null);
case StructuralTypeShaperExpression shaper:
- return new EntityReferenceExpression(shaper);
+ return new StructuralTypeReferenceExpression(shaper);
// var result = Visit(entityShaperExpression.ValueBufferExpression);
//
@@ -393,7 +383,7 @@ protected override Expression VisitExtension(Expression extensionExpression)
&& (convertedType == null
|| convertedType.IsAssignableFrom(ese.Type)))
{
- return new EntityReferenceExpression(shapedQuery.UpdateShaperExpression(innerExpression));
+ return new StructuralTypeReferenceExpression(shapedQuery.UpdateShaperExpression(innerExpression));
}
if (innerExpression is ProjectionBindingExpression pbe
@@ -492,11 +482,37 @@ protected override Expression VisitListInit(ListInitExpression listInitExpressio
///
protected override Expression VisitMember(MemberExpression memberExpression)
{
- var innerExpression = Visit(memberExpression.Expression);
+ var member = memberExpression.Member;
+ var inner = Visit(memberExpression.Expression);
+
+ // Try binding the member to a property on the structural type
+ if (TryBindMember(inner, MemberIdentity.Create(memberExpression.Member), out var expression, out _))
+ {
+ return expression;
+ }
+
+ // We handle translations for Nullable<> members here.
+ // These can't be handled in regular IMemberTranslators, since those only support scalars (SqlExpressions);
+ // but we also need to handle nullable value complex types.
+ if (member.DeclaringType?.IsGenericType == true
+ && member.DeclaringType.GetGenericTypeDefinition() == typeof(Nullable<>)
+ && inner is not null)
+ {
+ switch (member.Name)
+ {
+ case nameof(Nullable<>.Value):
+ return inner;
+ case nameof(Nullable<>.HasValue) when inner is SqlExpression sqlInner:
+ return sqlExpressionFactory.IsNotNull(sqlInner);
+ case nameof(Nullable<>.HasValue)
+ when inner is StructuralTypeReferenceExpression
+ && TryRewriteStructuralTypeEquality(
+ ExpressionType.NotEqual, inner, new SqlConstantExpression(null, memberExpression.Expression!.Type, null), equalsMethod: false, out var result):
+ return result;
+ }
+ }
- return TryBindMember(innerExpression, MemberIdentity.Create(memberExpression.Member), out var expression, out _)
- ? expression
- : (TranslationFailed(memberExpression.Expression, innerExpression, out var sqlInnerExpression)
+ return (TranslationFailed(memberExpression.Expression, inner, out var sqlInnerExpression)
? QueryCompilationContext.NotTranslatedExpression
: memberTranslatorProvider.Translate(
sqlInnerExpression, memberExpression.Member, memberExpression.Type, queryCompilationContext.Logger))
@@ -545,7 +561,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
var left = Visit(methodCallExpression.Object);
var right = Visit(RemoveObjectConvert(methodCallExpression.Arguments[0]));
- if (TryRewriteEntityEquality(
+ if (TryRewriteStructuralTypeEquality(
ExpressionType.Equal,
left == QueryCompilationContext.NotTranslatedExpression ? methodCallExpression.Object : left,
right == QueryCompilationContext.NotTranslatedExpression ? methodCallExpression.Arguments[0] : right,
@@ -579,7 +595,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
var left = Visit(RemoveObjectConvert(methodCallExpression.Arguments[0]));
var right = Visit(RemoveObjectConvert(methodCallExpression.Arguments[1]));
- if (TryRewriteEntityEquality(
+ if (TryRewriteStructuralTypeEquality(
ExpressionType.Equal,
left == QueryCompilationContext.NotTranslatedExpression ? methodCallExpression.Arguments[0] : left,
right == QueryCompilationContext.NotTranslatedExpression ? methodCallExpression.Arguments[1] : right,
@@ -803,10 +819,10 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression)
{
var operand = Visit(unaryExpression.Operand);
- if (operand is EntityReferenceExpression entityReferenceExpression
+ if (operand is StructuralTypeReferenceExpression structuralTypeReferenceExpression
&& unaryExpression.NodeType is ExpressionType.Convert or ExpressionType.ConvertChecked or ExpressionType.TypeAs)
{
- return entityReferenceExpression.Convert(unaryExpression.Type);
+ return structuralTypeReferenceExpression.Convert(unaryExpression.Type);
}
if (TranslationFailed(unaryExpression.Operand, operand, out var sqlOperand))
@@ -853,35 +869,41 @@ protected override Expression VisitTypeBinary(TypeBinaryExpression typeBinaryExp
{
var innerExpression = Visit(typeBinaryExpression.Expression);
- if (typeBinaryExpression.NodeType == ExpressionType.TypeIs
- && innerExpression is EntityReferenceExpression entityReferenceExpression)
+ if (typeBinaryExpression.NodeType != ExpressionType.TypeIs
+ || innerExpression is not StructuralTypeReferenceExpression typeReference)
{
- var entityType = entityReferenceExpression.EntityType;
- if (entityType.GetAllBaseTypesInclusive().Any(et => et.ClrType == typeBinaryExpression.TypeOperand))
- {
- return sqlExpressionFactory.Constant(true);
- }
+ return QueryCompilationContext.NotTranslatedExpression;
+ }
- var derivedType = entityType.GetDerivedTypes().SingleOrDefault(et => et.ClrType == typeBinaryExpression.TypeOperand);
- if (derivedType != null
- && TryBindMember(
- entityReferenceExpression,
- MemberIdentity.Create(entityType.GetDiscriminatorPropertyName()),
- out var discriminatorMember,
- out _)
- && discriminatorMember is SqlExpression discriminatorColumn)
- {
- var concreteEntityTypes = derivedType.GetConcreteDerivedTypesInclusive().ToList();
-
- return concreteEntityTypes.Count == 1
- ? sqlExpressionFactory.Equal(
- discriminatorColumn,
- sqlExpressionFactory.Constant(concreteEntityTypes[0].GetDiscriminatorValue(), discriminatorColumn.Type))
- : sqlExpressionFactory.In(
- discriminatorColumn,
- concreteEntityTypes
- .Select(et => sqlExpressionFactory.Constant(et.GetDiscriminatorValue(), discriminatorColumn.Type)).ToArray());
- }
+ if (typeReference.StructuralType is not IEntityType entityType)
+ {
+ return sqlExpressionFactory.Constant(typeReference.StructuralType.ClrType == typeBinaryExpression.TypeOperand);
+ }
+
+ if (entityType.GetAllBaseTypesInclusive().Any(et => et.ClrType == typeBinaryExpression.TypeOperand))
+ {
+ return sqlExpressionFactory.Constant(true);
+ }
+
+ var derivedType = entityType.GetDerivedTypes().SingleOrDefault(et => et.ClrType == typeBinaryExpression.TypeOperand);
+ if (derivedType != null
+ && TryBindMember(
+ typeReference,
+ MemberIdentity.Create(entityType.GetDiscriminatorPropertyName()),
+ out var discriminatorMember,
+ out _)
+ && discriminatorMember is SqlExpression discriminatorColumn)
+ {
+ var concreteEntityTypes = derivedType.GetConcreteDerivedTypesInclusive().ToList();
+
+ return concreteEntityTypes.Count == 1
+ ? sqlExpressionFactory.Equal(
+ discriminatorColumn,
+ sqlExpressionFactory.Constant(concreteEntityTypes[0].GetDiscriminatorValue(), discriminatorColumn.Type))
+ : sqlExpressionFactory.In(
+ discriminatorColumn,
+ concreteEntityTypes
+ .Select(et => sqlExpressionFactory.Constant(et.GetDiscriminatorValue(), discriminatorColumn.Type)).ToArray());
}
return QueryCompilationContext.NotTranslatedExpression;
@@ -901,7 +923,7 @@ public virtual bool TryBindMember(
[NotNullWhen(true)] out IPropertyBase? property,
bool wrapResultExpressionInReferenceExpression = true)
{
- if (source is not EntityReferenceExpression typeReference)
+ if (source is not StructuralTypeReferenceExpression typeReference)
{
expression = null;
property = null;
@@ -912,16 +934,16 @@ public virtual bool TryBindMember(
{
case { Parameter: { } shaper }:
var valueBufferExpression = Visit(shaper.ValueBufferExpression);
- var entityProjection = (EntityProjectionExpression)valueBufferExpression;
+ var structuralTypeProjection = (StructuralTypeProjectionExpression)valueBufferExpression;
expression = member switch
{
{ MemberInfo: { } memberInfo }
- => entityProjection.BindMember(
+ => structuralTypeProjection.BindMember(
memberInfo, typeReference.Type, clientEval: false, out property),
{ Name: { } name }
- => entityProjection.BindMember(
+ => structuralTypeProjection.BindMember(
name, typeReference.Type, clientEval: false, out property),
_ => throw new UnreachableException()
@@ -941,7 +963,7 @@ public virtual bool TryBindMember(
AddTranslationErrorDetails(
CoreStrings.QueryUnableToTranslateMember(
member.Name,
- typeReference.EntityType.DisplayName()));
+ typeReference.StructuralType.DisplayName()));
return false;
}
@@ -950,7 +972,7 @@ public virtual bool TryBindMember(
switch (expression)
{
case StructuralTypeShaperExpression shaper when wrapResultExpressionInReferenceExpression:
- expression = new EntityReferenceExpression(shaper);
+ expression = new StructuralTypeReferenceExpression(shaper);
return true;
// case ObjectArrayAccessExpression objectArrayProjectionExpression:
// expression = objectArrayProjectionExpression;
@@ -991,207 +1013,6 @@ private static Expression TryRemoveImplicitConvert(Expression expression)
return expression;
}
- private bool TryRewriteContainsEntity(Expression source, Expression item, [NotNullWhen(true)] out Expression? result)
- {
- result = null;
-
- if (item is not EntityReferenceExpression itemEntityReference)
- {
- return false;
- }
-
- var entityType = itemEntityReference.EntityType;
- var primaryKeyProperties = entityType.FindPrimaryKey()?.Properties;
-
- switch (primaryKeyProperties)
- {
- case null:
- throw new InvalidOperationException(
- CoreStrings.EntityEqualityOnKeylessEntityNotSupported(
- nameof(Queryable.Contains), entityType.DisplayName()));
-
- case { Count: > 1 }:
- throw new InvalidOperationException(
- CoreStrings.EntityEqualityOnCompositeKeyEntitySubqueryNotSupported(
- nameof(Queryable.Contains), entityType.DisplayName()));
- }
-
- var property = primaryKeyProperties[0];
- Expression rewrittenSource;
- switch (source)
- {
- case SqlConstantExpression sqlConstantExpression:
- var values = (IEnumerable)sqlConstantExpression.Value!;
- var propertyValueList =
- (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(property.ClrType.MakeNullable()))!;
- var propertyGetter = property.GetGetter();
- foreach (var value in values)
- {
- propertyValueList.Add(propertyGetter.GetClrValue(value));
- }
-
- rewrittenSource = Expression.Constant(propertyValueList);
- break;
-
- case SqlParameterExpression sqlParameterExpression:
- var lambda = Expression.Lambda(
- Expression.Call(
- ParameterListValueExtractorMethod.MakeGenericMethod(entityType.ClrType, property.ClrType.MakeNullable()),
- QueryCompilationContext.QueryContextParameter,
- Expression.Constant(sqlParameterExpression.Name, typeof(string)),
- Expression.Constant(property, typeof(IProperty))),
- QueryCompilationContext.QueryContextParameter
- );
-
- var newParameterName = $"{RuntimeParameterPrefix}{sqlParameterExpression.Name}_{property.Name}";
-
- rewrittenSource = queryCompilationContext.RegisterRuntimeParameter(newParameterName, lambda);
- break;
-
- default:
- return false;
- }
-
- result = Visit(
- Expression.Call(
- EnumerableMethods.Contains.MakeGenericMethod(property.ClrType.MakeNullable()),
- rewrittenSource,
- CreatePropertyAccessExpression(item, property)));
-
- return true;
- }
-
- private bool TryRewriteEntityEquality(
- ExpressionType nodeType,
- Expression left,
- Expression right,
- bool equalsMethod,
- [NotNullWhen(true)] out Expression? result)
- {
- var leftEntityReference = left as EntityReferenceExpression;
- var rightEntityReference = right as EntityReferenceExpression;
-
- if (leftEntityReference == null
- && rightEntityReference == null)
- {
- result = null;
- return false;
- }
-
- if (left is SqlConstantExpression { Value: null }
- || right is SqlConstantExpression { Value: null })
- {
- var nonNullEntityReference = (left is SqlConstantExpression { Value: null } ? rightEntityReference : leftEntityReference)!;
- var shaper = nonNullEntityReference.Parameter
- ?? (StructuralTypeShaperExpression)nonNullEntityReference.Subquery!.ShaperExpression;
-
- if (!shaper.IsNullable)
- {
- result = Visit(Expression.Constant(nodeType != ExpressionType.Equal));
- return true;
- }
-
- var access = Visit(shaper.ValueBufferExpression);
- result = new SqlBinaryExpression(
- nodeType,
- access,
- sqlExpressionFactory.Constant(null, typeof(object), CosmosTypeMapping.Default)!,
- typeof(bool),
- typeMappingSource.FindMapping(typeof(bool)))!;
- return true;
- }
-
- var leftEntityType = leftEntityReference?.EntityType;
- var rightEntityType = rightEntityReference?.EntityType;
- var entityType = leftEntityType ?? rightEntityType;
-
- Check.DebugAssert(entityType != null, "At least either side should be entityReference so entityType should be non-null.");
-
- if (leftEntityType != null
- && rightEntityType != null
- && leftEntityType.GetRootType() != rightEntityType.GetRootType())
- {
- result = sqlExpressionFactory.Constant(false);
- return true;
- }
-
- var primaryKeyProperties = entityType.FindPrimaryKey()?.Properties;
- if (primaryKeyProperties == null)
- {
- throw new InvalidOperationException(
- CoreStrings.EntityEqualityOnKeylessEntityNotSupported(
- nodeType == ExpressionType.Equal
- ? equalsMethod ? nameof(object.Equals) : "=="
- : equalsMethod
- ? "!" + nameof(object.Equals)
- : "!=",
- entityType.DisplayName()));
- }
-
- result = Visit(
- primaryKeyProperties.Select(p =>
- Expression.MakeBinary(
- nodeType,
- CreatePropertyAccessExpression(left, p),
- CreatePropertyAccessExpression(right, p)))
- .Aggregate((l, r) => nodeType == ExpressionType.Equal
- ? Expression.AndAlso(l, r)
- : Expression.OrElse(l, r)));
-
- return true;
- }
-
- private Expression CreatePropertyAccessExpression(Expression target, IProperty property)
- {
- switch (target)
- {
- case SqlConstantExpression sqlConstantExpression:
- return Expression.Constant(
- property.GetGetter().GetClrValue(sqlConstantExpression.Value!), property.ClrType.MakeNullable());
-
- case SqlParameterExpression sqlParameterExpression:
- var lambda = Expression.Lambda(
- Expression.Call(
- ParameterValueExtractorMethod.MakeGenericMethod(property.ClrType.MakeNullable()),
- QueryCompilationContext.QueryContextParameter,
- Expression.Constant(sqlParameterExpression.Name, typeof(string)),
- Expression.Constant(property, typeof(IProperty))),
- QueryCompilationContext.QueryContextParameter);
-
- var newParameterName = $"{RuntimeParameterPrefix}{sqlParameterExpression.Name}_{property.Name}";
-
- return queryCompilationContext.RegisterRuntimeParameter(newParameterName, lambda);
-
- case MemberInitExpression memberInitExpression
- when memberInitExpression.Bindings.SingleOrDefault(mb => mb.Member.Name == property.Name) is MemberAssignment
- memberAssignment:
- return memberAssignment.Expression;
-
- default:
- return target.CreateEFPropertyExpression(property);
- }
- }
-
- private static T? ParameterValueExtractor(QueryContext context, string baseParameterName, IProperty property)
- {
- var baseParameter = context.Parameters[baseParameterName];
- return baseParameter == null ? (T?)(object?)null : (T?)property.GetGetter().GetClrValue(baseParameter);
- }
-
- private static List? ParameterListValueExtractor(
- QueryContext context,
- string baseParameterName,
- IProperty property)
- {
- if (context.Parameters[baseParameterName] is not IEnumerable baseListParameter)
- {
- return null;
- }
-
- var getter = property.GetGetter();
- return baseListParameter.Select(e => e != null ? (TProperty?)getter.GetClrValue(e) : (TProperty?)(object?)null).ToList();
- }
-
private static bool TryEvaluateToConstant(Expression expression, [NotNullWhen(true)] out SqlConstantExpression? sqlConstantExpression)
{
if (CanEvaluate(expression))
@@ -1236,33 +1057,33 @@ private static bool TranslationFailed(Expression? original, Expression? translat
}
[DebuggerDisplay("{DebuggerDisplay(),nq}")]
- private sealed class EntityReferenceExpression : Expression
+ private sealed class StructuralTypeReferenceExpression : Expression
{
- public EntityReferenceExpression(StructuralTypeShaperExpression parameter)
+ public StructuralTypeReferenceExpression(StructuralTypeShaperExpression parameter)
{
Parameter = parameter;
- EntityType = (IEntityType)parameter.StructuralType;
+ StructuralType = parameter.StructuralType;
}
- public EntityReferenceExpression(ShapedQueryExpression subquery)
+ public StructuralTypeReferenceExpression(ShapedQueryExpression subquery)
{
Subquery = subquery;
- EntityType = (IEntityType)((StructuralTypeShaperExpression)subquery.ShaperExpression).StructuralType;
+ StructuralType = ((StructuralTypeShaperExpression)subquery.ShaperExpression).StructuralType;
}
- private EntityReferenceExpression(EntityReferenceExpression typeReference, ITypeBase structuralType)
+ private StructuralTypeReferenceExpression(StructuralTypeReferenceExpression typeReference, ITypeBase structuralType)
{
Parameter = typeReference.Parameter;
Subquery = typeReference.Subquery;
- EntityType = (IEntityType)structuralType;
+ StructuralType = structuralType;
}
public new StructuralTypeShaperExpression? Parameter { get; }
public ShapedQueryExpression? Subquery { get; }
- public IEntityType EntityType { get; }
+ public ITypeBase StructuralType { get; }
public override Type Type
- => EntityType.ClrType;
+ => StructuralType.ClrType;
public override ExpressionType NodeType
=> ExpressionType.Extension;
@@ -1275,9 +1096,9 @@ public Expression Convert(Type type)
return this;
}
- return EntityType is { } entityType
- && entityType.GetDerivedTypes().FirstOrDefault(et => et.ClrType == type) is { } derivedEntityType
- ? new EntityReferenceExpression(this, derivedEntityType)
+ return StructuralType is IEntityType entityType
+ && entityType.GetDerivedTypes().FirstOrDefault(et => et.ClrType == type) is { } derivedStructuralType
+ ? new StructuralTypeReferenceExpression(this, derivedStructuralType)
: QueryCompilationContext.NotTranslatedExpression;
}
diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/CollectionResultExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/CollectionResultExpression.cs
new file mode 100644
index 00000000000..71ececab9c0
--- /dev/null
+++ b/src/EFCore.Cosmos/Query/Internal/Expressions/CollectionResultExpression.cs
@@ -0,0 +1,65 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+// ReSharper disable once CheckNamespace
+
+namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
+
+///
+/// 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 class CollectionResultExpression(
+ Expression queryExpression,
+ IComplexProperty complexProperty)
+ : Expression, IPrintableExpression
+{
+ ///
+ /// The query expression to get the collection.
+ ///
+ public virtual Expression QueryExpression { get; } = queryExpression;
+
+ ///
+ /// The property associated with the collection. In cosmos, this can only be a complex property
+ ///
+ public virtual IComplexProperty ComplexProperty { get; } = complexProperty;
+
+ ///
+ public override Type Type
+ => QueryExpression.Type;
+
+ ///
+ public override ExpressionType NodeType
+ => ExpressionType.Extension;
+
+ ///
+ protected override Expression VisitChildren(ExpressionVisitor visitor)
+ => Update(visitor.Visit(QueryExpression));
+
+ ///
+ /// Creates a new expression that is like this one, but using the supplied children. If all of the children are the same, it will
+ /// return this expression.
+ ///
+ /// The property of the result.
+ /// This expression if no children changed, or an expression with the updated children.
+ public virtual CollectionResultExpression Update(Expression queryExpression)
+ => queryExpression == QueryExpression
+ ? this
+ : new CollectionResultExpression(queryExpression, ComplexProperty);
+
+ ///
+ public virtual void Print(ExpressionPrinter expressionPrinter)
+ {
+ expressionPrinter.AppendLine("CollectionResultExpression:");
+ using (expressionPrinter.Indent())
+ {
+ expressionPrinter.Append("QueryExpression:");
+ expressionPrinter.Visit(QueryExpression);
+ expressionPrinter.AppendLine();
+
+ expressionPrinter.Append("Complex Property:").AppendLine(ComplexProperty.ToString()!);
+ }
+ }
+}
diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectAccessExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectAccessExpression.cs
index 007394d7119..d793661e793 100644
--- a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectAccessExpression.cs
+++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectAccessExpression.cs
@@ -30,10 +30,33 @@ public ObjectAccessExpression(Expression @object, INavigation navigation)
CosmosStrings.NavigationPropertyIsNotAnEmbeddedEntity(
navigation.DeclaringEntityType.DisplayName(), navigation.Name));
- Navigation = navigation;
+ StructuralProperty = navigation;
+ StructuralType = navigation.TargetEntityType;
Object = @object;
}
+ ///
+ /// 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 ObjectAccessExpression(Expression @object, IComplexProperty complexProperty)
+ {
+ StructuralProperty = complexProperty;
+ PropertyName = complexProperty.Name;
+ Object = @object;
+ StructuralType = complexProperty.ComplexType;
+ }
+
+ ///
+ /// 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 ITypeBase StructuralType { get; }
+
///
/// 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
@@ -50,7 +73,7 @@ public override ExpressionType NodeType
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public override Type Type
- => Navigation.ClrType;
+ => StructuralProperty.ClrType;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -74,7 +97,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 INavigation Navigation { get; }
+ public virtual IPropertyBase StructuralProperty { get; }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -93,7 +116,9 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
///
public virtual ObjectAccessExpression Update(Expression outerExpression)
=> outerExpression != Object
- ? new ObjectAccessExpression(outerExpression, Navigation)
+ ? StructuralProperty is INavigation navigation
+ ? new ObjectAccessExpression(outerExpression, navigation)
+ : new ObjectAccessExpression(outerExpression, (IComplexProperty)StructuralProperty)
: this;
///
@@ -127,7 +152,7 @@ public override bool Equals(object? obj)
&& Equals(objectAccessExpression));
private bool Equals(ObjectAccessExpression objectAccessExpression)
- => Navigation == objectAccessExpression.Navigation
+ => StructuralProperty == objectAccessExpression.StructuralProperty
&& Object.Equals(objectAccessExpression.Object);
///
@@ -137,5 +162,5 @@ private bool Equals(ObjectAccessExpression objectAccessExpression)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public override int GetHashCode()
- => HashCode.Combine(Navigation, Object);
+ => HashCode.Combine(StructuralProperty, Object);
}
diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayAccessExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayAccessExpression.cs
index 4eb61d119f3..327f18ad594 100644
--- a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayAccessExpression.cs
+++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayAccessExpression.cs
@@ -27,7 +27,7 @@ public class ObjectArrayAccessExpression : Expression, IPrintableExpression, IAc
public ObjectArrayAccessExpression(
Expression @object,
INavigation navigation,
- EntityProjectionExpression? innerProjection = null)
+ StructuralTypeProjectionExpression? innerProjection = null)
{
var targetType = navigation.TargetEntityType;
Type = typeof(IEnumerable<>).MakeGenericType(targetType.ClrType);
@@ -37,10 +37,31 @@ public ObjectArrayAccessExpression(
CosmosStrings.NavigationPropertyIsNotAnEmbeddedEntity(
navigation.DeclaringEntityType.DisplayName(), navigation.Name));
- Navigation = navigation;
+ StructuralProperty = navigation;
Object = @object;
InnerProjection = innerProjection
- ?? new EntityProjectionExpression(new ObjectReferenceExpression(targetType, ""), targetType);
+ ?? new StructuralTypeProjectionExpression(new ObjectReferenceExpression(targetType, ""), targetType);
+ }
+
+ ///
+ /// 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 ObjectArrayAccessExpression(
+ Expression @object,
+ IComplexProperty complexProperty,
+ StructuralTypeProjectionExpression? innerProjection = null)
+ {
+ var targetType = complexProperty.ComplexType;
+ Type = typeof(IEnumerable<>).MakeGenericType(targetType.ClrType);
+
+ PropertyName = complexProperty.Name;
+ StructuralProperty = complexProperty;
+ Object = @object;
+ InnerProjection = innerProjection
+ ?? new StructuralTypeProjectionExpression(new ObjectReferenceExpression(targetType, ""), targetType);
}
///
@@ -82,7 +103,7 @@ public sealed override ExpressionType NodeType
/// 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 INavigation Navigation { get; }
+ public virtual IPropertyBase StructuralProperty { get; }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -90,7 +111,7 @@ public sealed override ExpressionType NodeType
/// 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 EntityProjectionExpression InnerProjection { get; }
+ public virtual StructuralTypeProjectionExpression InnerProjection { get; }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -103,7 +124,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
var accessExpression = visitor.Visit(Object);
var innerProjection = visitor.Visit(InnerProjection);
- return Update(accessExpression, (EntityProjectionExpression)innerProjection);
+ return Update(accessExpression, (StructuralTypeProjectionExpression)innerProjection);
}
///
@@ -114,9 +135,11 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
///
public virtual ObjectArrayAccessExpression Update(
Expression accessExpression,
- EntityProjectionExpression innerProjection)
+ StructuralTypeProjectionExpression innerProjection)
=> accessExpression != Object || innerProjection != InnerProjection
- ? new ObjectArrayAccessExpression(accessExpression, Navigation, innerProjection)
+ ? StructuralProperty is INavigation navigation
+ ? new ObjectArrayAccessExpression(accessExpression, navigation, innerProjection)
+ : new ObjectArrayAccessExpression(accessExpression, (IComplexProperty)StructuralProperty, innerProjection)
: this;
///
diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectReferenceExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectReferenceExpression.cs
index b2fee0d2605..50aee928f05 100644
--- a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectReferenceExpression.cs
+++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectReferenceExpression.cs
@@ -15,7 +15,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
/// 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 class ObjectReferenceExpression(IEntityType entityType, string name) : Expression, IPrintableExpression, IAccessExpression
+public class ObjectReferenceExpression(ITypeBase structuralType, string name) : Expression, IPrintableExpression, IAccessExpression
{
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -33,7 +33,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
- => EntityType.ClrType;
+ => StructuralType.ClrType;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -45,7 +45,7 @@ public override Type Type
// TODO: (CosmosProjectionBindingRemovingExpressionVisitorBase._projectionBindings has IAccessExpressions as keys, and so entity types
// TODO: need to participate in the equality etc.). Long-term, this should be a server-side SQL expression that knows nothing about
// TODO: the shaper side.
- public virtual IEntityType EntityType { get; } = entityType;
+ public virtual ITypeBase StructuralType { get; } = structuralType;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -105,7 +105,7 @@ public override bool Equals(object? obj)
private bool Equals(ObjectReferenceExpression objectReferenceExpression)
=> Name == objectReferenceExpression.Name
- && EntityType.Equals(objectReferenceExpression.EntityType);
+ && StructuralType.Equals(objectReferenceExpression.StructuralType);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs
index 622f4a8c72f..5f710a622d6 100644
--- a/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs
+++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs
@@ -38,14 +38,15 @@ public sealed class SelectExpression : Expression, IPrintableExpression
/// 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 SelectExpression(
+ private SelectExpression(
List sources,
SqlExpression? predicate,
List projections,
bool distinct,
List orderings,
SqlExpression? offset,
- SqlExpression? limit)
+ SqlExpression? limit,
+ bool usesClientProjection)
{
_sources = sources;
Predicate = predicate is SqlConstantExpression { Value: true } ? null : predicate;
@@ -54,6 +55,7 @@ public SelectExpression(
_orderings = orderings;
Offset = offset;
Limit = limit;
+ UsesClientProjection = usesClientProjection;
}
///
@@ -100,7 +102,8 @@ [new ProjectionExpression(sourceExpression, alias: null!, isValueProjection: tru
distinct: false,
orderings: [],
offset: null,
- limit: null);
+ limit: null,
+ usesClientProjection: false);
}
var source = new SourceExpression(sourceExpression, sourceAlias, withIn: true);
@@ -167,6 +170,18 @@ public IReadOnlyList Orderings
///
public bool IsDistinct { 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.
+ ///
+ ///
+ /// This property indicates whether the query uses client-side projection. We have to keep track of this
+ /// because of #34067. We can't apply distinct to queries with client-side projection.
+ ///
+ public bool UsesClientProjection { 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
@@ -280,8 +295,8 @@ public int AddToProjection(Expression sqlExpression)
/// 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 int AddToProjection(EntityProjectionExpression entityProjection)
- => AddToProjection(entityProjection, null);
+ public int AddToProjection(StructuralTypeProjectionExpression structuralTypeProjection)
+ => AddToProjection(structuralTypeProjection, null);
private int AddToProjection(Expression expression, string? alias)
{
@@ -323,6 +338,15 @@ private int AddToProjection(Expression expression, string? alias)
public void ApplyDistinct()
=> IsDistinct = true;
+ ///
+ /// 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 void IndicateClientProjection()
+ => UsesClientProjection = true;
+
///
/// 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
@@ -504,7 +528,7 @@ public Expression AddJoin(ShapedQueryExpression inner, Expression outerShaper, C
projectionToAdd = expression switch
{
SqlExpression e => new ScalarReferenceExpression(joinSource.Alias, e.Type, e.TypeMapping),
- EntityProjectionExpression e => e.Update(new ObjectReferenceExpression(e.EntityType, joinSource.Alias)),
+ StructuralTypeProjectionExpression e => e.Update(new ObjectReferenceExpression(e.StructuralType, joinSource.Alias)),
_ => throw new UnreachableException(
$"Unexpected expression type in projection when adding join: {expression.GetType().Name}")
@@ -618,7 +642,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
if (changed)
{
- var newSelectExpression = new SelectExpression(sources, predicate, projections, IsDistinct, orderings, offset, limit)
+ var newSelectExpression = new SelectExpression(sources, predicate, projections, IsDistinct, orderings, offset, limit, UsesClientProjection)
{
_projectionMapping = projectionMapping
};
@@ -649,7 +673,7 @@ public SelectExpression Update(
projectionMapping[projectionMember] = expression;
}
- return new SelectExpression(sources, predicate, projections, IsDistinct, orderings, offset, limit)
+ return new SelectExpression(sources, predicate, projections, IsDistinct, orderings, offset, limit, UsesClientProjection)
{
_projectionMapping = projectionMapping, ReadItemInfo = ReadItemInfo
};
@@ -662,7 +686,7 @@ public SelectExpression Update(
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public SelectExpression WithReadItemInfo(ReadItemInfo readItemInfo)
- => new(Sources.ToList(), Predicate, Projection.ToList(), IsDistinct, Orderings.ToList(), Offset, Limit)
+ => new(Sources.ToList(), Predicate, Projection.ToList(), IsDistinct, Orderings.ToList(), Offset, Limit, UsesClientProjection)
{
_projectionMapping = _projectionMapping, ReadItemInfo = readItemInfo
};
@@ -681,7 +705,7 @@ public SelectExpression WithSingleValueProjection()
projectionMapping[projectionMember] = expression;
}
- return new SelectExpression(Sources.ToList(), Predicate, Projection.ToList(), IsDistinct, Orderings.ToList(), Offset, Limit)
+ return new SelectExpression(Sources.ToList(), Predicate, Projection.ToList(), IsDistinct, Orderings.ToList(), Offset, Limit, UsesClientProjection)
{
_projectionMapping = projectionMapping
};
diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/EntityProjectionExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/StructuralTypeProjectionExpression.cs
similarity index 73%
rename from src/EFCore.Cosmos/Query/Internal/Expressions/EntityProjectionExpression.cs
rename to src/EFCore.Cosmos/Query/Internal/Expressions/StructuralTypeProjectionExpression.cs
index 11824d8a4b1..a4160d72e9d 100644
--- a/src/EFCore.Cosmos/Query/Internal/Expressions/EntityProjectionExpression.cs
+++ b/src/EFCore.Cosmos/Query/Internal/Expressions/StructuralTypeProjectionExpression.cs
@@ -12,10 +12,11 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal;
/// 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 class EntityProjectionExpression : Expression, IPrintableExpression, IAccessExpression
+public class StructuralTypeProjectionExpression : Expression, IPrintableExpression, IAccessExpression
{
private readonly Dictionary _propertyExpressionsMap = new();
private readonly Dictionary _navigationExpressionsMap = new();
+ private readonly Dictionary _complexPropertyExpressionsMap = new();
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -23,10 +24,10 @@ public class EntityProjectionExpression : Expression, IPrintableExpression, IAcc
/// 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 EntityProjectionExpression(Expression @object, IEntityType entityType)
+ public StructuralTypeProjectionExpression(Expression @object, ITypeBase structuralType)
{
Object = @object;
- EntityType = entityType;
+ StructuralType = structuralType;
PropertyName = (@object as IAccessExpression)?.PropertyName;
}
@@ -46,7 +47,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
- => EntityType.ClrType;
+ => StructuralType.ClrType;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -62,7 +63,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 IEntityType EntityType { get; }
+ public virtual ITypeBase StructuralType { get; }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -90,7 +91,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
public virtual Expression Update(Expression @object)
=> ReferenceEquals(@object, Object)
? this
- : new EntityProjectionExpression(@object, EntityType);
+ : new StructuralTypeProjectionExpression(@object, StructuralType);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -100,11 +101,11 @@ public virtual Expression Update(Expression @object)
///
public virtual Expression BindProperty(IProperty property, bool clientEval)
{
- if (!EntityType.IsAssignableFrom(property.DeclaringType)
- && !property.DeclaringType.IsAssignableFrom(EntityType))
+ if (!StructuralType.IsAssignableFrom(property.DeclaringType)
+ && !property.DeclaringType.IsAssignableFrom(StructuralType))
{
throw new InvalidOperationException(
- CosmosStrings.UnableToBindMemberToEntityProjection("property", property.Name, EntityType.DisplayName()));
+ CosmosStrings.UnableToBindMemberToEntityProjection("property", property.Name, StructuralType.DisplayName()));
}
if (!_propertyExpressionsMap.TryGetValue(property, out var expression))
@@ -136,11 +137,16 @@ public virtual Expression BindProperty(IProperty property, bool clientEval)
///
public virtual Expression BindNavigation(INavigation navigation, bool clientEval)
{
- if (!EntityType.IsAssignableFrom(navigation.DeclaringEntityType)
- && !navigation.DeclaringEntityType.IsAssignableFrom(EntityType))
+ if (StructuralType is not IEntityType entityType)
+ {
+ throw new UnreachableException("Navigations are only supported on entity types");
+ }
+
+ if (!entityType.IsAssignableFrom(navigation.DeclaringEntityType)
+ && !navigation.DeclaringEntityType.IsAssignableFrom(entityType))
{
throw new InvalidOperationException(
- CosmosStrings.UnableToBindMemberToEntityProjection("navigation", navigation.Name, EntityType.DisplayName()));
+ CosmosStrings.UnableToBindMemberToEntityProjection("navigation", navigation.Name, entityType.DisplayName()));
}
if (!_navigationExpressionsMap.TryGetValue(navigation, out var expression))
@@ -153,7 +159,7 @@ public virtual Expression BindNavigation(INavigation navigation, bool clientEval
nullable: true)
: new StructuralTypeShaperExpression(
navigation.TargetEntityType,
- new EntityProjectionExpression(new ObjectAccessExpression(Object, navigation), navigation.TargetEntityType),
+ new StructuralTypeProjectionExpression(new ObjectAccessExpression(Object, navigation), navigation.TargetEntityType),
nullable: !navigation.ForeignKey.IsRequiredDependent);
_navigationExpressionsMap[navigation] = expression;
@@ -170,6 +176,39 @@ public virtual Expression BindNavigation(INavigation navigation, bool clientEval
return 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 virtual Expression BindComplexProperty(IComplexProperty complexProperty, bool clientEval)
+ {
+ if (!StructuralType.IsAssignableFrom(complexProperty.DeclaringType)
+ && !complexProperty.DeclaringType.IsAssignableFrom(StructuralType))
+ {
+ throw new InvalidOperationException(
+ CosmosStrings.UnableToBindMemberToEntityProjection("complex property", complexProperty.Name, StructuralType.DisplayName()));
+ }
+
+ if (!_complexPropertyExpressionsMap.TryGetValue(complexProperty, out var expression))
+ {
+ // TODO: Unify ObjectAccessExpression and ObjectArrayAccessExpression
+ expression = complexProperty.IsCollection
+ ? new CollectionResultExpression(
+ new ObjectArrayAccessExpression(Object, complexProperty),
+ complexProperty)
+ : new StructuralTypeShaperExpression(
+ complexProperty.ComplexType,
+ new StructuralTypeProjectionExpression(new ObjectAccessExpression(Object, complexProperty), complexProperty.ComplexType),
+ nullable: complexProperty.IsNullable);
+
+ _complexPropertyExpressionsMap[complexProperty] = expression;
+ }
+
+ return 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
@@ -198,29 +237,41 @@ public virtual Expression BindNavigation(INavigation navigation, bool clientEval
private Expression? BindMember(MemberIdentity member, Type? entityClrType, bool clientEval, out IPropertyBase? propertyBase)
{
- var entityType = EntityType;
+ var structuralType = StructuralType;
if (entityClrType != null
- && !entityClrType.IsAssignableFrom(entityType.ClrType))
+ && !entityClrType.IsAssignableFrom(structuralType.ClrType))
{
- entityType = entityType.GetDerivedTypes().First(e => entityClrType.IsAssignableFrom(e.ClrType));
+ structuralType = structuralType.GetDerivedTypes().First(e => entityClrType.IsAssignableFrom(e.ClrType));
}
var property = member.MemberInfo == null
- ? entityType.FindProperty(member.Name!)
- : entityType.FindProperty(member.MemberInfo);
+ ? structuralType.FindProperty(member.Name!)
+ : structuralType.FindProperty(member.MemberInfo);
if (property != null)
{
propertyBase = property;
return BindProperty(property, clientEval);
}
- var navigation = member.MemberInfo == null
+ if (structuralType is IEntityType entityType)
+ {
+ var navigation = member.MemberInfo == null
? entityType.FindNavigation(member.Name!)
: entityType.FindNavigation(member.MemberInfo);
- if (navigation != null)
+ if (navigation != null)
+ {
+ propertyBase = navigation;
+ return BindNavigation(navigation, clientEval);
+ }
+ }
+
+ var complex = member.MemberInfo == null
+ ? structuralType.FindComplexProperty(member.Name!)
+ : structuralType.FindComplexProperty(member.MemberInfo);
+ if (complex != null)
{
- propertyBase = navigation;
- return BindNavigation(navigation, clientEval);
+ propertyBase = complex;
+ return BindComplexProperty(complex, clientEval);
}
// Entity member not found
@@ -234,16 +285,21 @@ public virtual Expression BindNavigation(INavigation navigation, bool clientEval
/// 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 EntityProjectionExpression UpdateEntityType(IEntityType derivedType)
+ public virtual StructuralTypeProjectionExpression UpdateEntityType(IEntityType derivedType)
{
- if (!derivedType.GetAllBaseTypes().Contains(EntityType))
+ if (StructuralType is not IEntityType entityType)
+ {
+ throw new UnreachableException($"{nameof(UpdateEntityType)} called on non-entity type '{StructuralType.DisplayName()}'");
+ }
+
+ if (!derivedType.GetAllBaseTypes().Contains(StructuralType))
{
throw new InvalidOperationException(
CosmosStrings.InvalidDerivedTypeInEntityProjection(
- derivedType.DisplayName(), EntityType.DisplayName()));
+ derivedType.DisplayName(), StructuralType.DisplayName()));
}
- return new EntityProjectionExpression(Object, derivedType);
+ return new StructuralTypeProjectionExpression(Object, derivedType);
}
///
@@ -264,12 +320,12 @@ void IPrintableExpression.Print(ExpressionPrinter expressionPrinter)
public override bool Equals(object? obj)
=> obj != null
&& (ReferenceEquals(this, obj)
- || obj is EntityProjectionExpression entityProjectionExpression
- && Equals(entityProjectionExpression));
+ || obj is StructuralTypeProjectionExpression structuralTypeProjectionExpression
+ && Equals(structuralTypeProjectionExpression));
- private bool Equals(EntityProjectionExpression entityProjectionExpression)
- => Equals(EntityType, entityProjectionExpression.EntityType)
- && Object.Equals(entityProjectionExpression.Object);
+ private bool Equals(StructuralTypeProjectionExpression structuralTypeProjectionExpression)
+ => Equals(StructuralType, structuralTypeProjectionExpression.StructuralType)
+ && Object.Equals(structuralTypeProjectionExpression.Object);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -278,7 +334,7 @@ private bool Equals(EntityProjectionExpression entityProjectionExpression)
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public override int GetHashCode()
- => HashCode.Combine(EntityType, Object);
+ => HashCode.Combine(StructuralType, Object);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -287,5 +343,5 @@ public override int GetHashCode()
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
public override string ToString()
- => $"EntityProjectionExpression: {EntityType.ShortName()}";
+ => $"StructuralTypeProjectionExpression: {StructuralType.ShortName()}";
}
diff --git a/src/EFCore.Cosmos/Query/Internal/SqlExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/SqlExpressionVisitor.cs
index 22d2b286bb1..f9728a62aaa 100644
--- a/src/EFCore.Cosmos/Query/Internal/SqlExpressionVisitor.cs
+++ b/src/EFCore.Cosmos/Query/Internal/SqlExpressionVisitor.cs
@@ -24,7 +24,7 @@ ShapedQueryExpression shapedQueryExpression
=> shapedQueryExpression.UpdateQueryExpression(Visit(shapedQueryExpression.QueryExpression)),
SelectExpression selectExpression => VisitSelect(selectExpression),
ProjectionExpression projectionExpression => VisitProjection(projectionExpression),
- EntityProjectionExpression entityProjectionExpression => VisitEntityProjection(entityProjectionExpression),
+ StructuralTypeProjectionExpression structuralTypeProjectionExpression => VisitStructuralTypeProjection(structuralTypeProjectionExpression),
ObjectArrayAccessExpression arrayProjectionExpression => VisitObjectArrayAccess(arrayProjectionExpression),
FromSqlExpression fromSqlExpression => VisitFromSql(fromSqlExpression),
ObjectReferenceExpression objectReferenceExpression => VisitObjectReference(objectReferenceExpression),
@@ -235,7 +235,7 @@ ShapedQueryExpression shapedQueryExpression
/// 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 abstract Expression VisitEntityProjection(EntityProjectionExpression entityProjectionExpression);
+ protected abstract Expression VisitStructuralTypeProjection(StructuralTypeProjectionExpression structuralTypeProjectionExpression);
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs
index 198530127be..1251f41f1a4 100644
--- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs
@@ -1056,7 +1056,7 @@ protected override Expression VisitTypeBinary(TypeBinaryExpression typeBinaryExp
if (typeReference.StructuralType is not IEntityType entityType)
{
- return Expression.Constant(typeReference.StructuralType.ClrType == typeBinaryExpression.TypeOperand);
+ return _sqlExpressionFactory.Constant(typeReference.StructuralType.ClrType == typeBinaryExpression.TypeOperand);
}
if (entityType.GetAllBaseTypesInclusive().Any(et => et.ClrType == typeBinaryExpression.TypeOperand))
diff --git a/src/EFCore/Update/Internal/InternalUpdateEntryExtensions.cs b/src/EFCore/Update/Internal/InternalUpdateEntryExtensions.cs
index d2ab9842ef7..aaba4ce641a 100644
--- a/src/EFCore/Update/Internal/InternalUpdateEntryExtensions.cs
+++ b/src/EFCore/Update/Internal/InternalUpdateEntryExtensions.cs
@@ -22,17 +22,6 @@ public static class InternalUpdateEntryExtensions
public static object? GetCurrentProviderValue(this IInternalEntry updateEntry, IProperty property)
{
var value = updateEntry.GetCurrentValue(property);
- var typeMapping = property.GetTypeMapping();
- value = value?.GetType().IsInteger() == true && typeMapping.ClrType.UnwrapNullableType().IsEnum
- ? Enum.ToObject(typeMapping.ClrType.UnwrapNullableType(), value)
- : value;
-
- var converter = typeMapping.Converter;
- if (converter != null)
- {
- value = converter.ConvertToProvider(value);
- }
-
- return value;
+ return property.ConvertToProviderValue(value);
}
}
diff --git a/src/EFCore/Update/Internal/PropertyExtensions.cs b/src/EFCore/Update/Internal/PropertyExtensions.cs
new file mode 100644
index 00000000000..0f7cb3655ed
--- /dev/null
+++ b/src/EFCore/Update/Internal/PropertyExtensions.cs
@@ -0,0 +1,35 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Update.Internal;
+
+///
+/// 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 class PropertyExtensions
+{
+ ///
+ /// 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 object? ConvertToProviderValue(this IProperty property, object? value)
+ {
+ var typeMapping = property.GetTypeMapping();
+ value = value?.GetType().IsInteger() == true && typeMapping.ClrType.UnwrapNullableType().IsEnum
+ ? Enum.ToObject(typeMapping.ClrType.UnwrapNullableType(), value)
+ : value;
+
+ var converter = typeMapping.Converter;
+ if (converter != null)
+ {
+ value = converter.ConvertToProvider(value);
+ }
+
+ return value;
+ }
+}
diff --git a/test/EFCore.Cosmos.FunctionalTests/CosmosComplexTypesTrackingTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosComplexTypesTrackingTest.cs
index 4204503ee8c..90284996d4b 100644
--- a/test/EFCore.Cosmos.FunctionalTests/CosmosComplexTypesTrackingTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/CosmosComplexTypesTrackingTest.cs
@@ -152,9 +152,13 @@ public override Task Can_change_state_from_Deleted_with_complex_field_record_col
}
public override Task Can_save_default_values_in_optional_complex_property_with_multiple_properties(bool async)
- // Optional complex properties are not supported on Cosmos
- // See https://github.com/dotnet/efcore/issues/31253
- => Task.CompletedTask;
+ {
+ if (!async)
+ {
+ throw SkipException.ForSkip("Cosmos does not support synchronous operations.");
+ }
+ return base.Can_save_default_values_in_optional_complex_property_with_multiple_properties(async);
+ }
protected override async Task ExecuteWithStrategyInTransactionAsync(Func testOperation, Func? nestedTestOperation1 = null, Func? nestedTestOperation2 = null, Func? nestedTestOperation3 = null)
{
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs
new file mode 100644
index 00000000000..d8cda5ed9eb
--- /dev/null
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs
@@ -0,0 +1,111 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Query;
+
+public class AdHocComplexTypeQueryCosmosTest(NonSharedFixture fixture) : AdHocComplexTypeQueryTestBase(fixture)
+{
+ protected override ITestStoreFactory NonSharedTestStoreFactory
+ => CosmosTestStoreFactory.Instance;
+
+ public override async Task Complex_type_equals_parameter_with_nested_types_with_property_of_same_name()
+ {
+ await base.Complex_type_equals_parameter_with_nested_types_with_property_of_same_name();
+
+ AssertSql(
+ """
+@entity_equality_container='{"Id":1,"Containee1":{"Id":2},"Containee2":{"Id":3}}'
+
+SELECT VALUE c
+FROM root c
+WHERE (c["ComplexContainer"] = @entity_equality_container)
+OFFSET 0 LIMIT 2
+""");
+ }
+
+ public override async Task Projecting_complex_property_does_not_auto_include_owned_types()
+ {
+ await base.Projecting_complex_property_does_not_auto_include_owned_types();
+
+ // #34067: Cosmos: Projecting out nested documents retrieves the entire document
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+""");
+ }
+
+ public override async Task Optional_complex_type_with_discriminator()
+ {
+ await base.Optional_complex_type_with_discriminator();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["AllOptionalsComplexType"] = null)
+OFFSET 0 LIMIT 2
+""");
+ }
+
+ public override async Task Non_optional_complex_type_with_all_nullable_properties()
+ {
+ await base.Non_optional_complex_type_with_all_nullable_properties();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+OFFSET 0 LIMIT 2
+""");
+ }
+
+ public override async Task Nullable_complex_type_with_discriminator_and_shadow_property()
+ {
+ await base.Nullable_complex_type_with_discriminator_and_shadow_property();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+""");
+ }
+
+ protected override DbContextOptionsBuilder AddNonSharedOptions(DbContextOptionsBuilder builder)
+ => base.AddNonSharedOptions(builder)
+ .ConfigureWarnings(w => w.Ignore(CosmosEventId.NoPartitionKeyDefined));
+
+ [ConditionalFact]
+ public virtual void Check_all_tests_overridden()
+ => TestHelpers.AssertAllMethodsOverridden(GetType());
+
+ protected TestSqlLoggerFactory TestSqlLoggerFactory
+ => (TestSqlLoggerFactory)ListLoggerFactory;
+
+ private void AssertSql(params string[] expected)
+ => TestSqlLoggerFactory.AssertBaseline(expected);
+
+ protected override Task> InitializeNonSharedTest(
+ Action? onModelCreating = null,
+ Action? onConfiguring = null,
+ Func? addServices = null,
+ Action? configureConventions = null,
+ Func? seed = null,
+ Func? shouldLogCategory = null,
+ Func? createTestStore = null,
+ bool usePooling = true,
+ bool useServiceProvider = true)
+ => base.InitializeNonSharedTest(model =>
+ {
+ onModelCreating?.Invoke(model);
+ AdHocCosmosTestHelpers.UseTestAutoIncrementIntIds(model);
+ },
+ onConfiguring,
+ addServices,
+ configureConventions,
+ seed,
+ shouldLogCategory,
+ createTestStore,
+ usePooling,
+ useServiceProvider);
+}
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs
index d308f79f4dc..ebe995e1558 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocCosmosTestHelpers.cs
@@ -3,6 +3,7 @@
using System.Net;
using Microsoft.Azure.Cosmos;
+using Microsoft.EntityFrameworkCore.Cosmos.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@@ -11,6 +12,29 @@ namespace Microsoft.EntityFrameworkCore.Query;
public class AdHocCosmosTestHelpers
{
+ public static void UseTestAutoIncrementIntIds(ModelBuilder modelBuilder)
+ {
+ foreach (var rootDocument in modelBuilder.Model.GetEntityTypes().Where(x => x.IsDocumentRoot()))
+ {
+ var primaryKey = rootDocument.FindPrimaryKey();
+
+ if (primaryKey != null && primaryKey.Properties.Count == 1 && primaryKey.Properties[0].ClrType == typeof(int))
+ {
+ var valueGenerator = new TestAutoIncrementIntValueGenerator();
+ primaryKey.Properties[0].SetValueGeneratorFactory((_, _) => valueGenerator);
+ }
+ }
+ }
+
+ private class TestAutoIncrementIntValueGenerator : ValueGenerator
+ {
+ private int _autoIncrementingId;
+
+ public override bool GeneratesTemporaryValues => false;
+
+ public override int Next(EntityEntry entry) => Interlocked.Increment(ref _autoIncrementingId);
+ }
+
public static async Task CreateCustomEntityHelperAsync(
Container container,
string json,
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs
new file mode 100644
index 00000000000..e92b46d76d0
--- /dev/null
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs
@@ -0,0 +1,230 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Azure.Cosmos;
+
+namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexProperties;
+
+public class ComplexPropertiesCollectionCosmosTest : ComplexPropertiesCollectionTestBase, IClassFixture
+{
+ public ComplexPropertiesCollectionCosmosTest(ComplexPropertiesCosmosFixture fixture, ITestOutputHelper testOutputHelper)
+ : base(fixture)
+ {
+ Fixture.TestSqlLoggerFactory.Clear();
+ Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper);
+ }
+
+ public override async Task Count()
+ {
+ await base.Count();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (ARRAY_LENGTH(c["AssociateCollection"]) = 2)
+""");
+ }
+
+ public override async Task Where()
+ {
+ await base.Where();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE ((
+ SELECT VALUE COUNT(1)
+ FROM a IN c["AssociateCollection"]
+ WHERE (a["Int"] != 8)) = 2)
+""");
+ }
+
+ [ConditionalFact]
+ public async Task Where_subquery_structural_equality()
+ {
+ var param = new AssociateType
+ {
+ Id = 1,
+ Name = "Name 1",
+ Int = 8,
+ String = "String 1",
+ Ints = new List { 1, 2, 3 },
+ RequiredNestedAssociate = new NestedAssociateType
+ {
+ Id = 1,
+ Name = "Name 1",
+ Int = 8,
+ String = "String 1",
+ Ints = new List { 1, 2, 3 }
+ },
+ NestedCollection = new List
+ {
+ new NestedAssociateType
+ {
+ Id = 1,
+ Name = "Name 1",
+ Int = 8,
+ String = "String 1",
+ Ints = new List { 1, 2, 3 }
+ }
+ }
+ };
+
+ await AssertQuery(
+ ss => ss.Set().Where(e => e.AssociateCollection[0] != param),
+ ss => ss.Set().Where(e => e.AssociateCollection.Count > 0 && e.AssociateCollection[0] != param));
+
+
+ AssertSql(
+ """
+@entity_equality_param='{"Id":1,"Int":8,"Ints":[1,2,3],"Name":"Name 1","String":"String 1","NestedCollection":[{"Id":1,"Int":8,"Ints":[1,2,3],"Name":"Name 1","String":"String 1"}],"OptionalNestedAssociate":null,"RequiredNestedAssociate":{"Id":1,"Int":8,"Ints":[1,2,3],"Name":"Name 1","String":"String 1"}}'
+
+SELECT VALUE c
+FROM root c
+WHERE (c["AssociateCollection"][0] != @entity_equality_param)
+""");
+ }
+
+ public override async Task OrderBy_ElementAt()
+ {
+ // 'ORDER BY' is not supported in subqueries.
+ await Assert.ThrowsAsync(() => base.OrderBy_ElementAt());
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (ARRAY(
+ SELECT VALUE a["Int"]
+ FROM a IN c["AssociateCollection"]
+ ORDER BY a["Id"])[0] = 8)
+""");
+ }
+
+ #region Distinct
+
+ public override Task Distinct()
+ => AssertTranslationFailed(base.Distinct);
+
+ public override async Task Distinct_projected(QueryTrackingBehavior queryTrackingBehavior)
+ {
+ await base.Distinct_projected(queryTrackingBehavior);
+
+ AssertSql(
+ """
+SELECT VALUE ARRAY(
+ SELECT DISTINCT VALUE a
+ FROM a IN c["AssociateCollection"])
+FROM root c
+ORDER BY c["Id"]
+""");
+ }
+
+ public override Task Distinct_over_projected_nested_collection()
+ => AssertTranslationFailed(base.Distinct_over_projected_nested_collection);
+
+ public override Task Distinct_over_projected_filtered_nested_collection()
+ => AssertTranslationFailed(base.Distinct_over_projected_filtered_nested_collection);
+
+ #endregion Distinct
+
+ #region Index
+
+ public override async Task Index_constant()
+ {
+ await base.Index_constant();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["AssociateCollection"][0]["Int"] = 8)
+""");
+ }
+
+ public override async Task Index_parameter()
+ {
+ await base.Index_parameter();
+
+ AssertSql(
+ """
+@i='0'
+
+SELECT VALUE c
+FROM root c
+WHERE (c["AssociateCollection"][@i]["Int"] = 8)
+""");
+ }
+
+ public override async Task Index_column()
+ {
+ // The specified query includes 'member indexer' which is currently not supported
+ await Assert.ThrowsAsync(() => base.Index_column());
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["AssociateCollection"][(c["Id"] - 1)]["Int"] = 8)
+""");
+ }
+
+ public override async Task Index_out_of_bounds()
+ {
+ await base.Index_out_of_bounds();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["AssociateCollection"][9999]["Int"] = 8)
+""");
+ }
+
+ public override async Task Index_on_nested_collection()
+ {
+ await base.Index_on_nested_collection();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["RequiredAssociate"]["NestedCollection"][0]["Int"] = 8)
+""");
+ }
+
+ #endregion Index
+
+ #region GroupBy
+
+ [ConditionalFact]
+ public override Task GroupBy()
+ => AssertTranslationFailed(base.GroupBy);
+
+ #endregion GroupBy
+
+ public override async Task Select_within_Select_within_Select_with_aggregates()
+ {
+ await base.Select_within_Select_within_Select_with_aggregates();
+
+ AssertSql(
+ """
+SELECT VALUE (
+ SELECT VALUE SUM((
+ SELECT VALUE MAX(n["Int"])
+ FROM n IN a["NestedCollection"]))
+ FROM a IN c["AssociateCollection"])
+FROM root c
+""");
+ }
+
+
+ [ConditionalFact]
+ public virtual void Check_all_tests_overridden()
+ => TestHelpers.AssertAllMethodsOverridden(GetType());
+
+ private void AssertSql(params string[] expected)
+ => Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
+}
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesMiscellaneousCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesMiscellaneousCosmosTest.cs
new file mode 100644
index 00000000000..d625d726dac
--- /dev/null
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesMiscellaneousCosmosTest.cs
@@ -0,0 +1,95 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexProperties;
+
+public class ComplexPropertiesMiscellaneousCosmosTest
+ : ComplexPropertiesMiscellaneousTestBase
+{
+ public ComplexPropertiesMiscellaneousCosmosTest(ComplexPropertiesCosmosFixture fixture, ITestOutputHelper outputHelper) : base(fixture)
+ {
+ Fixture.TestSqlLoggerFactory.Clear();
+ Fixture.TestSqlLoggerFactory.SetTestOutputHelper(outputHelper);
+ }
+
+ public override async Task Where_on_associate_scalar_property()
+ {
+ await base.Where_on_associate_scalar_property();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["RequiredAssociate"]["Int"] = 8)
+""");
+ }
+
+ public override async Task Where_on_optional_associate_scalar_property()
+ {
+ await base.Where_on_optional_associate_scalar_property();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["OptionalAssociate"]["Int"] = 8)
+""");
+ }
+
+ public override async Task Where_on_nested_associate_scalar_property()
+ {
+ await base.Where_on_nested_associate_scalar_property();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["RequiredAssociate"]["RequiredNestedAssociate"]["Int"] = 8)
+""");
+ }
+
+ #region Value types
+
+ public override async Task Where_property_on_non_nullable_value_type()
+ {
+ await base.Where_property_on_non_nullable_value_type();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["RequiredAssociate"]["Int"] = 8)
+""");
+ }
+
+ public override async Task Where_property_on_nullable_value_type_Value()
+ {
+ await base.Where_property_on_nullable_value_type_Value();
+
+ AssertSql("""
+SELECT VALUE c
+FROM root c
+WHERE (c["OptionalAssociate"]["Int"] = 8)
+""");
+ }
+
+ public override async Task Where_HasValue_on_nullable_value_type()
+ {
+ await base.Where_HasValue_on_nullable_value_type();
+
+ AssertSql("""
+SELECT VALUE c
+FROM root c
+WHERE (c["OptionalAssociate"] != null)
+""");
+ }
+
+ #endregion Value types
+
+ [ConditionalFact]
+ public virtual void Check_all_tests_overridden()
+ => TestHelpers.AssertAllMethodsOverridden(GetType());
+
+ private void AssertSql(params string[] expected)
+ => Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
+}
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesPrimitiveCollectionCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesPrimitiveCollectionCosmosTest.cs
new file mode 100644
index 00000000000..cf3fb5fba77
--- /dev/null
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesPrimitiveCollectionCosmosTest.cs
@@ -0,0 +1,97 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexProperties;
+
+public class ComplexPropertiesPrimitiveCollectionCosmosTest
+ : ComplexPropertiesPrimitiveCollectionTestBase
+{
+ public ComplexPropertiesPrimitiveCollectionCosmosTest(ComplexPropertiesCosmosFixture fixture, ITestOutputHelper outputHelper) : base(fixture)
+ {
+ Fixture.TestSqlLoggerFactory.Clear();
+ Fixture.TestSqlLoggerFactory.SetTestOutputHelper(outputHelper);
+ }
+
+ public override async Task Count()
+ {
+ await base.Count();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (ARRAY_LENGTH(c["RequiredAssociate"]["Ints"]) = 3)
+""");
+ }
+
+ public override async Task Index()
+ {
+ await base.Index();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["RequiredAssociate"]["Ints"][0] = 1)
+""");
+ }
+
+ public override async Task Contains()
+ {
+ await base.Contains();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE ARRAY_CONTAINS(c["RequiredAssociate"]["Ints"], 3)
+""");
+ }
+
+ public override async Task Any_predicate()
+ {
+ await base.Any_predicate();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE ARRAY_CONTAINS(c["RequiredAssociate"]["Ints"], 2)
+""");
+ }
+
+ public override async Task Nested_Count()
+ {
+ await base.Nested_Count();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (ARRAY_LENGTH(c["RequiredAssociate"]["RequiredNestedAssociate"]["Ints"]) = 3)
+""");
+ }
+
+ public override async Task Select_Sum()
+ {
+ await base.Select_Sum();
+
+ AssertSql(
+ """
+SELECT VALUE (
+ SELECT VALUE SUM(i0)
+ FROM i0 IN c["RequiredAssociate"]["Ints"])
+FROM root c
+WHERE ((
+ SELECT VALUE SUM(i)
+ FROM i IN c["RequiredAssociate"]["Ints"]) >= 6)
+""");
+ }
+
+ [ConditionalFact]
+ public virtual void Check_all_tests_overridden()
+ => TestHelpers.AssertAllMethodsOverridden(GetType());
+
+ private void AssertSql(params string[] expected)
+ => Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
+}
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesProjectionCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesProjectionCosmosTest.cs
index a18ecdc8a38..b6277c2b815 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesProjectionCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesProjectionCosmosTest.cs
@@ -1,8 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using Xunit.Sdk;
-
namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexProperties;
public class ComplexPropertiesProjectionCosmosTest : ComplexPropertiesProjectionTestBase
@@ -26,7 +24,6 @@ FROM root c
#region Scalar properties
- [ConditionalTheory(Skip = "TODO: Query projection")]
public override async Task Select_scalar_property_on_required_associate(QueryTrackingBehavior queryTrackingBehavior)
{
await base.Select_scalar_property_on_required_associate(queryTrackingBehavior);
@@ -38,7 +35,6 @@ FROM root c
""");
}
- [ConditionalTheory(Skip = "TODO: Query projection")]
public override async Task Select_property_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior)
{
// When OptionalAssociate is null, the property access on it evaluates to undefined in Cosmos, causing the
@@ -55,7 +51,6 @@ FROM root c
""");
}
- [ConditionalTheory(Skip = "TODO: Query projection")]
public override async Task Select_value_type_property_on_null_associate_throws(QueryTrackingBehavior queryTrackingBehavior)
{
// When OptionalAssociate is null, the property access on it evaluates to undefined in Cosmos, causing the
@@ -72,7 +67,6 @@ FROM root c
""");
}
- [ConditionalTheory(Skip = "TODO: Query projection")]
public override async Task Select_nullable_value_type_property_on_null_associate(QueryTrackingBehavior queryTrackingBehavior)
{
// When OptionalAssociate is null, the property access on it evaluates to undefined in Cosmos, causing the
@@ -174,7 +168,6 @@ FROM root c
""");
}
- [ConditionalTheory(Skip = "TODO: Query projection")]
public override async Task Select_untranslatable_method_on_associate_scalar_property(QueryTrackingBehavior queryTrackingBehavior)
{
await base.Select_untranslatable_method_on_associate_scalar_property(queryTrackingBehavior);
@@ -226,7 +219,6 @@ ORDER BY c["Id"]
""");
}
- [ConditionalTheory(Skip = "TODO: Query projection")]
public override async Task SelectMany_associate_collection(QueryTrackingBehavior queryTrackingBehavior)
{
await base.SelectMany_associate_collection(queryTrackingBehavior);
@@ -239,7 +231,6 @@ JOIN a IN c["AssociateCollection"]
""");
}
- [ConditionalTheory(Skip = "TODO: Query projection")]
public override async Task SelectMany_nested_collection_on_required_associate(QueryTrackingBehavior queryTrackingBehavior)
{
await base.SelectMany_nested_collection_on_required_associate(queryTrackingBehavior);
@@ -252,7 +243,6 @@ JOIN n IN c["RequiredAssociate"]["NestedCollection"]
""");
}
- [ConditionalTheory(Skip = "TODO: Query projection")]
public override async Task SelectMany_nested_collection_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior)
{
await base.SelectMany_nested_collection_on_optional_associate(queryTrackingBehavior);
@@ -308,6 +298,7 @@ public override Task Select_subquery_optional_related_FirstOrDefault(QueryTracki
#endregion Subquery
#region Value types
+
public override async Task Select_root_with_value_types(QueryTrackingBehavior queryTrackingBehavior)
{
await base.Select_root_with_value_types(queryTrackingBehavior);
@@ -331,6 +322,7 @@ ORDER BY c["Id"]
""");
}
+
public override async Task Select_nullable_value_type(QueryTrackingBehavior queryTrackingBehavior)
{
await base.Select_nullable_value_type(queryTrackingBehavior);
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesSetOperationsCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesSetOperationsCosmosTest.cs
new file mode 100644
index 00000000000..ab07d0fdee2
--- /dev/null
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesSetOperationsCosmosTest.cs
@@ -0,0 +1,67 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexProperties;
+
+public class ComplexPropertiesSetOperationsCosmosTest
+ : ComplexPropertiesSetOperationsTestBase
+{
+ public ComplexPropertiesSetOperationsCosmosTest(ComplexPropertiesCosmosFixture fixture, ITestOutputHelper outputHelper) : base(fixture)
+ {
+ Fixture.TestSqlLoggerFactory.Clear();
+ Fixture.TestSqlLoggerFactory.SetTestOutputHelper(outputHelper);
+ }
+
+ public override async Task Over_associate_collections()
+ {
+ await base.Over_associate_collections();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (ARRAY_LENGTH(ARRAY_CONCAT(ARRAY(
+ SELECT VALUE a
+ FROM a IN c["AssociateCollection"]
+ WHERE (a["Int"] = 8)), ARRAY(
+ SELECT VALUE a0
+ FROM a0 IN c["AssociateCollection"]
+ WHERE (a0["String"] = "foo")))) = 4)
+""");
+ }
+
+ public override Task Over_associate_collection_projected(QueryTrackingBehavior queryTrackingBehavior)
+ => Assert.ThrowsAsync(() => base.Over_associate_collection_projected(queryTrackingBehavior));
+
+ public override Task Over_assocate_collection_Select_nested_with_aggregates_projected(QueryTrackingBehavior queryTrackingBehavior)
+ => Assert.ThrowsAsync(
+ () => base.Over_assocate_collection_Select_nested_with_aggregates_projected(queryTrackingBehavior));
+
+ public override async Task Over_nested_associate_collection()
+ {
+ await base.Over_nested_associate_collection();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (ARRAY_LENGTH(ARRAY_CONCAT(ARRAY(
+ SELECT VALUE n
+ FROM n IN c["RequiredAssociate"]["NestedCollection"]
+ WHERE (n["Int"] = 8)), ARRAY(
+ SELECT VALUE n0
+ FROM n0 IN c["RequiredAssociate"]["NestedCollection"]
+ WHERE (n0["String"] = "foo")))) = 4)
+""");
+ }
+
+ public override Task Over_different_collection_properties()
+ => AssertTranslationFailed(base.Over_different_collection_properties);
+
+ [ConditionalFact]
+ public virtual void Check_all_tests_overridden()
+ => TestHelpers.AssertAllMethodsOverridden(GetType());
+
+ private void AssertSql(params string[] expected)
+ => Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
+}
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs
new file mode 100644
index 00000000000..6f0f36dbeaf
--- /dev/null
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs
@@ -0,0 +1,275 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexProperties;
+
+public class ComplexPropertiesStructuralEqualityCosmosTest : ComplexPropertiesStructuralEqualityTestBase
+{
+ public ComplexPropertiesStructuralEqualityCosmosTest(ComplexPropertiesCosmosFixture fixture, ITestOutputHelper outputHelper) : base(fixture)
+ {
+ Fixture.TestSqlLoggerFactory.Clear();
+ Fixture.TestSqlLoggerFactory.SetTestOutputHelper(outputHelper);
+ }
+
+ public override async Task Two_associates()
+ {
+ await base.Two_associates();
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["RequiredAssociate"] = c["OptionalAssociate"])
+""");
+ }
+
+ public override async Task Two_nested_associates()
+ {
+ await base.Two_nested_associates();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["RequiredAssociate"]["RequiredNestedAssociate"] = c["OptionalAssociate"]["RequiredNestedAssociate"])
+""");
+ }
+
+ public override async Task Not_equals()
+ {
+ await base.Not_equals();
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["RequiredAssociate"] != c["OptionalAssociate"])
+""");
+ }
+
+ public override async Task Associate_with_inline_null()
+ {
+ await base.Associate_with_inline_null();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["OptionalAssociate"] = null)
+""");
+ }
+
+ public override async Task Associate_with_parameter_null()
+ {
+ await base.Associate_with_parameter_null();
+
+ AssertSql(
+ """
+@entity_equality_related='null'
+
+SELECT VALUE c
+FROM root c
+WHERE (c["OptionalAssociate"] = @entity_equality_related)
+""");
+ }
+
+ public override async Task Nested_associate_with_inline_null()
+ {
+ await base.Nested_associate_with_inline_null();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["RequiredAssociate"]["OptionalNestedAssociate"] = null)
+""");
+ }
+
+ public override async Task Nested_associate_with_inline()
+ {
+ await base.Nested_associate_with_inline();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["RequiredAssociate"]["RequiredNestedAssociate"] = {"Id":1000,"Int":8,"Ints":[1,2,3],"Name":"Root1_RequiredAssociate_RequiredNestedAssociate","String":"foo"})
+""");
+ }
+
+ public override async Task Nested_associate_with_parameter()
+ {
+ await base.Nested_associate_with_parameter();
+
+ AssertSql(
+ """
+@entity_equality_nested='{"Id":1000,"Int":8,"Ints":[1,2,3],"Name":"Root1_RequiredAssociate_RequiredNestedAssociate","String":"foo"}'
+
+SELECT VALUE c
+FROM root c
+WHERE (c["RequiredAssociate"]["RequiredNestedAssociate"] = @entity_equality_nested)
+""");
+ }
+
+ [ConditionalFact]
+ public async Task Nested_associate_with_parameter_null()
+ {
+ NestedAssociateType? nested = null;
+ await AssertQuery(
+ ss => ss.Set().Where(e => e.RequiredAssociate.OptionalNestedAssociate == nested));
+
+ AssertSql(
+ """
+@entity_equality_nested='null'
+
+SELECT VALUE c
+FROM root c
+WHERE (c["RequiredAssociate"]["OptionalNestedAssociate"] = @entity_equality_nested)
+""");
+ }
+
+ [ConditionalFact]
+ public async Task Nested_associate_with_parameter_not_null()
+ {
+ NestedAssociateType? nested = null;
+ await AssertQuery(
+ ss => ss.Set().Where(e => e.RequiredAssociate.OptionalNestedAssociate != nested));
+
+ AssertSql(
+ """
+@entity_equality_nested='null'
+
+SELECT VALUE c
+FROM root c
+WHERE (c["RequiredAssociate"]["OptionalNestedAssociate"] != @entity_equality_nested)
+""");
+ }
+
+ public override async Task Two_nested_collections()
+ {
+ await base.Two_nested_collections();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["RequiredAssociate"]["NestedCollection"] = c["OptionalAssociate"]["NestedCollection"])
+""");
+}
+
+ public override async Task Nested_collection_with_inline()
+ {
+ await base.Nested_collection_with_inline();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["RequiredAssociate"]["NestedCollection"] = [{"Id":1002,"Int":8,"Ints":[1,2,3],"Name":"Root1_RequiredAssociate_NestedCollection_1","String":"foo"},{"Id":1003,"Int":8,"Ints":[1,2,3],"Name":"Root1_RequiredAssociate_NestedCollection_2","String":"foo"}])
+""");
+ }
+
+ public override async Task Nested_collection_with_parameter()
+ {
+ await base.Nested_collection_with_parameter();
+
+ AssertSql(
+ """
+@entity_equality_nestedCollection='[{"Id":1002,"Int":8,"Ints":[1,2,3],"Name":"Root1_RequiredAssociate_NestedCollection_1","String":"foo"},{"Id":1003,"Int":8,"Ints":[1,2,3],"Name":"Root1_RequiredAssociate_NestedCollection_2","String":"foo"}]'
+
+SELECT VALUE c
+FROM root c
+WHERE (c["RequiredAssociate"]["NestedCollection"] = @entity_equality_nestedCollection)
+""");
+ }
+
+ [ConditionalFact]
+ public override async Task Nullable_value_type_with_null()
+ {
+ await base.Nullable_value_type_with_null();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["OptionalAssociate"] = null)
+""");
+ }
+
+ #region Contains
+
+ public override async Task Contains_with_inline()
+ {
+ await base.Contains_with_inline();
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE EXISTS (
+ SELECT 1
+ FROM n IN c["RequiredAssociate"]["NestedCollection"]
+ WHERE (n = {"Id":1002,"Int":8,"Ints":[1,2,3],"Name":"Root1_RequiredAssociate_NestedCollection_1","String":"foo"}))
+""");
+ }
+
+ public override async Task Contains_with_parameter()
+ {
+ await base.Contains_with_parameter();
+
+ AssertSql(
+ """
+@entity_equality_nested='{"Id":1002,"Int":8,"Ints":[1,2,3],"Name":"Root1_RequiredAssociate_NestedCollection_1","String":"foo"}'
+
+SELECT VALUE c
+FROM root c
+WHERE EXISTS (
+ SELECT 1
+ FROM n IN c["RequiredAssociate"]["NestedCollection"]
+ WHERE (n = @entity_equality_nested))
+""");
+ }
+
+ public override async Task Contains_with_operators_composed_on_the_collection()
+ {
+ await base.Contains_with_operators_composed_on_the_collection();
+
+ AssertSql(
+ """
+@get_Item_Int='106'
+@entity_equality_get_Item='{"Id":3003,"Int":108,"Ints":[8,9,109],"Name":"Root3_RequiredAssociate_NestedCollection_2","String":"foo104"}'
+
+SELECT VALUE c
+FROM root c
+WHERE EXISTS (
+ SELECT 1
+ FROM n IN c["RequiredAssociate"]["NestedCollection"]
+ WHERE ((n["Int"] > @get_Item_Int) AND (n = @entity_equality_get_Item)))
+""");
+ }
+
+ public override async Task Contains_with_nested_and_composed_operators()
+ {
+ await base.Contains_with_nested_and_composed_operators();
+
+ AssertSql(
+ """
+@get_Item_Id='302'
+@entity_equality_get_Item='{"Id":303,"Int":130,"Ints":[8,9,131],"Name":"Root3_AssociateCollection_2","String":"foo115","NestedCollection":[{"Id":3014,"Int":136,"Ints":[8,9,137],"Name":"Root3_AssociateCollection_2_NestedCollection_1","String":"foo118"},{"Id":3015,"Int":138,"Ints":[8,9,139],"Name":"Root3_Root1_AssociateCollection_2_NestedCollection_2","String":"foo119"}],"OptionalNestedAssociate":{"Id":3013,"Int":134,"Ints":[8,9,135],"Name":"Root3_AssociateCollection_2_OptionalNestedAssociate","String":"foo117"},"RequiredNestedAssociate":{"Id":3012,"Int":132,"Ints":[8,9,133],"Name":"Root3_AssociateCollection_2_RequiredNestedAssociate","String":"foo116"}}'
+
+SELECT VALUE c
+FROM root c
+WHERE EXISTS (
+ SELECT 1
+ FROM a IN c["AssociateCollection"]
+ WHERE ((a["Id"] > @get_Item_Id) AND (a = @entity_equality_get_Item)))
+""");
+ }
+
+ #endregion Contains
+
+ [ConditionalFact]
+ public virtual void Check_all_tests_overridden()
+ => TestHelpers.AssertAllMethodsOverridden(GetType());
+
+ private void AssertSql(params string[] expected)
+ => Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
+}
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsProjectionCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsProjectionCosmosTest.cs
index 5ac5fcea75f..9027b062b25 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsProjectionCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsProjectionCosmosTest.cs
@@ -102,6 +102,12 @@ FROM root c
}
}
+ [ConditionalFact]
+ public Task Select_distinct_associate()
+ => AssertTranslationFailed(() => AssertQuery(
+ ss => ss.Set().Select(x => x.RequiredAssociate).Distinct(),
+ queryTrackingBehavior: QueryTrackingBehavior.NoTracking));
+
public override async Task Select_optional_associate(QueryTrackingBehavior queryTrackingBehavior)
{
await base.Select_optional_associate(queryTrackingBehavior);
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs
new file mode 100644
index 00000000000..f60f6ea82cd
--- /dev/null
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs
@@ -0,0 +1,456 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.EntityFrameworkCore.Cosmos.Internal;
+using Microsoft.EntityFrameworkCore.TestModels.ComplexTypeModel;
+
+namespace Microsoft.EntityFrameworkCore.Query;
+
+public class ComplexTypeQueryCosmosTest(ComplexTypeQueryCosmosTest.ComplexTypeQueryCosmosFixture fixture) : ComplexTypeQueryTestBase(fixture)
+{
+ public override Task Filter_on_property_inside_complex_type_after_subquery(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Filter_on_property_inside_complex_type_after_subquery(async), CosmosStrings.LimitOffsetNotSupportedInSubqueries);
+
+ public override Task Filter_on_property_inside_nested_complex_type_after_subquery(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Filter_on_property_inside_nested_complex_type_after_subquery(async), CosmosStrings.LimitOffsetNotSupportedInSubqueries);
+
+ public override Task Filter_on_required_property_inside_required_complex_type_on_optional_navigation(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Filter_on_required_property_inside_required_complex_type_on_optional_navigation(async), CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(Customer), nameof(CustomerGroup)));
+
+ public override Task Filter_on_required_property_inside_required_complex_type_on_required_navigation(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Filter_on_required_property_inside_required_complex_type_on_required_navigation(async), CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(Customer), nameof(CustomerGroup)));
+
+ public override Task Project_complex_type_via_optional_navigation(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Project_complex_type_via_optional_navigation(async), CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(Customer), nameof(CustomerGroup)));
+
+ public override Task Project_complex_type_via_required_navigation(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Project_complex_type_via_required_navigation(async), CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(Customer), nameof(CustomerGroup)));
+
+ public override Task Load_complex_type_after_subquery_on_entity_type(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Load_complex_type_after_subquery_on_entity_type(async), CosmosStrings.LimitOffsetNotSupportedInSubqueries);
+
+ public override Task Select_complex_type(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) =>
+ {
+ await base.Select_complex_type(async);
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+""");
+ });
+
+ public override Task Select_nested_complex_type(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) =>
+ {
+ await base.Select_nested_complex_type(async);
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+""");
+ });
+
+ public override Task Select_single_property_on_nested_complex_type(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) =>
+ {
+ await base.Select_single_property_on_nested_complex_type(async);
+
+ AssertSql(
+ """
+SELECT VALUE c["ShippingAddress"]["Country"]["FullName"]
+FROM root c
+""");
+ });
+
+ public override Task Select_complex_type_Where(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) =>
+ {
+ await base.Select_complex_type_Where(async);
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["ShippingAddress"]["ZipCode"] = 7728)
+""");
+ });
+
+ public override async Task Select_complex_type_Distinct(bool async)
+ => await AssertTranslationFailed(async () => await base.Select_complex_type_Distinct(async)); // Cosmos: Projecting out nested documents retrieves the entire document #34067
+
+ public override Task Complex_type_equals_complex_type(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) =>
+ {
+ await base.Complex_type_equals_complex_type(async);
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["ShippingAddress"] = c["BillingAddress"])
+""");
+ });
+
+ public override Task Complex_type_equals_constant(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) =>
+ {
+ await base.Complex_type_equals_constant(async);
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["ShippingAddress"] = {"AddressLine1":"804 S. Lakeshore Road","AddressLine2":null,"Tags":["foo","bar"],"ZipCode":38654,"Country":{"Code":"US","FullName":"United States"}})
+""");
+ });
+
+ public override Task Complex_type_equals_parameter(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) =>
+ {
+ await base.Complex_type_equals_parameter(async);
+
+ AssertSql(
+ """
+@entity_equality_address='{"AddressLine1":"804 S. Lakeshore Road","AddressLine2":null,"Tags":["foo","bar"],"ZipCode":38654,"Country":{"Code":"US","FullName":"United States"}}'
+
+SELECT VALUE c
+FROM root c
+WHERE (c["ShippingAddress"] = @entity_equality_address)
+""");
+ });
+
+ public override Task Subquery_over_complex_type(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Subquery_over_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Contains_over_complex_type(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Contains_over_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Concat_complex_type(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Concat_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Concat_entity_type_containing_complex_property(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Concat_entity_type_containing_complex_property(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Union_entity_type_containing_complex_property(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Union_entity_type_containing_complex_property(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Union_complex_type(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Union_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Concat_property_in_complex_type(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Concat_property_in_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Union_property_in_complex_type(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Union_property_in_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Concat_two_different_complex_type(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Concat_two_different_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Union_two_different_complex_type(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Union_two_different_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Filter_on_property_inside_struct_complex_type(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) =>
+ {
+ await base.Filter_on_property_inside_struct_complex_type(async);
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["ShippingAddress"]["ZipCode"] = 7728)
+""");
+ });
+
+ public override Task Filter_on_property_inside_nested_struct_complex_type(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) =>
+ {
+ await base.Filter_on_property_inside_nested_struct_complex_type(async);
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["ShippingAddress"]["Country"]["Code"] = "DE")
+""");
+ });
+
+ public override Task Filter_on_property_inside_struct_complex_type_after_subquery(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Filter_on_property_inside_struct_complex_type_after_subquery(async), CosmosStrings.LimitOffsetNotSupportedInSubqueries);
+
+ public override Task Filter_on_property_inside_nested_struct_complex_type_after_subquery(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Filter_on_property_inside_nested_struct_complex_type_after_subquery(async), CosmosStrings.LimitOffsetNotSupportedInSubqueries);
+
+ public override Task Filter_on_required_property_inside_required_struct_complex_type_on_optional_navigation(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Filter_on_required_property_inside_required_struct_complex_type_on_optional_navigation(async), CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(ValuedCustomer), nameof(ValuedCustomerGroup)));
+
+ public override Task Filter_on_required_property_inside_required_struct_complex_type_on_required_navigation(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Filter_on_required_property_inside_required_struct_complex_type_on_required_navigation(async), CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(ValuedCustomer), nameof(ValuedCustomerGroup)));
+
+ public override Task Project_struct_complex_type_via_optional_navigation(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Project_struct_complex_type_via_optional_navigation(async), CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(ValuedCustomer), nameof(ValuedCustomerGroup)));
+
+ public override Task Project_nullable_struct_complex_type_via_optional_navigation(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Project_nullable_struct_complex_type_via_optional_navigation(async), CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(ValuedCustomer), nameof(ValuedCustomerGroup)));
+
+ public override Task Project_struct_complex_type_via_required_navigation(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Project_struct_complex_type_via_required_navigation(async), CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(ValuedCustomer), nameof(ValuedCustomerGroup)));
+
+ public override Task Load_struct_complex_type_after_subquery_on_entity_type(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Load_struct_complex_type_after_subquery_on_entity_type(async), CosmosStrings.LimitOffsetNotSupportedInSubqueries);
+
+ public override Task Select_struct_complex_type(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) =>
+ {
+ await base.Select_struct_complex_type(async);
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+""");
+ });
+
+ public override Task Select_nested_struct_complex_type(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) =>
+ {
+ await base.Select_nested_struct_complex_type(async);
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+""");
+ });
+
+ public override Task Select_single_property_on_nested_struct_complex_type(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) =>
+ {
+ await base.Select_single_property_on_nested_struct_complex_type(async);
+
+ AssertSql(
+ """
+SELECT VALUE c["ShippingAddress"]["Country"]["FullName"]
+FROM root c
+""");
+ });
+
+ public override Task Select_struct_complex_type_Where(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) =>
+ {
+ await base.Select_struct_complex_type_Where(async);
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["ShippingAddress"]["ZipCode"] = 7728)
+""");
+ });
+
+ public override Task Select_struct_complex_type_Distinct(bool async)
+ => AssertTranslationFailed(() => base.Select_struct_complex_type_Distinct(async)); // #34067
+
+ public override Task Struct_complex_type_equals_struct_complex_type(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) =>
+ {
+ await base.Struct_complex_type_equals_struct_complex_type(async);
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["ShippingAddress"] = c["BillingAddress"])
+""");
+ });
+
+ public override Task Struct_complex_type_equals_constant(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) =>
+ {
+ await base.Struct_complex_type_equals_constant(async);
+
+ AssertSql(
+ """
+SELECT VALUE c
+FROM root c
+WHERE (c["ShippingAddress"] = {"AddressLine1":"804 S. Lakeshore Road","AddressLine2":null,"ZipCode":38654,"Country":{"Code":"US","FullName":"United States"}})
+""");
+ });
+
+ public override Task Struct_complex_type_equals_parameter(bool async)
+ => CosmosTestHelpers.Instance.NoSyncTest(async, async (async) =>
+ {
+ await base.Struct_complex_type_equals_parameter(async);
+
+ AssertSql(
+ """
+@entity_equality_address='{"AddressLine1":"804 S. Lakeshore Road","AddressLine2":null,"ZipCode":38654,"Country":{"Code":"US","FullName":"United States"}}'
+
+SELECT VALUE c
+FROM root c
+WHERE (c["ShippingAddress"] = @entity_equality_address)
+""");
+ });
+
+ public override Task Subquery_over_struct_complex_type(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Subquery_over_struct_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Contains_over_struct_complex_type(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Contains_over_struct_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Concat_struct_complex_type(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Concat_struct_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Concat_entity_type_containing_struct_complex_property(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Concat_entity_type_containing_struct_complex_property(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Union_entity_type_containing_struct_complex_property(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Union_entity_type_containing_struct_complex_property(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Union_struct_complex_type(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Union_struct_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Concat_property_in_struct_complex_type(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Concat_property_in_struct_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Union_property_in_struct_complex_type(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Union_property_in_struct_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Concat_two_different_struct_complex_type(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Concat_two_different_struct_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Union_two_different_struct_complex_type(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Union_two_different_struct_complex_type(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Project_same_entity_with_nested_complex_type_twice_with_pushdown(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Project_same_entity_with_nested_complex_type_twice_with_pushdown(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Project_same_nested_complex_type_twice_with_pushdown(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Project_same_nested_complex_type_twice_with_pushdown(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Project_same_entity_with_nested_complex_type_twice_with_double_pushdown(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Project_same_entity_with_nested_complex_type_twice_with_double_pushdown(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Project_same_nested_complex_type_twice_with_double_pushdown(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Project_same_nested_complex_type_twice_with_double_pushdown(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Project_same_entity_with_struct_nested_complex_type_twice_with_pushdown(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Project_same_entity_with_struct_nested_complex_type_twice_with_pushdown(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Project_same_struct_nested_complex_type_twice_with_pushdown(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Project_same_struct_nested_complex_type_twice_with_pushdown(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Project_same_entity_with_struct_nested_complex_type_twice_with_double_pushdown(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Project_same_entity_with_struct_nested_complex_type_twice_with_double_pushdown(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Project_same_struct_nested_complex_type_twice_with_double_pushdown(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Project_same_struct_nested_complex_type_twice_with_double_pushdown(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Union_of_same_entity_with_nested_complex_type_projected_twice_with_pushdown(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Union_of_same_entity_with_nested_complex_type_projected_twice_with_pushdown(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Union_of_same_entity_with_nested_complex_type_projected_twice_with_double_pushdown(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Union_of_same_entity_with_nested_complex_type_projected_twice_with_double_pushdown(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Union_of_same_nested_complex_type_projected_twice_with_pushdown(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Union_of_same_nested_complex_type_projected_twice_with_pushdown(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Union_of_same_nested_complex_type_projected_twice_with_double_pushdown(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Union_of_same_nested_complex_type_projected_twice_with_double_pushdown(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+ public override Task Same_entity_with_complex_type_projected_twice_with_pushdown_as_part_of_another_projection(bool async)
+ => AssertTranslationFailed(() => base.Same_entity_with_complex_type_projected_twice_with_pushdown_as_part_of_another_projection(async));
+
+ public override Task Same_complex_type_projected_twice_with_pushdown_as_part_of_another_projection(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Same_complex_type_projected_twice_with_pushdown_as_part_of_another_projection(async), CosmosStrings.NonCorrelatedSubqueriesNotSupported);
+
+
+ #region GroupBy
+
+ [ConditionalTheory(Skip = "#17313 Cosmos: Translate GroupBy")]
+ public override async Task GroupBy_over_property_in_nested_complex_type(bool async)
+ {
+ await base.GroupBy_over_property_in_nested_complex_type(async);
+
+ AssertSql(
+ """
+
+""");
+ }
+
+ [ConditionalTheory(Skip = "#17313 Cosmos: Translate GroupBy")]
+ public override async Task GroupBy_over_complex_type(bool async)
+ {
+ await base.GroupBy_over_complex_type(async);
+
+ AssertSql(
+ """
+
+""");
+ }
+
+ [ConditionalTheory(Skip = "#17313 Cosmos: Translate GroupBy")]
+ public override async Task GroupBy_over_nested_complex_type(bool async)
+ {
+ await base.GroupBy_over_nested_complex_type(async);
+
+ AssertSql(
+ """
+
+""");
+ }
+
+ [ConditionalTheory(Skip = "#17313 Cosmos: Translate GroupBy")]
+ public override async Task Entity_with_complex_type_with_group_by_and_first(bool async)
+ {
+ await base.Entity_with_complex_type_with_group_by_and_first(async);
+
+ AssertSql(
+ """
+
+""");
+ }
+
+ #endregion GroupBy
+
+ public override Task Projecting_property_of_complex_type_using_left_join_with_pushdown(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Projecting_property_of_complex_type_using_left_join_with_pushdown(async), CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(Customer), nameof(CustomerGroup)));
+
+ public override Task Projecting_complex_from_optional_navigation_using_conditional(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Projecting_complex_from_optional_navigation_using_conditional(async), CosmosStrings.MultipleRootEntityTypesReferencedInQuery(nameof(Customer), nameof(CustomerGroup)));
+
+ public override Task Project_entity_with_complex_type_pushdown_and_then_left_join(bool async)
+ => AssertTranslationFailedWithDetails(() => base.Project_entity_with_complex_type_pushdown_and_then_left_join(async), CosmosStrings.LimitOffsetNotSupportedInSubqueries);
+
+ [ConditionalFact]
+ public virtual void Check_all_tests_overridden()
+ => TestHelpers.AssertAllMethodsOverridden(GetType());
+
+ private void AssertSql(params string[] expected)
+ => Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
+
+ public class ComplexTypeQueryCosmosFixture : ComplexTypeQueryFixtureBase
+ {
+ public TestSqlLoggerFactory TestSqlLoggerFactory
+ => (TestSqlLoggerFactory)ListLoggerFactory;
+
+ protected override ITestStoreFactory TestStoreFactory
+ => CosmosTestStoreFactory.Instance;
+
+ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
+ => base.AddOptions(builder)
+ .ConfigureWarnings(w => w.Ignore(CosmosEventId.NoPartitionKeyDefined).Ignore(CoreEventId.MappedEntityTypeIgnoredWarning));
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context)
+ {
+ base.OnModelCreating(modelBuilder, context);
+ modelBuilder.Entity().ToContainer("Customers");
+ modelBuilder.Entity().ToContainer("CustomerGroups");
+ modelBuilder.Entity().ToContainer("ValuedCustomers");
+ modelBuilder.Entity().ToContainer("ValuedCustomerGroups");
+ }
+ }
+}
diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs
index 5e763d533da..db4d129b9b4 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindAggregateOperatorsQueryCosmosTest.cs
@@ -5,6 +5,7 @@
using Microsoft.Azure.Cosmos;
using Microsoft.EntityFrameworkCore.Cosmos.Internal;
using Microsoft.EntityFrameworkCore.TestModels.Northwind;
+using Microsoft.EntityFrameworkCore.TestUtilities.Xunit;
namespace Microsoft.EntityFrameworkCore.Query;
@@ -1449,7 +1450,7 @@ FROM root c
""");
});
- [ConditionalTheory(Skip = "Fails on CI #27688")]
+ [SkipOnCiCondition(SkipReason = "Fails on CI #27688")]
public override Task Distinct_Scalar(bool async)
=> Fixture.NoSyncTest(
async, async a =>
@@ -1458,9 +1459,8 @@ public override Task Distinct_Scalar(bool async)
AssertSql(
"""
-SELECT DISTINCT c[""City""]
+SELECT DISTINCT VALUE c["City"]
FROM root c
-WHERE (c[""$type""] = ""Customer"")
""");
});
diff --git a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs
index 5f7faf6978d..14c20d5e295 100644
--- a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs
+++ b/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs
@@ -231,14 +231,15 @@ public virtual async Task Nullable_complex_type_with_discriminator_and_shadow_pr
var contextFactory = await InitializeNonSharedTest(
seed: context =>
{
- context.Add(
- new Context37337.EntityType
+ var entity = new Context37337.EntityType
+ {
+ Prop = new Context37337.OptionalComplexProperty
{
- Prop = new Context37337.OptionalComplexProperty
- {
- OptionalValue = true
- }
- });
+ OptionalValue = true
+ }
+ };
+ context.Add(entity);
+ context.Entry(entity).Property("CreatedBy").CurrentValue = "Seeder";
return context.SaveChangesAsync();
});
@@ -250,9 +251,12 @@ public virtual async Task Nullable_complex_type_with_discriminator_and_shadow_pr
var entity = entities[0];
Assert.NotNull(entity.Prop);
Assert.True(entity.Prop.OptionalValue);
+
+ var entry = context.Entry(entity);
+ Assert.Equal("Seeder", entry.Property("CreatedBy").CurrentValue);
}
- private class Context37337(DbContextOptions options) : DbContext(options)
+ protected class Context37337(DbContextOptions options) : DbContext(options)
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
diff --git a/test/EFCore.Specification.Tests/Query/ComplexTypeQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/ComplexTypeQueryTestBase.cs
index 23062c6dc78..0cf5ec4e1b1 100644
--- a/test/EFCore.Specification.Tests/Query/ComplexTypeQueryTestBase.cs
+++ b/test/EFCore.Specification.Tests/Query/ComplexTypeQueryTestBase.cs
@@ -367,7 +367,7 @@ public virtual Task Struct_complex_type_equals_constant(bool async)
}));
[ConditionalTheory, MemberData(nameof(IsAsyncData))]
- public virtual Task Struct_complex_type_equals_parameter(bool async)
+ public virtual async Task Struct_complex_type_equals_parameter(bool async)
{
var address = new AddressStruct
{
@@ -376,7 +376,7 @@ public virtual Task Struct_complex_type_equals_parameter(bool async)
Country = new CountryStruct { FullName = "United States", Code = "US" }
};
- return AssertQuery(
+ await AssertQuery(
async,
ss => ss.Set().Where(c => c.ShippingAddress == address));
}