diff --git a/src/EFCore.Cosmos/Extensions/Internal/CosmosShapedQueryExpressionExtensions.cs b/src/EFCore.Cosmos/Extensions/Internal/CosmosShapedQueryExpressionExtensions.cs index be295279ee4..4f57188376d 100644 --- a/src/EFCore.Cosmos/Extensions/Internal/CosmosShapedQueryExpressionExtensions.cs +++ b/src/EFCore.Cosmos/Extensions/Internal/CosmosShapedQueryExpressionExtensions.cs @@ -167,7 +167,7 @@ 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 entityProjection) { projection = entityProjection.Object; } 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..e56e3601df4 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,7 +215,7 @@ protected override Expression VisitExtension(Expression extensionExpression) if (_clientEval) { - var entityProjection = (EntityProjectionExpression)projection; + var entityProjection = (StructuralTypeProjectionExpression)projection; return entityShaperExpression.Update( new ProjectionBindingExpression( @@ -306,11 +307,11 @@ protected override Expression VisitMember(MemberExpression memberExpression) var innerEntityProjection = 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 UnaryExpression unaryExpression - => (EntityProjectionExpression)((UnaryExpression)unaryExpression.Operand).Operand, + => (StructuralTypeProjectionExpression)((UnaryExpression)unaryExpression.Operand).Operand, _ => throw new InvalidOperationException(CoreStrings.TranslationFailed(memberExpression.Print())) }; @@ -326,7 +327,7 @@ UnaryExpression unaryExpression switch (navigationProjection) { - case EntityProjectionExpression entityProjection: + case StructuralTypeProjectionExpression entityProjection: return new StructuralTypeShaperExpression( navigation.TargetEntityType, Expression.Convert(Expression.Convert(entityProjection, typeof(object)), typeof(ValueBuffer)), @@ -527,14 +528,14 @@ when _collectionShaperMapping.TryGetValue(parameterExpression, out var collectio var innerEntityProjection = shaperExpression.ValueBufferExpression switch { - EntityProjectionExpression entityProjection + StructuralTypeProjectionExpression entityProjection => entityProjection, 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())) }; diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs index 8ac1a8dcaa9..4acf5951d2a 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosQuerySqlGenerator.cs @@ -62,7 +62,7 @@ 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 VisitEntityProjection(StructuralTypeProjectionExpression entityProjectionExpression) { Visit(entityProjectionExpression.Object); diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosQueryableMethodTranslatingExpressionVisitor.cs index fe5de54ca5f..da1cc20abdb 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( @@ -896,7 +904,7 @@ protected override ShapedQueryExpression TranslateCast(ShapedQueryExpression sou var projectionMember = projectionBindingExpression.ProjectionMember; Check.DebugAssert(new ProjectionMember().Equals(projectionMember), "Invalid ProjectionMember when processing OfType"); - var entityProjectionExpression = (EntityProjectionExpression)select.GetMappedProjection(projectionMember); + var entityProjectionExpression = (StructuralTypeProjectionExpression)select.GetMappedProjection(projectionMember); select.ReplaceProjectionMapping( new Dictionary { @@ -1131,9 +1139,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 +1278,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( @@ -1380,17 +1388,30 @@ 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: + { + Debug.Assert(collectionResult.Parameter != null, "CollectionResultExpression can't be bound to member without parameter."); + + var shaper = collectionResult.Parameter; + var targetStructuralType = shaper.StructuralType; + var projection = new StructuralTypeProjectionExpression( + new ObjectReferenceExpression(targetStructuralType, sourceAlias), targetStructuralType); + var select = SelectExpression.CreateForCollection( + shaper.ValueBufferExpression, + sourceAlias, + projection); + return CreateShapedQueryExpression(targetStructuralType, select); + } // Note that non-collection navigations/complex types are handled in CosmosSqlTranslatingExpressionVisitor // (no collection -> no queryable operators) @@ -1666,7 +1687,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..5985d8368ac 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,7 +108,7 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) objectArrayProjectionExpression.Object, storeName, parameterExpression.Type); break; - case EntityProjectionExpression entityProjectionExpression: + case StructuralTypeProjectionExpression entityProjectionExpression: var accessExpression = entityProjectionExpression.Object; _projectionBindings[accessExpression] = parameterExpression; @@ -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.PropertyBase.DeclaringType, accessExpression); } valueExpression = CreateGetValueExpression(accessExpression, (string)null, typeof(JObject)); @@ -165,16 +165,16 @@ when jObjectMethodCallExpression.Method.GetGenericMethodDefinition() == ToObject } else { - EntityProjectionExpression entityProjectionExpression; + StructuralTypeProjectionExpression entityProjectionExpression; if (newExpression.Arguments[0] is ProjectionBindingExpression projectionBindingExpression) { var projection = GetProjection(projectionBindingExpression); - entityProjectionExpression = (EntityProjectionExpression)projection.Expression; + entityProjectionExpression = (StructuralTypeProjectionExpression)projection.Expression; } else { var projection = ((UnaryExpression)((UnaryExpression)newExpression.Arguments[0]).Operand).Operand; - entityProjectionExpression = (EntityProjectionExpression)projection; + entityProjectionExpression = (StructuralTypeProjectionExpression)projection; } _materializationContextBindings[parameterExpression] = entityProjectionExpression.Object; @@ -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.PropertyBase.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..7ccc9d835b4 --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.StructuralEquality.cs @@ -0,0 +1,335 @@ +// 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 itemEntityReference || + itemEntityReference.StructuralType is not IEntityType entityType) // #36468 + { + 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) + { + 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 (leftShaper, leftComplexType) = ParseComplexType(left); + var (rightShaper, rightComplexType) = ParseComplexType(right); + + if (leftShaper is null && rightShaper is null) + { + result = null; + return false; + } + + var shaper = leftShaper ?? rightShaper; + var complexType = leftComplexType ?? rightComplexType; + + Debug.Assert(complexType != 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 leftAccess = leftShaper != null ? Visit(leftShaper.ValueBufferExpression) : ParseComplexAccess(left); + var rightAccess = rightShaper != null ? Visit(rightShaper.ValueBufferExpression) : ParseComplexAccess(right); + + if (leftAccess is null || leftAccess == QueryCompilationContext.NotTranslatedExpression || + rightAccess is null || rightAccess == QueryCompilationContext.NotTranslatedExpression) + { + result = null; + return false; + } + + result = new SqlBinaryExpression( + nodeType, + leftAccess, + rightAccess, + typeof(bool), + typeMappingSource.FindMapping(typeof(bool))!)!; + return true; + + (StructuralTypeShaperExpression?, IComplexType?) ParseComplexType(Expression expression) + => expression switch + { + StructuralTypeReferenceExpression { StructuralType: IComplexType type } reference + => (reference.Parameter ?? (StructuralTypeShaperExpression)reference.Subquery!.ShaperExpression, type), + CollectionResultExpression { ComplexProperty: IComplexProperty { ComplexType: var type } } collectionResult + => (collectionResult.Parameter ?? (StructuralTypeShaperExpression)collectionResult.Subquery!.ShaperExpression, type), + + _ => (null, null) + }; + + Expression? ParseComplexAccess(Expression expression) + => expression switch + { + SqlParameterExpression sqlParameterExpression + => CreateJsonQueryParameter(sqlParameterExpression), + SqlConstantExpression constant + => sqlExpressionFactory.Constant( + CosmosSerializationUtilities.SerializeObjectToComplexProperty(complexType, constant.Value, collection), + CosmosTypeMapping.Default), + + _ => null + }; + + Expression 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..b192483c687 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosSqlTranslatingExpressionVisitor.cs @@ -4,8 +4,8 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; using Microsoft.EntityFrameworkCore.Cosmos.Internal; -using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Query.Internal; using static Microsoft.EntityFrameworkCore.Infrastructure.ExpressionExtensions; namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; @@ -16,7 +16,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 +25,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)])!; @@ -179,7 +171,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,24 +202,24 @@ when TryRewriteEntityEquality( ?? QueryCompilationContext.NotTranslatedExpression; } - Expression ProcessGetType(EntityReferenceExpression entityReferenceExpression, Type comparisonType, bool match) + Expression ProcessGetType(StructuralTypeReferenceExpression structuralTypeReferenceExpression, Type comparisonType, bool match) { - var entityType = entityReferenceExpression.EntityType; + var structuralType = structuralTypeReferenceExpression.StructuralType; - if (entityType.BaseType == null - && !entityType.GetDirectlyDerivedTypes().Any()) + if (structuralType.BaseType == null + && !structuralType.GetDirectlyDerivedTypes().Any()) { // No hierarchy - return sqlExpressionFactory.Constant((entityType.ClrType == comparisonType) == match); + return sqlExpressionFactory.Constant((structuralType.ClrType == comparisonType) == match); } - if (entityType.GetAllBaseTypes().Any(e => e.ClrType == comparisonType)) + if (structuralType is IEntityType entityType && entityType.GetAllBaseTypes().Any(e => e.ClrType == comparisonType)) { // EntitySet will never contain a type of base type return sqlExpressionFactory.Constant(!match); } - var derivedType = entityType.GetDerivedTypesInclusive().SingleOrDefault(et => et.ClrType == comparisonType); + var derivedType = structuralType.GetDerivedTypesInclusive().SingleOrDefault(et => et.ClrType == comparisonType); // If no derived type matches then fail the translation if (derivedType != null) { @@ -240,8 +232,8 @@ Expression ProcessGetType(EntityReferenceExpression entityReferenceExpression, T // Or add predicate for matching that particular type discriminator value // All hierarchies have discriminator property if (TryBindMember( - entityReferenceExpression, - MemberIdentity.Create(entityType.GetDiscriminatorPropertyName()), + structuralTypeReferenceExpression, + MemberIdentity.Create(structuralType.GetDiscriminatorPropertyName()), out var discriminatorMember, out _) && discriminatorMember is SqlExpression discriminatorColumn) @@ -259,17 +251,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? structuralTypeReferenceExpression) { - entityReferenceExpression = null; + structuralTypeReferenceExpression = null; if (expression is not MethodCallExpression methodCallExpression || methodCallExpression.Method != GetTypeMethodInfo) { return false; } - entityReferenceExpression = Visit(methodCallExpression.Object) as EntityReferenceExpression; - return entityReferenceExpression != null; + structuralTypeReferenceExpression = Visit(methodCallExpression.Object) as StructuralTypeReferenceExpression; + return structuralTypeReferenceExpression != null; } static bool IsTypeConstant(Expression expression, [NotNullWhen(true)] out Type? type) @@ -340,8 +332,10 @@ protected override Expression VisitExtension(Expression extensionExpression) { switch (extensionExpression) { - case EntityProjectionExpression: - case EntityReferenceExpression: + case StructuralTypeProjectionExpression: + case StructuralTypeReferenceExpression: + case ObjectArrayAccessExpression: + case CollectionResultExpression: case SqlExpression: return extensionExpression; @@ -349,7 +343,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 +387,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 @@ -494,6 +488,19 @@ protected override Expression VisitMember(MemberExpression memberExpression) { var innerExpression = Visit(memberExpression.Expression); + if (innerExpression != null && memberExpression.Member.DeclaringType?.IsNullableValueType() == true) + { + if (memberExpression.Member.Name == nameof(Nullable<>.HasValue)) + { + return Visit(Expression.NotEqual(memberExpression.Expression!, Expression.Constant(null, memberExpression.Member.DeclaringType))); + } + + if (memberExpression.Member.Name == nameof(Nullable<>.Value)) + { + return Visit(memberExpression.Expression)!; + } + } + return TryBindMember(innerExpression, MemberIdentity.Create(memberExpression.Member), out var expression, out _) ? expression : (TranslationFailed(memberExpression.Expression, innerExpression, out var sqlInnerExpression) @@ -545,7 +552,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 +586,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 +810,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)) @@ -854,9 +861,9 @@ protected override Expression VisitTypeBinary(TypeBinaryExpression typeBinaryExp var innerExpression = Visit(typeBinaryExpression.Expression); if (typeBinaryExpression.NodeType == ExpressionType.TypeIs - && innerExpression is EntityReferenceExpression entityReferenceExpression) + && innerExpression is StructuralTypeReferenceExpression structuralTypeReferenceExpression + && structuralTypeReferenceExpression.StructuralType is IEntityType entityType) { - var entityType = entityReferenceExpression.EntityType; if (entityType.GetAllBaseTypesInclusive().Any(et => et.ClrType == typeBinaryExpression.TypeOperand)) { return sqlExpressionFactory.Constant(true); @@ -865,7 +872,7 @@ protected override Expression VisitTypeBinary(TypeBinaryExpression typeBinaryExp var derivedType = entityType.GetDerivedTypes().SingleOrDefault(et => et.ClrType == typeBinaryExpression.TypeOperand); if (derivedType != null && TryBindMember( - entityReferenceExpression, + structuralTypeReferenceExpression, MemberIdentity.Create(entityType.GetDiscriminatorPropertyName()), out var discriminatorMember, out _) @@ -901,7 +908,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,7 +919,7 @@ public virtual bool TryBindMember( { case { Parameter: { } shaper }: var valueBufferExpression = Visit(shaper.ValueBufferExpression); - var entityProjection = (EntityProjectionExpression)valueBufferExpression; + var entityProjection = (StructuralTypeProjectionExpression)valueBufferExpression; expression = member switch { @@ -941,25 +948,23 @@ public virtual bool TryBindMember( AddTranslationErrorDetails( CoreStrings.QueryUnableToTranslateMember( member.Name, - typeReference.EntityType.DisplayName())); + typeReference.StructuralType.DisplayName())); return false; } Check.DebugAssert(property is not null, "Property cannot be null if binding result was non-null"); - switch (expression) + if (wrapResultExpressionInReferenceExpression) { - case StructuralTypeShaperExpression shaper when wrapResultExpressionInReferenceExpression: - expression = new EntityReferenceExpression(shaper); - return true; - // case ObjectArrayAccessExpression objectArrayProjectionExpression: - // expression = objectArrayProjectionExpression; - // return true; - default: - return true; + switch (expression) + { + case StructuralTypeShaperExpression shaper: + expression = new StructuralTypeReferenceExpression(shaper); + return true; + } } - // return true; + return true; } private static Expression TryRemoveImplicitConvert(Expression expression) @@ -991,207 +996,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 +1040,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 +1079,9 @@ public Expression Convert(Type type) return this; } - return EntityType is { } entityType + return StructuralType is { } entityType && entityType.GetDerivedTypes().FirstOrDefault(et => et.ClrType == type) is { } derivedEntityType - ? new EntityReferenceExpression(this, derivedEntityType) + ? new StructuralTypeReferenceExpression(this, derivedEntityType) : QueryCompilationContext.NotTranslatedExpression; } diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/CollectionResultExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/CollectionResultExpression.cs new file mode 100644 index 00000000000..81a2b668a02 --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/CollectionResultExpression.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// 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. +/// +[DebuggerDisplay("{DebuggerDisplay(),nq}")] +public class CollectionResultExpression : 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 CollectionResultExpression(StructuralTypeShaperExpression parameter) + { + Parameter = parameter; + ComplexProperty = ((IComplexType)parameter.StructuralType).ComplexProperty; + } + + /// + /// 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 CollectionResultExpression(ShapedQueryExpression subquery) + { + Subquery = subquery; + ComplexProperty = ((IComplexType)((StructuralTypeShaperExpression)subquery.ShaperExpression).StructuralType).ComplexProperty; + } + + /// + /// 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 new StructuralTypeShaperExpression? Parameter { 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 + /// 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 ShapedQueryExpression? Subquery { 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 + /// 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 IComplexProperty ComplexProperty { 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 + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override Type Type + => ComplexProperty.ClrType; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override ExpressionType NodeType + => ExpressionType.Extension; + + private string DebuggerDisplay() + => this switch + { + { Parameter: not null } => Parameter.DebuggerDisplay(), + { Subquery: not null } => ExpressionPrinter.Print(Subquery!), + _ => throw new UnreachableException() + }; +} diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectAccessExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectAccessExpression.cs index 007394d7119..db210ebc9bd 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectAccessExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectAccessExpression.cs @@ -30,10 +30,33 @@ public ObjectAccessExpression(Expression @object, INavigation navigation) CosmosStrings.NavigationPropertyIsNotAnEmbeddedEntity( navigation.DeclaringEntityType.DisplayName(), navigation.Name)); - Navigation = navigation; + PropertyBase = navigation; + 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) + { + PropertyBase = 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; + => PropertyBase.ClrType; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -74,7 +97,7 @@ public override Type Type /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual INavigation Navigation { get; } + public virtual IPropertyBase PropertyBase { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -93,7 +116,9 @@ protected override Expression VisitChildren(ExpressionVisitor visitor) /// public virtual ObjectAccessExpression Update(Expression outerExpression) => outerExpression != Object - ? new ObjectAccessExpression(outerExpression, Navigation) + ? PropertyBase is INavigation navigation + ? new ObjectAccessExpression(outerExpression, navigation) + : new ObjectAccessExpression(outerExpression, (IComplexProperty)PropertyBase) : this; /// @@ -127,7 +152,7 @@ public override bool Equals(object? obj) && Equals(objectAccessExpression)); private bool Equals(ObjectAccessExpression objectAccessExpression) - => Navigation == objectAccessExpression.Navigation + => PropertyBase == objectAccessExpression.PropertyBase && Object.Equals(objectAccessExpression.Object); /// @@ -137,5 +162,5 @@ private bool Equals(ObjectAccessExpression objectAccessExpression) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override int GetHashCode() - => HashCode.Combine(Navigation, Object); + => HashCode.Combine(PropertyBase, Object); } diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayAccessExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectArrayAccessExpression.cs index 4eb61d119f3..737332e94e4 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; + PropertyBase = 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; + PropertyBase = 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 PropertyBase { 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) + ? PropertyBase is INavigation navigation + ? new ObjectArrayAccessExpression(accessExpression, navigation, innerProjection) + : new ObjectArrayAccessExpression(accessExpression, (IComplexProperty)PropertyBase, innerProjection) : this; /// diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectReferenceExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/ObjectReferenceExpression.cs index b2fee0d2605..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..b8069a7678e 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/SelectExpression.cs @@ -167,6 +167,14 @@ 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. + /// + 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,7 +288,7 @@ 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) + public int AddToProjection(StructuralTypeProjectionExpression entityProjection) => AddToProjection(entityProjection, null); private int AddToProjection(Expression expression, string? alias) @@ -323,6 +331,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 +521,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}") diff --git a/src/EFCore.Cosmos/Query/Internal/Expressions/EntityProjectionExpression.cs b/src/EFCore.Cosmos/Query/Internal/Expressions/StructuralTypeProjectionExpression.cs similarity index 74% rename from src/EFCore.Cosmos/Query/Internal/Expressions/EntityProjectionExpression.cs rename to src/EFCore.Cosmos/Query/Internal/Expressions/StructuralTypeProjectionExpression.cs index 11824d8a4b1..9e9bc4d9ed4 100644 --- a/src/EFCore.Cosmos/Query/Internal/Expressions/EntityProjectionExpression.cs +++ b/src/EFCore.Cosmos/Query/Internal/Expressions/StructuralTypeProjectionExpression.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.EntityFrameworkCore.Cosmos.Internal; +using Microsoft.EntityFrameworkCore.Metadata.Internal; // ReSharper disable once CheckNamespace namespace Microsoft.EntityFrameworkCore.Cosmos.Query.Internal; @@ -12,10 +13,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 +25,10 @@ public class EntityProjectionExpression : Expression, IPrintableExpression, IAcc /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public EntityProjectionExpression(Expression @object, IEntityType entityType) + public StructuralTypeProjectionExpression(Expression @object, ITypeBase structuralType) { Object = @object; - EntityType = entityType; + StructuralType = structuralType; PropertyName = (@object as IAccessExpression)?.PropertyName; } @@ -46,7 +48,7 @@ public sealed override ExpressionType NodeType /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public override Type Type - => EntityType.ClrType; + => StructuralType.ClrType; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -62,7 +64,7 @@ public override Type Type /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual IEntityType EntityType { get; } + public virtual ITypeBase StructuralType { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -90,7 +92,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 +102,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 +138,11 @@ 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.IsAssignableFrom(navigation.DeclaringEntityType) + && !navigation.DeclaringEntityType.IsAssignableFrom(StructuralType)) { throw new InvalidOperationException( - CosmosStrings.UnableToBindMemberToEntityProjection("navigation", navigation.Name, EntityType.DisplayName())); + CosmosStrings.UnableToBindMemberToEntityProjection("navigation", navigation.Name, StructuralType.DisplayName())); } if (!_navigationExpressionsMap.TryGetValue(navigation, out var expression)) @@ -153,7 +155,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 +172,41 @@ 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 StructuralTypeShaperExpression( + complexProperty.ComplexType, + new ObjectArrayAccessExpression(Object, complexProperty), + nullable: true)) + : 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 +235,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 +283,16 @@ 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 (!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,11 +313,11 @@ void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) public override bool Equals(object? obj) => obj != null && (ReferenceEquals(this, obj) - || obj is EntityProjectionExpression entityProjectionExpression + || obj is StructuralTypeProjectionExpression entityProjectionExpression && Equals(entityProjectionExpression)); - private bool Equals(EntityProjectionExpression entityProjectionExpression) - => Equals(EntityType, entityProjectionExpression.EntityType) + private bool Equals(StructuralTypeProjectionExpression entityProjectionExpression) + => Equals(StructuralType, entityProjectionExpression.StructuralType) && Object.Equals(entityProjectionExpression.Object); /// @@ -278,7 +327,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 +336,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..235d07fea10 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 entityProjectionExpression => VisitEntityProjection(entityProjectionExpression), 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 VisitEntityProjection(StructuralTypeProjectionExpression entityProjectionExpression); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to 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 c62d5eeccc1..fbfe3a30ce7 100644 --- a/test/EFCore.Cosmos.FunctionalTests/CosmosComplexTypesTrackingTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/CosmosComplexTypesTrackingTest.cs @@ -112,9 +112,13 @@ protected override Task TrackAndSaveTest(EntityState state, bool async, } 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..2c22522e2ee --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs @@ -0,0 +1,110 @@ +// 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 TestStoreFactory => 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 AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(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> InitializeAsync( + 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.InitializeAsync(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 74a4959debb..a24e255af4c 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); @@ -305,6 +295,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); @@ -328,6 +319,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 fc8e9d376d6..f8e08023da2 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 e336dd34498..72f8e94a72e 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 InitializeAsync( 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)); }