diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelValidator.cs b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelValidator.cs index b02f420ce26..85bfafde2f1 100644 --- a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelValidator.cs +++ b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelValidator.cs @@ -83,14 +83,6 @@ static void ValidateType(ITypeBase typeBase, IDiagnosticsLogger.Value) && expression is not null) + { + var nullCheck = Expression.Not( + Expression.Property(expression, nameof(Nullable<>.HasValue))); + return Expression.Condition( + nullCheck, + Expression.Default(memberExpression.Type), + Expression.Property(expression, nameof(Nullable<>.Value))); + } + Expression updatedMemberExpression = memberExpression.Update( - expression != null ? MatchTypes(expression, memberExpression.Expression!.Type) : expression); + expression != null ? MatchTypes(expression, memberExpression.Expression!.Type) : expression); if (expression?.Type.IsNullableType() == true) { var nullableReturnType = memberExpression.Type.MakeNullable(); - if (!memberExpression.Type.IsNullableType()) + + if (!updatedMemberExpression.Type.IsNullableType()) { updatedMemberExpression = Expression.Convert(updatedMemberExpression, nullableReturnType); } + Expression nullCheck; + if (expression.Type.IsNullableValueType()) + { + // For Nullable, use HasValue property instead of equality comparison + // to avoid issues with value types that don't define the == operator + nullCheck = Expression.Not( + Expression.Property(expression, nameof(Nullable<>.HasValue))); + } + else + { + nullCheck = Expression.Equal(expression, Expression.Default(expression.Type)); + } + updatedMemberExpression = Expression.Condition( - Expression.Equal(expression, Expression.Default(expression.Type)), - Expression.Constant(null, nullableReturnType), + nullCheck, + Expression.Default(nullableReturnType), updatedMemberExpression); } @@ -639,8 +664,21 @@ UnaryExpression unaryExpression updatedMethodCallExpression = Expression.Convert(updatedMethodCallExpression, nullableReturnType); } + Expression nullCheck; + if (@object.Type.IsNullableValueType()) + { + // For Nullable, use HasValue property instead of equality comparison + // to avoid issues with value types that don't define the == operator + nullCheck = Expression.Not( + Expression.Property(@object, nameof(Nullable<>.HasValue))); + } + else + { + nullCheck = Expression.Equal(@object, Expression.Constant(null, @object.Type)); + } + return Expression.Condition( - Expression.Equal(@object, Expression.Default(@object.Type)), + nullCheck, Expression.Constant(null, nullableReturnType), updatedMethodCallExpression); } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs index 3f826412a13..34721a08057 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs @@ -340,15 +340,15 @@ 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) + if (existingEntityType is not null && existingEntityType != structuralType) { AddTranslationErrorDetails( - CosmosStrings.MultipleRootEntityTypesReferencedInQuery(entityType.DisplayName(), existingEntityType.DisplayName())); + CosmosStrings.MultipleRootEntityTypesReferencedInQuery(structuralType.DisplayName(), existingEntityType.DisplayName())); return null; } @@ -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,11 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou return null; } + if (source.ShaperExpression is StructuralTypeShaperExpression { StructuralType: IComplexType }) + { + return null; + } + select.ApplyDistinct(); return source; @@ -607,7 +612,7 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou var translatedSelect = new SelectExpression( - new EntityProjectionExpression(translation, (IEntityType)projectedStructuralTypeShaper.StructuralType)); + new EntityProjectionExpression(translation, projectedStructuralTypeShaper.StructuralType)); return source.Update( translatedSelect, new StructuralTypeShaperExpression( @@ -1132,8 +1137,8 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s slice, alias, new EntityProjectionExpression( - new ObjectReferenceExpression((IEntityType)projectedStructuralTypeShaper.StructuralType, alias), - (IEntityType)projectedStructuralTypeShaper.StructuralType)); + new ObjectReferenceExpression(projectedStructuralTypeShaper.StructuralType, alias), + projectedStructuralTypeShaper.StructuralType)); return source.Update( translatedSelect, new StructuralTypeShaperExpression( @@ -1271,8 +1276,8 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s slice, alias, new EntityProjectionExpression( - new ObjectReferenceExpression((IEntityType)projectedStructuralTypeShaper.StructuralType, alias), - (IEntityType)projectedStructuralTypeShaper.StructuralType)); + new ObjectReferenceExpression(projectedStructuralTypeShaper.StructuralType, alias), + projectedStructuralTypeShaper.StructuralType)); return source.Update( translatedSelect, new StructuralTypeShaperExpression( @@ -1390,7 +1395,17 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s return CreateShapedQueryExpression(targetEntityType, select); } - // TODO: Collection of complex type (#31253) + case StructuralTypeShaperExpression shaper when property is IComplexProperty { IsCollection: true }: + { + var targetEntityType = shaper.StructuralType; + var projection = new EntityProjectionExpression( + new ObjectReferenceExpression(targetEntityType, sourceAlias), targetEntityType); + var select = SelectExpression.CreateForCollection( + shaper.ValueBufferExpression, + sourceAlias, + projection); + return CreateShapedQueryExpression(targetEntityType, select); + } // Note that non-collection navigations/complex types are handled in CosmosSqlTranslatingExpressionVisitor // (no collection -> no queryable operators) @@ -1658,7 +1673,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/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitor.cs index e02ac1de670..2ac13ce34ce 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitor.cs @@ -8,10 +8,11 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; public partial class CosmosShapedQueryCompilingExpressionVisitor { private sealed class CosmosProjectionBindingRemovingExpressionVisitor( + IStructuralTypeMaterializerSource entityMaterializerSource, SelectExpression selectExpression, ParameterExpression jTokenParameter, bool trackQueryResults) - : CosmosProjectionBindingRemovingExpressionVisitorBase(jTokenParameter, trackQueryResults) + : CosmosProjectionBindingRemovingExpressionVisitorBase(entityMaterializerSource, jTokenParameter, trackQueryResults) { protected override ProjectionExpression GetProjection(ProjectionBindingExpression projectionBindingExpression) => selectExpression.Projection[GetProjectionIndex(projectionBindingExpression)]; diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs index de333edebc8..66d291f72b6 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs @@ -16,6 +16,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; public partial class CosmosShapedQueryCompilingExpressionVisitor { private abstract class CosmosProjectionBindingRemovingExpressionVisitorBase( + IStructuralTypeMaterializerSource entityMaterializerSource, ParameterExpression jTokenParameter, bool trackQueryResults) : ExpressionVisitor @@ -47,25 +48,69 @@ private readonly IDictionary _materializationCo private readonly IDictionary _projectionBindings = new Dictionary(); - private readonly IDictionary _ownerMappings - = new Dictionary(); + private readonly IDictionary _ownerMappings + = new Dictionary(); private readonly IDictionary _ordinalParameterBindings = new Dictionary(); + private readonly Dictionary _complexPropertyBindings + = new Dictionary(); + private List _pendingIncludes = []; + private int _currentComplexIndex; + private static readonly MethodInfo ToObjectWithSerializerMethodInfo = typeof(CosmosProjectionBindingRemovingExpressionVisitorBase) .GetRuntimeMethods().Single(mi => mi.Name == nameof(SafeToObjectWithSerializer)); + private ParameterExpression _currentJObject; + private ParameterExpression _currentEntityTypeParam; + private ITypeBase _currentStructuralType; + private int _currentComplexArrayIndex; + + protected override Expression VisitBlock(BlockExpression node) + { + var entityTypeParam = node.Variables.FirstOrDefault(x => x.Type.IsAssignableTo(typeof(ITypeBase))); + if (entityTypeParam != null) + { + var previousStructuralType = _currentStructuralType; + var previousEntityTypeParam = _currentEntityTypeParam; + _currentEntityTypeParam = entityTypeParam; + var visited = base.VisitBlock(node); + _currentEntityTypeParam = previousEntityTypeParam; + _currentStructuralType = previousStructuralType; + return visited; + } + + var jObjectParam = node.Variables.FirstOrDefault(x => x.Type == typeof(JObject)); + if (jObjectParam != null) + { + var previousJObject = _currentJObject; + _currentJObject = jObjectParam; + var visited = base.VisitBlock(node); + _currentJObject = previousJObject; + return visited; + } + + return base.VisitBlock(node); + } + protected override Expression VisitBinary(BinaryExpression binaryExpression) { if (binaryExpression.NodeType == ExpressionType.Assign) { if (binaryExpression.Left is ParameterExpression parameterExpression) { + // @TODO: This appears to not work if there is a discriminator... + if (parameterExpression == _currentEntityTypeParam && binaryExpression.Right is ConstantExpression constantExpression) + { + _currentStructuralType = (ITypeBase)constantExpression.Value; + return binaryExpression; + } + if (parameterExpression.Type == typeof(JObject) || parameterExpression.Type == typeof(JArray)) { @@ -138,7 +183,7 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) accessExpression = objectAccessExpression.Object; storeNames.Add(objectAccessExpression.PropertyName); _ownerMappings[objectAccessExpression] - = (objectAccessExpression.Navigation.DeclaringEntityType, accessExpression); + = (objectAccessExpression.PropertyBase.DeclaringType, accessExpression); } valueExpression = CreateGetValueExpression(accessExpression, (string)null, typeof(JObject)); @@ -189,9 +234,121 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) } } - if (binaryExpression.Left is MemberExpression { Member: FieldInfo { IsInitOnly: true } } memberExpression) + if (binaryExpression.Left is MemberExpression simpleMember) { - return memberExpression.Assign(Visit(binaryExpression.Right)); + if (_currentStructuralType != null) + { + var complexProperty = _currentStructuralType.GetComplexProperties().FirstOrDefault(x => x.GetMemberInfo(true, true) == simpleMember.Member); + if (complexProperty == null) + { + return simpleMember.Assign(Visit(binaryExpression.Right)); + } + + if (complexProperty.IsCollection) + { + // @TODO: cleanup this code duplication with the collection shaper visit + _currentComplexArrayIndex++; + var complexJArrayVariable = Variable( + typeof(JArray), + "complexJArray" + _currentComplexArrayIndex); + + var assignVariable2 = Assign(complexJArrayVariable, + Call( + ToObjectWithSerializerMethodInfo.MakeGenericMethod(typeof(JArray)), + Call(_currentJObject, GetItemMethodInfo, + Constant(complexProperty.Name) // @TODO: Get json property name + ) + ) + ); + + var selectParameter = Parameter(typeof(JObject), "selectComplexObject" + _currentComplexArrayIndex); + var temp = Parameter(typeof(MaterializationContext), "temp" + _currentComplexArrayIndex); + _materializationContextBindings[temp] = selectParameter; + var materializeExpression = (BlockExpression)entityMaterializerSource.CreateMaterializeExpression( + new StructuralTypeMaterializerSourceParameters(complexProperty.ComplexType, "complexType", complexProperty.ClrType, false, QueryTrackingBehavior: null), + temp); + + var previousCurrentJObject2 = _currentJObject; + var previousStructuralType2 = _currentStructuralType; + _currentJObject = selectParameter; + _currentStructuralType = complexProperty.ComplexType; + materializeExpression = (BlockExpression)Visit(materializeExpression); + _currentJObject = previousCurrentJObject2; + _currentStructuralType = previousStructuralType2; + + var select = Call( + EnumerableMethods.Select.MakeGenericMethod(typeof(JObject), complexProperty.ComplexType.ClrType), + Call( + EnumerableMethods.Cast.MakeGenericMethod(typeof(JObject)), + complexJArrayVariable), + Lambda(materializeExpression, selectParameter)); + + var populateExpression = + Call( + PopulateCollectionMethodInfo.MakeGenericMethod(complexProperty.ComplexType.ClrType, complexProperty.ClrType), + Constant(complexProperty.GetCollectionAccessor()), + select + ); + + var newRight2 = populateExpression; + + var test = Condition(Equal(complexJArrayVariable, Constant(null, assignVariable2.Type)), + binaryExpression.Right, + newRight2 + ); + + return Block( + [complexJArrayVariable], + [ + assignVariable2, + simpleMember.Assign(test) + ] + ); + } + + _currentComplexIndex++; + var complexJObjectVariable = Variable( + typeof(JObject), + "complexJObject" + _currentComplexIndex); + + var assignVariable = Assign(complexJObjectVariable, + Call( + ToObjectWithSerializerMethodInfo.MakeGenericMethod(typeof(JObject)), + Call(_currentJObject, GetItemMethodInfo, + Constant(complexProperty.Name) // @TODO: Get json property name + ) + ) + ); + + var previousStructuralType = _currentStructuralType; + var previousCurrentJObject = _currentJObject; + _currentJObject = complexJObjectVariable; + _currentStructuralType = complexProperty.ComplexType; + var right = Visit(binaryExpression.Right); + _currentJObject = previousCurrentJObject; + _currentStructuralType = previousStructuralType; + + Expression newRight; + if (right is ConditionalExpression conditional) + { + newRight = Condition(Equal(complexJObjectVariable, Constant(null, complexJObjectVariable.Type)), + conditional.IfTrue, + conditional.IfFalse); + } + else + { + newRight = right; + } + + return Block( + [complexJObjectVariable], + [ + assignVariable, + simpleMember.Assign(newRight) + ] + ); + + } } } @@ -288,7 +445,7 @@ protected override Expression VisitExtension(Expression extensionExpression) var accessExpression = objectArrayAccess.InnerProjection.Object; _projectionBindings[accessExpression] = jObjectParameter; _ownerMappings[accessExpression] = - (objectArrayAccess.Navigation.DeclaringEntityType, objectArrayAccess.Object); + (objectArrayAccess.PropertyBase.DeclaringType, objectArrayAccess.Object); _ordinalParameterBindings[accessExpression] = Add( ordinalParameter, Constant(1, typeof(int))); @@ -631,6 +788,11 @@ private Expression CreateGetValueExpression( return _projectionBindings[jTokenExpression]; } + if (property.DeclaringType is IComplexType c) + { + jTokenExpression = _currentJObject;// @TODO: Json name + } + var entityType = property.DeclaringType as IEntityType; var ownership = entityType?.FindOwnership(); var storeName = property.GetJsonPropertyName(); diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs index f79539eb0b7..c845648e110 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs @@ -67,6 +67,7 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery } shaperBody = new CosmosProjectionBindingRemovingExpressionVisitor( + Dependencies.EntityMaterializerSource, selectExpression, jTokenParameter, QueryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.TrackAll) .Visit(shaperBody); diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs index f963e03ee06..995053933c7 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -2,9 +2,15 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections; +using System.Data.SqlTypes; using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.Cosmos.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Query.Internal.Expressions; +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Internal; using static Microsoft.EntityFrameworkCore.Infrastructure.ExpressionExtensions; namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; @@ -26,9 +32,13 @@ public 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))!; @@ -209,24 +219,24 @@ when TryRewriteEntityEquality( ?? QueryCompilationContext.NotTranslatedExpression; } - Expression ProcessGetType(EntityReferenceExpression entityReferenceExpression, Type comparisonType, bool match) + Expression ProcessGetType(StructuralTypeReferenceExpression structuralReferenceExpression, Type comparisonType, bool match) { - var entityType = entityReferenceExpression.EntityType; + var structuralType = structuralReferenceExpression.EntityType; - if (entityType.BaseType == null - && !entityType.GetDirectlyDerivedTypes().Any()) + if (structuralType.BaseType == null + && !structuralType.GetDirectlyDerivedTypes().Any()) { // No hierarchy - return sqlExpressionFactory.Constant((entityType.ClrType == comparisonType) == match); + return sqlExpressionFactory.Constant((structuralType.ClrType == comparisonType) == match); } - if (entityType.GetAllBaseTypes().Any(e => e.ClrType == comparisonType)) + if (structuralType is IEntityType entityType && entityType.GetAllBaseTypes().Any(e => e.ClrType == comparisonType)) { // EntitySet will never contain a type of base type return sqlExpressionFactory.Constant(!match); } - var derivedType = entityType.GetDerivedTypesInclusive().SingleOrDefault(et => et.ClrType == comparisonType); + var derivedType = structuralType.GetDerivedTypesInclusive().SingleOrDefault(et => et.ClrType == comparisonType); // If no derived type matches then fail the translation if (derivedType != null) { @@ -239,8 +249,8 @@ Expression ProcessGetType(EntityReferenceExpression entityReferenceExpression, T // Or add predicate for matching that particular type discriminator value // All hierarchies have discriminator property if (TryBindMember( - entityReferenceExpression, - MemberIdentity.Create(entityType.GetDiscriminatorPropertyName()), + structuralReferenceExpression, + MemberIdentity.Create(structuralType.GetDiscriminatorPropertyName()), out var discriminatorMember, out _) && discriminatorMember is SqlExpression discriminatorColumn) @@ -258,7 +268,7 @@ 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? entityReferenceExpression) { entityReferenceExpression = null; if (expression is not MethodCallExpression methodCallExpression @@ -267,7 +277,7 @@ bool IsGetTypeMethodCall(Expression expression, [NotNullWhen(true)] out EntityRe return false; } - entityReferenceExpression = Visit(methodCallExpression.Object) as EntityReferenceExpression; + entityReferenceExpression = Visit(methodCallExpression.Object) as StructuralTypeReferenceExpression; return entityReferenceExpression != null; } @@ -340,7 +350,7 @@ protected override Expression VisitExtension(Expression extensionExpression) switch (extensionExpression) { case EntityProjectionExpression: - case EntityReferenceExpression: + case StructuralTypeReferenceExpression: case SqlExpression: return extensionExpression; @@ -348,7 +358,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); // @@ -392,7 +402,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 @@ -493,6 +503,22 @@ protected override Expression VisitMember(MemberExpression memberExpression) { var innerExpression = Visit(memberExpression.Expression); + if (innerExpression != null) + { + if (memberExpression.Member.DeclaringType?.IsNullableValueType() == true) + { + if (memberExpression.Member.Name == "HasValue") + { + return Visit(Expression.NotEqual(memberExpression.Expression!, Expression.Constant(null, memberExpression.Member.DeclaringType))); + } + + if (memberExpression.Member.Name == "Value") + { + return Visit(memberExpression.Expression)!; + } + } + } + return TryBindMember(innerExpression, MemberIdentity.Create(memberExpression.Member), out var expression, out _) ? expression : (TranslationFailed(memberExpression.Expression, innerExpression, out var sqlInnerExpression) @@ -802,7 +828,7 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression) { var operand = Visit(unaryExpression.Operand); - if (operand is EntityReferenceExpression entityReferenceExpression + if (operand is StructuralTypeReferenceExpression entityReferenceExpression && unaryExpression.NodeType is ExpressionType.Convert or ExpressionType.ConvertChecked or ExpressionType.TypeAs) { return entityReferenceExpression.Convert(unaryExpression.Type); @@ -851,9 +877,9 @@ protected override Expression VisitTypeBinary(TypeBinaryExpression typeBinaryExp var innerExpression = Visit(typeBinaryExpression.Expression); if (typeBinaryExpression.NodeType == ExpressionType.TypeIs - && innerExpression is EntityReferenceExpression entityReferenceExpression) + && innerExpression is StructuralTypeReferenceExpression entityReferenceExpression + && entityReferenceExpression.EntityType is IEntityType entityType) { - var entityType = entityReferenceExpression.EntityType; if (entityType.GetAllBaseTypesInclusive().Any(et => et.ClrType == typeBinaryExpression.TypeOperand)) { return sqlExpressionFactory.Constant(true); @@ -898,7 +924,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; @@ -947,7 +973,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; @@ -992,12 +1018,11 @@ private bool TryRewriteContainsEntity(Expression source, Expression item, [NotNu { result = null; - if (item is not EntityReferenceExpression itemEntityReference) + if (item is not StructuralTypeReferenceExpression itemEntityReference || itemEntityReference.EntityType is not IEntityType entityType) { return false; } - var entityType = itemEntityReference.EntityType; var primaryKeyProperties = entityType.FindPrimaryKey()?.Properties; switch (primaryKeyProperties) @@ -1065,85 +1090,154 @@ private bool TryRewriteEntityEquality( bool equalsMethod, [NotNullWhen(true)] out Expression? result) { - var leftEntityReference = left as EntityReferenceExpression; - var rightEntityReference = right as EntityReferenceExpression; - - if (leftEntityReference == null - && rightEntityReference == null) + var structuralReference = left as StructuralTypeReferenceExpression ?? right as StructuralTypeReferenceExpression; + if (structuralReference == null) { result = null; return false; } + var structuralType = structuralReference.EntityType; + var compareReference = structuralReference == left ? right : left; - if (IsNullSqlConstantExpression(left) - || IsNullSqlConstantExpression(right)) + // Null equality + if (IsNullSqlConstantExpression(compareReference)) { - var nonNullEntityReference = (IsNullSqlConstantExpression(left) ? rightEntityReference : leftEntityReference)!; - var entityType1 = nonNullEntityReference.EntityType; - var primaryKeyProperties1 = entityType1.FindPrimaryKey()?.Properties; - if (primaryKeyProperties1 == null) + // Treat type as object for null comparison + var access = new SqlObjectAccessExpression((Expression?)structuralReference.Subquery ?? structuralReference.Parameter ?? throw new UnreachableException()); + result = sqlExpressionFactory.MakeBinary(nodeType, access, sqlExpressionFactory.Constant(null, typeof(object), null)!, typeMappingSource.FindMapping(typeof(bool)))!; + return true; + } + + // IEntityType type comparison + if (structuralType is IEntityType entityType) + { + if (entityType.FindPrimaryKey()?.Properties is not { } primaryKeyProperties) { throw new InvalidOperationException( - CoreStrings.EntityEqualityOnKeylessEntityNotSupported( - nodeType == ExpressionType.Equal - ? equalsMethod ? nameof(object.Equals) : "==" - : equalsMethod - ? "!" + nameof(object.Equals) - : "!=", - entityType1.DisplayName())); + CoreStrings.EntityEqualityOnKeylessEntityNotSupported( + nodeType == ExpressionType.Equal + ? equalsMethod ? nameof(object.Equals) : "==" + : equalsMethod + ? "!" + nameof(object.Equals) + : "!=", + entityType.DisplayName())); } - result = Visit( - primaryKeyProperties1.Select(p => - Expression.MakeBinary( - nodeType, CreatePropertyAccessExpression(nonNullEntityReference, p), - Expression.Constant(null, p.ClrType.MakeNullable()))) - .Aggregate((l, r) => nodeType == ExpressionType.Equal ? Expression.OrElse(l, r) : Expression.AndAlso(l, r))); + if (compareReference is StructuralTypeReferenceExpression compareStructuralTypeReference) + { + // Comparing of 2 different entity types is always false. + if (structuralType.GetRootType() != compareStructuralTypeReference.EntityType.GetRootType()) + { + // @TODO: Why not throw.. + result = Visit(Expression.Constant(false)); + return true; + } + } - return true; + // Compare primary keys of entity type + result = CreateStructuralComparison(primaryKeyProperties); + + return result is not null; } - var leftEntityType = leftEntityReference?.EntityType; - var rightEntityType = rightEntityReference?.EntityType; - var entityType = leftEntityType ?? rightEntityType; + // Complex type equality + else if (structuralType is IComplexType complexType) + { + if (complexType.ComplexProperty.IsCollection) + { + // @TODO: We could compare by: + /* + WHERE ARRAY_LENGTH(c.items) = ARRAY_LENGTH(@items) + AND NOT EXISTS ( + SELECT VALUE i + FROM i IN c.items + WHERE NOT ARRAY_CONTAINS(@items, i, true) + ) + * */ + result = null; + return false; + } - Check.DebugAssert(entityType != null, "At least either side should be entityReference so entityType should be non-null."); + // Compare to another structural type reference x => x.ComplexProp1 == x.ComplexProp2 || + // Compare to constant complex type x => x.ComplexProp1 == new ComplexType() + // Compare to parameter complex type x => x.ComplexProp1 == param + if (compareReference is StructuralTypeReferenceExpression compareStructuralTypeReference && compareStructuralTypeReference.EntityType.ClrType == structuralType.ClrType || + compareReference is SqlConstantExpression constant && constant.Type.MakeNullable() == structuralType.ClrType.MakeNullable() || + compareReference is SqlParameterExpression parameter && parameter.Type.MakeNullable() == structuralType.ClrType.MakeNullable()) + { + if (compareReference is SqlParameterExpression p) + { + compareReference = new SqlParameterExpression( + p.Name, + structuralType.ClrType, + new CosmosTypeMapping(typeof(object), null, null, null, null) + ); + } - if (leftEntityType != null - && rightEntityType != null - && leftEntityType.GetRootType() != rightEntityType.GetRootType()) - { - result = sqlExpressionFactory.Constant(false); - return true; + var allProperties = complexType.GetComplexProperties().Cast().Concat(complexType.GetProperties()); + result = CreateStructuralComparison(allProperties); + + return result is not null; + } } - var primaryKeyProperties = entityType.FindPrimaryKey()?.Properties; - if (primaryKeyProperties == null) + Expression? CreateStructuralComparison(IEnumerable properties) + => CreateStructuralComparisonBy(properties, p => CreatePropertyAccessExpression(right, p)); + + Expression? CreateStructuralComparisonBy(IEnumerable properties, Func rightValueFactory) { - throw new InvalidOperationException( - CoreStrings.EntityEqualityOnKeylessEntityNotSupported( - nodeType == ExpressionType.Equal - ? equalsMethod ? nameof(object.Equals) : "==" - : equalsMethod - ? "!" + nameof(object.Equals) - : "!=", - entityType.DisplayName())); - } + var propertyCompare = properties.Select(p => + Expression.MakeBinary( + nodeType, + CreatePropertyAccessExpression(left, p), + rightValueFactory(p))) + .Aggregate((l, r) => nodeType == ExpressionType.Equal + ? Expression.AndAlso(l, r) + : Expression.OrElse(l, r)); + + if (compareReference.Type.IsNullableType() && compareReference is not SqlConstantExpression { Value: not null }) + { + Expression compareNullCompareReference = compareReference; + if (compareReference is SqlParameterExpression sqlParameterExpression) + { + var lambda = Expression.Lambda( + Expression.Condition( + Expression.Equal( + Expression.Call(ParameterValueExtractorMethod.MakeGenericMethod(sqlParameterExpression.Type.MakeNullable()), QueryCompilationContext.QueryContextParameter, Expression.Constant(sqlParameterExpression.Name, typeof(string))), + Expression.Constant(null) + ), + Expression.Constant(null), + Expression.Constant(new object()) + ), + QueryCompilationContext.QueryContextParameter + ); + + var newParameterName = $"{RuntimeParameterPrefix}{sqlParameterExpression.Name}"; + var queryParam = queryCompilationContext.RegisterRuntimeParameter(newParameterName, lambda); + compareNullCompareReference = new SqlParameterExpression(queryParam.Name, queryParam.Type, CosmosTypeMapping.Default); + } - 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 Visit(Expression.OrElse( + Expression.AndAlso( + Expression.Equal(structuralReference, sqlExpressionFactory.Constant(null, typeof(object), null)!), + Expression.Equal(compareNullCompareReference, sqlExpressionFactory.Constant(null, typeof(object), null)!)) + , + Expression.AndAlso( + Expression.NotEqual(compareNullCompareReference, sqlExpressionFactory.Constant(null, typeof(object), null)!), + propertyCompare + ) + ) + ); + } - return true; + return Visit(propertyCompare); + } + + result = null; + return false; } - private Expression CreatePropertyAccessExpression(Expression target, IProperty property) + private Expression CreatePropertyAccessExpression(Expression target, IPropertyBase property) { switch (target) { @@ -1154,10 +1248,10 @@ private Expression CreatePropertyAccessExpression(Expression target, IProperty p case SqlParameterExpression sqlParameterExpression: var lambda = Expression.Lambda( Expression.Call( - ParameterValueExtractorMethod.MakeGenericMethod(property.ClrType.MakeNullable()), + ParameterPropertyValueExtractorMethod.MakeGenericMethod(property.ClrType.MakeNullable()), QueryCompilationContext.QueryContextParameter, Expression.Constant(sqlParameterExpression.Name, typeof(string)), - Expression.Constant(property, typeof(IProperty))), + Expression.Constant(property, typeof(IPropertyBase))), QueryCompilationContext.QueryContextParameter); var newParameterName = $"{RuntimeParameterPrefix}{sqlParameterExpression.Name}_{property.Name}"; @@ -1174,12 +1268,18 @@ when memberInitExpression.Bindings.SingleOrDefault(mb => mb.Member.Name == prope } } - private static T? ParameterValueExtractor(QueryContext context, string baseParameterName, IProperty 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, @@ -1241,21 +1341,21 @@ 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; + EntityType = parameter.StructuralType; } - public EntityReferenceExpression(ShapedQueryExpression subquery) + public StructuralTypeReferenceExpression(ShapedQueryExpression subquery) { Subquery = subquery; - EntityType = (IEntityType)((StructuralTypeShaperExpression)subquery.ShaperExpression).StructuralType; + EntityType = ((StructuralTypeShaperExpression)subquery.ShaperExpression).StructuralType; } - private EntityReferenceExpression(EntityReferenceExpression typeReference, ITypeBase structuralType) + private StructuralTypeReferenceExpression(StructuralTypeReferenceExpression typeReference, ITypeBase structuralType) { Parameter = typeReference.Parameter; Subquery = typeReference.Subquery; @@ -1264,7 +1364,7 @@ private EntityReferenceExpression(EntityReferenceExpression typeReference, IType public new StructuralTypeShaperExpression? Parameter { get; } public ShapedQueryExpression? Subquery { get; } - public IEntityType EntityType { get; } + public ITypeBase EntityType { get; } public override Type Type => EntityType.ClrType; @@ -1282,7 +1382,7 @@ public Expression Convert(Type type) return EntityType is { } entityType && entityType.GetDerivedTypes().FirstOrDefault(et => et.ClrType == type) is { } derivedEntityType - ? new EntityReferenceExpression(this, derivedEntityType) + ? new StructuralTypeReferenceExpression(this, derivedEntityType) : QueryCompilationContext.NotTranslatedExpression; } diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/EntityProjectionExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/EntityProjectionExpression.cs index 11824d8a4b1..bf7c7e63448 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/EntityProjectionExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/EntityProjectionExpression.cs @@ -12,10 +12,12 @@ 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 EntityProjectionExpression : Expression, IPrintableExpression, IAccessExpression // @TODO: StructuralTypeProjectionExpression? { 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 +25,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 EntityProjectionExpression(Expression @object, ITypeBase type) { Object = @object; - EntityType = entityType; + EntityType = type; PropertyName = (@object as IAccessExpression)?.PropertyName; } @@ -62,7 +64,7 @@ public override Type Type /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual IEntityType EntityType { get; } + public virtual ITypeBase EntityType { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -170,6 +172,40 @@ 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 (!EntityType.IsAssignableFrom(complexProperty.DeclaringType) + && !complexProperty.DeclaringType.IsAssignableFrom(EntityType)) + { + throw new InvalidOperationException( + CosmosStrings.UnableToBindMemberToEntityProjection("navigation", complexProperty.Name, EntityType.DisplayName())); + } + + if (!_complexPropertyExpressionsMap.TryGetValue(complexProperty, out var expression)) + { + // TODO: Unify ObjectAccessExpression and ObjectArrayAccessExpression + expression = complexProperty.IsCollection + ? new StructuralTypeShaperExpression( + complexProperty.ComplexType, + new ObjectArrayAccessExpression(Object, complexProperty), + nullable: true) + : new StructuralTypeShaperExpression( + complexProperty.ComplexType, + new EntityProjectionExpression(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 +234,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 type = EntityType; if (entityClrType != null - && !entityClrType.IsAssignableFrom(entityType.ClrType)) + && !entityClrType.IsAssignableFrom(type.ClrType)) { - entityType = entityType.GetDerivedTypes().First(e => entityClrType.IsAssignableFrom(e.ClrType)); + type = type.GetDerivedTypes().First(e => entityClrType.IsAssignableFrom(e.ClrType)); } var property = member.MemberInfo == null - ? entityType.FindProperty(member.Name!) - : entityType.FindProperty(member.MemberInfo); + ? type.FindProperty(member.Name!) + : type.FindProperty(member.MemberInfo); if (property != null) { propertyBase = property; return BindProperty(property, clientEval); } - var navigation = member.MemberInfo == null - ? entityType.FindNavigation(member.Name!) - : entityType.FindNavigation(member.MemberInfo); - if (navigation != null) + if (type is IEntityType entityType) + { + var navigation = member.MemberInfo == null + ? entityType.FindNavigation(member.Name!) + : entityType.FindNavigation(member.MemberInfo); + if (navigation != null) + { + propertyBase = navigation; + return BindNavigation(navigation, clientEval); + } + } + + var complex = member.MemberInfo == null + ? type.FindComplexProperty(member.Name!) + : type.FindComplexProperty(member.MemberInfo); + if (complex != null) { - propertyBase = navigation; - return BindNavigation(navigation, clientEval); + propertyBase = complex; + return BindComplexProperty(complex, clientEval); } // Entity member not found diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectAccessExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectAccessExpression.cs index 007394d7119..a131408704d 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; + PropertyBase = navigation; + TypeBase = 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) + { + PropertyBase = complexProperty; + PropertyName = complexProperty.Name; + Object = @object; + TypeBase = 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 ITypeBase TypeBase { 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; + => PropertyBase.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 PropertyBase { 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) + ? PropertyBase is INavigation navigation + ? new ObjectAccessExpression(outerExpression, navigation) + : new ObjectAccessExpression(outerExpression, (IComplexProperty)PropertyBase) : this; /// @@ -127,7 +152,7 @@ public override bool Equals(object? obj) && Equals(objectAccessExpression)); private bool Equals(ObjectAccessExpression objectAccessExpression) - => Navigation == objectAccessExpression.Navigation + => PropertyBase == objectAccessExpression.PropertyBase && 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(PropertyBase, Object); } diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayAccessExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayAccessExpression.cs index 4eb61d119f3..096ca6d674c 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayAccessExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayAccessExpression.cs @@ -36,8 +36,28 @@ public ObjectArrayAccessExpression( ?? throw new InvalidOperationException( CosmosStrings.NavigationPropertyIsNotAnEmbeddedEntity( navigation.DeclaringEntityType.DisplayName(), navigation.Name)); + PropertyBase = navigation; + Object = @object; + InnerProjection = innerProjection + ?? new EntityProjectionExpression(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, + EntityProjectionExpression? innerProjection = null) + { + var targetType = complexProperty.ComplexType; + Type = typeof(IEnumerable<>).MakeGenericType(targetType.ClrType); - Navigation = navigation; + PropertyName = complexProperty.Name; + PropertyBase = complexProperty; Object = @object; InnerProjection = innerProjection ?? new EntityProjectionExpression(new ObjectReferenceExpression(targetType, ""), targetType); @@ -82,7 +102,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 PropertyBase { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -116,7 +136,9 @@ public virtual ObjectArrayAccessExpression Update( Expression accessExpression, EntityProjectionExpression innerProjection) => accessExpression != Object || innerProjection != InnerProjection - ? new ObjectArrayAccessExpression(accessExpression, Navigation, innerProjection) + ? PropertyBase is INavigation navigation + ? new ObjectArrayAccessExpression(accessExpression, navigation, innerProjection) + : new ObjectArrayAccessExpression(accessExpression, (IComplexProperty)PropertyBase, innerProjection) : this; /// diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectReferenceExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectReferenceExpression.cs index b2fee0d2605..2f9cbc8ceb0 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 type, string name) : Expression, IPrintableExpression, IAccessExpression { /// /// 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 EntityType { get; } = type; /// /// 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/SqlObjectAccessExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs new file mode 100644 index 00000000000..4fe2f8c54ac --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SqlObjectAccessExpression.cs @@ -0,0 +1,90 @@ +// 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.Storage.Internal; + +namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal.Expressions; + +/// +/// Represents an structural type object access on a CosmosJSON 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. +/// +[DebuggerDisplay("{Microsoft.EntityFrameworkCore.Query.ExpressionPrinter.Print(this), nq}")] +public class SqlObjectAccessExpression(Expression @object) + : SqlExpression(typeof(object), CosmosTypeMapping.Default), IAccessExpression +{ + /// + /// 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 Object { get; } = @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 virtual string? PropertyName => null; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + => Update(visitor.Visit(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 virtual SqlObjectAccessExpression Update(Expression @object) + => ReferenceEquals(@object, Object) + ? this + : new SqlObjectAccessExpression(@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. + /// + protected override void Print(ExpressionPrinter expressionPrinter) + => expressionPrinter.Visit(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 override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is SqlObjectAccessExpression expression + && Equals(expression)); + + private bool Equals(SqlObjectAccessExpression expression) + => base.Equals(expression) + && Object.Equals(expression.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 override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), Object); +} diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs index 52a8e13f04a..6e45c8bfb8d 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosDatabaseWrapper.cs @@ -22,7 +22,7 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; /// public class CosmosDatabaseWrapper : Database, IResettableService { - private readonly Dictionary _documentCollections = new(); + private readonly Dictionary _documentCollections = new(); private readonly ICosmosClientWrapper _cosmosClient; private readonly bool _sensitiveLoggingEnabled; @@ -295,7 +295,7 @@ private SaveGroups CreateSaveGroups(IList entries) { var entityType = entry.EntityType; var documentSource = GetDocumentSource(entityType); - var collectionId = documentSource.GetContainerId(); + var collectionId = entry.EntityType.GetContainer()!; var operation = entry.EntityState switch { EntityState.Added => CosmosCudOperation.Create, @@ -546,12 +546,12 @@ private async Task SaveAsync(CosmosUpdateEntry updateEntry, CancellationTo /// 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 DocumentSource GetDocumentSource(IEntityType entityType) + public virtual DocumentSource GetDocumentSource(ITypeBase structuralType) { - if (!_documentCollections.TryGetValue(entityType, out var documentSource)) + if (!_documentCollections.TryGetValue(structuralType, out var documentSource)) { _documentCollections.Add( - entityType, documentSource = new DocumentSource(entityType, this)); + structuralType, documentSource = new DocumentSource(structuralType, this)); } return documentSource; diff --git a/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs b/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs index 75b9a7f415a..52fc55bcee4 100644 --- a/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs +++ b/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs @@ -10,6 +10,9 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Update.Internal; +// #16707 +#pragma warning disable EF1001 // Internal EF Core API usage. + /// /// 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 @@ -18,9 +21,8 @@ namespace Microsoft.EntityFrameworkCore.Cosmos.Update.Internal; /// public class DocumentSource { - private readonly string _containerId; private readonly CosmosDatabaseWrapper _database; - private readonly IEntityType _entityType; + private readonly ITypeBase _entityType; private readonly IProperty? _idProperty; private readonly IProperty? _jObjectProperty; @@ -30,24 +32,14 @@ public class DocumentSource /// 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 DocumentSource(IEntityType entityType, CosmosDatabaseWrapper database) + public DocumentSource(ITypeBase entityType, CosmosDatabaseWrapper database) { - _containerId = entityType.GetContainer()!; _database = database; _entityType = entityType; _idProperty = entityType.GetProperties().FirstOrDefault(p => p.GetJsonPropertyName() == CosmosJsonIdConvention.IdPropertyJsonName); _jObjectProperty = entityType.FindProperty(CosmosPartitionKeyInPrimaryKeyConvention.JObjectPropertyName); } - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual string GetContainerId() - => _containerId; - /// /// 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 @@ -112,30 +104,149 @@ public virtual JObject CreateDocument(IUpdateEntry entry, int? ordinal) } else if (fk.IsUnique) { -#pragma warning disable EF1001 // Internal EF Core API usage. - // #16707 var dependentEntry = ((InternalEntityEntry)entry).StateManager.TryGetEntry(embeddedValue, fk.DeclaringEntityType)!; document[embeddedPropertyName] = _database.GetDocumentSource(dependentEntry.EntityType).CreateDocument(dependentEntry); -#pragma warning restore EF1001 // Internal EF Core API usage. } else { SetTemporaryOrdinals(entry, fk, embeddedValue); -#pragma warning disable EF1001 // Internal EF Core API usage. - // #16707 var stateManager = ((InternalEntityEntry)entry).StateManager; -#pragma warning restore EF1001 // Internal EF Core API usage. var embeddedOrdinal = 1; var array = new JArray(); foreach (var dependent in (IEnumerable)embeddedValue) { -#pragma warning disable EF1001 // Internal EF Core API usage. - // #16707 var dependentEntry = stateManager.TryGetEntry(dependent, fk.DeclaringEntityType)!; array.Add(_database.GetDocumentSource(dependentEntry.EntityType).CreateDocument(dependentEntry, embeddedOrdinal)); -#pragma warning restore EF1001 // Internal EF Core API usage. + embeddedOrdinal++; + } + + document[embeddedPropertyName] = array; + } + } + + // @TODO: refactor to avoid duplication with complex property source + foreach (var complexProperty in entry.EntityType.GetComplexProperties()) + { + var embeddedValue = entry.GetCurrentValue(complexProperty); + var embeddedPropertyName = complexProperty.Name; // @TODO: Get json property name + if (embeddedValue == null) + { + document[embeddedPropertyName] = null; + } + else if (!complexProperty.IsCollection) + { + document[embeddedPropertyName] = _database.GetDocumentSource(complexProperty.ComplexType).CreateDocument(entry, complexProperty, embeddedValue); + } + else + { + var internalEntry = (InternalEntityEntry)entry; + + var embeddedOrdinal = 0; + var array = new JArray(); + foreach (var dependent in (IEnumerable)embeddedValue) + { + var dependentEntry = internalEntry.GetComplexCollectionEntry(complexProperty, embeddedOrdinal); + array.Add(_database.GetDocumentSource(dependentEntry.ComplexType).CreateDocument(dependentEntry, complexProperty, dependent)); + embeddedOrdinal++; + } + + document[embeddedPropertyName] = array; + } + } + + return document; + } + + /// + /// 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 JObject CreateDocument(IUpdateEntry entry, IComplexProperty complexProperty, object currentValue) + { + var document = new JObject(); + foreach (var property in complexProperty.ComplexType.GetProperties()) + { + var storeName = property.GetJsonPropertyName(); + if (storeName.Length != 0) + { + document[storeName] = ConvertPropertyValue(property, entry); + } + } + + foreach (var nestedComplexProperty in complexProperty.ComplexType.GetComplexProperties()) + { + var embeddedValue = entry.GetCurrentValue(nestedComplexProperty); + var embeddedPropertyName = nestedComplexProperty.Name; // @TODO: Get json property name + if (embeddedValue == null) + { + document[embeddedPropertyName] = null; + } + else if (!nestedComplexProperty.IsCollection) + { + document[embeddedPropertyName] = _database.GetDocumentSource(nestedComplexProperty.ComplexType).CreateDocument(entry, nestedComplexProperty, embeddedValue); + } + else + { + var internalEntry = (InternalEntityEntry)entry; + + var embeddedOrdinal = 0; + var array = new JArray(); + foreach (var dependent in (IEnumerable)embeddedValue) + { + var dependentEntry = internalEntry.GetComplexCollectionEntry(nestedComplexProperty, embeddedOrdinal); + array.Add(_database.GetDocumentSource(dependentEntry.ComplexType).CreateDocument(dependentEntry, nestedComplexProperty, dependent)); + embeddedOrdinal++; + } + + document[embeddedPropertyName] = array; + } + } + + return document; + } + + /// + /// 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 JObject CreateDocument(InternalComplexEntry entry, IComplexProperty complexProperty, object currentValue) + { + var document = new JObject(); + foreach (var property in complexProperty.ComplexType.GetProperties()) + { + var storeName = property.GetJsonPropertyName(); + if (storeName.Length != 0) + { + document[storeName] = ConvertPropertyValue(property, entry); + } + } + + foreach (var nestedComplexProperty in complexProperty.ComplexType.GetComplexProperties()) + { + var embeddedValue = entry.GetCurrentValue(nestedComplexProperty); + var embeddedPropertyName = nestedComplexProperty.Name; // @TODO: Get json property name + if (embeddedValue == null) + { + document[embeddedPropertyName] = null; + } + else if (!nestedComplexProperty.IsCollection) + { + document[embeddedPropertyName] = _database.GetDocumentSource(nestedComplexProperty.ComplexType).CreateDocument(entry, nestedComplexProperty, embeddedValue); + } + else + { + var embeddedOrdinal = 0; + var array = new JArray(); + foreach (var dependent in (IEnumerable)embeddedValue) + { + var dependentEntry = entry.GetComplexCollectionEntry(nestedComplexProperty, embeddedOrdinal); + array.Add(_database.GetDocumentSource(dependentEntry.ComplexType).CreateDocument(dependentEntry, nestedComplexProperty, dependent)); embeddedOrdinal++; } @@ -161,6 +272,7 @@ public virtual JObject CreateDocument(IUpdateEntry entry, int? ordinal) /// 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. /// + // @TODO: Comlex properties public virtual JObject? UpdateDocument(JObject document, IUpdateEntry entry, int? ordinal) { var anyPropertyUpdated = false; @@ -209,10 +321,7 @@ public virtual JObject CreateDocument(IUpdateEntry entry, int? ordinal) } else if (fk.IsUnique) { -#pragma warning disable EF1001 // Internal EF Core API usage. - // #16707 var embeddedEntry = ((InternalEntityEntry)entry).StateManager.TryGetEntry(embeddedValue, fk.DeclaringEntityType)!; -#pragma warning restore EF1001 // Internal EF Core API usage. var embeddedDocument = embeddedDocumentSource.GetCurrentDocument(embeddedEntry); embeddedDocument = embeddedDocument != null @@ -229,19 +338,13 @@ public virtual JObject CreateDocument(IUpdateEntry entry, int? ordinal) { SetTemporaryOrdinals(entry, fk, embeddedValue); -#pragma warning disable EF1001 // Internal EF Core API usage. - // #16707 var stateManager = ((InternalEntityEntry)entry).StateManager; -#pragma warning restore EF1001 // Internal EF Core API usage. var embeddedOrdinal = 1; var array = new JArray(); foreach (var dependent in (IEnumerable)embeddedValue) { -#pragma warning disable EF1001 // Internal EF Core API usage. - // #16707 var embeddedEntry = stateManager.TryGetEntry(dependent, fk.DeclaringEntityType)!; -#pragma warning restore EF1001 // Internal EF Core API usage. var embeddedDocument = embeddedDocumentSource.GetCurrentDocument(embeddedEntry); embeddedDocument = embeddedDocument != null @@ -269,15 +372,10 @@ private static void SetTemporaryOrdinals( var ordinalKeyProperty = FindOrdinalKeyProperty(fk.DeclaringEntityType); if (ordinalKeyProperty != null) { -#pragma warning disable EF1001 // Internal EF Core API usage. - // #16707 var stateManager = ((InternalEntityEntry)entry).StateManager; -#pragma warning restore EF1001 // Internal EF Core API usage. var shouldSetTemporaryKeys = false; foreach (var dependent in (IEnumerable)embeddedValue) { -#pragma warning disable EF1001 // Internal EF Core API usage. - // #16707 var embeddedEntry = stateManager.TryGetEntry(dependent, fk.DeclaringEntityType)!; if ((int)embeddedEntry.GetCurrentValue(ordinalKeyProperty)! != embeddedOrdinal @@ -286,7 +384,6 @@ private static void SetTemporaryOrdinals( shouldSetTemporaryKeys = true; break; } -#pragma warning restore EF1001 // Internal EF Core API usage. embeddedOrdinal++; } @@ -296,12 +393,9 @@ private static void SetTemporaryOrdinals( var temporaryOrdinal = -1; foreach (var dependent in (IEnumerable)embeddedValue) { -#pragma warning disable EF1001 // Internal EF Core API usage. - // #16707 var embeddedEntry = stateManager.TryGetEntry(dependent, fk.DeclaringEntityType)!; embeddedEntry.SetTemporaryValue(ordinalKeyProperty, temporaryOrdinal, setModified: false); -#pragma warning restore EF1001 // Internal EF Core API usage. temporaryOrdinal--; } @@ -330,4 +424,13 @@ private static void SetTemporaryOrdinals( ? null : (value as JToken) ?? JToken.FromObject(value, CosmosClientWrapper.Serializer); } + + private static JToken? ConvertPropertyValue(IProperty property, InternalComplexEntry entry) + { + var value = entry.GetCurrentValue(property); + return value == null + ? null + : (value as JToken) ?? JToken.FromObject(value, CosmosClientWrapper.Serializer); + } } +#pragma warning restore EF1001 // Internal EF Core API usage. 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..7e5d1b5e165 --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCollectionCosmosTest.cs @@ -0,0 +1,173 @@ +// 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) +"""); + } + + 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 Task Distinct_projected(QueryTrackingBehavior queryTrackingBehavior) + => AssertTranslationFailed(() => base.Distinct_projected(queryTrackingBehavior)); + + 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_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/ComplexPropertiesCosmosFixture.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCosmosFixture.cs new file mode 100644 index 00000000000..214c025f24a --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCosmosFixture.cs @@ -0,0 +1,42 @@ +// 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 ComplexPropertiesCosmosFixture : ComplexPropertiesFixtureBase +{ + public ComplexPropertiesCosmosFixture() + { + //Environment.SetEnvironmentVariable("EF_TEST_REWRITE_BASELINES", "1"); + } + 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)); + + public Task NoSyncTest(bool async, Func testCode) + => CosmosTestHelpers.Instance.NoSyncTest(async, testCode); + + public void NoSyncTest(Action testCode) + => CosmosTestHelpers.Instance.NoSyncTest(testCode); + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.Ignore(); + + modelBuilder.Entity() + .ToContainer("RootEntities") + .HasNoDiscriminator(); + + modelBuilder.Entity() + .ToContainer("ValueRootEntities") + .HasNoDiscriminator(); + } +} 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..ed72d52fdc4 --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesMiscellaneousCosmosTest.cs @@ -0,0 +1,98 @@ +// 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() + { + // @TODO: Structural equality. + await base.Where_HasValue_on_nullable_value_type(); + + AssertSql(""" +SELECT VALUE c +FROM root c +WHERE (c["OptionalAssociate"] != null) +"""); + //var ex = await Assert.ThrowsAsync(() => base.Where_HasValue_on_nullable_value_type()); + //Assert.Equal(CoreStrings.EntityEqualityOnKeylessEntityNotSupported("!=", "ValueRootEntity.OptionalAssociate#ValueAssociateType"), ex.Message); + } + + #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 new file mode 100644 index 00000000000..fc9d8af5e10 --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesProjectionCosmosTest.cs @@ -0,0 +1,397 @@ +// 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 ComplexPropertiesProjectionCosmosTest : ComplexPropertiesProjectionTestBase +{ + public ComplexPropertiesProjectionCosmosTest(ComplexPropertiesCosmosFixture fixture, ITestOutputHelper outputHelper) : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(outputHelper); + } + + public override async Task Select_root(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_root(queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE c +FROM root c +"""); + } + + #region Scalar properties + + public override async Task Select_scalar_property_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_scalar_property_on_required_associate(queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE c["RequiredAssociate"]["String"] +FROM root c +"""); + } + + 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 + // result to be filtered out entirely. + await AssertQuery( + ss => ss.Set().Select(x => x.OptionalAssociate!.String), + ss => ss.Set().Where(x => x.OptionalAssociate != null).Select(x => x.OptionalAssociate!.String), + queryTrackingBehavior: queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE c["OptionalAssociate"]["String"] +FROM root c +"""); + } + + 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 + // result to be filtered out entirely. + await AssertQuery( + ss => ss.Set().Select(x => x.OptionalAssociate!.Int), + ss => ss.Set().Where(x => x.OptionalAssociate != null).Select(x => x.OptionalAssociate!.Int), + queryTrackingBehavior: queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE c["OptionalAssociate"]["Int"] +FROM root c +"""); + } + + 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 + // result to be filtered out entirely. + await AssertQuery( + ss => ss.Set().Select(x => (int?)x.OptionalAssociate!.Int), + ss => ss.Set().Where(x => x.OptionalAssociate != null).Select(x => (int?)x.OptionalAssociate!.Int), + queryTrackingBehavior: queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE c["OptionalAssociate"]["Int"] +FROM root c +"""); + } + + #endregion Scalar properties + + #region Structural properties + + public override async Task Select_associate(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_associate(queryTrackingBehavior); + + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + AssertSql( + """ +SELECT VALUE c +FROM root c +"""); + } + } + + public override async Task Select_optional_associate(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_optional_associate(queryTrackingBehavior); + + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + AssertSql( + """ +SELECT VALUE c +FROM root c +"""); + } + } + + public override async Task Select_required_nested_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_required_nested_on_required_associate(queryTrackingBehavior); + + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + AssertSql( + """ +SELECT VALUE c +FROM root c +"""); + } + } + + public override async Task Select_optional_nested_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_optional_nested_on_required_associate(queryTrackingBehavior); + + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + AssertSql( + """ +SELECT VALUE c +FROM root c +"""); + } + } + + public override async Task Select_required_nested_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) + { + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + await base.Select_required_nested_on_optional_associate(queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE c +FROM root c +"""); + } + } + + public override async Task Select_optional_nested_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) + { + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + await base.Select_optional_nested_on_optional_associate(queryTrackingBehavior); + + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + AssertSql( + """ +SELECT VALUE c +FROM root c +"""); + } + } + } + + public override Task Select_required_associate_via_optional_navigation(QueryTrackingBehavior queryTrackingBehavior) + // We don't support (inter-document) navigations with Cosmos. + => Assert.ThrowsAsync(() => base.Select_required_associate_via_optional_navigation(queryTrackingBehavior)); + + public override async Task Select_unmapped_associate_scalar_property(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_unmapped_associate_scalar_property(queryTrackingBehavior); + + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + AssertSql( + """ +SELECT VALUE c +FROM root c +"""); + } + } + + public override async Task Select_untranslatable_method_on_associate_scalar_property(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_untranslatable_method_on_associate_scalar_property(queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE c["RequiredAssociate"]["Int"] +FROM root c +"""); + } + + #endregion Structural properties + + #region Structural collection properties + + public override async Task Select_associate_collection(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_associate_collection(queryTrackingBehavior); + + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + AssertSql( + """ +SELECT VALUE c +FROM root c +ORDER BY c["Id"] +"""); + } + } + + public override async Task Select_nested_collection_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) + { + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + await base.Select_nested_collection_on_required_associate(queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE c +FROM root c +ORDER BY c["Id"] +"""); + } + } + + public override async Task Select_nested_collection_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) + { + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + await base.Select_nested_collection_on_optional_associate(queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE c +FROM root c +ORDER BY c["Id"] +"""); + } + } + + public override async Task SelectMany_associate_collection(QueryTrackingBehavior queryTrackingBehavior) + { + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + await base.SelectMany_associate_collection(queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE a +FROM root c +JOIN a IN c["AssociateCollection"] +"""); + } + } + + public override async Task SelectMany_nested_collection_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) + { + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + await base.SelectMany_nested_collection_on_required_associate(queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE n +FROM root c +JOIN n IN c["RequiredAssociate"]["NestedCollection"] +"""); + } + } + + public override async Task SelectMany_nested_collection_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) + { + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + // The given key 'n' was not present in the dictionary + await base.SelectMany_nested_collection_on_optional_associate(queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE n +FROM root c +JOIN n IN c["OptionalAssociate"]["NestedCollection"] +"""); + } + } + + #endregion Structural collection properties + + #region Multiple + + public override async Task Select_root_duplicated(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_root_duplicated(queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE c +FROM root c +"""); + } + + #endregion Multiple + + #region Subquery + + public override async Task Select_subquery_required_related_FirstOrDefault(QueryTrackingBehavior queryTrackingBehavior) + { + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + await AssertTranslationFailed(() => base.Select_subquery_required_related_FirstOrDefault(queryTrackingBehavior)); + } + } + + public override async Task Select_subquery_optional_related_FirstOrDefault(QueryTrackingBehavior queryTrackingBehavior) + { + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + await AssertTranslationFailed(() => base.Select_subquery_required_related_FirstOrDefault(queryTrackingBehavior)); + } + } + + #endregion Subquery + + #region Value types + + public override async Task Select_root_with_value_types(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_root_with_value_types(queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE c +FROM root c +"""); + } + + public override async Task Select_non_nullable_value_type(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_non_nullable_value_type(queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE c +FROM root c +ORDER BY c["Id"] +"""); + } + + + public override async Task Select_nullable_value_type(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_nullable_value_type(queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE c +FROM root c +ORDER BY c["Id"] +"""); + } + + public override async Task Select_nullable_value_type_with_Value(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_nullable_value_type_with_Value(queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE c +FROM root c +ORDER BY c["Id"] +"""); + } + + #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/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..d849872b710 --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesStructuralEqualityCosmosTest.cs @@ -0,0 +1,170 @@ +// 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 Task Two_associates() + => AssertTranslationFailed(base.Two_associates); + + public override async Task Two_nested_associates() + { + await base.Two_nested_associates(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (((c["RequiredAssociate"]["RequiredNestedAssociate"] = null) AND (c["OptionalAssociate"]["RequiredNestedAssociate"] = null)) OR ((c["OptionalAssociate"]["RequiredNestedAssociate"] != null) AND (((((c["RequiredAssociate"]["RequiredNestedAssociate"]["Id"] = c["OptionalAssociate"]["RequiredNestedAssociate"]["Id"]) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Int"] = c["OptionalAssociate"]["RequiredNestedAssociate"]["Int"])) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Ints"] = c["OptionalAssociate"]["RequiredNestedAssociate"]["Ints"])) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Name"] = c["OptionalAssociate"]["RequiredNestedAssociate"]["Name"])) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["String"] = c["OptionalAssociate"]["RequiredNestedAssociate"]["String"])))) +"""); + } + + public override Task Not_equals() + => AssertTranslationFailed(base.Not_equals); + + 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 Task Associate_with_parameter_null() + => AssertTranslationFailed(base.Associate_with_parameter_null); + + 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) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Int"] = 8)) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Ints"] = [1,2,3])) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Name"] = "Root1_RequiredAssociate_RequiredNestedAssociate")) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["String"] = "foo")) +"""); + } + + public override async Task Nested_associate_with_parameter() + { + await base.Nested_associate_with_parameter(); + + AssertSql( + """ +@entity_equality_nested='{}' +@entity_equality_nested_Id='1000' +@entity_equality_nested_Int='8' +@entity_equality_nested_Ints='[1,2,3]' +@entity_equality_nested_Name='Root1_RequiredAssociate_RequiredNestedAssociate' +@entity_equality_nested_String='foo' + +SELECT VALUE c +FROM root c +WHERE (((c["RequiredAssociate"]["RequiredNestedAssociate"] = null) AND (@entity_equality_nested = null)) OR ((@entity_equality_nested != null) AND (((((c["RequiredAssociate"]["RequiredNestedAssociate"]["Id"] = @entity_equality_nested_Id) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Int"] = @entity_equality_nested_Int)) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Ints"] = @entity_equality_nested_Ints)) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["Name"] = @entity_equality_nested_Name)) AND (c["RequiredAssociate"]["RequiredNestedAssociate"]["String"] = @entity_equality_nested_String)))) +"""); + } + + [ConditionalFact] + public async Task Nested_associate_with_parameter_null() + { + NestedAssociateType? nested = null; + await AssertQuery( + ss => ss.Set().Where(e => e.RequiredAssociate.OptionalNestedAssociate == nested), + ss => ss.Set().Where(e => e.RequiredAssociate.OptionalNestedAssociate == nested)); + + AssertSql( + """ +@entity_equality_nested=null +@entity_equality_nested_Id=null +@entity_equality_nested_Int=null +@entity_equality_nested_Ints=null +@entity_equality_nested_Name=null +@entity_equality_nested_String=null + +SELECT VALUE c +FROM root c +WHERE (((c["RequiredAssociate"]["OptionalNestedAssociate"] = null) AND (@entity_equality_nested = null)) OR ((@entity_equality_nested != null) AND (((((c["RequiredAssociate"]["OptionalNestedAssociate"]["Id"] = @entity_equality_nested_Id) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Int"] = @entity_equality_nested_Int)) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Ints"] = @entity_equality_nested_Ints)) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["Name"] = @entity_equality_nested_Name)) AND (c["RequiredAssociate"]["OptionalNestedAssociate"]["String"] = @entity_equality_nested_String)))) +"""); + } + + public override Task Two_nested_collections() + => AssertTranslationFailed(base.Two_nested_collections); + + public override Task Nested_collection_with_inline() + => AssertTranslationFailed(base.Nested_collection_with_inline); + + public override Task Nested_collection_with_parameter() + => AssertTranslationFailed(base.Nested_collection_with_parameter); + + [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() + { + // No backing field could be found for property 'RootEntity.RequiredRelated#RelatedType.NestedCollection#NestedType.RelatedTypeRootEntityId' and the property does not have a getter. + await Assert.ThrowsAsync(() => base.Contains_with_inline()); + + AssertSql(); + } + + public override async Task Contains_with_parameter() + { + + await Assert.ThrowsAsync(base.Contains_with_parameter); + } + + public override async Task Contains_with_operators_composed_on_the_collection() + { + + await Assert.ThrowsAsync(base.Contains_with_operators_composed_on_the_collection); + } + + public override async Task Contains_with_nested_and_composed_operators() + { + await Assert.ThrowsAsync(base.Contains_with_nested_and_composed_operators); + } + + #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/ComplexProperties/CosmosComplexCollectionTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/CosmosComplexCollectionTest.cs new file mode 100644 index 00000000000..82517c9c030 --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/CosmosComplexCollectionTest.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. + +using static Microsoft.EntityFrameworkCore.Query.Associations.ComplexProperties.CosmosComplexCollectionTest; + +namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexProperties; + +public class CosmosComplexCollectionTest(CosmosComplexCollectionTestFixture fixture) : IClassFixture +{ + [ConditionalFact] + public async Task CanAddAndQuery() + { + //var entity = new Entity() + //{ + // Id = Guid.NewGuid(), + // ComplexType = new ComplexType { Name = "One" }, + //}; + + using (var context = fixture.CreateContext()) + { + //context.Add(entity); + //await context.SaveChangesAsync(); + + var result = await context.Entities.Where(x => x.ComplexType != null).ToListAsync(); + } + } + + public class CosmosComplexCollectionTestFixture : SharedStoreFixtureBase + { + protected override string StoreName + => nameof(CosmosComplexCollectionTest); + + protected override ITestStoreFactory TestStoreFactory + => CosmosTestStoreFactory.Instance; + } + + public class CosmosComplexCollectionDbContext : DbContext + { + public CosmosComplexCollectionDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasPartitionKey(x => x.PartitionKey).OwnsOne(e => e.ComplexType); + } + + public DbSet Entities { get; set; } = null!; + } + + public class Entity + { + public Guid Id { get; set; } + + public string PartitionKey { get; set; } = ""; + + public ComplexType? ComplexType { get; set; } = new(); + } + + public class ComplexType + { + public string Name { get; set; } = null!; + } +} diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs index b19b56da69b..bed53ec3287 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/OwnedNavigations/OwnedNavigationsStructuralEqualityCosmosTest.cs @@ -48,14 +48,32 @@ WHERE false """); } - public override Task Associate_with_inline_null() - => Assert.ThrowsAsync(() => base.Associate_with_inline_null()); + 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 Task Associate_with_parameter_null() => Assert.ThrowsAsync(() => base.Associate_with_parameter_null()); - public override Task Nested_associate_with_inline_null() - => Assert.ThrowsAsync(() => base.Nested_associate_with_inline_null()); + 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() { diff --git a/test/EFCore.Cosmos.Tests/Infrastructure/CosmosModelValidatorTest.cs b/test/EFCore.Cosmos.Tests/Infrastructure/CosmosModelValidatorTest.cs index bf4fe3014b0..e2e97b79a80 100644 --- a/test/EFCore.Cosmos.Tests/Infrastructure/CosmosModelValidatorTest.cs +++ b/test/EFCore.Cosmos.Tests/Infrastructure/CosmosModelValidatorTest.cs @@ -542,7 +542,7 @@ public virtual void Detects_unmappable_list_property() } [ConditionalFact] - public virtual void Detects_complex_type_collection() + public virtual void Passes_on_complex_type_collection() { var modelBuilder = CreateConventionModelBuilder(); modelBuilder.Entity(b => @@ -550,10 +550,7 @@ public virtual void Detects_complex_type_collection() b.ComplexCollection(e => e.ComplexTypes); }); - VerifyError( - CosmosStrings.ComplexTypeCollectionsNotSupported( - nameof(ComplexTypeInCollection), - nameof(EntityWithComplexTypeCollection.ComplexTypes)), modelBuilder); + Validate(modelBuilder); } private class EntityWithComplexTypeCollection