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