diff --git a/src/EFCore.Cosmos/Extensions/CosmosServiceCollectionExtensions.cs b/src/EFCore.Cosmos/Extensions/CosmosServiceCollectionExtensions.cs index 09018b0f2a5..93d0f28ec50 100644 --- a/src/EFCore.Cosmos/Extensions/CosmosServiceCollectionExtensions.cs +++ b/src/EFCore.Cosmos/Extensions/CosmosServiceCollectionExtensions.cs @@ -10,6 +10,7 @@ using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Microsoft.EntityFrameworkCore.Cosmos.ValueGeneration.Internal; using Microsoft.EntityFrameworkCore.Infrastructure.Internal; +using Microsoft.EntityFrameworkCore.Query.Internal; // ReSharper disable once CheckNamespace namespace Microsoft.Extensions.DependencyInjection; @@ -94,6 +95,7 @@ public static IServiceCollection AddCosmos( public static IServiceCollection AddEntityFrameworkCosmos(this IServiceCollection serviceCollection) { var builder = new EntityFrameworkServicesBuilder(serviceCollection) + .TryAdd() .TryAdd() .TryAdd>() .TryAdd() diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs index 72abbf75413..936b034cbe1 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosProjectionBindingExpressionVisitor.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Diagnostics.CodeAnalysis; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Cosmos.Internal; @@ -350,26 +351,73 @@ UnaryExpression unaryExpression throw new InvalidOperationException(CoreStrings.TranslationFailed(memberExpression.Print())); } - Expression NullSafeUpdate(Expression? expression) + Expression NullSafeUpdate(Expression? innerExpression) { - Expression updatedMemberExpression = memberExpression.Update( - expression != null ? MatchTypes(expression, memberExpression.Expression!.Type) : expression); + if (innerExpression is null) + { + return memberExpression.Update(innerExpression); + } + + var expressionValue = Expression.Parameter(innerExpression.Type); + var assignment = Expression.Assign(expressionValue, innerExpression); + + // Special case for when query is projecting 'nullable.Value' where 'nullable' is of type Nullable + // In this case we return default(T) when 'nullable' is null + if (innerExpression.Type.IsNullableType() + && !memberExpression.Type.IsNullableType() + && memberExpression.Expression is MemberExpression outerMember + && outerMember.Type.IsNullableValueType() + && memberExpression.Member.Name == nameof(Nullable<>.Value)) + { + // Use HasValue property instead of equality comparison + // to avoid issues with value types that don't define the == operator + var nullCheck = Expression.Not( + Expression.Property(expressionValue, nameof(Nullable<>.HasValue))); + var conditionalExpression = Expression.Condition( + nullCheck, + Expression.Default(memberExpression.Type), + Expression.Property(expressionValue, nameof(Nullable<>.Value))); + + return Expression.Block( + [expressionValue], + assignment, + conditionalExpression); + } + + Expression updatedMemberExpression = memberExpression.Update(MatchTypes(expressionValue, memberExpression.Expression!.Type)); - if (expression?.Type.IsNullableType() == true) + if (innerExpression.Type.IsNullableType()) { var nullableReturnType = memberExpression.Type.MakeNullable(); - if (!memberExpression.Type.IsNullableType()) + + if (!updatedMemberExpression.Type.IsNullableType()) { updatedMemberExpression = Expression.Convert(updatedMemberExpression, nullableReturnType); } + Expression nullCheck; + if (innerExpression.Type.IsNullableValueType()) + { + // For Nullable, use HasValue property instead of equality comparison + // to avoid issues with value types that don't define the == operator + nullCheck = Expression.Not( + Expression.Property(expressionValue, nameof(Nullable<>.HasValue))); + } + else + { + nullCheck = Expression.Equal(expressionValue, Expression.Default(innerExpression.Type)); + } + updatedMemberExpression = Expression.Condition( - Expression.Equal(expression, Expression.Default(expression.Type)), - Expression.Constant(null, nullableReturnType), + nullCheck, + Expression.Default(nullableReturnType), updatedMemberExpression); } - return updatedMemberExpression; + return Expression.Block( + [expressionValue], + assignment, + updatedMemberExpression); } } @@ -639,8 +687,21 @@ UnaryExpression unaryExpression updatedMethodCallExpression = Expression.Convert(updatedMethodCallExpression, nullableReturnType); } + Expression nullCheck; + if (@object.Type.IsNullableValueType()) + { + // For Nullable, use HasValue property instead of equality comparison + // to avoid issues with value types that don't define the == operator + nullCheck = Expression.Not( + Expression.Property(@object, nameof(Nullable<>.HasValue))); + } + else + { + nullCheck = Expression.Equal(@object, Expression.Constant(null, @object.Type)); + } + return Expression.Condition( - Expression.Equal(@object, Expression.Default(@object.Type)), + nullCheck, Expression.Constant(null, nullableReturnType), updatedMethodCallExpression); } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs index de333edebc8..aafd01a66e8 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.CosmosProjectionBindingRemovingExpressionVisitorBase.cs @@ -20,9 +20,9 @@ private abstract class CosmosProjectionBindingRemovingExpressionVisitorBase( bool trackQueryResults) : ExpressionVisitor { - private static readonly MethodInfo GetItemMethodInfo - = typeof(JObject).GetRuntimeProperties() - .Single(pi => pi.Name == "Item" && pi.GetIndexParameters()[0].ParameterType == typeof(string)) + public static readonly MethodInfo GetItemMethodInfo + = typeof(JToken).GetRuntimeProperties() + .Single(pi => pi.Name == "Item" && pi.GetIndexParameters()[0].ParameterType == typeof(object)) .GetMethod; private static readonly PropertyInfo JTokenTypePropertyInfo @@ -56,7 +56,7 @@ private readonly IDictionary _ordinalParameterBindings private List _pendingIncludes = []; - private static readonly MethodInfo ToObjectWithSerializerMethodInfo + public static readonly MethodInfo ToObjectWithSerializerMethodInfo = typeof(CosmosProjectionBindingRemovingExpressionVisitorBase) .GetRuntimeMethods().Single(mi => mi.Name == nameof(SafeToObjectWithSerializer)); @@ -72,18 +72,7 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) string storeName = null; // Values injected by JObjectInjectingExpressionVisitor - var projectionExpression = ((UnaryExpression)binaryExpression.Right).Operand; - - if (projectionExpression is UnaryExpression - { - NodeType: ExpressionType.Convert, - Operand: UnaryExpression operand - }) - { - // Unwrap EntityProjectionExpression when the root entity is not projected - // That is, this is handling the projection of a non-root entity type. - projectionExpression = operand.Operand; - } + var projectionExpression = binaryExpression.Right.UnwrapTypeConversion(out _); switch (projectionExpression) { @@ -154,7 +143,10 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) } break; - + case MethodCallExpression { Method.IsGenericMethod: true } jObjectMethodCallExpression + when jObjectMethodCallExpression.Method.GetGenericMethodDefinition() == ToObjectWithSerializerMethodInfo: + // JObject assignment already uses ToObjectWithSerializerMethodInfo. This can happen because code was generated for complex properties that already leverages JObject correctly. + return binaryExpression; default: throw new UnreachableException(); } @@ -166,19 +158,27 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) { var newExpression = (NewExpression)binaryExpression.Right; - EntityProjectionExpression entityProjectionExpression; - if (newExpression.Arguments[0] is ProjectionBindingExpression projectionBindingExpression) + if (newExpression.Arguments[0] is ComplexPropertyBindingExpression complexPropertyBindingExpression) { - var projection = GetProjection(projectionBindingExpression); - entityProjectionExpression = (EntityProjectionExpression)projection.Expression; + _materializationContextBindings[parameterExpression] = complexPropertyBindingExpression; + _projectionBindings[complexPropertyBindingExpression] = complexPropertyBindingExpression.JObjectParameter; } else { - var projection = ((UnaryExpression)((UnaryExpression)newExpression.Arguments[0]).Operand).Operand; - entityProjectionExpression = (EntityProjectionExpression)projection; - } + EntityProjectionExpression entityProjectionExpression; + if (newExpression.Arguments[0] is ProjectionBindingExpression projectionBindingExpression) + { + var projection = GetProjection(projectionBindingExpression); + entityProjectionExpression = (EntityProjectionExpression)projection.Expression; + } + else + { + var projection = ((UnaryExpression)((UnaryExpression)newExpression.Arguments[0]).Operand).Operand; + entityProjectionExpression = (EntityProjectionExpression)projection; + } - _materializationContextBindings[parameterExpression] = entityProjectionExpression.Object; + _materializationContextBindings[parameterExpression] = entityProjectionExpression.Object; + } var updatedExpression = New( newExpression.Constructor, @@ -595,7 +595,7 @@ private static Expression AddToCollectionNavigation( relatedEntity, Constant(true)); - private static readonly MethodInfo PopulateCollectionMethodInfo + public static readonly MethodInfo PopulateCollectionMethodInfo = typeof(CosmosProjectionBindingRemovingExpressionVisitorBase).GetTypeInfo() .GetDeclaredMethod(nameof(PopulateCollection)); diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs index f79539eb0b7..b727e42d12f 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs @@ -22,6 +22,8 @@ public partial class CosmosShapedQueryCompilingExpressionVisitor( IQuerySqlGeneratorFactory querySqlGeneratorFactory) : ShapedQueryCompilingExpressionVisitor(dependencies, cosmosQueryCompilationContext) { + private int _currentComplexIndex; + private ParameterExpression _parentJObject; private readonly Type _contextType = cosmosQueryCompilationContext.ContextType; private readonly bool _threadSafetyChecksEnabled = dependencies.CoreSingletonOptions.AreThreadSafetyChecksEnabled; @@ -39,6 +41,7 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery } var jTokenParameter = Parameter(typeof(JToken), "jToken"); + _parentJObject = jTokenParameter; var shaperBody = shapedQueryExpression.ShaperExpression; @@ -170,4 +173,124 @@ private static PartitionKey GeneratePartitionKey( return builder.Build(); } + + /// + /// 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 void AddStructuralTypeInitialization(StructuralTypeShaperExpression shaper, ParameterExpression instanceVariable, List variables, List expressions) + { + foreach (var complexProperty in shaper.StructuralType.GetComplexProperties()) + { + var member = MakeMemberAccess(instanceVariable, complexProperty.GetMemberInfo(true, true)); + expressions.Add(complexProperty.IsCollection + ? CreateComplexCollectionAssignmentBlock(member, complexProperty) + : CreateComplexPropertyAssignmentBlock(member, complexProperty)); + } + } + + private BlockExpression CreateComplexPropertyAssignmentBlock(MemberExpression memberExpression, IComplexProperty complexProperty) + { + var jObjectVariable = Parameter(typeof(JObject), "complexJObject" + ++_currentComplexIndex); + var assignJObjectVariable = Assign(jObjectVariable, + Call( + CosmosProjectionBindingRemovingExpressionVisitorBase.ToObjectWithSerializerMethodInfo.MakeGenericMethod(typeof(JObject)), + Call(_parentJObject, CosmosProjectionBindingRemovingExpressionVisitorBase.GetItemMethodInfo, + Constant(complexProperty.Name)))); + + var materializeExpression = CreateComplexTypeMaterializeExpression(complexProperty, jObjectVariable); + if (complexProperty.IsNullable) + { + materializeExpression = Condition(Equal(jObjectVariable, Constant(null)), + Default(complexProperty.ClrType.MakeNullable()), + ConvertChecked(materializeExpression, complexProperty.ClrType.MakeNullable())); + } + + return Block( + [jObjectVariable], + [ + assignJObjectVariable, + memberExpression.Assign(materializeExpression) + ] + ); + } + + private BlockExpression CreateComplexCollectionAssignmentBlock(MemberExpression memberExpression, IComplexProperty complexProperty) + { + var complexJArrayVariable = Variable( + typeof(JArray), + "complexJArray" + ++_currentComplexIndex); + + var assignJArrayVariable = Assign(complexJArrayVariable, + Call( + CosmosProjectionBindingRemovingExpressionVisitorBase.ToObjectWithSerializerMethodInfo.MakeGenericMethod(typeof(JArray)), + Call(_parentJObject, CosmosProjectionBindingRemovingExpressionVisitorBase.GetItemMethodInfo, + Constant(complexProperty.Name)))); + + var jObjectParameter = Parameter(typeof(JObject), "complexJObject" + _currentComplexIndex); + var materializeExpression = CreateComplexTypeMaterializeExpression(complexProperty, jObjectParameter); + + var select = Call( + EnumerableMethods.Select.MakeGenericMethod(typeof(JObject), complexProperty.ComplexType.ClrType), + Call( + EnumerableMethods.Cast.MakeGenericMethod(typeof(JObject)), + complexJArrayVariable), + Lambda(materializeExpression, jObjectParameter)); + + Expression populateExpression = + Call( + CosmosProjectionBindingRemovingExpressionVisitorBase.PopulateCollectionMethodInfo.MakeGenericMethod(complexProperty.ComplexType.ClrType, complexProperty.ClrType), + Constant(complexProperty.GetCollectionAccessor()), + select); + + if (complexProperty.IsNullable) + { + populateExpression = Condition(Equal(complexJArrayVariable, Constant(null)), + Default(complexProperty.ClrType.MakeNullable()), + ConvertChecked(populateExpression, complexProperty.ClrType.MakeNullable())); + } + + return Block( + [complexJArrayVariable], + [ + assignJArrayVariable, + memberExpression.Assign(populateExpression) + ] + ); + } + + private Expression CreateComplexTypeMaterializeExpression(IComplexProperty complexProperty, ParameterExpression jObjectParameter) + { + var tempValueBuffer = new ComplexPropertyBindingExpression(complexProperty, jObjectParameter); + var structuralTypeShaperExpression = new StructuralTypeShaperExpression( + complexProperty.ComplexType, + tempValueBuffer, + false); + + var oldParentJObject = _parentJObject; + _parentJObject = jObjectParameter; + var materializeExpression = InjectStructuralTypeMaterializers(structuralTypeShaperExpression); + _parentJObject = oldParentJObject; + + if (complexProperty.ComplexType.ClrType.IsNullableType()) + { + materializeExpression = Condition(Equal(jObjectParameter, Constant(null)), + Default(complexProperty.ComplexType.ClrType), + materializeExpression); + } + + return materializeExpression; + } + + private sealed class ComplexPropertyBindingExpression(IComplexProperty complexProperty, ParameterExpression jObjectParameter) : Expression + { + public override Type Type => typeof(ValueBuffer); + + public override ExpressionType NodeType => ExpressionType.Extension; + + public IComplexProperty ComplexProperty { get; } = complexProperty; + public ParameterExpression JObjectParameter { get; } = jObjectParameter; + } } diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosStructuralTypeMaterializerSource.cs b/src/EFCore.Cosmos/Query/Internal/CosmosStructuralTypeMaterializerSource.cs new file mode 100644 index 00000000000..3eed32b4e39 --- /dev/null +++ b/src/EFCore.Cosmos/Query/Internal/CosmosStructuralTypeMaterializerSource.cs @@ -0,0 +1,23 @@ +// 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.Internal; + +#pragma warning disable EF1001 // StructuralTypeMaterializerSource is pubternal + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public class CosmosStructuralTypeMaterializerSource(StructuralTypeMaterializerSourceDependencies dependencies) + : StructuralTypeMaterializerSource(dependencies) +{ + /// + /// Complex properties are not handled in the initial materialization expression, + /// so we can more easily generate the necessary nested materialization expressions later in CosmosShapedQueryCompilingExpressionVisitor. + /// + protected override bool ReadComplexTypeDirectly(IComplexType complexType) + => false; +} diff --git a/test/EFCore.Cosmos.FunctionalTests/CosmosComplexTypesTrackingTest.cs b/test/EFCore.Cosmos.FunctionalTests/CosmosComplexTypesTrackingTest.cs index 6362ece9712..cc82de50f03 100644 --- a/test/EFCore.Cosmos.FunctionalTests/CosmosComplexTypesTrackingTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/CosmosComplexTypesTrackingTest.cs @@ -21,11 +21,10 @@ public async Task Can_reorder_complex_collection_elements() var last = pub.Activities.Last(); await context.SaveChangesAsync(); - // TODO: Can be asserted after binding has been implemented. - //await using var assertContext = CreateContext(); - //var dbPub = await assertContext.Set().FirstAsync(x => x.Id == pub.Id); - //Assert.Equivalent(first, dbPub.Activities[0]); - //Assert.Equivalent(last, dbPub.Activities.Last()); + await using var assertContext = CreateContext(); + var dbPub = await assertContext.Set().FirstAsync(x => x.Id == pub.Id); + Assert.Equivalent(first, dbPub.Activities[0]); + Assert.Equivalent(last, dbPub.Activities.Last()); } [ConditionalFact] @@ -39,10 +38,9 @@ public async Task Can_change_complex_collection_element() pub.Activities[0].Name = "Changed123"; await context.SaveChangesAsync(); - // TODO: Can be asserted after binding has been implemented. - //await using var assertContext = CreateContext(); - //var dbPub = await assertContext.Set().FirstAsync(x => x.Id == pub.Id); - //Assert.Equivalent("Changed123", dbPub.Activities[0].Name); + await using var assertContext = CreateContext(); + var dbPub = await assertContext.Set().FirstAsync(x => x.Id == pub.Id); + Assert.Equivalent("Changed123", dbPub.Activities[0].Name); } [ConditionalFact] @@ -56,11 +54,10 @@ public async Task Can_add_complex_collection_element() pub.Activities.Add(new ActivityWithCollection { Name = "NewActivity" }); await context.SaveChangesAsync(); - // TODO: Can be asserted after binding has been implemented. - //await using var assertContext = CreateContext(); - //var dbPub = await assertContext.Set().FirstAsync(x => x.Id == pub.Id); - //Assert.Equivalent("NewActivity", dbPub.Activities.Last().Name); - //Assert.Equivalent(pub.Activities.Count, dbPub.Activities.Count); + await using var assertContext = CreateContext(); + var dbPub = await assertContext.Set().FirstAsync(x => x.Id == pub.Id); + Assert.Equivalent("NewActivity", dbPub.Activities.Last().Name); + Assert.Equivalent(pub.Activities.Count, dbPub.Activities.Count); } [ConditionalFact] diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCosmosFixture.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCosmosFixture.cs new file mode 100644 index 00000000000..dbfbd64ea0a --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesCosmosFixture.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.EntityFrameworkCore.Query.Associations.ComplexProperties; + +public class ComplexPropertiesCosmosFixture : ComplexPropertiesFixtureBase +{ + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected override ITestStoreFactory TestStoreFactory + => CosmosTestStoreFactory.Instance; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(builder) + .ConfigureWarnings(w => w.Ignore(CosmosEventId.NoPartitionKeyDefined).Ignore(CoreEventId.MappedEntityTypeIgnoredWarning)); + + public Task NoSyncTest(bool async, Func testCode) + => CosmosTestHelpers.Instance.NoSyncTest(async, testCode); + + public void NoSyncTest(Action testCode) + => CosmosTestHelpers.Instance.NoSyncTest(testCode); + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.Ignore(); + + modelBuilder.Entity() + .ToContainer("RootEntities") + .HasNoDiscriminator(); + + modelBuilder.Entity() + .ToContainer("ValueRootEntities") + .HasNoDiscriminator(); + } +} diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesProjectionCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesProjectionCosmosTest.cs new file mode 100644 index 00000000000..d9131e861f6 --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/Query/Associations/ComplexProperties/ComplexPropertiesProjectionCosmosTest.cs @@ -0,0 +1,434 @@ +// 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 +{ + public ComplexPropertiesProjectionCosmosTest(ComplexPropertiesCosmosFixture fixture, ITestOutputHelper outputHelper) : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + Fixture.TestSqlLoggerFactory.SetTestOutputHelper(outputHelper); + } + + public override async Task Select_root(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_root(queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE c +FROM root c +"""); + } + + #region Scalar properties + + [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); + + AssertSql( + """ +SELECT VALUE c["RequiredAssociate"]["String"] +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 + // result to be filtered out entirely. + await AssertQuery( + ss => ss.Set().Select(x => x.OptionalAssociate!.String), + ss => ss.Set().Where(x => x.OptionalAssociate != null).Select(x => x.OptionalAssociate!.String), + queryTrackingBehavior: queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE c["OptionalAssociate"]["String"] +FROM root c +"""); + } + + [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 + // result to be filtered out entirely. + await AssertQuery( + ss => ss.Set().Select(x => x.OptionalAssociate!.Int), + ss => ss.Set().Where(x => x.OptionalAssociate != null).Select(x => x.OptionalAssociate!.Int), + queryTrackingBehavior: queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE c["OptionalAssociate"]["Int"] +FROM root c +"""); + } + + [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 + // result to be filtered out entirely. + await AssertQuery( + ss => ss.Set().Select(x => (int?)x.OptionalAssociate!.Int), + ss => ss.Set().Where(x => x.OptionalAssociate != null).Select(x => (int?)x.OptionalAssociate!.Int), + queryTrackingBehavior: queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE c["OptionalAssociate"]["Int"] +FROM root c +"""); + } + + #endregion Scalar properties + + #region Structural properties + + public override async Task Select_associate(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_associate(queryTrackingBehavior); + + if (queryTrackingBehavior is QueryTrackingBehavior.TrackAll) + { + throw SkipException.ForSkip("Complex type tracking not supported."); + } + + AssertSql( + """ +SELECT VALUE c +FROM root c +"""); + } + + public override async Task Select_optional_associate(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_optional_associate(queryTrackingBehavior); + + if (queryTrackingBehavior is QueryTrackingBehavior.TrackAll) + { + throw SkipException.ForSkip("Complex type tracking not supported."); + } + + AssertSql( + """ +SELECT VALUE c +FROM root c +"""); + } + + public override async Task Select_required_nested_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_required_nested_on_required_associate(queryTrackingBehavior); + + if (queryTrackingBehavior is QueryTrackingBehavior.TrackAll) + { + throw SkipException.ForSkip("Complex type tracking not supported."); + } + + AssertSql( + """ +SELECT VALUE c +FROM root c +"""); + } + + public override async Task Select_optional_nested_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_optional_nested_on_required_associate(queryTrackingBehavior); + + if (queryTrackingBehavior is QueryTrackingBehavior.TrackAll) + { + throw SkipException.ForSkip("Complex type tracking not supported."); + } + + AssertSql( + """ +SELECT VALUE c +FROM root c +"""); + } + + public override async Task Select_required_nested_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) + { + if (queryTrackingBehavior is QueryTrackingBehavior.TrackAll) + { + throw SkipException.ForSkip("Complex type tracking not supported."); + } + + await base.Select_required_nested_on_optional_associate(queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE c +FROM root c +"""); + } + + public override async Task Select_optional_nested_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) + { + if (queryTrackingBehavior is QueryTrackingBehavior.TrackAll) + { + throw SkipException.ForSkip("Complex type tracking not supported."); + } + + await base.Select_optional_nested_on_optional_associate(queryTrackingBehavior); + + if (queryTrackingBehavior is not QueryTrackingBehavior.TrackAll) + { + AssertSql( + """ +SELECT VALUE c +FROM root c +"""); + } + } + + public override Task Select_required_associate_via_optional_navigation(QueryTrackingBehavior queryTrackingBehavior) + // We don't support (inter-document) navigations with Cosmos. + => Assert.ThrowsAsync(() => base.Select_required_associate_via_optional_navigation(queryTrackingBehavior)); + + public override async Task Select_unmapped_associate_scalar_property(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_unmapped_associate_scalar_property(queryTrackingBehavior); + + if (queryTrackingBehavior is QueryTrackingBehavior.TrackAll) + { + throw SkipException.ForSkip("Complex type tracking not supported."); + } + + AssertSql( + """ +SELECT VALUE c +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); + + AssertSql( + """ +SELECT VALUE c["RequiredAssociate"]["Int"] +FROM root c +"""); + } + + #endregion Structural properties + + #region Structural collection properties + + public override async Task Select_associate_collection(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_associate_collection(queryTrackingBehavior); + + if (queryTrackingBehavior is QueryTrackingBehavior.TrackAll) + { + throw SkipException.ForSkip("Complex type tracking not supported."); + } + + AssertSql( + """ +SELECT VALUE c +FROM root c +ORDER BY c["Id"] +"""); + } + + public override async Task Select_nested_collection_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) + { + if (queryTrackingBehavior is QueryTrackingBehavior.TrackAll) + { + throw SkipException.ForSkip("Complex type tracking not supported."); + } + + await base.Select_nested_collection_on_required_associate(queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE c +FROM root c +ORDER BY c["Id"] +"""); + } + + public override async Task Select_nested_collection_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) + { + if (queryTrackingBehavior is QueryTrackingBehavior.TrackAll) + { + throw SkipException.ForSkip("Complex type tracking not supported."); + } + + await base.Select_nested_collection_on_optional_associate(queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE c +FROM root c +ORDER BY c["Id"] +"""); + } + + [ConditionalTheory(Skip = "TODO: Query projection")] + public override async Task SelectMany_associate_collection(QueryTrackingBehavior queryTrackingBehavior) + { + if (queryTrackingBehavior is QueryTrackingBehavior.TrackAll) + { + throw SkipException.ForSkip("Complex type tracking not supported."); + } + + await base.SelectMany_associate_collection(queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE a +FROM root c +JOIN a IN c["AssociateCollection"] +"""); + } + + [ConditionalTheory(Skip = "TODO: Query projection")] + public override async Task SelectMany_nested_collection_on_required_associate(QueryTrackingBehavior queryTrackingBehavior) + { + if (queryTrackingBehavior is QueryTrackingBehavior.TrackAll) + { + throw SkipException.ForSkip("Complex type tracking not supported."); + } + + await base.SelectMany_nested_collection_on_required_associate(queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE n +FROM root c +JOIN n IN c["RequiredAssociate"]["NestedCollection"] +"""); + } + + [ConditionalTheory(Skip = "TODO: Query projection")] + public override async Task SelectMany_nested_collection_on_optional_associate(QueryTrackingBehavior queryTrackingBehavior) + { + if (queryTrackingBehavior is QueryTrackingBehavior.TrackAll) + { + throw SkipException.ForSkip("Complex type tracking not supported."); + } + + await base.SelectMany_nested_collection_on_optional_associate(queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE n +FROM root c +JOIN n IN c["OptionalAssociate"]["NestedCollection"] +"""); + } + + #endregion Structural collection properties + + #region Multiple + + public override async Task Select_root_duplicated(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_root_duplicated(queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE c +FROM root c +"""); + } + + #endregion Multiple + + #region Subquery + + public override async Task Select_subquery_required_related_FirstOrDefault(QueryTrackingBehavior queryTrackingBehavior) + { + if (queryTrackingBehavior is QueryTrackingBehavior.TrackAll) + { + throw SkipException.ForSkip("Complex type tracking not supported."); + } + + await AssertTranslationFailed(() => base.Select_subquery_required_related_FirstOrDefault(queryTrackingBehavior)); + } + + public override async Task Select_subquery_optional_related_FirstOrDefault(QueryTrackingBehavior queryTrackingBehavior) + { + if (queryTrackingBehavior is QueryTrackingBehavior.TrackAll) + { + throw SkipException.ForSkip("Complex type tracking not supported."); + } + + await AssertTranslationFailed(() => base.Select_subquery_optional_related_FirstOrDefault(queryTrackingBehavior)); + } + + #endregion Subquery + + #region Value types + public override async Task Select_root_with_value_types(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_root_with_value_types(queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE c +FROM root c +"""); + } + + public override async Task Select_non_nullable_value_type(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_non_nullable_value_type(queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE c +FROM root c +ORDER BY c["Id"] +"""); + } + + public override async Task Select_nullable_value_type(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_nullable_value_type(queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE c +FROM root c +ORDER BY c["Id"] +"""); + } + + public override async Task Select_nullable_value_type_with_Value(QueryTrackingBehavior queryTrackingBehavior) + { + await base.Select_nullable_value_type_with_Value(queryTrackingBehavior); + + AssertSql( + """ +SELECT VALUE c +FROM root c +ORDER BY c["Id"] +"""); + } + + #endregion Value types + + + [ConditionalFact] + public virtual void Check_all_tests_overridden() + => TestHelpers.AssertAllMethodsOverridden(GetType()); + + private void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); +}