diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs deleted file mode 100644 index 8fbfd4e2842..00000000000 --- a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs +++ /dev/null @@ -1,446 +0,0 @@ -// 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; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class AdHocComplexTypeQueryCosmosTest(NonSharedFixture fixture) : AdHocComplexTypeQueryTestBase(fixture) -{ - protected override ITestStoreFactory NonSharedTestStoreFactory - => CosmosTestStoreFactory.Instance; - - // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/288 (Complex-type equality comparisons return no results) - public override async Task Complex_type_equals_parameter_with_nested_types_with_property_of_same_name() - { - CosmosTestEnvironment.SkipOnLinuxEmulator(); - - 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(); - - AssertSql( - """ -SELECT VALUE c["Complex"] -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 -""", - // - """ -SELECT VALUE c -FROM root c -"""); - } - - 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 Non_optional_complex_type_with_all_nullable_properties_via_left_join() - { - Assert.Equal( - CosmosStrings.UpdateConflict("1"), - (await Assert.ThrowsAsync( - base.Non_optional_complex_type_with_all_nullable_properties_via_left_join)).Message); - - AssertSql(); - } - - 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 -"""); - } - - public override async Task Update_entity_with_nullable_complex_type_and_discriminator_does_not_throw() - { - await base.Update_entity_with_nullable_complex_type_and_discriminator_does_not_throw(); - - AssertSql( - """ -SELECT VALUE c -FROM root c -OFFSET 0 LIMIT 2 -""", - // - """ -SELECT VALUE c -FROM root c -OFFSET 0 LIMIT 2 -"""); - } - - public override async Task Nullable_complex_type_with_discriminator_null_to_non_null_roundtrip() - { - await base.Nullable_complex_type_with_discriminator_null_to_non_null_roundtrip(); - - AssertSql( - """ -SELECT VALUE c -FROM root c -OFFSET 0 LIMIT 2 -""", - // - """ -SELECT VALUE c -FROM root c -OFFSET 0 LIMIT 2 -"""); - } - - public override async Task Nullable_complex_type_with_discriminator_non_null_to_null_roundtrip() - { - await base.Nullable_complex_type_with_discriminator_non_null_to_null_roundtrip(); - - AssertSql( - """ -SELECT VALUE c -FROM root c -OFFSET 0 LIMIT 2 -""", - // - """ -SELECT VALUE c -FROM root c -OFFSET 0 LIMIT 2 -"""); - } - - public override async Task Nullable_complex_type_with_discriminator_update_non_null_entity_roundtrip() - { - await base.Nullable_complex_type_with_discriminator_update_non_null_entity_roundtrip(); - - AssertSql( - """ -SELECT VALUE c -FROM root c -OFFSET 0 LIMIT 2 -""", - // - """ -SELECT VALUE c -FROM root c -OFFSET 0 LIMIT 2 -"""); - } - - public override async Task Nullable_complex_type_with_discriminator_set_to_different_value() - => await base.Nullable_complex_type_with_discriminator_set_to_different_value(); - - public override async Task Nullable_complex_type_with_discriminator_set_to_null() - { - // On Cosmos, setting the discriminator shadow property to null doesn't affect materialization - // because the complex property's data is still present in the JSON document. - var contextFactory = await InitializeNonSharedTest(); - - Guid entityId; - await using (var context = contextFactory.CreateDbContext()) - { - var entity = new Context38119.EntityType - { - Id = Guid.NewGuid(), - Prop = new Context38119.OptionalComplexProperty { OptionalValue = true } - }; - context.Add(entity); - entityId = entity.Id; - - var discriminatorEntry = context.Entry(entity).ComplexProperty(e => e.Prop).Property("Discriminator"); - Assert.Equal("OptionalComplexProperty", discriminatorEntry.CurrentValue); - discriminatorEntry.CurrentValue = null; - await context.SaveChangesAsync(); - } - - await using (var context = contextFactory.CreateDbContext()) - { - var entity = await context.Set().SingleAsync(e => e.Id == entityId); - Assert.NotNull(entity.Prop); - Assert.True(entity.Prop.OptionalValue); - } - } - - public override async Task Nested_nullable_complex_type_with_discriminator_null_to_non_null_roundtrip() - { - await base.Nested_nullable_complex_type_with_discriminator_null_to_non_null_roundtrip(); - - AssertSql( - """ -SELECT VALUE c -FROM root c -OFFSET 0 LIMIT 2 -""", - // - """ -SELECT VALUE c -FROM root c -OFFSET 0 LIMIT 2 -"""); - } - - public override async Task Can_query_by_complex_type_property_with_index() - { - await base.Can_query_by_complex_type_property_with_index(); - - AssertSql( - """ -SELECT VALUE c -FROM root c -WHERE (c["Address"]["City"] = "Seattle") -OFFSET 0 LIMIT 2 -"""); - } - - public override async Task Can_update_entity_with_index_on_complex_type_property() - { - await base.Can_update_entity_with_index_on_complex_type_property(); - - AssertSql( - """ -SELECT VALUE c -FROM root c -OFFSET 0 LIMIT 2 -""", - // - """ -SELECT VALUE c -FROM root c -OFFSET 0 LIMIT 2 -"""); - } - - public override async Task Can_delete_entity_with_index_on_complex_type_property() - { - await base.Can_delete_entity_with_index_on_complex_type_property(); - - AssertSql( - """ -SELECT VALUE c -FROM root c -OFFSET 0 LIMIT 2 -""", - // - """ -SELECT VALUE COUNT(1) -FROM root c -"""); - } - - public override async Task Can_query_by_alternate_key_on_complex_type_property() - { - await base.Can_query_by_alternate_key_on_complex_type_property(); - - AssertSql( - """ -SELECT VALUE c -FROM root c -WHERE (c["Address"]["City"] = "Redmond") -OFFSET 0 LIMIT 2 -"""); - } - - public override async Task Can_save_batch_swapping_alternate_key_values_on_complex_type_property() - { - // Unlike relational providers, Cosmos can't ORDER BY the primary-key path 'Id.Id': the model defines an - // explicit index (on Address.PostalCode), which disables automatic indexing and therefore excludes the - // 'Id.Id' path from the index. Order client-side instead - the test's intent (reading the alternate-key - // values while building batch edges) is unaffected. - var contextFactory = await InitializeNonSharedTest( - seed: context => - { - context.AddRange( - new Context31246.Person - { - Id = new Context31246.StronglyTypedId(1), - Address = new Context31246.Address { City = "Seattle", PostalCode = "98101" } - }, - new Context31246.Person - { - Id = new Context31246.StronglyTypedId(2), - Address = new Context31246.Address { City = "Redmond", PostalCode = "98052" } - }); - return context.SaveChangesAsync(); - }); - - await using (var context = contextFactory.CreateDbContext()) - { - var people = (await context.Set().ToListAsync()).OrderBy(p => p.Id.Id).ToList(); - people[0].Address.PostalCode = "98103"; - people[1].Address.PostalCode = "98054"; - await context.SaveChangesAsync(); - } - - await using (var context = contextFactory.CreateDbContext()) - { - var people = (await context.Set().ToListAsync()).OrderBy(p => p.Id.Id).ToList(); - Assert.Equal("98103", people[0].Address.PostalCode); - Assert.Equal("98054", people[1].Address.PostalCode); - } - } - - [Fact] - public virtual async Task Can_use_complex_type_key_with_discriminator_in_json_id() - { - var contextFactory = await InitializeNonSharedTest( - seed: context => - { - context.AddRange( - new ComplexKeyDiscriminatorContext.Customer { Id = new ComplexKeyDiscriminatorContext.CustomerId(1), Name = "Alice" }, - new ComplexKeyDiscriminatorContext.Customer { Id = new ComplexKeyDiscriminatorContext.CustomerId(2), Name = "Bob" }); - return context.SaveChangesAsync(); - }); - - await using var context = contextFactory.CreateDbContext(); - - var customer = await context.Set().SingleAsync(c => c.Id.Value == 1); - Assert.Equal(new ComplexKeyDiscriminatorContext.CustomerId(1), customer.Id); - Assert.Equal("Alice", customer.Name); - - AssertSql( - """ -SELECT VALUE c -FROM root c -WHERE (c["Id"]["Value"] = 1) -OFFSET 0 LIMIT 2 -"""); - } - - [Fact] - public virtual async Task Can_use_nested_complex_type_key_with_discriminator_in_json_id() - { - var contextFactory = await InitializeNonSharedTest( - seed: context => - { - context.AddRange( - new NestedComplexKeyDiscriminatorContext.Order - { - Key = new NestedComplexKeyDiscriminatorContext.OrderKey - { - Inner = new NestedComplexKeyDiscriminatorContext.InnerKey { Value = 1 } - }, - Description = "First" - }, - new NestedComplexKeyDiscriminatorContext.Order - { - Key = new NestedComplexKeyDiscriminatorContext.OrderKey - { - Inner = new NestedComplexKeyDiscriminatorContext.InnerKey { Value = 2 } - }, - Description = "Second" - }); - return context.SaveChangesAsync(); - }); - - await using var context = contextFactory.CreateDbContext(); - - var order = await context.Set().SingleAsync(o => o.Key.Inner.Value == 2); - Assert.Equal(2, order.Key.Inner.Value); - Assert.Equal("Second", order.Description); - - AssertSql( - """ -SELECT VALUE c -FROM root c -WHERE (c["Key"]["Inner"]["Value"] = 2) -OFFSET 0 LIMIT 2 -"""); - } - - protected class ComplexKeyDiscriminatorContext(DbContextOptions options) : DbContext(options) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - => modelBuilder.Entity(b => - { - b.ComplexProperty(e => e.Id); - b.HasKey(e => e.Id.Value); - b.Property(e => e.Id.Value).ValueGeneratedNever(); - b.HasDiscriminatorInJsonId(); - }); - - public readonly record struct CustomerId(int Value); - - public class Customer - { - public CustomerId Id { get; set; } - public string Name { get; set; } = null!; - } - } - - protected class NestedComplexKeyDiscriminatorContext(DbContextOptions options) : DbContext(options) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - => modelBuilder.Entity(b => - { - b.ComplexProperty(e => e.Key, kb => kb.ComplexProperty(k => k.Inner)); - b.HasKey(e => e.Key.Inner.Value); - b.Property(e => e.Key.Inner.Value).ValueGeneratedNever(); - b.HasDiscriminatorInJsonId(); - }); - - public class Order - { - public OrderKey Key { get; set; } = null!; - public string Description { get; set; } = null!; - } - - public class OrderKey - { - public InnerKey Inner { get; set; } = null!; - } - - public class InnerKey - { - public int Value { get; set; } - } - } - - protected override DbContextOptionsBuilder AddNonSharedOptions(DbContextOptionsBuilder builder) - => base.AddNonSharedOptions(builder) - .ConfigureWarnings(w => w.Ignore(CosmosEventId.NoPartitionKeyDefined)); - - [Fact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); - - protected TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ListLoggerFactory; - - private void AssertSql(params string[] expected) - => TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocJsonQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocJsonQueryCosmosTest.cs deleted file mode 100644 index 3dd063a8cea..00000000000 --- a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocJsonQueryCosmosTest.cs +++ /dev/null @@ -1,4108 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Text.Json; -using System.Text.Json.Nodes; -using Microsoft.EntityFrameworkCore.Cosmos.Internal; -using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class AdHocJsonQueryCosmosTest(NonSharedFixture fixture) : AdHocJsonQueryTestBase(fixture) -{ - #region 21006 - - public override Task Project_root_with_missing_scalars(bool async) - => CosmosTestHelpers.Instance.NoSyncTest(async, async async => - { - await base.Project_root_with_missing_scalars(async); - - AssertSql( - """ -SELECT VALUE c -FROM root c -WHERE (c["Id"] < 4) -"""); - }); - - [Theory(Skip = "issue #35702")] - public override Task Project_top_level_json_entity_with_missing_scalars(bool async) - => CosmosTestHelpers.Instance.NoSyncTest(async, async async => - { - await base.Project_top_level_json_entity_with_missing_scalars(async); - - AssertSql(); - }); - - public override async Task Project_nested_json_entity_with_missing_scalars(bool async) - { - // Throws sync not supported exception for sync - if (async) - { - await AssertTranslationFailed(() => base.Project_nested_json_entity_with_missing_scalars(async)); - - AssertSql(); - } - } - - public override Task Project_top_level_entity_with_null_value_required_scalars(bool async) - => CosmosTestHelpers.Instance.NoSyncTest(async, async async => - { - await base.Project_top_level_entity_with_null_value_required_scalars(async); - - AssertSql( - """ -SELECT c["Id"], c["RequiredReference"] -FROM root c -WHERE (c["Id"] = 4) -"""); - }); - - [Fact] - public virtual async Task Project_entity_with_null_value_required_scalars() - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - onModelCreating: OnModelCreating21006, - seed: Seed21006); - - await using var context = contextFactory.CreateDbContext(); - - var result = await context.Set().Where(x => x.Id == 4).AsNoTracking().ToListAsync(); - - var nullScalars = result.Single(); - - Assert.Equal(default, nullScalars.RequiredReference.Number); - } - - [Fact] - public virtual async Task Project_null_value_required_scalar() - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - onModelCreating: OnModelCreating21006, - seed: Seed21006); - - await using var context = contextFactory.CreateDbContext(); - - // Same as in 10.0 - var ex = await Assert.ThrowsAsync(() => context.Set().Where(x => x.Id == 4).Select(x => x.RequiredReference.Number).ToListAsync()); - Assert.Equal("Nullable object must have a value.", ex.Message); - } - - public override Task Project_root_entity_with_missing_required_navigation(bool async) - => CosmosTestHelpers.Instance.NoSyncTest(async, async async => - { - await base.Project_root_entity_with_missing_required_navigation(async); - - AssertSql( - """ -ReadItem(?, ?) -"""); - }); - - public override async Task Project_missing_required_navigation(bool async) - { - // Throws sync not supported exception for sync - if (async) - { - // Cosmos will filter out undefined result - var ex = await Assert.ThrowsAsync(() => base.Project_missing_required_navigation(async)); - Assert.Equal("Sequence contains no elements", ex.Message); - - AssertSql( - """ -SELECT VALUE c["RequiredReference"]["NestedRequiredReference"] -FROM root c -WHERE (c["Id"] = 5) -"""); - } - } - - public override Task Project_root_entity_with_null_required_navigation(bool async) - => CosmosTestHelpers.Instance.NoSyncTest(async, async async => - { - await base.Project_root_entity_with_null_required_navigation(async); - - AssertSql( - """ -ReadItem(?, ?) -"""); - }); - - public override Task Project_null_required_navigation(bool async) - => CosmosTestHelpers.Instance.NoSyncTest(async, async async => - { - await base.Project_null_required_navigation(async); - - AssertSql( - """ -SELECT VALUE c["RequiredReference"] -FROM root c -WHERE (c["Id"] = 6) -"""); - }); - - public override async Task Project_missing_required_scalar(bool async) - { - // Throws sync not supported exception for sync - if (async) - { - // https://github.com/dotnet/efcore/issues/38298#issuecomment-4726236589 - var exception = await Assert.ThrowsAsync(() => base.Project_missing_required_scalar(async)); - - Assert.Equal(CosmosStrings.ProjectionUndefined, exception.Message); - - AssertSql( - """ -SELECT c["Id"], c["RequiredReference"]["Number"] -FROM root c -WHERE (c["Id"] = 2) -"""); - } - } - - public override Task Project_null_required_scalar(bool async) - => CosmosTestHelpers.Instance.NoSyncTest(async, async async => - { - await base.Project_null_required_scalar(async); - - AssertSql( - """ -SELECT c["Id"], c["RequiredReference"]["Number"] -FROM root c -WHERE (c["Id"] = 4) -"""); - }); - - protected override void OnModelCreating21006(ModelBuilder modelBuilder) - { - base.OnModelCreating21006(modelBuilder); - - modelBuilder.Entity().ToContainer("Entities"); - } - - protected override async Task Seed21006(Context21006 context) - { - await base.Seed21006(context); - - var wrapper = (CosmosClientWrapper)context.GetService(); - var singletonWrapper = context.GetService(); - var entitiesContainer = singletonWrapper.Client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var missingTopLevel = - """ -{ - "Id": 2, - "$type": "Entity", - "Name": "e2", - "id": "2", - "Collection": - [ - { - "Text": "e2 c1", - "NestedCollection": [ - { - "DoB": "2000-01-01T00:00:00", - "Text": "e2 c1 c1" - }, - { - "DoB": "2000-01-01T00:00:00", - "Text": "e2 c1 c2" - } - ], - "NestedOptionalReference": { - "DoB": "2000-01-01T00:00:00", - "Text": "e2 c1 nor" - }, - "NestedRequiredReference": { - "DoB": "2000-01-01T00:00:00", - "Text": "e2 c1 nrr" - } - }, - { - "Text": "e2 c2", - "NestedCollection": [ - { - "DoB": "2000-01-01T00:00:00", - "Text": "e2 c2 c1" - }, - { - "DoB": "2000-01-01T00:00:00", - "Text": "e2 c2 c2" - } - ], - "NestedOptionalReference": { - "DoB": "2000-01-01T00:00:00", - "Text": "e2 c2 nor" - }, - "NestedRequiredReference": { - "DoB": "2000-01-01T00:00:00", - "Text": "e2 c2 nrr" - } - } - ], - "OptionalReference": { - "Text": "e2 or", - "NestedCollection": - [ - { - "DoB": "2000-01-01T00:00:00", - "Text": "e2 or c1" - }, - { - "DoB": "2000-01-01T00:00:00", - "Text": "e2 or c2" - } - ], - "NestedOptionalReference": { - "DoB": "2000-01-01T00:00:00", - "Text": "e2 or nor" - }, - "NestedRequiredReference": { - "DoB": "2000-01-01T00:00:00", - "Text": "e2 or nrr" - } - }, - "RequiredReference": { - "Text": "e2 rr", - "NestedCollection": - [ - { - "DoB": "2000-01-01T00:00:00", - "Text": "e2 rr c1" - }, - { - "DoB": "2000-01-01T00:00:00", - "Text": "e2 rr c2" - } - ], - "NestedOptionalReference": { - "DoB": "2000-01-01T00:00:00", - "Text": "e2 rr nor" - }, - "NestedRequiredReference": { - "DoB": "2000-01-01T00:00:00", - "Text": "e2 rr nrr" - } - } -} -"""; - - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - entitiesContainer, - missingTopLevel, - CancellationToken.None); - - var missingNested = - """ -{ - "Id": 3, - "$type": "Entity", - "Name": "e3", - "id": "3", - "Collection": - [ - { - "Number": 7.0, - "Text": "e3 c1", - "NestedCollection": - [ - { - "Text": "e3 c1 c1" - }, - { - "Text": "e3 c1 c2" - } - ], - "NestedOptionalReference": { - "Text": "e3 c1 nor" - }, - "NestedRequiredReference": { - "Text": "e3 c1 nrr" - } - }, - { - "Number": 7.0, - "Text": "e3 c2", - "NestedCollection": - [ - { - "Text": "e3 c2 c1" - }, - { - "Text": "e3 c2 c2" - } - ], - "NestedOptionalReference": { - "Text": "e3 c2 nor" - }, - "NestedRequiredReference": { - "Text": "e3 c2 nrr" - } - } - ], - "OptionalReference": { - "Number": 7.0, - "Text": "e3 or", - "NestedCollection": - [ - { - "Text": "e3 or c1" - }, - { - "Text": "e3 or c2" - } - ], - "NestedOptionalReference": { - "Text": "e3 or nor" - }, - "NestedRequiredReference": { - "Text": "e3 or nrr" - } - }, - "RequiredReference": { - "Number": 7.0, - "Text": "e3 rr", - "NestedCollection": - [ - { - "Text": "e3 rr c1" - }, - { - "Text": "e3 rr c2" - } - ], - "NestedOptionalReference": { - "Text": "e3 rr nor" - }, - "NestedRequiredReference": { - "Text": "e3 rr nrr" - } - } -} -"""; - - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - entitiesContainer, - missingNested, - CancellationToken.None); - - var nullTopLevel = - """ -{ - "Id": 4, - "$type": "Entity", - "Name": "e4", - "id": "4", - "Collection": - [ - { - "Number": null, - "Text": "e4 c1", - "NestedCollection": - [ - { - "DoB": "2000-01-01T00:00:00", - "Text": "e4 c1 c1" - }, - { - "DoB": "2000-01-01T00:00:00", - "Text": "e4 c1 c2" - } - ], - "NestedOptionalReference": { - "DoB": "2000-01-01T00:00:00", - "Text": "e4 c1 nor" - }, - "NestedRequiredReference": { - "DoB": "2000-01-01T00:00:00", - "Text": "e4 c1 nrr" - } - }, - { - "Number": null, - "Text": "e4 c2", - "NestedCollection": - [ - { - "DoB": "2000-01-01T00:00:00", - "Text": "e4 c2 c1" - }, - { - "DoB": "2000-01-01T00:00:00", - "Text": "e4 c2 c2" - } - ], - "NestedOptionalReference": { - "DoB": "2000-01-01T00:00:00", - "Text": "e4 c2 nor" - }, - "NestedRequiredReference": { - "DoB": "2000-01-01T00:00:00", - "Text": "e4 c2 nrr" - } - } - ], - "OptionalReference": { - "Number": null, - "Text": "e4 or", - "NestedCollection": - [ - { - "DoB": "2000-01-01T00:00:00", - "Text": "e4 or c1" - }, - { - "DoB": "2000-01-01T00:00:00", - "Text": "e4 or c2" - } - ], - "NestedOptionalReference": { - "DoB": "2000-01-01T00:00:00", - "Text": "e4 or nor" - }, - "NestedRequiredReference": { - "DoB": "2000-01-01T00:00:00", - "Text": "e4 or nrr" - } - }, - "RequiredReference": { - "Number": null, - "Text": "e4 rr", - "NestedCollection": - [ - { - "DoB": "2000-01-01T00:00:00", - "Text": "e4 rr c1" - }, - { - "DoB": "2000-01-01T00:00:00", - "Text": "e4 rr c2" - } - ], - "NestedOptionalReference": { - "DoB": "2000-01-01T00:00:00", - "Text": "e4 rr nor" - }, - "NestedRequiredReference": { - "DoB": "2000-01-01T00:00:00", - "Text": "e4 rr nrr" - } - } -} -"""; - - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - entitiesContainer, - nullTopLevel, - CancellationToken.None); - - var missingRequiredNav = - """ -{ - "Id": 5, - "$type": "Entity", - "Name": "e5", - "id": "5", - "Collection": - [ - { - "Number": 7.0, - "Text": "e5 c1", - "NestedCollection": - [ - { - "DoB": "2000-01-01T00:00:00", - "Text": "e5 c1 c1" - }, - { - "DoB": "2000-01-01T00:00:00", - "Text": "e5 c1 c2" - } - ], - "NestedOptionalReference": { - "DoB": "2000-01-01T00:00:00", - "Text": "e5 c1 nor" - } - }, - { - "Number": 7.0, - "Text": "e5 c2", - "NestedCollection": - [ - { - "DoB": "2000-01-01T00:00:00", - "Text": "e5 c2 c1" - }, - { - "DoB": "2000-01-01T00:00:00", - "Text": "e5 c2 c2" - } - ], - "NestedOptionalReference": { - "DoB": "2000-01-01T00:00:00", - "Text": "e5 c2 nor" - } - } - ], - "OptionalReference": { - "Number": 7.0, - "Text": "e5 or", - "NestedCollection": - [ - { - "DoB": "2000-01-01T00:00:00", - "Text": "e5 or c1" - }, - { - "DoB": "2000-01-01T00:00:00", - "Text": "e5 or c2" - } - ], - "NestedOptionalReference": { - "DoB": "2000-01-01T00:00:00", - "Text": "e5 or nor" - } - }, - "RequiredReference": { - "Number": 7.0, - "Text": "e5 rr", - "NestedCollection": - [ - { - "DoB": "2000-01-01T00:00:00", - "Text": "e5 rr c1" - }, - { - "DoB": "2000-01-01T00:00:00", - "Text": "e5 rr c2" - } - ], - "NestedOptionalReference": { - "DoB": "2000-01-01T00:00:00", - "Text": "e5 rr nor" - } - } -} -"""; - - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - entitiesContainer, - missingRequiredNav, - CancellationToken.None); - - var nullRequiredNav = - """ -{ - "Id": 6, - "$type": "Entity", - "Name": "e6", - "id": "6", - "Collection": - [ - { - "Number": 7.0, - "Text": "e6 c1", - "NestedCollection": - [ - { - "DoB": "2000-01-01T00:00:00", - "Text": "e6 c1 c1" - }, - { - "DoB": "2000-01-01T00:00:00", - "Text": "e6 c1 c2" - } - ], - "NestedOptionalReference": { - "DoB": "2000-01-01T00:00:00", - "Text": "e6 c1 nor" - }, - "NestedRequiredReference": null - }, - { - "Number": 7.0, - "Text": "e6 c2", - "NestedCollection": - [ - { - "DoB": "2000-01-01T00:00:00", - "Text": "e6 c2 c1" - }, - { - "DoB": "2000-01-01T00:00:00", - "Text": "e6 c2 c2" - } - ], - "NestedOptionalReference": { - "DoB": "2000-01-01T00:00:00", - "Text": "e6 c2 nor" - }, - "NestedRequiredReference": null - } - ], - "OptionalReference": { - "Number": 7.0, - "Text": "e6 or", - "NestedCollection": - [ - { - "DoB": "2000-01-01T00:00:00", - "Text": "e6 or c1" - }, - { - "DoB": "2000-01-01T00:00:00", - "Text": "e6 or c2" - } - ], - "NestedOptionalReference": { - "DoB": "2000-01-01T00:00:00", - "Text": "e6 or nor" - }, - "NestedRequiredReference": null - }, - "RequiredReference": { - "Number": 7.0, - "Text": "e6 rr", - "NestedCollection": - [ - { - "DoB": "2000-01-01T00:00:00", - "Text": "e6 rr c1" - }, - { - "DoB": "2000-01-01T00:00:00", - "Text": "e6 rr c2" - } - ], - "NestedOptionalReference": { - "DoB": "2000-01-01T00:00:00", - "Text": "e6 rr nor" - }, - "NestedRequiredReference": null - } -} -"""; - - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - entitiesContainer, - nullRequiredNav, - CancellationToken.None); - } - - #endregion - - #region 29219 - - // Cosmos returns unexpected number of results (i.e. not returning row with non-existent NullableScalar - // this is by design behavior in Cosmos, so we just skip the test to avoid validation error - public override Task Can_project_nullable_json_property_when_the_element_in_json_is_not_present() - => Task.CompletedTask; - - protected override void OnModelCreating29219(ModelBuilder modelBuilder) - { - base.OnModelCreating29219(modelBuilder); - - modelBuilder.Entity().ToContainer("Entities"); - } - - protected override async Task Seed29219(DbContext context) - { - await base.Seed29219(context); - - var wrapper = (CosmosClientWrapper)context.GetService(); - var singletonWrapper = context.GetService(); - var entitiesContainer = singletonWrapper.Client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var missingNullableScalars = - """ -{ - "Id": 3, - "$type": "MyEntity", - "id": "3", - "Collection": - [ - { - "NonNullableScalar" : 10001 - } - ], - "Reference": { - "NonNullableScalar" : 30 - } -} -"""; - - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - entitiesContainer, - missingNullableScalars, - CancellationToken.None); - } - - #endregion - - #region 30028 - - [Theory(Skip = "issue #35702")] - public override Task Missing_navigation_works_with_deduplication(bool async) - => base.Missing_navigation_works_with_deduplication(async); - - // missing array comes out as empty on Cosmos - public override Task Accessing_missing_navigation_works() - => Task.CompletedTask; - - protected override void OnModelCreating30028(ModelBuilder modelBuilder) - { - base.OnModelCreating30028(modelBuilder); - - modelBuilder.Entity().ToContainer("Entities"); - } - - protected override async Task Seed30028(DbContext context) - { - var wrapper = (CosmosClientWrapper)context.GetService(); - var singletonWrapper = context.GetService(); - var entitiesContainer = singletonWrapper.Client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var complete = - """ -{ - "Id": 1, - "$type": "MyEntity", - "id": "1", - "Json": { - "RootName":"e1", - "Collection": - [ - { - "BranchName":"e1 c1", - "Nested":{ - "LeafName":"e1 c1 l" - } - }, - { - "BranchName":"e1 c2", - "Nested":{ - "LeafName":"e1 c2 l" - } - } - ], - "OptionalReference":{ - "BranchName":"e1 or", - "Nested":{ - "LeafName":"e1 or l" - } - }, - "RequiredReference":{ - "BranchName":"e1 rr", - "Nested":{ - "LeafName":"e1 rr l" - } - } - } -} -"""; - - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - entitiesContainer, - complete, - CancellationToken.None); - - var missingCollection = - """ -{ - "Id": 2, - "$type": "MyEntity", - "id": "2", - "Json": { - "RootName":"e2", - "OptionalReference":{ - "BranchName":"e2 or", - "Nested":{ - "LeafName":"e2 or l" - } - }, - "RequiredReference":{ - "BranchName":"e2 rr", - "Nested":{ - "LeafName":"e2 rr l" - } - } - } -} -"""; - - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - entitiesContainer, - missingCollection, - CancellationToken.None); - - var missingOptionalReference = - """ -{ - "Id": 3, - "$type": "MyEntity", - "id": "3", - "Json": { - "RootName":"e3", - "Collection": - [ - { - "BranchName":"e3 c1", - "Nested":{ - "LeafName":"e3 c1 l" - } - }, - { - "BranchName":"e3 c2", - "Nested":{ - "LeafName":"e3 c2 l" - } - } - ], - "RequiredReference":{ - "BranchName":"e3 rr", - "Nested":{ - "LeafName":"e3 rr l" - } - } - } -} -"""; - - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - entitiesContainer, - missingOptionalReference, - CancellationToken.None); - - var missingRequiredReference = - """ -{ - "Id": 4, - "$type": "MyEntity", - "id": "4", - "Json": { - "RootName":"e4", - "Collection": - [ - { - "BranchName":"e4 c1", - "Nested":{ - "LeafName":"e4 c1 l" - } - }, - { - "BranchName":"e4 c2", - "Nested":{ - "LeafName":"e4 c2 l" - } - } - ], - "OptionalReference":{ - "BranchName":"e4 or", - "Nested":{ - "LeafName":"e4 or l" - } - } - } -} -"""; - - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - entitiesContainer, - missingRequiredReference, - CancellationToken.None); - } - - #endregion - - #region 33046 - - protected override void OnModelCreating33046(ModelBuilder modelBuilder) - { - base.OnModelCreating33046(modelBuilder); - - modelBuilder.Entity().ToContainer("Reviews"); - } - - protected override async Task Seed33046(DbContext context) - { - var wrapper = (CosmosClientWrapper)context.GetService(); - var singletonWrapper = context.GetService(); - var entitiesContainer = singletonWrapper.Client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Reviews"); - - var json = - """ -{ - "Id": 1, - "$type": "Review", - "id": "1", - "Rounds": - [ - { - "RoundNumber":11, - "SubRounds": - [ - { - "SubRoundNumber":111 - }, - { - "SubRoundNumber":112 - } - ] - } - ] -} -"""; - - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - entitiesContainer, - json, - CancellationToken.None); - } - - #endregion - - #region 34960 - - public override async Task Try_project_collection_but_JSON_is_entity() - { - var message = (await Assert.ThrowsAsync(() => base.Try_project_collection_but_JSON_is_entity())) - .Message; - - Assert.Equal(CoreStrings.JsonReaderInvalidTokenType(JsonTokenType.StartObject), message); - } - - public override async Task Try_project_reference_but_JSON_is_collection() - { - var message = (await Assert.ThrowsAsync(() => base.Try_project_reference_but_JSON_is_collection())) - .Message; - - Assert.Equal(CoreStrings.JsonReaderInvalidTokenType(JsonTokenType.StartArray), message); - } - - protected override void OnModelCreating34960(ModelBuilder modelBuilder) - { - base.OnModelCreating34960(modelBuilder); - modelBuilder.Entity().ToContainer("Entities"); - modelBuilder.Entity().ToContainer("Junk"); - } - - protected override async Task Seed34960(Context34960 context) - { - await base.Seed34960(context); - - var wrapper = (CosmosClientWrapper)context.GetService(); - var singletonWrapper = context.GetService(); - var entitiesContainer = singletonWrapper.Client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var json = - """ -{ - "Id": 4, - "$type": "Entity", - "id": "4", - "Collection": null, - "Reference": null -} -"""; - - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - entitiesContainer, - json, - CancellationToken.None); - - var junkContainer = singletonWrapper.Client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Junk"); - - var objectWhereCollectionShouldBe = - """ -{ - "Id": 1, - "$type": "JunkEntity", - "id": "1", - "Collection": { "DoB":"2000-01-01T00:00:00","Text":"junk" }, - "Reference": null -} -"""; - - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - junkContainer, - objectWhereCollectionShouldBe, - CancellationToken.None); - - var collectionWhereEntityShouldBe = - """ -{ - "Id": 2, - "$type": "JunkEntity", - "id": "2", - "Collection": null, - "Reference": [{ "DoB":"2000-01-01T00:00:00","Text":"junk" }] -} -"""; - - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - junkContainer, - collectionWhereEntityShouldBe, - CancellationToken.None); - } - - #endregion - - #region ArrayOfPrimitives - - public override async Task Project_element_of_json_array_of_primitives() - { - // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/335 - CosmosTestEnvironment.SkipOnLinuxEmulator(); - - await base.Project_element_of_json_array_of_primitives(); - } - - protected override void OnModelCreatingArrayOfPrimitives(ModelBuilder modelBuilder) - => base.OnModelCreatingArrayOfPrimitives(modelBuilder); - - #endregion - - #region JunkInJson - - protected override void OnModelCreatingJunkInJson(ModelBuilder modelBuilder) - { - base.OnModelCreatingJunkInJson(modelBuilder); - - modelBuilder.Entity().ToContainer("Entities"); - } - - protected override async Task SeedJunkInJson(DbContext context) - { - var wrapper = (CosmosClientWrapper)context.GetService(); - var singletonWrapper = context.GetService(); - var entitiesContainer = singletonWrapper.Client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var json = - """ -{ - "Id": 1, - "$type": "MyEntity", - "id": "1", - "Collection": - [ - { - "JunkReference": { - "Something":"SomeValue" - }, - "Name":"c11", - "JunkProperty1":50, - "Number":11.5, - "JunkCollection1":[], - "JunkCollection2": - [ - { - "Foo":"junk value" - } - ], - "NestedCollection": - [ - { - "DoB":"2002-04-01T00:00:00", - "DummyProp":"Dummy value" - }, - { - "DoB":"2002-04-02T00:00:00", - "DummyReference":{ - "Foo":5 - } - } - ], - "NestedReference":{ - "DoB":"2002-03-01T00:00:00" - } - }, - { - "Name":"c12", - "Number":12.5, - "NestedCollection": - [ - { - "DoB":"2002-06-01T00:00:00" - }, - { - "DoB":"2002-06-02T00:00:00" - } - ], - "NestedDummy":59, - "NestedReference":{ - "DoB":"2002-05-01T00:00:00" - } - } - ], - "CollectionWithCtor": - [ - { - "MyBool":true, - "Name":"c11 ctor", - "JunkReference":{ - "Something":"SomeValue", - "JunkCollection": - [ - { - "Foo":"junk value" - } - ] - }, - "NestedCollection": - [ - { - "DoB":"2002-08-01T00:00:00" - }, - { - "DoB":"2002-08-02T00:00:00" - } - ], - "NestedReference":{ - "DoB":"2002-07-01T00:00:00" - } - }, - { - "MyBool":false, - "Name":"c12 ctor", - "NestedCollection": - [ - { - "DoB":"2002-10-01T00:00:00" - }, - { - "DoB":"2002-10-02T00:00:00" - } - ], - "JunkCollection": - [ - { - "Foo":"junk value" - } - ], - "NestedReference":{ - "DoB":"2002-09-01T00:00:00" - } - } - ], - "Reference": { - "Name":"r1", - "JunkCollection": - [ - { - "Foo":"junk value" - } - ], - "JunkReference":{ - "Something":"SomeValue" - }, - "Number":1.5, - "NestedCollection": - [ - { - "DoB":"2000-02-01T00:00:00", - "JunkReference":{ - "Something":"SomeValue" - } - }, - { - "DoB":"2000-02-02T00:00:00" - } - ], - "NestedReference":{ - "DoB":"2000-01-01T00:00:00" - } - }, - "ReferenceWithCtor":{ - "MyBool":true, - "JunkCollection": - [ - { - "Foo":"junk value" - } - ], - "Name":"r1 ctor", - "JunkReference":{ - "Something":"SomeValue" - }, - "NestedCollection": - [ - { - "DoB":"2001-02-01T00:00:00" - }, - { - "DoB":"2001-02-02T00:00:00" - } - ], - "NestedReference":{ - "JunkCollection": - [ - { - "Foo":"junk value" - } - ], - "DoB":"2001-01-01T00:00:00" - } - } -} -"""; - - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - entitiesContainer, - json, - CancellationToken.None); - } - - #endregion - - #region TrickyBuffering - - protected override void OnModelCreatingTrickyBuffering(ModelBuilder modelBuilder) - { - base.OnModelCreatingTrickyBuffering(modelBuilder); - - modelBuilder.Entity().ToContainer("Entities"); - } - - protected override async Task SeedTrickyBuffering(DbContext context) - { - var wrapper = (CosmosClientWrapper)context.GetService(); - var singletonWrapper = context.GetService(); - var entitiesContainer = singletonWrapper.Client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var json = - """ -{ - "Id": 1, - "$type": "MyEntity", - "id": "1", - "Reference": { - "Name": "r1", - "Number": 7, - "JunkReference": { - "Something": "SomeValue" - }, - "JunkCollection": - [ - { - "Foo": "junk value" - } - ], - "NestedReference": { - "DoB": "2000-01-01T00:00:00" - }, - "NestedCollection": - [ - { - "DoB": "2000-02-01T00:00:00", - "JunkReference": { - "Something": "SomeValue" - } - }, - { - "DoB": "2000-02-02T00:00:00" - } - ] - } -} -"""; - - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - entitiesContainer, - json, - CancellationToken.None); - } - - #endregion - - #region ShadowProperties - - public override async Task Project_shadow_properties_from_json_entity() - { - // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/335 - CosmosTestEnvironment.SkipOnLinuxEmulator(); - - await base.Project_shadow_properties_from_json_entity(); - } - - protected override void OnModelCreatingShadowProperties(ModelBuilder modelBuilder) - { - base.OnModelCreatingShadowProperties(modelBuilder); - - modelBuilder.Entity(b => - { - b.ToContainer("Entities"); - - //b.OwnsOne(x => x.Reference, b => - //{ - // // b.ToJson().HasColumnType(JsonColumnType); - // b.Property("ShadowString"); - //}); - - b.OwnsOne( - x => x.ReferenceWithCtor, b => - { - // b.ToJson().HasColumnType(JsonColumnType); - b.Property("Shadow_Int").ToJsonProperty("ShadowInt"); - }); - - //b.OwnsMany( - // x => x.Collection, b => - // { - // // b.ToJson().HasColumnType(JsonColumnType); - // b.Property("ShadowDouble"); - // }); - - //b.OwnsMany( - // x => x.CollectionWithCtor, b => - // { - // //b.ToJson().HasColumnType(JsonColumnType); - // b.Property("ShadowNullableByte"); - // }); - }); - } - - protected override async Task SeedShadowProperties(DbContext context) - { - var wrapper = (CosmosClientWrapper)context.GetService(); - var singletonWrapper = context.GetService(); - var entitiesContainer = singletonWrapper.Client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var json = - """ -{ - "Id": 1, - "$type": "MyEntity", - "id": "1", - "Name": "e1", - "Collection": - [ - { - "Name":"e1_c1","ShadowDouble":5.5 - }, - { - "ShadowDouble":20.5,"Name":"e1_c2" - } - ], - "CollectionWithCtor": - [ - { - "Name":"e1_c1 ctor","ShadowNullableByte":6 - }, - { - "ShadowNullableByte":null,"Name":"e1_c2 ctor" - } - ], - "Reference": { "Name":"e1_r", "ShadowString":"Foo" }, - "ReferenceWithCtor": { "ShadowInt":143,"Name":"e1_r ctor" } -} -"""; - - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - entitiesContainer, - json, - CancellationToken.None); - } - - #endregion - - #region LazyLoadingProxies - - protected override void OnModelCreatingLazyLoadingProxies(ModelBuilder modelBuilder) - => base.OnModelCreatingLazyLoadingProxies(modelBuilder); - - #endregion - - #region NotICollection - - protected override void OnModelCreatingNotICollection(ModelBuilder modelBuilder) - { - base.OnModelCreatingNotICollection(modelBuilder); - - modelBuilder.Entity().ToContainer("Entities"); - } - - protected override async Task SeedNotICollection(DbContext context) - { - var wrapper = (CosmosClientWrapper)context.GetService(); - var singletonWrapper = context.GetService(); - var entitiesContainer = singletonWrapper.Client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var json1 = - """ -{ - "Id": 1, - "$type": "MyEntity", - "id": "1", - "Json": - { - "Collection": - [ - { - "Bar":11,"Foo":"c11" - }, - { - "Bar":12,"Foo":"c12" - }, - { - "Bar":13,"Foo":"c13" - } - ] - } -} -"""; - - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - entitiesContainer, - json1, - CancellationToken.None); - - var json2 = - """ -{ - "Id": 2, - "$type": "MyEntity", - "id": "2", - "Json": { - "Collection": - [ - { - "Bar":21,"Foo":"c21" - }, - { - "Bar":22,"Foo":"c22" - } - ] - } -} -"""; - - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - entitiesContainer, - json2, - CancellationToken.None); - } - - #endregion - - #region BadJsonProperties - - // missing collection comes back as empty on Cosmos - public override Task Bad_json_properties_empty_navigations(bool noTracking) - => Task.CompletedTask; - - // Insertion of the bad data fails thanks to json validation by Cosmos DB - public override Task Bad_json_properties_null_navigations(bool noTracking) - => Task.CompletedTask; - - public override Task Bad_json_properties_null_scalars(bool noTracking) - => Task.CompletedTask; - - // Insertion of the bad data is deduplicated by Cosmos DB - public override Task Bad_json_properties_duplicated_navigations(bool noTracking) - => Task.CompletedTask; - - public override Task Bad_json_properties_duplicated_scalars(bool noTracking) - => Task.CompletedTask; - - protected override void OnModelCreatingBadJsonProperties(ModelBuilder modelBuilder) - { - base.OnModelCreatingBadJsonProperties(modelBuilder); - - modelBuilder.Entity().ToContainer("Entities"); - } - - protected override async Task SeedBadJsonProperties(ContextBadJsonProperties context) - { - var wrapper = (CosmosClientWrapper)context.GetService(); - var singletonWrapper = context.GetService(); - var entitiesContainer = singletonWrapper.Client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var baseline = - """ -{ - "Id": 1, - "$type": "Entity", - "id": "1", - "Scenario": "baseline", - "OptionalReference": {"NestedOptional": { "Text":"or no" }, "NestedRequired": { "Text":"or nr" }, "NestedCollection": [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ] }, - "RequiredReference": {"NestedOptional": { "Text":"rr no" }, "NestedRequired": { "Text":"rr nr" }, "NestedCollection": [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ] }, - "Collection": - [ - {"NestedOptional": { "Text":"c 1 no" }, "NestedRequired": { "Text":"c 1 nr" }, "NestedCollection": [ { "Text":"c 1 nc 1" }, { "Text":"c 1 nc 2" } ] }, - {"NestedOptional": { "Text":"c 2 no" }, "NestedRequired": { "Text":"c 2 nr" }, "NestedCollection": [ { "Text":"c 2 nc 1" }, { "Text":"c 2 nc 2" } ] } - ] -} -"""; - - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - entitiesContainer, - baseline, - CancellationToken.None); - - var duplicatedNavigations = - """ -{ - "Id": 2, - "$type": "Entity", - "id": "2", - "Scenario": "duplicated navigations", - "OptionalReference": {"NestedOptional": { "Text":"or no" }, "NestedOptional": { "Text":"or no dupnav" }, "NestedRequired": { "Text":"or nr" }, "NestedCollection": [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ], "NestedCollection": [ { "Text":"or nc 1 dupnav" }, { "Text":"or nc 2 dupnav" } ], "NestedRequired": { "Text":"or nr dupnav" } }, - "RequiredReference": {"NestedOptional": { "Text":"rr no" }, "NestedOptional": { "Text":"rr no dupnav" }, "NestedRequired": { "Text":"rr nr" }, "NestedCollection": [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ], "NestedCollection": [ { "Text":"rr nc 1 dupnav" }, { "Text":"rr nc 2 dupnav" } ], "NestedRequired": { "Text":"rr nr dupnav" } }, - "Collection": - [ - {"NestedOptional": { "Text":"c 1 no" }, "NestedOptional": { "Text":"c 1 no dupnav" }, "NestedRequired": { "Text":"c 1 nr" }, "NestedCollection": [ { "Text":"c 1 nc 1" }, { "Text":"c 1 nc 2" } ], "NestedCollection": [ { "Text":"c 1 nc 1 dupnav" }, { "Text":"c 1 nc 2 dupnav" } ], "NestedRequired": { "Text":"c 1 nr dupnav" } }, - {"NestedOptional": { "Text":"c 2 no" }, "NestedOptional": { "Text":"c 2 no dupnav" }, "NestedRequired": { "Text":"c 2 nr" }, "NestedCollection": [ { "Text":"c 2 nc 1" }, { "Text":"c 2 nc 2" } ], "NestedCollection": [ { "Text":"c 2 nc 1 dupnav" }, { "Text":"c 2 nc 2 dupnav" } ], "NestedRequired": { "Text":"c 2 nr dupnav" } } - ] -} -"""; - - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - entitiesContainer, - duplicatedNavigations, - CancellationToken.None); - - var duplicatedScalars = - """ -{ - "Id": 3, - "$type": "Entity", - "id": "3", - "Scenario": "duplicated scalars", - "OptionalReference": {"NestedOptional": { "Text":"or no", "Text":"or no dupprop" }, "NestedRequired": { "Text":"or nr", "Text":"or nr dupprop" }, "NestedCollection": [ { "Text":"or nc 1", "Text":"or nc 1 dupprop" }, { "Text":"or nc 2", "Text":"or nc 2 dupprop" } ] }, - "RequiredReference": {"NestedOptional": { "Text":"rr no", "Text":"rr no dupprop" }, "NestedRequired": { "Text":"rr nr", "Text":"rr nr dupprop" }, "NestedCollection": [ { "Text":"rr nc 1", "Text":"rr nc 1 dupprop" }, { "Text":"rr nc 2", "Text":"rr nc 2 dupprop" } ] }, - "Collection": - [ - {"NestedOptional": { "Text":"c 1 no", "Text":"c 1 no dupprop" }, "NestedRequired": { "Text":"c 1 nr", "Text":"c 1 nr dupprop" }, "NestedCollection": [ { "Text":"c 1 nc 1", "Text":"c 1 nc 1 dupprop" }, { "Text":"c 1 nc 2", "Text":"c 1 nc 2 dupprop" } ] }, - {"NestedOptional": { "Text":"c 2 no", "Text":"c 2 no dupprop" }, "NestedRequired": { "Text":"c 2 nr", "Text":"c 2 nr dupprop" }, "NestedCollection": [ { "Text":"c 2 nc 1", "Text":"c 2 nc 1 dupprop" }, { "Text":"c 2 nc 2", "Text":"c 2 nc 2 dupprop" } ] } - ] -} -"""; - - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - entitiesContainer, - duplicatedScalars, - CancellationToken.None); - - var emptyNavs = - """ -{ - "Id": 4, - "$type": "Entity", - "id": "4", - "Scenario": "empty navigation property names", - "OptionalReference": {"": { "Text":"or no" }, "": { "Text":"or nr" }, "": [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ] }, - "RequiredReference": {"": { "Text":"rr no" }, "": { "Text":"rr nr" }, "": [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ] }, - "Collection": - [ - {"": { "Text":"c 1 no" }, "": { "Text":"c 1 nr" }, "": [ { "Text":"c 1 nc 1" }, { "Text":"c 1 nc 2" } ] }, - {"": { "Text":"c 2 no" }, "": { "Text":"c 2 nr" }, "": [ { "Text":"c 2 nc 1" }, { "Text":"c 2 nc 2" } ] } - ] -} -"""; - - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - entitiesContainer, - emptyNavs, - CancellationToken.None); - - var emptyScalars = - """ -{ - "Id": 5, - "$type": "Entity", - "id": "5", - "Scenario": "empty scalar property names", - "OptionalReference": {"NestedOptional": { "":"or no" }, "NestedRequired": { "":"or nr" }, "NestedCollection": [ { "":"or nc 1" }, { "":"or nc 2" } ] }, - "RequiredReference": {"NestedOptional": { "":"rr no" }, "NestedRequired": { "":"rr nr" }, "NestedCollection": [ { "":"rr nc 1" }, { "":"rr nc 2" } ] }, - "Collection": - [ - {"NestedOptional": { "":"c 1 no" }, "NestedRequired": { "":"c 1 nr" }, "NestedCollection": [ { "":"c 1 nc 1" }, { "":"c 1 nc 2" } ] }, - {"NestedOptional": { "":"c 2 no" }, "NestedRequired": { "":"c 2 nr" }, "NestedCollection": [ { "":"c 2 nc 1" }, { "":"c 2 nc 2" } ] } - ] -} -"""; - - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - entitiesContainer, - emptyScalars, - CancellationToken.None); - - // Insertion of the bad data fails thanks to json validation by Cosmos DB - // nullNavs or "Scenario": "null navigation property names" can not be tested - // nullScalars or "Scenario": "null scalar property names" can not be tested - } - - #endregion - - #region Bad primary keys - - // TODO: This only asserts the current behavior of the provider, but the behavior is probably not ideal - // Consider: - // - Never storing foreign key properties in json, For projections when the foreign key is a member, we would need to translate to the owner id in the query pipeline - // - Not allowing (explicit?) primary keys on embedded entity types - // - Deprecating owned entity types as a whole - - // The workaround for old providers is: - // For missing implicit PK properties on embedded collection entities, use ordinal - - [Theory, InlineData(true), InlineData(false)] - public async Task Primary_key_baseline(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var entity = new ContextBadPrimaryKeyJsonProperties.Entity - { - Id = 1, - Associate = new() { Id = 2 }, - KeyedAssociate = new() { Id = 3 }, // Overwritten to 1 (entity id) - AssociateCollection = - [ - new() { Id = 4 }, - new() { Id = 5 } - ], - KeyedAssociateCollection = - [ - new() { Id = 6 }, - new() { Id = 7 } - ], - ForeignKeyAssociate = new() { Id = 8 }, - KeyedForeignKeyAssociate = new() { Id = 9 }, - ForeignKeyAssociateCollection = - [ - new() { Id = 10 }, - new() { Id = 11 } - ], - KeyedForeignKeyAssociateCollection = - [ - new() { Id = 12 }, - new() { Id = 13 } - ] - }; - - context.Add(entity); - await context.SaveChangesAsync(); - }); - - using var context = contextFactory.CreateDbContext(); - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entityJson = JsonNode.Parse(""" -{ - "$type": "Entity", - "Id": 1, - "id": "1", - "Associate": { - "Id": 2 - }, - "KeyedAssociate": {}, - "AssociateCollection": [ - { - "Id": 4 - }, - { - "Id": 5 - } - ], - "KeyedAssociateCollection": [ - { - "Id": 6, - "EntityId": 1 - }, - { - "Id": 7, - "EntityId": 1 - } - ], - "ForeignKeyAssociate": { - "Id": 8 - }, - "KeyedForeignKeyAssociate": { - "Id": 9, - "EntityId": 1 - }, - "ForeignKeyAssociateCollection": [ - { - "Id": 10 - }, - { - "Id": 11 - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": 12, - "EntityId": 1 - }, - { - "Id": 13, - "EntityId": 1 - } - ] -} -""")!.AsObject(); - - var dbJson = JsonNode.Parse( - new StreamReader( - (await container.ReadItemStreamAsync("1", Azure.Cosmos.PartitionKey.None)).Content).ReadToEnd())!.AsObject(); - foreach (var property in dbJson.Where(x => x.Key.StartsWith("_")).ToList()) - { - dbJson.Remove(property.Key); - } - - var text = dbJson.ToString(); - - Assert.True(JsonNode.DeepEquals(entityJson, dbJson)); - - var entity = await context.Entities.SingleAsync(); - Assert.Equal(2, entity.Associate.Id); - Assert.Equal(1, entity.KeyedAssociate.Id); // Is overwritten.. - - Assert.Equal(4, entity.AssociateCollection.First().Id); - Assert.Equal(6, entity.KeyedAssociateCollection.First().Id); - - Assert.Equal(8, entity.ForeignKeyAssociate.Id); - Assert.Equal(9, entity.KeyedForeignKeyAssociate.Id); - - Assert.Equal(10, entity.ForeignKeyAssociateCollection.First().Id); - Assert.Equal(12, entity.KeyedForeignKeyAssociateCollection.First().Id); - } - - [Theory, InlineData(true), InlineData(false)] - public async Task Root_null_primary_key_throws(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "Id": null, - "id": "1", - "Associate": { - "Id": 2 - }, - "KeyedAssociate": {}, - "AssociateCollection": [ - { - "Id": 4 - }, - { - "Id": 5 - } - ], - "KeyedAssociateCollection": [ - { - "Id": 6, - "EntityId": 1 - }, - { - "Id": 7, - "EntityId": 1 - } - ], - "ForeignKeyAssociate": { - "Id": 8 - }, - "KeyedForeignKeyAssociate": { - "Id": 9, - "EntityId": 1 - }, - "ForeignKeyAssociateCollection": [ - { - "Id": 10 - }, - { - "Id": 11 - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": 12, - "EntityId": 1 - }, - { - "Id": 13, - "EntityId": 1 - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var ex = await Assert.ThrowsAsync(() => query.SingleOrDefaultAsync()); - Assert.Equal(CoreStrings.InvalidKeyValue("Entity", "Id"), ex.Message); - } - - [Theory, InlineData(true), InlineData(false)] - public async Task Root_missing_primary_key_throws(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "id": "1", - "Associate": { - "Id": 2 - }, - "KeyedAssociate": {}, - "AssociateCollection": [ - { - "Id": 4 - }, - { - "Id": 5 - } - ], - "KeyedAssociateCollection": [ - { - "Id": 6, - "EntityId": 1 - }, - { - "Id": 7, - "EntityId": 1 - } - ], - "ForeignKeyAssociate": { - "Id": 8 - }, - "KeyedForeignKeyAssociate": { - "Id": 9, - "EntityId": 1 - }, - "ForeignKeyAssociateCollection": [ - { - "Id": 10 - }, - { - "Id": 11 - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": 12, - "EntityId": 1 - }, - { - "Id": 13, - "EntityId": 1 - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var ex = await Assert.ThrowsAsync(() => query.SingleOrDefaultAsync()); - - Assert.Equal(CoreStrings.InvalidKeyValue("Entity", "Id"), ex.Message); - } - - [Theory, InlineData(true), InlineData(false)] - public async Task Associate_null_non_primary_key_doesnt_throw(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "Id": 1, - "id": "1", - "Associate": { - "Id": null - }, - "KeyedAssociate": {}, - "AssociateCollection": [ - { - "Id": 4 - }, - { - "Id": 5 - } - ], - "KeyedAssociateCollection": [ - { - "Id": 6, - "EntityId": 1 - }, - { - "Id": 7, - "EntityId": 1 - } - ], - "ForeignKeyAssociate": { - "Id": 8 - }, - "KeyedForeignKeyAssociate": { - "Id": 9, - "EntityId": 1 - }, - "ForeignKeyAssociateCollection": [ - { - "Id": 10 - }, - { - "Id": 11 - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": 12, - "EntityId": 1 - }, - { - "Id": 13, - "EntityId": 1 - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var result = await query.SingleAsync(); - Assert.Equal(0, result.Associate.Id); - } - - [Theory, InlineData(true), InlineData(false)] - public async Task Associate_missing_non_primary_key_doesnt_throw(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "Id": 1, - "id": "1", - "Associate": { - }, - "KeyedAssociate": {}, - "AssociateCollection": [ - { - "Id": 4 - }, - { - "Id": 5 - } - ], - "KeyedAssociateCollection": [ - { - "Id": 6, - "EntityId": 1 - }, - { - "Id": 7, - "EntityId": 1 - } - ], - "ForeignKeyAssociate": { - "Id": 8 - }, - "KeyedForeignKeyAssociate": { - "Id": 9, - "EntityId": 1 - }, - "ForeignKeyAssociateCollection": [ - { - "Id": 10 - }, - { - "Id": 11 - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": 12, - "EntityId": 1 - }, - { - "Id": 13, - "EntityId": 1 - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var result = await query.SingleAsync(); - Assert.Equal(0, result.Associate.Id); - } - - [Theory, InlineData(true), InlineData(false)] - public async Task KeyedAssociate_null_primary_key_uses_implicit(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "Id": 1, - "id": "1", - "Associate": { - "Id": 2 - }, - "KeyedAssociate": { - "Id": null - }, - "AssociateCollection": [ - { - "Id": 4 - }, - { - "Id": 5 - } - ], - "KeyedAssociateCollection": [ - { - "Id": 6, - "EntityId": 1 - }, - { - "Id": 7, - "EntityId": 1 - } - ], - "ForeignKeyAssociate": { - "Id": 8 - }, - "KeyedForeignKeyAssociate": { - "Id": 9, - "EntityId": 1 - }, - "ForeignKeyAssociateCollection": [ - { - "Id": 10 - }, - { - "Id": 11 - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": 12, - "EntityId": 1 - }, - { - "Id": 13, - "EntityId": 1 - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var result = await query.SingleAsync(); - Assert.Equal(1, result.KeyedAssociate.Id); - } - - [Theory, InlineData(true), InlineData(false)] - public async Task KeyedAssociate_incorrect_primary_key_uses_implicit(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "Id": 1, - "id": "1", - "Associate": { - "Id": 2 - }, - "KeyedAssociate": { - "Id": 99 - }, - "AssociateCollection": [ - { - "Id": 4 - }, - { - "Id": 5 - } - ], - "KeyedAssociateCollection": [ - { - "Id": 6, - "EntityId": 1 - }, - { - "Id": 7, - "EntityId": 1 - } - ], - "ForeignKeyAssociate": { - "Id": 8 - }, - "KeyedForeignKeyAssociate": { - "Id": 9, - "EntityId": 1 - }, - "ForeignKeyAssociateCollection": [ - { - "Id": 10 - }, - { - "Id": 11 - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": 12, - "EntityId": 1 - }, - { - "Id": 13, - "EntityId": 1 - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var result = await query.SingleAsync(); - Assert.Equal(1, result.KeyedAssociate.Id); - } - - [Theory, InlineData(true), InlineData(false)] - public async Task AssociateCollection_null_primary_key_throws(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "Id": 1, - "id": "1", - "Associate": { - "Id": 2 - }, - "KeyedAssociate": {}, - "AssociateCollection": [ - { - "Id": null - }, - { - "Id": null - } - ], - "KeyedAssociateCollection": [ - { - "Id": 6, - "EntityId": 1 - }, - { - "Id": 7, - "EntityId": 1 - } - ], - "ForeignKeyAssociate": { - "Id": 8 - }, - "KeyedForeignKeyAssociate": { - "Id": 9, - "EntityId": 1 - }, - "ForeignKeyAssociateCollection": [ - { - "Id": 10 - }, - { - "Id": 11 - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": 12, - "EntityId": 1 - }, - { - "Id": 13, - "EntityId": 1 - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var ex = await Assert.ThrowsAsync(() => query.SingleAsync()); - Assert.Equal(CoreStrings.InvalidKeyValue("Entity.AssociateCollection#AssociateEntity", "Id"), ex.Message); - } - - [Theory, InlineData(true), InlineData(false)] - public async Task AssociateCollection_missing_primary_key_doesnt_throw_and_uses_ordinal_workaround(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "Id": 1, - "id": "1", - "Associate": { - "Id": 2 - }, - "KeyedAssociate": {}, - "AssociateCollection": [ - { - }, - { - } - ], - "KeyedAssociateCollection": [ - { - "Id": 6, - "EntityId": 1 - }, - { - "Id": 7, - "EntityId": 1 - } - ], - "ForeignKeyAssociate": { - "Id": 8 - }, - "KeyedForeignKeyAssociate": { - "Id": 9, - "EntityId": 1 - }, - "ForeignKeyAssociateCollection": [ - { - "Id": 10 - }, - { - "Id": 11 - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": 12, - "EntityId": 1 - }, - { - "Id": 13, - "EntityId": 1 - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var result = await query.SingleAsync(); - Assert.Equal(1, result.AssociateCollection.First().Id); - Assert.Equal(2, result.AssociateCollection.Last().Id); - } - - [Theory, InlineData(true), InlineData(false)] - public async Task KeyedAssociateCollection_null_primary_key_throws(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "Id": 1, - "id": "1", - "Associate": { - "Id": 2 - }, - "KeyedAssociate": {}, - "AssociateCollection": [ - { - "Id": 4 - }, - { - "Id": 5 - } - ], - "KeyedAssociateCollection": [ - { - "Id": null, - "EntityId": 1 - }, - { - "Id": null, - "EntityId": 1 - } - ], - "ForeignKeyAssociate": { - "Id": 8 - }, - "KeyedForeignKeyAssociate": { - "Id": 9, - "EntityId": 1 - }, - "ForeignKeyAssociateCollection": [ - { - "Id": 10 - }, - { - "Id": 11 - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": 12, - "EntityId": 1 - }, - { - "Id": 13, - "EntityId": 1 - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var ex = await Assert.ThrowsAsync(() => query.SingleAsync()); - Assert.Equal(CoreStrings.InvalidKeyValue("Entity.KeyedAssociateCollection#AssociateEntity", "Id"), ex.Message); - } - - [Theory, InlineData(true), InlineData(false)] - public async Task KeyedAssociateCollection_missing_primary_key_throws(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "Id": 1, - "id": "1", - "Associate": { - "Id": 2 - }, - "KeyedAssociate": {}, - "AssociateCollection": [ - { - "Id": 4 - }, - { - "Id": 5 - } - ], - "KeyedAssociateCollection": [ - { - "EntityId": 1 - }, - { - "EntityId": 1 - } - ], - "ForeignKeyAssociate": { - "Id": 8 - }, - "KeyedForeignKeyAssociate": { - "Id": 9, - "EntityId": 1 - }, - "ForeignKeyAssociateCollection": [ - { - "Id": 10 - }, - { - "Id": 11 - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": 12, - "EntityId": 1 - }, - { - "Id": 13, - "EntityId": 1 - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var ex = await Assert.ThrowsAsync(() => query.SingleAsync()); - Assert.Equal(CoreStrings.InvalidKeyValue("Entity.KeyedAssociateCollection#AssociateEntity", "Id"), ex.Message); - } - - [Theory, InlineData(true), InlineData(false)] - public async Task KeyedAssociateCollection_null_foreign_key_doesnt_throw(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "Id": 1, - "id": "1", - "Associate": { - "Id": 2 - }, - "KeyedAssociate": {}, - "AssociateCollection": [ - { - "Id": 4 - }, - { - "Id": 5 - } - ], - "KeyedAssociateCollection": [ - { - "Id": 6, - "EntityId": null - }, - { - "Id": 7, - "EntityId": null - } - ], - "ForeignKeyAssociate": { - "Id": 8 - }, - "KeyedForeignKeyAssociate": { - "Id": 9, - "EntityId": 1 - }, - "ForeignKeyAssociateCollection": [ - { - "Id": 10 - }, - { - "Id": 11 - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": 12, - "EntityId": 1 - }, - { - "Id": 13, - "EntityId": 1 - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var result = await query.SingleAsync(); - if (!tracking) - { - Assert.Equal(6, result.KeyedAssociateCollection.First().Id); - Assert.Equal(7, result.KeyedAssociateCollection.Last().Id); - } - else - { - Assert.Empty(result.KeyedAssociateCollection); - } - } - - [Theory, InlineData(true), InlineData(false)] - public async Task KeyedAssociateCollection_missing_foreign_key_doesnt_throw(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "Id": 1, - "id": "1", - "Associate": { - "Id": 2 - }, - "KeyedAssociate": {}, - "AssociateCollection": [ - { - "Id": 4 - }, - { - "Id": 5 - } - ], - "KeyedAssociateCollection": [ - { - "Id": 6 - }, - { - "Id": 7 - } - ], - "ForeignKeyAssociate": { - "Id": 8 - }, - "KeyedForeignKeyAssociate": { - "Id": 9, - "EntityId": 1 - }, - "ForeignKeyAssociateCollection": [ - { - "Id": 10 - }, - { - "Id": 11 - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": 12, - "EntityId": 1 - }, - { - "Id": 13, - "EntityId": 1 - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var result = await query.SingleAsync(); - if (!tracking) - { - Assert.Equal(6, result.KeyedAssociateCollection.First().Id); - Assert.Equal(7, result.KeyedAssociateCollection.Last().Id); - } - else - { - Assert.Empty(result.KeyedAssociateCollection); - } - } - - [Theory, InlineData(true), InlineData(false)] - public async Task KeyedAssociateCollection_incorrect_foreign_key_doesnt_throw(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "Id": 1, - "id": "1", - "Associate": { - "Id": 2 - }, - "KeyedAssociate": {}, - "AssociateCollection": [ - { - "Id": 4 - }, - { - "Id": 5 - } - ], - "KeyedAssociateCollection": [ - { - "Id": 6, - "EntityId": 99 - }, - { - "Id": 7, - "EntityId": 99 - } - ], - "ForeignKeyAssociate": { - "Id": 8 - }, - "KeyedForeignKeyAssociate": { - "Id": 9, - "EntityId": 1 - }, - "ForeignKeyAssociateCollection": [ - { - "Id": 10 - }, - { - "Id": 11 - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": 12, - "EntityId": 1 - }, - { - "Id": 13, - "EntityId": 1 - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var result = await query.SingleAsync(); - if (!tracking) - { - Assert.Equal(6, result.KeyedAssociateCollection.First().Id); - Assert.Equal(7, result.KeyedAssociateCollection.Last().Id); - } - else - { - Assert.Empty(result.KeyedAssociateCollection); - } - } - - [Theory, InlineData(true), InlineData(false)] - public async Task ForeignKeyAssociate_null_non_primary_key_doesnt_throw(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "Id": 1, - "id": "1", - "Associate": { - "Id": 2 - }, - "KeyedAssociate": {}, - "AssociateCollection": [ - { - "Id": 3 - }, - { - "Id": 4 - } - ], - "KeyedAssociateCollection": [ - { - "Id": 5, - "EntityId": 1 - }, - { - "Id": 6, - "EntityId": 1 - } - ], - "ForeignKeyAssociate": { - "Id": null - }, - "KeyedForeignKeyAssociate": { - "Id": 9, - "EntityId": 1 - }, - "ForeignKeyAssociateCollection": [ - { - "Id": 10 - }, - { - "Id": 11 - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": 12, - "EntityId": 1 - }, - { - "Id": 13, - "EntityId": 1 - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var result = await query.SingleAsync(); - Assert.Equal(0, result.ForeignKeyAssociate.Id); - } - - [Theory, InlineData(true), InlineData(false)] - public async Task ForeignKeyAssociate_missing_non_primary_key_doesnt_throw(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "Id": 1, - "id": "1", - "Associate": { - "Id": 2 - }, - "KeyedAssociate": {}, - "AssociateCollection": [ - { - "Id": 3 - }, - { - "Id": 4 - } - ], - "KeyedAssociateCollection": [ - { - "Id": 5, - "EntityId": 1 - }, - { - "Id": 6, - "EntityId": 1 - } - ], - "ForeignKeyAssociate": { - }, - "KeyedForeignKeyAssociate": { - "Id": 9, - "EntityId": 1 - }, - "ForeignKeyAssociateCollection": [ - { - "Id": 10 - }, - { - "Id": 11 - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": 12, - "EntityId": 1 - }, - { - "Id": 13, - "EntityId": 1 - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var result = await query.SingleAsync(); - Assert.Equal(0, result.ForeignKeyAssociate.Id); - } - - [Theory, InlineData(true), InlineData(false)] - public async Task KeyedForeignKeyAssociate_null_primary_key_throws(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "Id": 1, - "id": "1", - "Associate": { - "Id": 2 - }, - "KeyedAssociate": {}, - "AssociateCollection": [ - { - "Id": 4 - }, - { - "Id": 5 - } - ], - "KeyedAssociateCollection": [ - { - "Id": 6, - "EntityId": 1 - }, - { - "Id": 7, - "EntityId": 1 - } - ], - "ForeignKeyAssociate": { - "Id": 8 - }, - "KeyedForeignKeyAssociate": { - "Id": null, - "EntityId": 1 - }, - "ForeignKeyAssociateCollection": [ - { - "Id": 10 - }, - { - "Id": 11 - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": 12, - "EntityId": 1 - }, - { - "Id": 13, - "EntityId": 1 - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var ex = await Assert.ThrowsAsync(() => query.SingleOrDefaultAsync()); - Assert.Equal(CoreStrings.InvalidKeyValue("Entity.KeyedForeignKeyAssociate#ForeignKeyAssociateEntity", "Id"), ex.Message); - } - - [Theory, InlineData(true), InlineData(false)] - public async Task KeyedForeignKeyAssociate_missing_primary_key_throws(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "Id": 1, - "id": "1", - "Associate": { - "Id": 2 - }, - "KeyedAssociate": {}, - "AssociateCollection": [ - { - "Id": 4 - }, - { - "Id": 5 - } - ], - "KeyedAssociateCollection": [ - { - "Id": 6, - "EntityId": 1 - }, - { - "Id": 7, - "EntityId": 1 - } - ], - "ForeignKeyAssociate": { - "Id": 8 - }, - "KeyedForeignKeyAssociate": { - "EntityId": 1 - }, - "ForeignKeyAssociateCollection": [ - { - "Id": 10 - }, - { - "Id": 11 - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": 12, - "EntityId": 1 - }, - { - "Id": 13, - "EntityId": 1 - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var ex = await Assert.ThrowsAsync(() => query.SingleOrDefaultAsync()); - Assert.Equal(CoreStrings.InvalidKeyValue("Entity.KeyedForeignKeyAssociate#ForeignKeyAssociateEntity", "Id"), ex.Message); - } - - [Theory, InlineData(true), InlineData(false)] - public async Task KeyedForeignKeyAssociate_null_foreign_key_doesnt_throw(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "Id": 1, - "id": "1", - "Associate": { - "Id": 2 - }, - "KeyedAssociate": {}, - "AssociateCollection": [ - { - "Id": 4 - }, - { - "Id": 5 - } - ], - "KeyedAssociateCollection": [ - { - "Id": 6, - "EntityId": 1 - }, - { - "Id": 7, - "EntityId": 1 - } - ], - "ForeignKeyAssociate": { - "Id": 8 - }, - "KeyedForeignKeyAssociate": { - "Id": 9, - "EntityId": null - }, - "ForeignKeyAssociateCollection": [ - { - "Id": 10 - }, - { - "Id": 11 - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": 12, - "EntityId": 1 - }, - { - "Id": 13, - "EntityId": 1 - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var result = await query.SingleAsync(); - if (tracking) - { - Assert.Null(result.KeyedForeignKeyAssociate); - } - else - { - Assert.NotNull(result.KeyedForeignKeyAssociate); - Assert.Equal(9, result.KeyedForeignKeyAssociate.Id); - Assert.Equal(0, result.KeyedForeignKeyAssociate.EntityId); - } - } - - [Theory, InlineData(true), InlineData(false)] - public async Task KeyedForeignKeyAssociate_missing_foreign_key_doesnt_throw(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "Id": 1, - "id": "1", - "Associate": { - "Id": 2 - }, - "KeyedAssociate": {}, - "AssociateCollection": [ - { - "Id": 4 - }, - { - "Id": 5 - } - ], - "KeyedAssociateCollection": [ - { - "Id": 6, - "EntityId": 1 - }, - { - "Id": 7, - "EntityId": 1 - } - ], - "ForeignKeyAssociate": { - "Id": 8 - }, - "KeyedForeignKeyAssociate": { - "Id": 9, - "EntityId": null - }, - "ForeignKeyAssociateCollection": [ - { - "Id": 10 - }, - { - "Id": 11 - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": 12, - "EntityId": 1 - }, - { - "Id": 13, - "EntityId": 1 - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var result = await query.SingleAsync(); - if (tracking) - { - Assert.Null(result.KeyedForeignKeyAssociate); - } - else - { - Assert.NotNull(result.KeyedForeignKeyAssociate); - Assert.Equal(9, result.KeyedForeignKeyAssociate.Id); - Assert.Equal(0, result.KeyedForeignKeyAssociate.EntityId); - } - } - - [Theory, InlineData(true), InlineData(false)] - public async Task KeyedForeignKeyAssociate_incorrect_foreign_key_doesnt_throw(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "Id": 1, - "id": "1", - "Associate": { - "Id": 2 - }, - "KeyedAssociate": {}, - "AssociateCollection": [ - { - "Id": 4 - }, - { - "Id": 5 - } - ], - "KeyedAssociateCollection": [ - { - "Id": 6, - "EntityId": 1 - }, - { - "Id": 7, - "EntityId": 1 - } - ], - "ForeignKeyAssociate": { - "Id": 8 - }, - "KeyedForeignKeyAssociate": { - "Id": 9, - "EntityId": 99 - }, - "ForeignKeyAssociateCollection": [ - { - "Id": 10 - }, - { - "Id": 11 - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": 12, - "EntityId": 1 - }, - { - "Id": 13, - "EntityId": 1 - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var result = await query.SingleAsync(); - if (tracking) - { - Assert.Null(result.KeyedForeignKeyAssociate); - } - else - { - Assert.NotNull(result.KeyedForeignKeyAssociate); - Assert.Equal(9, result.KeyedForeignKeyAssociate.Id); - Assert.Equal(99, result.KeyedForeignKeyAssociate.EntityId); - } - } - - [Theory, InlineData(true), InlineData(false)] - public async Task ForeignKeyAssociateCollection_null_primary_key_throws(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "Id": 1, - "id": "1", - "Associate": { - "Id": 2 - }, - "KeyedAssociate": {}, - "AssociateCollection": [ - { - "Id": 4 - }, - { - "Id": 5 - } - ], - "KeyedAssociateCollection": [ - { - "Id": 6, - "EntityId": 1 - }, - { - "Id": 7, - "EntityId": 1 - } - ], - "ForeignKeyAssociate": { - "Id": 8 - }, - "KeyedForeignKeyAssociate": { - "Id": 9, - "EntityId": 1 - }, - "ForeignKeyAssociateCollection": [ - { - "Id": null - }, - { - "Id": null - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": 12, - "EntityId": 1 - }, - { - "Id": 13, - "EntityId": 1 - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var ex = await Assert.ThrowsAsync(() => query.SingleOrDefaultAsync()); - Assert.Equal(CoreStrings.InvalidKeyValue("Entity.ForeignKeyAssociateCollection#ForeignKeyAssociateEntity", "Id"), ex.Message); - } - - [Theory, InlineData(true), InlineData(false)] - public async Task ForeignKeyAssociateCollection_missing_primary_key_doesnt_throw_and_uses_ordinal_workaround(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "Id": 1, - "id": "1", - "Associate": { - "Id": 2 - }, - "KeyedAssociate": {}, - "AssociateCollection": [ - { - "Id": 4 - }, - { - "Id": 5 - } - ], - "KeyedAssociateCollection": [ - { - "Id": 6, - "EntityId": 1 - }, - { - "Id": 7, - "EntityId": 1 - } - ], - "ForeignKeyAssociate": { - "Id": 8 - }, - "KeyedForeignKeyAssociate": { - "Id": 9, - "EntityId": 1 - }, - "ForeignKeyAssociateCollection": [ - { - }, - { - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": 12, - "EntityId": 1 - }, - { - "Id": 13, - "EntityId": 1 - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var result = await query.SingleAsync(); - Assert.Equal(1, result.ForeignKeyAssociateCollection.First().Id); - Assert.Equal(2, result.ForeignKeyAssociateCollection.Last().Id); - } - - [Theory, InlineData(true), InlineData(false)] - public async Task KeyedForeignKeyAssociateCollection_null_primary_key_throws(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "Id": 1, - "id": "1", - "Associate": { - "Id": 2 - }, - "KeyedAssociate": {}, - "AssociateCollection": [ - { - "Id": 4 - }, - { - "Id": 5 - } - ], - "KeyedAssociateCollection": [ - { - "Id": 6, - "EntityId": 1 - }, - { - "Id": 7, - "EntityId": 1 - } - ], - "ForeignKeyAssociate": { - "Id": 8 - }, - "KeyedForeignKeyAssociate": { - "Id": 9, - "EntityId": 1 - }, - "ForeignKeyAssociateCollection": [ - { - "Id": 10 - }, - { - "Id": 11 - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": null, - "EntityId": 1 - }, - { - "Id": null, - "EntityId": 1 - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var ex = await Assert.ThrowsAsync(() => query.SingleOrDefaultAsync()); - Assert.Equal(CoreStrings.InvalidKeyValue("Entity.KeyedForeignKeyAssociateCollection#ForeignKeyAssociateEntity", "Id"), ex.Message); - } - - [Theory, InlineData(true), InlineData(false)] - public async Task KeyedForeignKeyAssociateCollection_missing_primary_key_throws(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "Id": 1, - "id": "1", - "Associate": { - "Id": 2 - }, - "KeyedAssociate": {}, - "AssociateCollection": [ - { - "Id": 4 - }, - { - "Id": 5 - } - ], - "KeyedAssociateCollection": [ - { - "Id": 6, - "EntityId": 1 - }, - { - "Id": 7, - "EntityId": 1 - } - ], - "ForeignKeyAssociate": { - "Id": 8 - }, - "KeyedForeignKeyAssociate": { - "Id": 9, - "EntityId": 1 - }, - "ForeignKeyAssociateCollection": [ - { - "Id": 10 - }, - { - "Id": 11 - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "EntityId": 1 - }, - { - "EntityId": 1 - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var ex = await Assert.ThrowsAsync(() => query.SingleOrDefaultAsync()); - Assert.Equal(CoreStrings.InvalidKeyValue("Entity.KeyedForeignKeyAssociateCollection#ForeignKeyAssociateEntity", "Id"), ex.Message); - } - - [Theory, InlineData(true), InlineData(false)] - public async Task KeyedForeignKeyAssociateCollection_null_foreign_key_doesnt_throw(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "Id": 1, - "id": "1", - "Associate": { - "Id": 2 - }, - "KeyedAssociate": {}, - "AssociateCollection": [ - { - "Id": 4 - }, - { - "Id": 5 - } - ], - "KeyedAssociateCollection": [ - { - "Id": 6, - "EntityId": 1 - }, - { - "Id": 7, - "EntityId": 1 - } - ], - "ForeignKeyAssociate": { - "Id": 8 - }, - "KeyedForeignKeyAssociate": { - "Id": 9, - "EntityId": 1 - }, - "ForeignKeyAssociateCollection": [ - { - "Id": 10 - }, - { - "Id": 11 - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": 12, - "EntityId": null - }, - { - "Id": 13, - "EntityId": null - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var result = await query.SingleAsync(); - if (tracking) - { - Assert.Empty(result.KeyedForeignKeyAssociateCollection); - } - else - { - foreach (var item in result.KeyedForeignKeyAssociateCollection) - { - Assert.Equal(0, item.EntityId); - } - } - } - - [Theory, InlineData(true), InlineData(false)] - public async Task KeyedForeignKeyAssociateCollection_missing_foreign_key_doesnt_throw(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "Id": 1, - "id": "1", - "Associate": { - "Id": 2 - }, - "KeyedAssociate": {}, - "AssociateCollection": [ - { - "Id": 4 - }, - { - "Id": 5 - } - ], - "KeyedAssociateCollection": [ - { - "Id": 6, - "EntityId": 1 - }, - { - "Id": 7, - "EntityId": 1 - } - ], - "ForeignKeyAssociate": { - "Id": 8 - }, - "KeyedForeignKeyAssociate": { - "Id": 9, - "EntityId": 1 - }, - "ForeignKeyAssociateCollection": [ - { - "Id": 10 - }, - { - "Id": 11 - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": 12 - }, - { - "Id": 13 - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var result = await query.SingleAsync(); - if (tracking) - { - Assert.Empty(result.KeyedForeignKeyAssociateCollection); - } - else - { - foreach (var item in result.KeyedForeignKeyAssociateCollection) - { - Assert.Equal(0, item.EntityId); - } - } - } - - [Theory, InlineData(true), InlineData(false)] - public async Task KeyedForeignKeyAssociateCollection_incorrect_foreign_key_doesnt_throw(bool tracking) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: async context => - { - var client = context.Database.GetCosmosClient(); - var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); - - var entity = """ -{ - "$type": "Entity", - "Id": 1, - "id": "1", - "Associate": { - "Id": 2 - }, - "KeyedAssociate": {}, - "AssociateCollection": [ - { - "Id": 4 - }, - { - "Id": 5 - } - ], - "KeyedAssociateCollection": [ - { - "Id": 6, - "EntityId": 1 - }, - { - "Id": 7, - "EntityId": 1 - } - ], - "ForeignKeyAssociate": { - "Id": 8 - }, - "KeyedForeignKeyAssociate": { - "Id": 9, - "EntityId": 1 - }, - "ForeignKeyAssociateCollection": [ - { - "Id": 10 - }, - { - "Id": 11 - } - ], - "KeyedForeignKeyAssociateCollection": [ - { - "Id": 12, - "EntityId": 99 - }, - { - "Id": 13, - "EntityId": 99 - } - ] -} -"""; - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - container, - entity, - CancellationToken.None); - }); - - using var context = contextFactory.CreateDbContext(); - IQueryable query = context.Entities; - if (!tracking) - { - query = query.AsNoTracking(); - } - - var result = await query.SingleAsync(); - if (tracking) - { - Assert.Empty(result.KeyedForeignKeyAssociateCollection); - } - else - { - foreach (var item in result.KeyedForeignKeyAssociateCollection) - { - Assert.Equal(99, item.EntityId); - } - } - } - - private class ContextBadPrimaryKeyJsonProperties : DbContext - { - public ContextBadPrimaryKeyJsonProperties(DbContextOptions options) - : base(options) - { - } - - public DbSet Entities { get; set; } = null!; - - public class Entity - { - public int Id { get; set; } - - public AssociateEntity Associate { get; set; } = null!; - public AssociateEntity KeyedAssociate { get; set; } = null!; - - public List AssociateCollection { get; set; } = new(); - public List KeyedAssociateCollection { get; set; } = new(); - - public ForeignKeyAssociateEntity ForeignKeyAssociate { get; set; } = null!; - public ForeignKeyAssociateEntity KeyedForeignKeyAssociate { get; set; } = null!; - - public List ForeignKeyAssociateCollection { get; set; } = new(); - public List KeyedForeignKeyAssociateCollection { get; set; } = new(); - } - - public class AssociateEntity - { - public int Id { get; set; } - } - - public class ForeignKeyAssociateEntity - { - public int Id { get; set; } - - public int EntityId { get; set; } - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - var entity = modelBuilder.Entity(); - entity.ToContainer("Entities"); - entity.OwnsOne(x => x.Associate); - entity.OwnsOne(x => x.KeyedAssociate, b => b.HasKey(x => x.Id)); - entity.OwnsMany(x => x.AssociateCollection); - entity.OwnsMany(x => x.KeyedAssociateCollection, b => b.HasKey(x => x.Id)); - - entity.OwnsOne(x => x.ForeignKeyAssociate); - entity.OwnsOne(x => x.KeyedForeignKeyAssociate, b => b.HasKey(x => x.Id)); - entity.OwnsMany(x => x.ForeignKeyAssociateCollection); - entity.OwnsMany(x => x.KeyedForeignKeyAssociateCollection, b => b.HasKey(x => x.Id)); - } - } - - #endregion - - protected TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ListLoggerFactory; - - private void AssertSql(params string[] expected) - => TestSqlLoggerFactory.AssertBaseline(expected); - - protected static async Task AssertTranslationFailed(Func query) - => Assert.Contains( - CoreStrings.TranslationFailed("")[48..], - (await Assert.ThrowsAsync(query)) - .Message); - - protected override DbContextOptionsBuilder AddNonSharedOptions(DbContextOptionsBuilder builder) - => builder.ConfigureWarnings(b => b.Ignore(CosmosEventId.NoPartitionKeyDefined)); - - protected override ITestStoreFactory NonSharedTestStoreFactory - => CosmosTestStoreFactory.Instance; -} diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs index 17974345b67..5e4d89b10f1 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeQueryCosmosTest.cs @@ -548,6 +548,430 @@ public override Task Project_entity_with_complex_type_pushdown_and_then_left_joi () => base.Project_entity_with_complex_type_pushdown_and_then_left_join(async), CosmosStrings.LimitOffsetNotSupportedInSubqueries); + #region Non-shared test resources + + // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/288 (Complex-type equality comparisons return no results) + public override async Task Complex_type_equals_parameter_with_nested_types_with_property_of_same_name() + { + CosmosTestEnvironment.SkipOnLinuxEmulator(); + + 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(); + + AssertSql( + """ +SELECT VALUE c["Complex"] +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 +""", + // + """ +SELECT VALUE c +FROM root c +"""); + } + + 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 Non_optional_complex_type_with_all_nullable_properties_via_left_join() + { + Assert.Equal( + CosmosStrings.UpdateConflict("1"), + (await Assert.ThrowsAsync( + base.Non_optional_complex_type_with_all_nullable_properties_via_left_join)).Message); + + AssertSql(); + } + + 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 +"""); + } + + public override async Task Update_entity_with_nullable_complex_type_and_discriminator_does_not_throw() + { + await base.Update_entity_with_nullable_complex_type_and_discriminator_does_not_throw(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +""", + // + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +"""); + } + + public override async Task Nullable_complex_type_with_discriminator_null_to_non_null_roundtrip() + { + await base.Nullable_complex_type_with_discriminator_null_to_non_null_roundtrip(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +""", + // + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +"""); + } + + public override async Task Nullable_complex_type_with_discriminator_non_null_to_null_roundtrip() + { + await base.Nullable_complex_type_with_discriminator_non_null_to_null_roundtrip(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +""", + // + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +"""); + } + + public override async Task Nullable_complex_type_with_discriminator_update_non_null_entity_roundtrip() + { + await base.Nullable_complex_type_with_discriminator_update_non_null_entity_roundtrip(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +""", + // + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +"""); + } + + public override async Task Nullable_complex_type_with_discriminator_set_to_different_value() + => await base.Nullable_complex_type_with_discriminator_set_to_different_value(); + + public override async Task Nullable_complex_type_with_discriminator_set_to_null() + { + // On Cosmos, setting the discriminator shadow property to null doesn't affect materialization + // because the complex property's data is still present in the JSON document. + var contextFactory = await InitializeNonSharedTest(); + + Guid entityId; + await using (var context = contextFactory.CreateDbContext()) + { + var entity = new Context38119.EntityType + { + Id = Guid.NewGuid(), + Prop = new Context38119.OptionalComplexProperty { OptionalValue = true } + }; + context.Add(entity); + entityId = entity.Id; + + var discriminatorEntry = context.Entry(entity).ComplexProperty(e => e.Prop).Property("Discriminator"); + Assert.Equal("OptionalComplexProperty", discriminatorEntry.CurrentValue); + discriminatorEntry.CurrentValue = null; + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateDbContext()) + { + var entity = await context.Set().SingleAsync(e => e.Id == entityId); + Assert.NotNull(entity.Prop); + Assert.True(entity.Prop.OptionalValue); + } + } + + public override async Task Nested_nullable_complex_type_with_discriminator_null_to_non_null_roundtrip() + { + await base.Nested_nullable_complex_type_with_discriminator_null_to_non_null_roundtrip(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +""", + // + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +"""); + } + + public override async Task Can_query_by_complex_type_property_with_index() + { + await base.Can_query_by_complex_type_property_with_index(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["Address"]["City"] = "Seattle") +OFFSET 0 LIMIT 2 +"""); + } + + public override async Task Can_update_entity_with_index_on_complex_type_property() + { + await base.Can_update_entity_with_index_on_complex_type_property(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +""", + // + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +"""); + } + + public override async Task Can_delete_entity_with_index_on_complex_type_property() + { + await base.Can_delete_entity_with_index_on_complex_type_property(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +""", + // + """ +SELECT VALUE COUNT(1) +FROM root c +"""); + } + + public override async Task Can_query_by_alternate_key_on_complex_type_property() + { + await base.Can_query_by_alternate_key_on_complex_type_property(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["Address"]["City"] = "Redmond") +OFFSET 0 LIMIT 2 +"""); + } + + public override async Task Can_save_batch_swapping_alternate_key_values_on_complex_type_property() + { + // Unlike relational providers, Cosmos can't ORDER BY the primary-key path 'Id.Id': the model defines an + // explicit index (on Address.PostalCode), which disables automatic indexing and therefore excludes the + // 'Id.Id' path from the index. Order client-side instead - the test's intent (reading the alternate-key + // values while building batch edges) is unaffected. + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + context.AddRange( + new Context31246.Person + { + Id = new Context31246.StronglyTypedId(1), + Address = new Context31246.Address { City = "Seattle", PostalCode = "98101" } + }, + new Context31246.Person + { + Id = new Context31246.StronglyTypedId(2), + Address = new Context31246.Address { City = "Redmond", PostalCode = "98052" } + }); + return context.SaveChangesAsync(); + }); + + await using (var context = contextFactory.CreateDbContext()) + { + var people = (await context.Set().ToListAsync()).OrderBy(p => p.Id.Id).ToList(); + people[0].Address.PostalCode = "98103"; + people[1].Address.PostalCode = "98054"; + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateDbContext()) + { + var people = (await context.Set().ToListAsync()).OrderBy(p => p.Id.Id).ToList(); + Assert.Equal("98103", people[0].Address.PostalCode); + Assert.Equal("98054", people[1].Address.PostalCode); + } + } + + [Fact] + public virtual async Task Can_use_complex_type_key_with_discriminator_in_json_id() + { + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + context.AddRange( + new ComplexKeyDiscriminatorContext.Customer { Id = new ComplexKeyDiscriminatorContext.CustomerId(1), Name = "Alice" }, + new ComplexKeyDiscriminatorContext.Customer { Id = new ComplexKeyDiscriminatorContext.CustomerId(2), Name = "Bob" }); + return context.SaveChangesAsync(); + }); + + await using var context = contextFactory.CreateDbContext(); + + var customer = await context.Set().SingleAsync(c => c.Id.Value == 1); + Assert.Equal(new ComplexKeyDiscriminatorContext.CustomerId(1), customer.Id); + Assert.Equal("Alice", customer.Name); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["Id"]["Value"] = 1) +OFFSET 0 LIMIT 2 +"""); + } + + [Fact] + public virtual async Task Can_use_nested_complex_type_key_with_discriminator_in_json_id() + { + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + context.AddRange( + new NestedComplexKeyDiscriminatorContext.Order + { + Key = new NestedComplexKeyDiscriminatorContext.OrderKey + { + Inner = new NestedComplexKeyDiscriminatorContext.InnerKey { Value = 1 } + }, + Description = "First" + }, + new NestedComplexKeyDiscriminatorContext.Order + { + Key = new NestedComplexKeyDiscriminatorContext.OrderKey + { + Inner = new NestedComplexKeyDiscriminatorContext.InnerKey { Value = 2 } + }, + Description = "Second" + }); + return context.SaveChangesAsync(); + }); + + await using var context = contextFactory.CreateDbContext(); + + var order = await context.Set().SingleAsync(o => o.Key.Inner.Value == 2); + Assert.Equal(2, order.Key.Inner.Value); + Assert.Equal("Second", order.Description); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["Key"]["Inner"]["Value"] = 2) +OFFSET 0 LIMIT 2 +"""); + } + + protected class ComplexKeyDiscriminatorContext(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.ComplexProperty(e => e.Id); + b.HasKey(e => e.Id.Value); + b.Property(e => e.Id.Value).ValueGeneratedNever(); + b.HasDiscriminatorInJsonId(); + }); + + public readonly record struct CustomerId(int Value); + + public class Customer + { + public CustomerId Id { get; set; } + public string Name { get; set; } = null!; + } + } + + protected class NestedComplexKeyDiscriminatorContext(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.ComplexProperty(e => e.Key, kb => kb.ComplexProperty(k => k.Inner)); + b.HasKey(e => e.Key.Inner.Value); + b.Property(e => e.Key.Inner.Value).ValueGeneratedNever(); + b.HasDiscriminatorInJsonId(); + }); + + public class Order + { + public OrderKey Key { get; set; } = null!; + public string Description { get; set; } = null!; + } + + public class OrderKey + { + public InnerKey Inner { get; set; } = null!; + } + + public class InnerKey + { + public int Value { get; set; } + } + } + + #endregion + [Fact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeToJsonPropertyQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeToJsonPropertyQueryCosmosTest.cs index e829c455abd..8076ea87bc0 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeToJsonPropertyQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/ComplexTypeToJsonPropertyQueryCosmosTest.cs @@ -487,6 +487,300 @@ public override Task Same_entity_with_complex_type_projected_twice_with_pushdown public override Task Same_complex_type_projected_twice_with_pushdown_as_part_of_another_projection(bool async) => AssertTranslationFailed(() => base.Same_complex_type_projected_twice_with_pushdown_as_part_of_another_projection(async)); + // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/288 (Complex-type equality comparisons return no results) + public override async Task Complex_type_equals_parameter_with_nested_types_with_property_of_same_name() + { + CosmosTestEnvironment.SkipOnLinuxEmulator(); + + await base.Complex_type_equals_parameter_with_nested_types_with_property_of_same_name(); + } + + public override async Task Projecting_complex_property_does_not_auto_include_owned_types() + { + await base.Projecting_complex_property_does_not_auto_include_owned_types(); + + AssertSql( + """ +SELECT VALUE c["Complex"] +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 +""", + // + """ +SELECT VALUE c +FROM root c +"""); + } + + 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 Non_optional_complex_type_with_all_nullable_properties_via_left_join() + { + Assert.Equal( + CosmosStrings.UpdateConflict("1"), + (await Assert.ThrowsAsync( + base.Non_optional_complex_type_with_all_nullable_properties_via_left_join)).Message); + + AssertSql(); + } + + 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 +"""); + } + + public override async Task Nullable_complex_type_with_discriminator_null_to_non_null_roundtrip() + { + await base.Nullable_complex_type_with_discriminator_null_to_non_null_roundtrip(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +""", + // + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +"""); + } + + public override async Task Nullable_complex_type_with_discriminator_non_null_to_null_roundtrip() + { + await base.Nullable_complex_type_with_discriminator_non_null_to_null_roundtrip(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +""", + // + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +"""); + } + + public override async Task Nullable_complex_type_with_discriminator_update_non_null_entity_roundtrip() + { + await base.Nullable_complex_type_with_discriminator_update_non_null_entity_roundtrip(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +""", + // + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +"""); + } + + public override Task Nullable_complex_type_with_discriminator_set_to_different_value() + => base.Nullable_complex_type_with_discriminator_set_to_different_value(); + + public override async Task Nullable_complex_type_with_discriminator_set_to_null() + { + // On Cosmos, setting the discriminator shadow property to null doesn't affect materialization + // because the complex property's data is still present in the JSON document. + var contextFactory = await InitializeNonSharedTest(); + + Guid entityId; + await using (var context = contextFactory.CreateDbContext()) + { + var entity = new Context38119.EntityType + { + Id = Guid.NewGuid(), + Prop = new Context38119.OptionalComplexProperty { OptionalValue = true } + }; + context.Add(entity); + entityId = entity.Id; + + var discriminatorEntry = context.Entry(entity).ComplexProperty(e => e.Prop).Property("Discriminator"); + Assert.Equal("OptionalComplexProperty", discriminatorEntry.CurrentValue); + discriminatorEntry.CurrentValue = null; + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateDbContext()) + { + var entity = await context.Set().SingleAsync(e => e.Id == entityId); + Assert.NotNull(entity.Prop); + Assert.True(entity.Prop.OptionalValue); + } + } + + public override async Task Nested_nullable_complex_type_with_discriminator_null_to_non_null_roundtrip() + { + await base.Nested_nullable_complex_type_with_discriminator_null_to_non_null_roundtrip(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +""", + // + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +"""); + } + + public override async Task Update_entity_with_nullable_complex_type_and_discriminator_does_not_throw() + { + await base.Update_entity_with_nullable_complex_type_and_discriminator_does_not_throw(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +""", + // + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +"""); + } + + public override async Task Can_query_by_complex_type_property_with_index() + { + await base.Can_query_by_complex_type_property_with_index(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["Address"]["City"] = "Seattle") +OFFSET 0 LIMIT 2 +"""); + } + + public override async Task Can_update_entity_with_index_on_complex_type_property() + { + await base.Can_update_entity_with_index_on_complex_type_property(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +""", + // + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +"""); + } + + public override async Task Can_delete_entity_with_index_on_complex_type_property() + { + await base.Can_delete_entity_with_index_on_complex_type_property(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +OFFSET 0 LIMIT 2 +""", + // + """ +SELECT VALUE COUNT(1) +FROM root c +"""); + } + + public override async Task Can_query_by_alternate_key_on_complex_type_property() + { + await base.Can_query_by_alternate_key_on_complex_type_property(); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["Address"]["City"] = "Redmond") +OFFSET 0 LIMIT 2 +"""); + } + + public override async Task Can_save_batch_swapping_alternate_key_values_on_complex_type_property() + { + // Unlike relational providers, Cosmos can't ORDER BY the primary-key path 'Id.Id': the model defines an + // explicit index (on Address.PostalCode), which disables automatic indexing and therefore excludes the + // 'Id.Id' path from the index. Order client-side instead - the test's intent (reading the alternate-key + // values while building batch edges) is unaffected. + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + context.AddRange( + new Context31246.Person + { + Id = new Context31246.StronglyTypedId(1), + Address = new Context31246.Address { City = "Seattle", PostalCode = "98101" } + }, + new Context31246.Person + { + Id = new Context31246.StronglyTypedId(2), + Address = new Context31246.Address { City = "Redmond", PostalCode = "98052" } + }); + return context.SaveChangesAsync(); + }); + + await using (var context = contextFactory.CreateDbContext()) + { + var people = (await context.Set().ToListAsync()).OrderBy(p => p.Id.Id).ToList(); + people[0].Address.PostalCode = "98103"; + people[1].Address.PostalCode = "98054"; + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateDbContext()) + { + var people = (await context.Set().ToListAsync()).OrderBy(p => p.Id.Id).ToList(); + Assert.Equal("98103", people[0].Address.PostalCode); + Assert.Equal("98054", people[1].Address.PostalCode); + } + } + #region GroupBy [Theory(Skip = "#17313 Cosmos: Translate GroupBy")] diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/JsonQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/JsonQueryCosmosTest.cs index d9559253c2c..d868de51efe 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/JsonQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/JsonQueryCosmosTest.cs @@ -1,8 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Text.Json; +using System.Text.Json.Nodes; using Microsoft.Azure.Cosmos; using Microsoft.EntityFrameworkCore.Cosmos.Internal; +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; using Microsoft.EntityFrameworkCore.TestModels.JsonQuery; namespace Microsoft.EntityFrameworkCore.Query; @@ -2275,6 +2278,4099 @@ public override async Task Project_json_entity_in_tracking_query_fails_even_when Assert.Equal(CoreStrings.OwnedEntitiesCannotBeTrackedWithoutTheirOwner, message); } + protected override void ConfigureWarnings(WarningsConfigurationBuilder builder) + { + base.ConfigureWarnings(builder); + + builder.Ignore(CosmosEventId.NoPartitionKeyDefined, CosmosEventId.PrimaryKeyValueNotSet); + } + + #region Non-shared test resources + +#nullable disable + + #region 21006 + + public override Task Project_root_with_missing_scalars(bool async) + => CosmosTestHelpers.Instance.NoSyncTest(async, async async => + { + await base.Project_root_with_missing_scalars(async); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["Id"] < 4) +"""); + }); + + [Theory(Skip = "issue #35702")] + public override Task Project_top_level_json_entity_with_missing_scalars(bool async) + => CosmosTestHelpers.Instance.NoSyncTest(async, async async => + { + await base.Project_top_level_json_entity_with_missing_scalars(async); + + AssertSql(); + }); + + public override async Task Project_nested_json_entity_with_missing_scalars(bool async) + { + // Throws sync not supported exception for sync + if (async) + { + await AssertTranslationFailed(() => base.Project_nested_json_entity_with_missing_scalars(async)); + + AssertSql(); + } + } + + public override Task Project_top_level_entity_with_null_value_required_scalars(bool async) + => CosmosTestHelpers.Instance.NoSyncTest(async, async async => + { + await base.Project_top_level_entity_with_null_value_required_scalars(async); + + AssertSql( + """ +SELECT c["Id"], c["RequiredReference"] +FROM root c +WHERE (c["Id"] = 4) +"""); + }); + + [Fact] + public virtual async Task Project_entity_with_null_value_required_scalars() + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateDbContext(); + + var result = await context.Set().Where(x => x.Id == 4).AsNoTracking().ToListAsync(); + + var nullScalars = result.Single(); + + Assert.Equal(default, nullScalars.RequiredReference.Number); + } + + [Fact] + public virtual async Task Project_null_value_required_scalar() + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateDbContext(); + + // Same as in 10.0 + var ex = await Assert.ThrowsAsync(() => context.Set().Where(x => x.Id == 4).Select(x => x.RequiredReference.Number).ToListAsync()); + Assert.Equal("Nullable object must have a value.", ex.Message); + } + + public override Task Project_root_entity_with_missing_required_navigation(bool async) + => CosmosTestHelpers.Instance.NoSyncTest(async, async async => + { + await base.Project_root_entity_with_missing_required_navigation(async); + + AssertSql( + """ +ReadItem(None, 5) +"""); + }); + + public override async Task Project_missing_required_navigation(bool async) + { + // Throws sync not supported exception for sync + if (async) + { + // Cosmos will filter out undefined result + var ex = await Assert.ThrowsAsync(() => base.Project_missing_required_navigation(async)); + Assert.Equal("Sequence contains no elements", ex.Message); + + AssertSql( + """ +SELECT VALUE c["RequiredReference"]["NestedRequiredReference"] +FROM root c +WHERE (c["Id"] = 5) +"""); + } + } + + public override Task Project_root_entity_with_null_required_navigation(bool async) + => CosmosTestHelpers.Instance.NoSyncTest(async, async async => + { + await base.Project_root_entity_with_null_required_navigation(async); + + AssertSql( + """ +ReadItem(None, 6) +"""); + }); + + public override Task Project_null_required_navigation(bool async) + => CosmosTestHelpers.Instance.NoSyncTest(async, async async => + { + await base.Project_null_required_navigation(async); + + AssertSql( + """ +SELECT VALUE c["RequiredReference"] +FROM root c +WHERE (c["Id"] = 6) +"""); + }); + + public override async Task Project_missing_required_scalar(bool async) + { + // Throws sync not supported exception for sync + if (async) + { + // https://github.com/dotnet/efcore/issues/38298#issuecomment-4726236589 + var exception = await Assert.ThrowsAsync(() => base.Project_missing_required_scalar(async)); + + Assert.Equal(CosmosStrings.ProjectionUndefined, exception.Message); + + AssertSql( + """ +SELECT c["Id"], c["RequiredReference"]["Number"] +FROM root c +WHERE (c["Id"] = 2) +"""); + } + } + + public override Task Project_null_required_scalar(bool async) + => CosmosTestHelpers.Instance.NoSyncTest(async, async async => + { + await base.Project_null_required_scalar(async); + + AssertSql( + """ +SELECT c["Id"], c["RequiredReference"]["Number"] +FROM root c +WHERE (c["Id"] = 4) +"""); + }); + + protected override void OnModelCreating21006(ModelBuilder modelBuilder) + { + base.OnModelCreating21006(modelBuilder); + + modelBuilder.Entity().ToContainer("Entities"); + } + + protected override async Task Seed21006(Context21006 context) + { + await base.Seed21006(context); + + var wrapper = (CosmosClientWrapper)context.GetService(); + var singletonWrapper = context.GetService(); + var entitiesContainer = singletonWrapper.Client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var missingTopLevel = + """ +{ + "Id": 2, + "$type": "Entity", + "Name": "e2", + "id": "2", + "Collection": + [ + { + "Text": "e2 c1", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 c1 c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 c1 c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 c1 nor" + }, + "NestedRequiredReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 c1 nrr" + } + }, + { + "Text": "e2 c2", + "NestedCollection": [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 c2 c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 c2 c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 c2 nor" + }, + "NestedRequiredReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 c2 nrr" + } + } + ], + "OptionalReference": { + "Text": "e2 or", + "NestedCollection": + [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 or c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 or c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 or nor" + }, + "NestedRequiredReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 or nrr" + } + }, + "RequiredReference": { + "Text": "e2 rr", + "NestedCollection": + [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 rr c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 rr c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 rr nor" + }, + "NestedRequiredReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e2 rr nrr" + } + } +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + missingTopLevel, + CancellationToken.None); + + var missingNested = + """ +{ + "Id": 3, + "$type": "Entity", + "Name": "e3", + "id": "3", + "Collection": + [ + { + "Number": 7.0, + "Text": "e3 c1", + "NestedCollection": + [ + { + "Text": "e3 c1 c1" + }, + { + "Text": "e3 c1 c2" + } + ], + "NestedOptionalReference": { + "Text": "e3 c1 nor" + }, + "NestedRequiredReference": { + "Text": "e3 c1 nrr" + } + }, + { + "Number": 7.0, + "Text": "e3 c2", + "NestedCollection": + [ + { + "Text": "e3 c2 c1" + }, + { + "Text": "e3 c2 c2" + } + ], + "NestedOptionalReference": { + "Text": "e3 c2 nor" + }, + "NestedRequiredReference": { + "Text": "e3 c2 nrr" + } + } + ], + "OptionalReference": { + "Number": 7.0, + "Text": "e3 or", + "NestedCollection": + [ + { + "Text": "e3 or c1" + }, + { + "Text": "e3 or c2" + } + ], + "NestedOptionalReference": { + "Text": "e3 or nor" + }, + "NestedRequiredReference": { + "Text": "e3 or nrr" + } + }, + "RequiredReference": { + "Number": 7.0, + "Text": "e3 rr", + "NestedCollection": + [ + { + "Text": "e3 rr c1" + }, + { + "Text": "e3 rr c2" + } + ], + "NestedOptionalReference": { + "Text": "e3 rr nor" + }, + "NestedRequiredReference": { + "Text": "e3 rr nrr" + } + } +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + missingNested, + CancellationToken.None); + + var nullTopLevel = + """ +{ + "Id": 4, + "$type": "Entity", + "Name": "e4", + "id": "4", + "Collection": + [ + { + "Number": null, + "Text": "e4 c1", + "NestedCollection": + [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 c1 c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 c1 c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 c1 nor" + }, + "NestedRequiredReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 c1 nrr" + } + }, + { + "Number": null, + "Text": "e4 c2", + "NestedCollection": + [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 c2 c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 c2 c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 c2 nor" + }, + "NestedRequiredReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 c2 nrr" + } + } + ], + "OptionalReference": { + "Number": null, + "Text": "e4 or", + "NestedCollection": + [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 or c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 or c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 or nor" + }, + "NestedRequiredReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 or nrr" + } + }, + "RequiredReference": { + "Number": null, + "Text": "e4 rr", + "NestedCollection": + [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 rr c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 rr c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 rr nor" + }, + "NestedRequiredReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e4 rr nrr" + } + } +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + nullTopLevel, + CancellationToken.None); + + var missingRequiredNav = + """ +{ + "Id": 5, + "$type": "Entity", + "Name": "e5", + "id": "5", + "Collection": + [ + { + "Number": 7.0, + "Text": "e5 c1", + "NestedCollection": + [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 c1 c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 c1 c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 c1 nor" + } + }, + { + "Number": 7.0, + "Text": "e5 c2", + "NestedCollection": + [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 c2 c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 c2 c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 c2 nor" + } + } + ], + "OptionalReference": { + "Number": 7.0, + "Text": "e5 or", + "NestedCollection": + [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 or c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 or c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 or nor" + } + }, + "RequiredReference": { + "Number": 7.0, + "Text": "e5 rr", + "NestedCollection": + [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 rr c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 rr c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e5 rr nor" + } + } +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + missingRequiredNav, + CancellationToken.None); + + var nullRequiredNav = + """ +{ + "Id": 6, + "$type": "Entity", + "Name": "e6", + "id": "6", + "Collection": + [ + { + "Number": 7.0, + "Text": "e6 c1", + "NestedCollection": + [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 c1 c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 c1 c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 c1 nor" + }, + "NestedRequiredReference": null + }, + { + "Number": 7.0, + "Text": "e6 c2", + "NestedCollection": + [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 c2 c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 c2 c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 c2 nor" + }, + "NestedRequiredReference": null + } + ], + "OptionalReference": { + "Number": 7.0, + "Text": "e6 or", + "NestedCollection": + [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 or c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 or c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 or nor" + }, + "NestedRequiredReference": null + }, + "RequiredReference": { + "Number": 7.0, + "Text": "e6 rr", + "NestedCollection": + [ + { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 rr c1" + }, + { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 rr c2" + } + ], + "NestedOptionalReference": { + "DoB": "2000-01-01T00:00:00", + "Text": "e6 rr nor" + }, + "NestedRequiredReference": null + } +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + nullRequiredNav, + CancellationToken.None); + } + + #endregion + + #region 29219 + + // Cosmos returns unexpected number of results (i.e. not returning row with non-existent NullableScalar + // this is by design behavior in Cosmos, so we just skip the test to avoid validation error + public override Task Can_project_nullable_json_property_when_the_element_in_json_is_not_present() + => Task.CompletedTask; + + protected override void OnModelCreating29219(ModelBuilder modelBuilder) + { + base.OnModelCreating29219(modelBuilder); + + modelBuilder.Entity().ToContainer("Entities"); + } + + protected override async Task Seed29219(DbContext context) + { + await base.Seed29219(context); + + var wrapper = (CosmosClientWrapper)context.GetService(); + var singletonWrapper = context.GetService(); + var entitiesContainer = singletonWrapper.Client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var missingNullableScalars = + """ +{ + "Id": 3, + "$type": "MyEntity", + "id": "3", + "Collection": + [ + { + "NonNullableScalar" : 10001 + } + ], + "Reference": { + "NonNullableScalar" : 30 + } +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + missingNullableScalars, + CancellationToken.None); + } + + #endregion + + #region 30028 + + [Theory(Skip = "issue #35702")] + public override Task Missing_navigation_works_with_deduplication(bool async) + => base.Missing_navigation_works_with_deduplication(async); + + // missing array comes out as empty on Cosmos + public override Task Accessing_missing_navigation_works() + => Task.CompletedTask; + + protected override void OnModelCreating30028(ModelBuilder modelBuilder) + { + base.OnModelCreating30028(modelBuilder); + + modelBuilder.Entity().ToContainer("Entities"); + } + + protected override async Task Seed30028(DbContext context) + { + var wrapper = (CosmosClientWrapper)context.GetService(); + var singletonWrapper = context.GetService(); + var entitiesContainer = singletonWrapper.Client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var complete = + """ +{ + "Id": 1, + "$type": "MyEntity", + "id": "1", + "Json": { + "RootName":"e1", + "Collection": + [ + { + "BranchName":"e1 c1", + "Nested":{ + "LeafName":"e1 c1 l" + } + }, + { + "BranchName":"e1 c2", + "Nested":{ + "LeafName":"e1 c2 l" + } + } + ], + "OptionalReference":{ + "BranchName":"e1 or", + "Nested":{ + "LeafName":"e1 or l" + } + }, + "RequiredReference":{ + "BranchName":"e1 rr", + "Nested":{ + "LeafName":"e1 rr l" + } + } + } +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + complete, + CancellationToken.None); + + var missingCollection = + """ +{ + "Id": 2, + "$type": "MyEntity", + "id": "2", + "Json": { + "RootName":"e2", + "OptionalReference":{ + "BranchName":"e2 or", + "Nested":{ + "LeafName":"e2 or l" + } + }, + "RequiredReference":{ + "BranchName":"e2 rr", + "Nested":{ + "LeafName":"e2 rr l" + } + } + } +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + missingCollection, + CancellationToken.None); + + var missingOptionalReference = + """ +{ + "Id": 3, + "$type": "MyEntity", + "id": "3", + "Json": { + "RootName":"e3", + "Collection": + [ + { + "BranchName":"e3 c1", + "Nested":{ + "LeafName":"e3 c1 l" + } + }, + { + "BranchName":"e3 c2", + "Nested":{ + "LeafName":"e3 c2 l" + } + } + ], + "RequiredReference":{ + "BranchName":"e3 rr", + "Nested":{ + "LeafName":"e3 rr l" + } + } + } +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + missingOptionalReference, + CancellationToken.None); + + var missingRequiredReference = + """ +{ + "Id": 4, + "$type": "MyEntity", + "id": "4", + "Json": { + "RootName":"e4", + "Collection": + [ + { + "BranchName":"e4 c1", + "Nested":{ + "LeafName":"e4 c1 l" + } + }, + { + "BranchName":"e4 c2", + "Nested":{ + "LeafName":"e4 c2 l" + } + } + ], + "OptionalReference":{ + "BranchName":"e4 or", + "Nested":{ + "LeafName":"e4 or l" + } + } + } +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + missingRequiredReference, + CancellationToken.None); + } + + #endregion + + #region 33046 + + protected override void OnModelCreating33046(ModelBuilder modelBuilder) + { + base.OnModelCreating33046(modelBuilder); + + modelBuilder.Entity().ToContainer("Reviews"); + } + + protected override async Task Seed33046(DbContext context) + { + var wrapper = (CosmosClientWrapper)context.GetService(); + var singletonWrapper = context.GetService(); + var entitiesContainer = singletonWrapper.Client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Reviews"); + + var json = + """ +{ + "Id": 1, + "$type": "Review", + "id": "1", + "Rounds": + [ + { + "RoundNumber":11, + "SubRounds": + [ + { + "SubRoundNumber":111 + }, + { + "SubRoundNumber":112 + } + ] + } + ] +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + json, + CancellationToken.None); + } + + #endregion + + #region 34960 + + public override async Task Try_project_collection_but_JSON_is_entity() + { + var message = (await Assert.ThrowsAsync(() => base.Try_project_collection_but_JSON_is_entity())) + .Message; + + Assert.Equal(CoreStrings.JsonReaderInvalidTokenType(JsonTokenType.StartObject), message); + } + + public override async Task Try_project_reference_but_JSON_is_collection() + { + var message = (await Assert.ThrowsAsync(() => base.Try_project_reference_but_JSON_is_collection())) + .Message; + + Assert.Equal(CoreStrings.JsonReaderInvalidTokenType(JsonTokenType.StartArray), message); + } + + protected override void OnModelCreating34960(ModelBuilder modelBuilder) + { + base.OnModelCreating34960(modelBuilder); + modelBuilder.Entity().ToContainer("Entities"); + modelBuilder.Entity().ToContainer("Junk"); + } + + protected override async Task Seed34960(Context34960 context) + { + await base.Seed34960(context); + + var wrapper = (CosmosClientWrapper)context.GetService(); + var singletonWrapper = context.GetService(); + var entitiesContainer = singletonWrapper.Client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var json = + """ +{ + "Id": 4, + "$type": "Entity", + "id": "4", + "Collection": null, + "Reference": null +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + json, + CancellationToken.None); + + var junkContainer = singletonWrapper.Client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Junk"); + + var objectWhereCollectionShouldBe = + """ +{ + "Id": 1, + "$type": "JunkEntity", + "id": "1", + "Collection": { "DoB":"2000-01-01T00:00:00","Text":"junk" }, + "Reference": null +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + junkContainer, + objectWhereCollectionShouldBe, + CancellationToken.None); + + var collectionWhereEntityShouldBe = + """ +{ + "Id": 2, + "$type": "JunkEntity", + "id": "2", + "Collection": null, + "Reference": [{ "DoB":"2000-01-01T00:00:00","Text":"junk" }] +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + junkContainer, + collectionWhereEntityShouldBe, + CancellationToken.None); + } + + #endregion + + #region ArrayOfPrimitives + + public override async Task Project_element_of_json_array_of_primitives() + { + // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/335 + CosmosTestEnvironment.SkipOnLinuxEmulator(); + + await base.Project_element_of_json_array_of_primitives(); + } + + protected override void OnModelCreatingArrayOfPrimitives(ModelBuilder modelBuilder) + => base.OnModelCreatingArrayOfPrimitives(modelBuilder); + + #endregion + + #region JunkInJson + + protected override void OnModelCreatingJunkInJson(ModelBuilder modelBuilder) + { + base.OnModelCreatingJunkInJson(modelBuilder); + + modelBuilder.Entity().ToContainer("Entities"); + } + + protected override async Task SeedJunkInJson(DbContext context) + { + var wrapper = (CosmosClientWrapper)context.GetService(); + var singletonWrapper = context.GetService(); + var entitiesContainer = singletonWrapper.Client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var json = + """ +{ + "Id": 1, + "$type": "MyEntity", + "id": "1", + "Collection": + [ + { + "JunkReference": { + "Something":"SomeValue" + }, + "Name":"c11", + "JunkProperty1":50, + "Number":11.5, + "JunkCollection1":[], + "JunkCollection2": + [ + { + "Foo":"junk value" + } + ], + "NestedCollection": + [ + { + "DoB":"2002-04-01T00:00:00", + "DummyProp":"Dummy value" + }, + { + "DoB":"2002-04-02T00:00:00", + "DummyReference":{ + "Foo":5 + } + } + ], + "NestedReference":{ + "DoB":"2002-03-01T00:00:00" + } + }, + { + "Name":"c12", + "Number":12.5, + "NestedCollection": + [ + { + "DoB":"2002-06-01T00:00:00" + }, + { + "DoB":"2002-06-02T00:00:00" + } + ], + "NestedDummy":59, + "NestedReference":{ + "DoB":"2002-05-01T00:00:00" + } + } + ], + "CollectionWithCtor": + [ + { + "MyBool":true, + "Name":"c11 ctor", + "JunkReference":{ + "Something":"SomeValue", + "JunkCollection": + [ + { + "Foo":"junk value" + } + ] + }, + "NestedCollection": + [ + { + "DoB":"2002-08-01T00:00:00" + }, + { + "DoB":"2002-08-02T00:00:00" + } + ], + "NestedReference":{ + "DoB":"2002-07-01T00:00:00" + } + }, + { + "MyBool":false, + "Name":"c12 ctor", + "NestedCollection": + [ + { + "DoB":"2002-10-01T00:00:00" + }, + { + "DoB":"2002-10-02T00:00:00" + } + ], + "JunkCollection": + [ + { + "Foo":"junk value" + } + ], + "NestedReference":{ + "DoB":"2002-09-01T00:00:00" + } + } + ], + "Reference": { + "Name":"r1", + "JunkCollection": + [ + { + "Foo":"junk value" + } + ], + "JunkReference":{ + "Something":"SomeValue" + }, + "Number":1.5, + "NestedCollection": + [ + { + "DoB":"2000-02-01T00:00:00", + "JunkReference":{ + "Something":"SomeValue" + } + }, + { + "DoB":"2000-02-02T00:00:00" + } + ], + "NestedReference":{ + "DoB":"2000-01-01T00:00:00" + } + }, + "ReferenceWithCtor":{ + "MyBool":true, + "JunkCollection": + [ + { + "Foo":"junk value" + } + ], + "Name":"r1 ctor", + "JunkReference":{ + "Something":"SomeValue" + }, + "NestedCollection": + [ + { + "DoB":"2001-02-01T00:00:00" + }, + { + "DoB":"2001-02-02T00:00:00" + } + ], + "NestedReference":{ + "JunkCollection": + [ + { + "Foo":"junk value" + } + ], + "DoB":"2001-01-01T00:00:00" + } + } +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + json, + CancellationToken.None); + } + + #endregion + + #region TrickyBuffering + + protected override void OnModelCreatingTrickyBuffering(ModelBuilder modelBuilder) + { + base.OnModelCreatingTrickyBuffering(modelBuilder); + + modelBuilder.Entity().ToContainer("Entities"); + } + + protected override async Task SeedTrickyBuffering(DbContext context) + { + var wrapper = (CosmosClientWrapper)context.GetService(); + var singletonWrapper = context.GetService(); + var entitiesContainer = singletonWrapper.Client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var json = + """ +{ + "Id": 1, + "$type": "MyEntity", + "id": "1", + "Reference": { + "Name": "r1", + "Number": 7, + "JunkReference": { + "Something": "SomeValue" + }, + "JunkCollection": + [ + { + "Foo": "junk value" + } + ], + "NestedReference": { + "DoB": "2000-01-01T00:00:00" + }, + "NestedCollection": + [ + { + "DoB": "2000-02-01T00:00:00", + "JunkReference": { + "Something": "SomeValue" + } + }, + { + "DoB": "2000-02-02T00:00:00" + } + ] + } +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + json, + CancellationToken.None); + } + + #endregion + + #region ShadowProperties + + public override async Task Project_shadow_properties_from_json_entity() + { + // https://github.com/Azure/azure-cosmos-db-emulator-docker/issues/335 + CosmosTestEnvironment.SkipOnLinuxEmulator(); + + await base.Project_shadow_properties_from_json_entity(); + } + + protected override void OnModelCreatingShadowProperties(ModelBuilder modelBuilder) + { + base.OnModelCreatingShadowProperties(modelBuilder); + + modelBuilder.Entity(b => + { + b.ToContainer("Entities"); + + //b.OwnsOne(x => x.Reference, b => + //{ + // // b.ToJson().HasColumnType(JsonColumnType); + // b.Property("ShadowString"); + //}); + + b.OwnsOne( + x => x.ReferenceWithCtor, b => + { + // b.ToJson().HasColumnType(JsonColumnType); + b.Property("Shadow_Int").ToJsonProperty("ShadowInt"); + }); + + //b.OwnsMany( + // x => x.Collection, b => + // { + // // b.ToJson().HasColumnType(JsonColumnType); + // b.Property("ShadowDouble"); + // }); + + //b.OwnsMany( + // x => x.CollectionWithCtor, b => + // { + // //b.ToJson().HasColumnType(JsonColumnType); + // b.Property("ShadowNullableByte"); + // }); + }); + } + + protected override async Task SeedShadowProperties(DbContext context) + { + var wrapper = (CosmosClientWrapper)context.GetService(); + var singletonWrapper = context.GetService(); + var entitiesContainer = singletonWrapper.Client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var json = + """ +{ + "Id": 1, + "$type": "MyEntity", + "id": "1", + "Name": "e1", + "Collection": + [ + { + "Name":"e1_c1","ShadowDouble":5.5 + }, + { + "ShadowDouble":20.5,"Name":"e1_c2" + } + ], + "CollectionWithCtor": + [ + { + "Name":"e1_c1 ctor","ShadowNullableByte":6 + }, + { + "ShadowNullableByte":null,"Name":"e1_c2 ctor" + } + ], + "Reference": { "Name":"e1_r", "ShadowString":"Foo" }, + "ReferenceWithCtor": { "ShadowInt":143,"Name":"e1_r ctor" } +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + json, + CancellationToken.None); + } + + #endregion + + #region LazyLoadingProxies + + protected override void OnModelCreatingLazyLoadingProxies(ModelBuilder modelBuilder) + => base.OnModelCreatingLazyLoadingProxies(modelBuilder); + + #endregion + + #region NotICollection + + protected override void OnModelCreatingNotICollection(ModelBuilder modelBuilder) + { + base.OnModelCreatingNotICollection(modelBuilder); + + modelBuilder.Entity().ToContainer("Entities"); + } + + protected override async Task SeedNotICollection(DbContext context) + { + var wrapper = (CosmosClientWrapper)context.GetService(); + var singletonWrapper = context.GetService(); + var entitiesContainer = singletonWrapper.Client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var json1 = + """ +{ + "Id": 1, + "$type": "MyEntity", + "id": "1", + "Json": + { + "Collection": + [ + { + "Bar":11,"Foo":"c11" + }, + { + "Bar":12,"Foo":"c12" + }, + { + "Bar":13,"Foo":"c13" + } + ] + } +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + json1, + CancellationToken.None); + + var json2 = + """ +{ + "Id": 2, + "$type": "MyEntity", + "id": "2", + "Json": { + "Collection": + [ + { + "Bar":21,"Foo":"c21" + }, + { + "Bar":22,"Foo":"c22" + } + ] + } +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + json2, + CancellationToken.None); + } + + #endregion + + #region BadJsonProperties + + // missing collection comes back as empty on Cosmos + public override Task Bad_json_properties_empty_navigations(bool noTracking) + => Task.CompletedTask; + + // Insertion of the bad data fails thanks to json validation by Cosmos DB + public override Task Bad_json_properties_null_navigations(bool noTracking) + => Task.CompletedTask; + + public override Task Bad_json_properties_null_scalars(bool noTracking) + => Task.CompletedTask; + + // Insertion of the bad data is deduplicated by Cosmos DB + public override Task Bad_json_properties_duplicated_navigations(bool noTracking) + => Task.CompletedTask; + + public override Task Bad_json_properties_duplicated_scalars(bool noTracking) + => Task.CompletedTask; + + protected override void OnModelCreatingBadJsonProperties(ModelBuilder modelBuilder) + { + base.OnModelCreatingBadJsonProperties(modelBuilder); + + modelBuilder.Entity().ToContainer("Entities"); + } + + protected override async Task SeedBadJsonProperties(ContextBadJsonProperties context) + { + var wrapper = (CosmosClientWrapper)context.GetService(); + var singletonWrapper = context.GetService(); + var entitiesContainer = singletonWrapper.Client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var baseline = + """ +{ + "Id": 1, + "$type": "Entity", + "id": "1", + "Scenario": "baseline", + "OptionalReference": {"NestedOptional": { "Text":"or no" }, "NestedRequired": { "Text":"or nr" }, "NestedCollection": [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ] }, + "RequiredReference": {"NestedOptional": { "Text":"rr no" }, "NestedRequired": { "Text":"rr nr" }, "NestedCollection": [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ] }, + "Collection": + [ + {"NestedOptional": { "Text":"c 1 no" }, "NestedRequired": { "Text":"c 1 nr" }, "NestedCollection": [ { "Text":"c 1 nc 1" }, { "Text":"c 1 nc 2" } ] }, + {"NestedOptional": { "Text":"c 2 no" }, "NestedRequired": { "Text":"c 2 nr" }, "NestedCollection": [ { "Text":"c 2 nc 1" }, { "Text":"c 2 nc 2" } ] } + ] +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + baseline, + CancellationToken.None); + + var duplicatedNavigations = + """ +{ + "Id": 2, + "$type": "Entity", + "id": "2", + "Scenario": "duplicated navigations", + "OptionalReference": {"NestedOptional": { "Text":"or no" }, "NestedOptional": { "Text":"or no dupnav" }, "NestedRequired": { "Text":"or nr" }, "NestedCollection": [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ], "NestedCollection": [ { "Text":"or nc 1 dupnav" }, { "Text":"or nc 2 dupnav" } ], "NestedRequired": { "Text":"or nr dupnav" } }, + "RequiredReference": {"NestedOptional": { "Text":"rr no" }, "NestedOptional": { "Text":"rr no dupnav" }, "NestedRequired": { "Text":"rr nr" }, "NestedCollection": [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ], "NestedCollection": [ { "Text":"rr nc 1 dupnav" }, { "Text":"rr nc 2 dupnav" } ], "NestedRequired": { "Text":"rr nr dupnav" } }, + "Collection": + [ + {"NestedOptional": { "Text":"c 1 no" }, "NestedOptional": { "Text":"c 1 no dupnav" }, "NestedRequired": { "Text":"c 1 nr" }, "NestedCollection": [ { "Text":"c 1 nc 1" }, { "Text":"c 1 nc 2" } ], "NestedCollection": [ { "Text":"c 1 nc 1 dupnav" }, { "Text":"c 1 nc 2 dupnav" } ], "NestedRequired": { "Text":"c 1 nr dupnav" } }, + {"NestedOptional": { "Text":"c 2 no" }, "NestedOptional": { "Text":"c 2 no dupnav" }, "NestedRequired": { "Text":"c 2 nr" }, "NestedCollection": [ { "Text":"c 2 nc 1" }, { "Text":"c 2 nc 2" } ], "NestedCollection": [ { "Text":"c 2 nc 1 dupnav" }, { "Text":"c 2 nc 2 dupnav" } ], "NestedRequired": { "Text":"c 2 nr dupnav" } } + ] +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + duplicatedNavigations, + CancellationToken.None); + + var duplicatedScalars = + """ +{ + "Id": 3, + "$type": "Entity", + "id": "3", + "Scenario": "duplicated scalars", + "OptionalReference": {"NestedOptional": { "Text":"or no", "Text":"or no dupprop" }, "NestedRequired": { "Text":"or nr", "Text":"or nr dupprop" }, "NestedCollection": [ { "Text":"or nc 1", "Text":"or nc 1 dupprop" }, { "Text":"or nc 2", "Text":"or nc 2 dupprop" } ] }, + "RequiredReference": {"NestedOptional": { "Text":"rr no", "Text":"rr no dupprop" }, "NestedRequired": { "Text":"rr nr", "Text":"rr nr dupprop" }, "NestedCollection": [ { "Text":"rr nc 1", "Text":"rr nc 1 dupprop" }, { "Text":"rr nc 2", "Text":"rr nc 2 dupprop" } ] }, + "Collection": + [ + {"NestedOptional": { "Text":"c 1 no", "Text":"c 1 no dupprop" }, "NestedRequired": { "Text":"c 1 nr", "Text":"c 1 nr dupprop" }, "NestedCollection": [ { "Text":"c 1 nc 1", "Text":"c 1 nc 1 dupprop" }, { "Text":"c 1 nc 2", "Text":"c 1 nc 2 dupprop" } ] }, + {"NestedOptional": { "Text":"c 2 no", "Text":"c 2 no dupprop" }, "NestedRequired": { "Text":"c 2 nr", "Text":"c 2 nr dupprop" }, "NestedCollection": [ { "Text":"c 2 nc 1", "Text":"c 2 nc 1 dupprop" }, { "Text":"c 2 nc 2", "Text":"c 2 nc 2 dupprop" } ] } + ] +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + duplicatedScalars, + CancellationToken.None); + + var emptyNavs = + """ +{ + "Id": 4, + "$type": "Entity", + "id": "4", + "Scenario": "empty navigation property names", + "OptionalReference": {"": { "Text":"or no" }, "": { "Text":"or nr" }, "": [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ] }, + "RequiredReference": {"": { "Text":"rr no" }, "": { "Text":"rr nr" }, "": [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ] }, + "Collection": + [ + {"": { "Text":"c 1 no" }, "": { "Text":"c 1 nr" }, "": [ { "Text":"c 1 nc 1" }, { "Text":"c 1 nc 2" } ] }, + {"": { "Text":"c 2 no" }, "": { "Text":"c 2 nr" }, "": [ { "Text":"c 2 nc 1" }, { "Text":"c 2 nc 2" } ] } + ] +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + emptyNavs, + CancellationToken.None); + + var emptyScalars = + """ +{ + "Id": 5, + "$type": "Entity", + "id": "5", + "Scenario": "empty scalar property names", + "OptionalReference": {"NestedOptional": { "":"or no" }, "NestedRequired": { "":"or nr" }, "NestedCollection": [ { "":"or nc 1" }, { "":"or nc 2" } ] }, + "RequiredReference": {"NestedOptional": { "":"rr no" }, "NestedRequired": { "":"rr nr" }, "NestedCollection": [ { "":"rr nc 1" }, { "":"rr nc 2" } ] }, + "Collection": + [ + {"NestedOptional": { "":"c 1 no" }, "NestedRequired": { "":"c 1 nr" }, "NestedCollection": [ { "":"c 1 nc 1" }, { "":"c 1 nc 2" } ] }, + {"NestedOptional": { "":"c 2 no" }, "NestedRequired": { "":"c 2 nr" }, "NestedCollection": [ { "":"c 2 nc 1" }, { "":"c 2 nc 2" } ] } + ] +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + emptyScalars, + CancellationToken.None); + + // Insertion of the bad data fails thanks to json validation by Cosmos DB + // nullNavs or "Scenario": "null navigation property names" can not be tested + // nullScalars or "Scenario": "null scalar property names" can not be tested + } + + #endregion + + #region Bad primary keys + + // TODO: This only asserts the current behavior of the provider, but the behavior is probably not ideal + // Consider: + // - Never storing foreign key properties in json, For projections when the foreign key is a member, we would need to translate to the owner id in the query pipeline + // - Not allowing (explicit?) primary keys on embedded entity types + // - Deprecating owned entity types as a whole + + // The workaround for old providers is: + // For missing implicit PK properties on embedded collection entities, use ordinal + + [Theory, InlineData(true), InlineData(false)] + public async Task Primary_key_baseline(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var entity = new ContextBadPrimaryKeyJsonProperties.Entity + { + Id = 1, + Associate = new() { Id = 2 }, + KeyedAssociate = new() { Id = 3 }, // Overwritten to 1 (entity id) + AssociateCollection = + [ + new() { Id = 4 }, + new() { Id = 5 } + ], + KeyedAssociateCollection = + [ + new() { Id = 6 }, + new() { Id = 7 } + ], + ForeignKeyAssociate = new() { Id = 8 }, + KeyedForeignKeyAssociate = new() { Id = 9 }, + ForeignKeyAssociateCollection = + [ + new() { Id = 10 }, + new() { Id = 11 } + ], + KeyedForeignKeyAssociateCollection = + [ + new() { Id = 12 }, + new() { Id = 13 } + ] + }; + + context.Add(entity); + await context.SaveChangesAsync(); + }); + + using var context = contextFactory.CreateDbContext(); + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entityJson = JsonNode.Parse(""" +{ + "$type": "Entity", + "Id": 1, + "id": "1", + "Associate": { + "Id": 2 + }, + "KeyedAssociate": {}, + "AssociateCollection": [ + { + "Id": 4 + }, + { + "Id": 5 + } + ], + "KeyedAssociateCollection": [ + { + "Id": 6, + "EntityId": 1 + }, + { + "Id": 7, + "EntityId": 1 + } + ], + "ForeignKeyAssociate": { + "Id": 8 + }, + "KeyedForeignKeyAssociate": { + "Id": 9, + "EntityId": 1 + }, + "ForeignKeyAssociateCollection": [ + { + "Id": 10 + }, + { + "Id": 11 + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": 12, + "EntityId": 1 + }, + { + "Id": 13, + "EntityId": 1 + } + ] +} +""")!.AsObject(); + + var dbJson = JsonNode.Parse( + new StreamReader( + (await container.ReadItemStreamAsync("1", Azure.Cosmos.PartitionKey.None)).Content).ReadToEnd())!.AsObject(); + foreach (var property in dbJson.Where(x => x.Key.StartsWith("_")).ToList()) + { + dbJson.Remove(property.Key); + } + + var text = dbJson.ToString(); + + Assert.True(JsonNode.DeepEquals(entityJson, dbJson)); + + var entity = await context.Entities.SingleAsync(); + Assert.Equal(2, entity.Associate.Id); + Assert.Equal(1, entity.KeyedAssociate.Id); // Is overwritten.. + + Assert.Equal(4, entity.AssociateCollection.First().Id); + Assert.Equal(6, entity.KeyedAssociateCollection.First().Id); + + Assert.Equal(8, entity.ForeignKeyAssociate.Id); + Assert.Equal(9, entity.KeyedForeignKeyAssociate.Id); + + Assert.Equal(10, entity.ForeignKeyAssociateCollection.First().Id); + Assert.Equal(12, entity.KeyedForeignKeyAssociateCollection.First().Id); + } + + [Theory, InlineData(true), InlineData(false)] + public async Task Root_null_primary_key_throws(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "Id": null, + "id": "1", + "Associate": { + "Id": 2 + }, + "KeyedAssociate": {}, + "AssociateCollection": [ + { + "Id": 4 + }, + { + "Id": 5 + } + ], + "KeyedAssociateCollection": [ + { + "Id": 6, + "EntityId": 1 + }, + { + "Id": 7, + "EntityId": 1 + } + ], + "ForeignKeyAssociate": { + "Id": 8 + }, + "KeyedForeignKeyAssociate": { + "Id": 9, + "EntityId": 1 + }, + "ForeignKeyAssociateCollection": [ + { + "Id": 10 + }, + { + "Id": 11 + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": 12, + "EntityId": 1 + }, + { + "Id": 13, + "EntityId": 1 + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var ex = await Assert.ThrowsAsync(() => query.SingleOrDefaultAsync()); + Assert.Equal(CoreStrings.InvalidKeyValue("Entity", "Id"), ex.Message); + } + + [Theory, InlineData(true), InlineData(false)] + public async Task Root_missing_primary_key_throws(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "id": "1", + "Associate": { + "Id": 2 + }, + "KeyedAssociate": {}, + "AssociateCollection": [ + { + "Id": 4 + }, + { + "Id": 5 + } + ], + "KeyedAssociateCollection": [ + { + "Id": 6, + "EntityId": 1 + }, + { + "Id": 7, + "EntityId": 1 + } + ], + "ForeignKeyAssociate": { + "Id": 8 + }, + "KeyedForeignKeyAssociate": { + "Id": 9, + "EntityId": 1 + }, + "ForeignKeyAssociateCollection": [ + { + "Id": 10 + }, + { + "Id": 11 + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": 12, + "EntityId": 1 + }, + { + "Id": 13, + "EntityId": 1 + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var ex = await Assert.ThrowsAsync(() => query.SingleOrDefaultAsync()); + + Assert.Equal(CoreStrings.InvalidKeyValue("Entity", "Id"), ex.Message); + } + + [Theory, InlineData(true), InlineData(false)] + public async Task Associate_null_non_primary_key_doesnt_throw(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "Id": 1, + "id": "1", + "Associate": { + "Id": null + }, + "KeyedAssociate": {}, + "AssociateCollection": [ + { + "Id": 4 + }, + { + "Id": 5 + } + ], + "KeyedAssociateCollection": [ + { + "Id": 6, + "EntityId": 1 + }, + { + "Id": 7, + "EntityId": 1 + } + ], + "ForeignKeyAssociate": { + "Id": 8 + }, + "KeyedForeignKeyAssociate": { + "Id": 9, + "EntityId": 1 + }, + "ForeignKeyAssociateCollection": [ + { + "Id": 10 + }, + { + "Id": 11 + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": 12, + "EntityId": 1 + }, + { + "Id": 13, + "EntityId": 1 + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var result = await query.SingleAsync(); + Assert.Equal(0, result.Associate.Id); + } + + [Theory, InlineData(true), InlineData(false)] + public async Task Associate_missing_non_primary_key_doesnt_throw(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "Id": 1, + "id": "1", + "Associate": { + }, + "KeyedAssociate": {}, + "AssociateCollection": [ + { + "Id": 4 + }, + { + "Id": 5 + } + ], + "KeyedAssociateCollection": [ + { + "Id": 6, + "EntityId": 1 + }, + { + "Id": 7, + "EntityId": 1 + } + ], + "ForeignKeyAssociate": { + "Id": 8 + }, + "KeyedForeignKeyAssociate": { + "Id": 9, + "EntityId": 1 + }, + "ForeignKeyAssociateCollection": [ + { + "Id": 10 + }, + { + "Id": 11 + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": 12, + "EntityId": 1 + }, + { + "Id": 13, + "EntityId": 1 + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var result = await query.SingleAsync(); + Assert.Equal(0, result.Associate.Id); + } + + [Theory, InlineData(true), InlineData(false)] + public async Task KeyedAssociate_null_primary_key_uses_implicit(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "Id": 1, + "id": "1", + "Associate": { + "Id": 2 + }, + "KeyedAssociate": { + "Id": null + }, + "AssociateCollection": [ + { + "Id": 4 + }, + { + "Id": 5 + } + ], + "KeyedAssociateCollection": [ + { + "Id": 6, + "EntityId": 1 + }, + { + "Id": 7, + "EntityId": 1 + } + ], + "ForeignKeyAssociate": { + "Id": 8 + }, + "KeyedForeignKeyAssociate": { + "Id": 9, + "EntityId": 1 + }, + "ForeignKeyAssociateCollection": [ + { + "Id": 10 + }, + { + "Id": 11 + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": 12, + "EntityId": 1 + }, + { + "Id": 13, + "EntityId": 1 + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var result = await query.SingleAsync(); + Assert.Equal(1, result.KeyedAssociate.Id); + } + + [Theory, InlineData(true), InlineData(false)] + public async Task KeyedAssociate_incorrect_primary_key_uses_implicit(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "Id": 1, + "id": "1", + "Associate": { + "Id": 2 + }, + "KeyedAssociate": { + "Id": 99 + }, + "AssociateCollection": [ + { + "Id": 4 + }, + { + "Id": 5 + } + ], + "KeyedAssociateCollection": [ + { + "Id": 6, + "EntityId": 1 + }, + { + "Id": 7, + "EntityId": 1 + } + ], + "ForeignKeyAssociate": { + "Id": 8 + }, + "KeyedForeignKeyAssociate": { + "Id": 9, + "EntityId": 1 + }, + "ForeignKeyAssociateCollection": [ + { + "Id": 10 + }, + { + "Id": 11 + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": 12, + "EntityId": 1 + }, + { + "Id": 13, + "EntityId": 1 + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var result = await query.SingleAsync(); + Assert.Equal(1, result.KeyedAssociate.Id); + } + + [Theory, InlineData(true), InlineData(false)] + public async Task AssociateCollection_null_primary_key_throws(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "Id": 1, + "id": "1", + "Associate": { + "Id": 2 + }, + "KeyedAssociate": {}, + "AssociateCollection": [ + { + "Id": null + }, + { + "Id": null + } + ], + "KeyedAssociateCollection": [ + { + "Id": 6, + "EntityId": 1 + }, + { + "Id": 7, + "EntityId": 1 + } + ], + "ForeignKeyAssociate": { + "Id": 8 + }, + "KeyedForeignKeyAssociate": { + "Id": 9, + "EntityId": 1 + }, + "ForeignKeyAssociateCollection": [ + { + "Id": 10 + }, + { + "Id": 11 + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": 12, + "EntityId": 1 + }, + { + "Id": 13, + "EntityId": 1 + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var ex = await Assert.ThrowsAsync(() => query.SingleAsync()); + Assert.Equal(CoreStrings.InvalidKeyValue("Entity.AssociateCollection#AssociateEntity", "Id"), ex.Message); + } + + [Theory, InlineData(true), InlineData(false)] + public async Task AssociateCollection_missing_primary_key_doesnt_throw_and_uses_ordinal_workaround(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "Id": 1, + "id": "1", + "Associate": { + "Id": 2 + }, + "KeyedAssociate": {}, + "AssociateCollection": [ + { + }, + { + } + ], + "KeyedAssociateCollection": [ + { + "Id": 6, + "EntityId": 1 + }, + { + "Id": 7, + "EntityId": 1 + } + ], + "ForeignKeyAssociate": { + "Id": 8 + }, + "KeyedForeignKeyAssociate": { + "Id": 9, + "EntityId": 1 + }, + "ForeignKeyAssociateCollection": [ + { + "Id": 10 + }, + { + "Id": 11 + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": 12, + "EntityId": 1 + }, + { + "Id": 13, + "EntityId": 1 + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var result = await query.SingleAsync(); + Assert.Equal(1, result.AssociateCollection.First().Id); + Assert.Equal(2, result.AssociateCollection.Last().Id); + } + + [Theory, InlineData(true), InlineData(false)] + public async Task KeyedAssociateCollection_null_primary_key_throws(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "Id": 1, + "id": "1", + "Associate": { + "Id": 2 + }, + "KeyedAssociate": {}, + "AssociateCollection": [ + { + "Id": 4 + }, + { + "Id": 5 + } + ], + "KeyedAssociateCollection": [ + { + "Id": null, + "EntityId": 1 + }, + { + "Id": null, + "EntityId": 1 + } + ], + "ForeignKeyAssociate": { + "Id": 8 + }, + "KeyedForeignKeyAssociate": { + "Id": 9, + "EntityId": 1 + }, + "ForeignKeyAssociateCollection": [ + { + "Id": 10 + }, + { + "Id": 11 + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": 12, + "EntityId": 1 + }, + { + "Id": 13, + "EntityId": 1 + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var ex = await Assert.ThrowsAsync(() => query.SingleAsync()); + Assert.Equal(CoreStrings.InvalidKeyValue("Entity.KeyedAssociateCollection#AssociateEntity", "Id"), ex.Message); + } + + [Theory, InlineData(true), InlineData(false)] + public async Task KeyedAssociateCollection_missing_primary_key_throws(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "Id": 1, + "id": "1", + "Associate": { + "Id": 2 + }, + "KeyedAssociate": {}, + "AssociateCollection": [ + { + "Id": 4 + }, + { + "Id": 5 + } + ], + "KeyedAssociateCollection": [ + { + "EntityId": 1 + }, + { + "EntityId": 1 + } + ], + "ForeignKeyAssociate": { + "Id": 8 + }, + "KeyedForeignKeyAssociate": { + "Id": 9, + "EntityId": 1 + }, + "ForeignKeyAssociateCollection": [ + { + "Id": 10 + }, + { + "Id": 11 + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": 12, + "EntityId": 1 + }, + { + "Id": 13, + "EntityId": 1 + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var ex = await Assert.ThrowsAsync(() => query.SingleAsync()); + Assert.Equal(CoreStrings.InvalidKeyValue("Entity.KeyedAssociateCollection#AssociateEntity", "Id"), ex.Message); + } + + [Theory, InlineData(true), InlineData(false)] + public async Task KeyedAssociateCollection_null_foreign_key_doesnt_throw(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "Id": 1, + "id": "1", + "Associate": { + "Id": 2 + }, + "KeyedAssociate": {}, + "AssociateCollection": [ + { + "Id": 4 + }, + { + "Id": 5 + } + ], + "KeyedAssociateCollection": [ + { + "Id": 6, + "EntityId": null + }, + { + "Id": 7, + "EntityId": null + } + ], + "ForeignKeyAssociate": { + "Id": 8 + }, + "KeyedForeignKeyAssociate": { + "Id": 9, + "EntityId": 1 + }, + "ForeignKeyAssociateCollection": [ + { + "Id": 10 + }, + { + "Id": 11 + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": 12, + "EntityId": 1 + }, + { + "Id": 13, + "EntityId": 1 + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var result = await query.SingleAsync(); + if (!tracking) + { + Assert.Equal(6, result.KeyedAssociateCollection.First().Id); + Assert.Equal(7, result.KeyedAssociateCollection.Last().Id); + } + else + { + Assert.Empty(result.KeyedAssociateCollection); + } + } + + [Theory, InlineData(true), InlineData(false)] + public async Task KeyedAssociateCollection_missing_foreign_key_doesnt_throw(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "Id": 1, + "id": "1", + "Associate": { + "Id": 2 + }, + "KeyedAssociate": {}, + "AssociateCollection": [ + { + "Id": 4 + }, + { + "Id": 5 + } + ], + "KeyedAssociateCollection": [ + { + "Id": 6 + }, + { + "Id": 7 + } + ], + "ForeignKeyAssociate": { + "Id": 8 + }, + "KeyedForeignKeyAssociate": { + "Id": 9, + "EntityId": 1 + }, + "ForeignKeyAssociateCollection": [ + { + "Id": 10 + }, + { + "Id": 11 + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": 12, + "EntityId": 1 + }, + { + "Id": 13, + "EntityId": 1 + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var result = await query.SingleAsync(); + if (!tracking) + { + Assert.Equal(6, result.KeyedAssociateCollection.First().Id); + Assert.Equal(7, result.KeyedAssociateCollection.Last().Id); + } + else + { + Assert.Empty(result.KeyedAssociateCollection); + } + } + + [Theory, InlineData(true), InlineData(false)] + public async Task KeyedAssociateCollection_incorrect_foreign_key_doesnt_throw(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "Id": 1, + "id": "1", + "Associate": { + "Id": 2 + }, + "KeyedAssociate": {}, + "AssociateCollection": [ + { + "Id": 4 + }, + { + "Id": 5 + } + ], + "KeyedAssociateCollection": [ + { + "Id": 6, + "EntityId": 99 + }, + { + "Id": 7, + "EntityId": 99 + } + ], + "ForeignKeyAssociate": { + "Id": 8 + }, + "KeyedForeignKeyAssociate": { + "Id": 9, + "EntityId": 1 + }, + "ForeignKeyAssociateCollection": [ + { + "Id": 10 + }, + { + "Id": 11 + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": 12, + "EntityId": 1 + }, + { + "Id": 13, + "EntityId": 1 + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var result = await query.SingleAsync(); + if (!tracking) + { + Assert.Equal(6, result.KeyedAssociateCollection.First().Id); + Assert.Equal(7, result.KeyedAssociateCollection.Last().Id); + } + else + { + Assert.Empty(result.KeyedAssociateCollection); + } + } + + [Theory, InlineData(true), InlineData(false)] + public async Task ForeignKeyAssociate_null_non_primary_key_doesnt_throw(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "Id": 1, + "id": "1", + "Associate": { + "Id": 2 + }, + "KeyedAssociate": {}, + "AssociateCollection": [ + { + "Id": 3 + }, + { + "Id": 4 + } + ], + "KeyedAssociateCollection": [ + { + "Id": 5, + "EntityId": 1 + }, + { + "Id": 6, + "EntityId": 1 + } + ], + "ForeignKeyAssociate": { + "Id": null + }, + "KeyedForeignKeyAssociate": { + "Id": 9, + "EntityId": 1 + }, + "ForeignKeyAssociateCollection": [ + { + "Id": 10 + }, + { + "Id": 11 + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": 12, + "EntityId": 1 + }, + { + "Id": 13, + "EntityId": 1 + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var result = await query.SingleAsync(); + Assert.Equal(0, result.ForeignKeyAssociate.Id); + } + + [Theory, InlineData(true), InlineData(false)] + public async Task ForeignKeyAssociate_missing_non_primary_key_doesnt_throw(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "Id": 1, + "id": "1", + "Associate": { + "Id": 2 + }, + "KeyedAssociate": {}, + "AssociateCollection": [ + { + "Id": 3 + }, + { + "Id": 4 + } + ], + "KeyedAssociateCollection": [ + { + "Id": 5, + "EntityId": 1 + }, + { + "Id": 6, + "EntityId": 1 + } + ], + "ForeignKeyAssociate": { + }, + "KeyedForeignKeyAssociate": { + "Id": 9, + "EntityId": 1 + }, + "ForeignKeyAssociateCollection": [ + { + "Id": 10 + }, + { + "Id": 11 + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": 12, + "EntityId": 1 + }, + { + "Id": 13, + "EntityId": 1 + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var result = await query.SingleAsync(); + Assert.Equal(0, result.ForeignKeyAssociate.Id); + } + + [Theory, InlineData(true), InlineData(false)] + public async Task KeyedForeignKeyAssociate_null_primary_key_throws(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "Id": 1, + "id": "1", + "Associate": { + "Id": 2 + }, + "KeyedAssociate": {}, + "AssociateCollection": [ + { + "Id": 4 + }, + { + "Id": 5 + } + ], + "KeyedAssociateCollection": [ + { + "Id": 6, + "EntityId": 1 + }, + { + "Id": 7, + "EntityId": 1 + } + ], + "ForeignKeyAssociate": { + "Id": 8 + }, + "KeyedForeignKeyAssociate": { + "Id": null, + "EntityId": 1 + }, + "ForeignKeyAssociateCollection": [ + { + "Id": 10 + }, + { + "Id": 11 + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": 12, + "EntityId": 1 + }, + { + "Id": 13, + "EntityId": 1 + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var ex = await Assert.ThrowsAsync(() => query.SingleOrDefaultAsync()); + Assert.Equal(CoreStrings.InvalidKeyValue("Entity.KeyedForeignKeyAssociate#ForeignKeyAssociateEntity", "Id"), ex.Message); + } + + [Theory, InlineData(true), InlineData(false)] + public async Task KeyedForeignKeyAssociate_missing_primary_key_throws(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "Id": 1, + "id": "1", + "Associate": { + "Id": 2 + }, + "KeyedAssociate": {}, + "AssociateCollection": [ + { + "Id": 4 + }, + { + "Id": 5 + } + ], + "KeyedAssociateCollection": [ + { + "Id": 6, + "EntityId": 1 + }, + { + "Id": 7, + "EntityId": 1 + } + ], + "ForeignKeyAssociate": { + "Id": 8 + }, + "KeyedForeignKeyAssociate": { + "EntityId": 1 + }, + "ForeignKeyAssociateCollection": [ + { + "Id": 10 + }, + { + "Id": 11 + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": 12, + "EntityId": 1 + }, + { + "Id": 13, + "EntityId": 1 + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var ex = await Assert.ThrowsAsync(() => query.SingleOrDefaultAsync()); + Assert.Equal(CoreStrings.InvalidKeyValue("Entity.KeyedForeignKeyAssociate#ForeignKeyAssociateEntity", "Id"), ex.Message); + } + + [Theory, InlineData(true), InlineData(false)] + public async Task KeyedForeignKeyAssociate_null_foreign_key_doesnt_throw(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "Id": 1, + "id": "1", + "Associate": { + "Id": 2 + }, + "KeyedAssociate": {}, + "AssociateCollection": [ + { + "Id": 4 + }, + { + "Id": 5 + } + ], + "KeyedAssociateCollection": [ + { + "Id": 6, + "EntityId": 1 + }, + { + "Id": 7, + "EntityId": 1 + } + ], + "ForeignKeyAssociate": { + "Id": 8 + }, + "KeyedForeignKeyAssociate": { + "Id": 9, + "EntityId": null + }, + "ForeignKeyAssociateCollection": [ + { + "Id": 10 + }, + { + "Id": 11 + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": 12, + "EntityId": 1 + }, + { + "Id": 13, + "EntityId": 1 + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var result = await query.SingleAsync(); + if (tracking) + { + Assert.Null(result.KeyedForeignKeyAssociate); + } + else + { + Assert.NotNull(result.KeyedForeignKeyAssociate); + Assert.Equal(9, result.KeyedForeignKeyAssociate.Id); + Assert.Equal(0, result.KeyedForeignKeyAssociate.EntityId); + } + } + + [Theory, InlineData(true), InlineData(false)] + public async Task KeyedForeignKeyAssociate_missing_foreign_key_doesnt_throw(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "Id": 1, + "id": "1", + "Associate": { + "Id": 2 + }, + "KeyedAssociate": {}, + "AssociateCollection": [ + { + "Id": 4 + }, + { + "Id": 5 + } + ], + "KeyedAssociateCollection": [ + { + "Id": 6, + "EntityId": 1 + }, + { + "Id": 7, + "EntityId": 1 + } + ], + "ForeignKeyAssociate": { + "Id": 8 + }, + "KeyedForeignKeyAssociate": { + "Id": 9, + "EntityId": null + }, + "ForeignKeyAssociateCollection": [ + { + "Id": 10 + }, + { + "Id": 11 + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": 12, + "EntityId": 1 + }, + { + "Id": 13, + "EntityId": 1 + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var result = await query.SingleAsync(); + if (tracking) + { + Assert.Null(result.KeyedForeignKeyAssociate); + } + else + { + Assert.NotNull(result.KeyedForeignKeyAssociate); + Assert.Equal(9, result.KeyedForeignKeyAssociate.Id); + Assert.Equal(0, result.KeyedForeignKeyAssociate.EntityId); + } + } + + [Theory, InlineData(true), InlineData(false)] + public async Task KeyedForeignKeyAssociate_incorrect_foreign_key_doesnt_throw(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "Id": 1, + "id": "1", + "Associate": { + "Id": 2 + }, + "KeyedAssociate": {}, + "AssociateCollection": [ + { + "Id": 4 + }, + { + "Id": 5 + } + ], + "KeyedAssociateCollection": [ + { + "Id": 6, + "EntityId": 1 + }, + { + "Id": 7, + "EntityId": 1 + } + ], + "ForeignKeyAssociate": { + "Id": 8 + }, + "KeyedForeignKeyAssociate": { + "Id": 9, + "EntityId": 99 + }, + "ForeignKeyAssociateCollection": [ + { + "Id": 10 + }, + { + "Id": 11 + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": 12, + "EntityId": 1 + }, + { + "Id": 13, + "EntityId": 1 + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var result = await query.SingleAsync(); + if (tracking) + { + Assert.Null(result.KeyedForeignKeyAssociate); + } + else + { + Assert.NotNull(result.KeyedForeignKeyAssociate); + Assert.Equal(9, result.KeyedForeignKeyAssociate.Id); + Assert.Equal(99, result.KeyedForeignKeyAssociate.EntityId); + } + } + + [Theory, InlineData(true), InlineData(false)] + public async Task ForeignKeyAssociateCollection_null_primary_key_throws(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "Id": 1, + "id": "1", + "Associate": { + "Id": 2 + }, + "KeyedAssociate": {}, + "AssociateCollection": [ + { + "Id": 4 + }, + { + "Id": 5 + } + ], + "KeyedAssociateCollection": [ + { + "Id": 6, + "EntityId": 1 + }, + { + "Id": 7, + "EntityId": 1 + } + ], + "ForeignKeyAssociate": { + "Id": 8 + }, + "KeyedForeignKeyAssociate": { + "Id": 9, + "EntityId": 1 + }, + "ForeignKeyAssociateCollection": [ + { + "Id": null + }, + { + "Id": null + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": 12, + "EntityId": 1 + }, + { + "Id": 13, + "EntityId": 1 + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var ex = await Assert.ThrowsAsync(() => query.SingleOrDefaultAsync()); + Assert.Equal(CoreStrings.InvalidKeyValue("Entity.ForeignKeyAssociateCollection#ForeignKeyAssociateEntity", "Id"), ex.Message); + } + + [Theory, InlineData(true), InlineData(false)] + public async Task ForeignKeyAssociateCollection_missing_primary_key_doesnt_throw_and_uses_ordinal_workaround(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "Id": 1, + "id": "1", + "Associate": { + "Id": 2 + }, + "KeyedAssociate": {}, + "AssociateCollection": [ + { + "Id": 4 + }, + { + "Id": 5 + } + ], + "KeyedAssociateCollection": [ + { + "Id": 6, + "EntityId": 1 + }, + { + "Id": 7, + "EntityId": 1 + } + ], + "ForeignKeyAssociate": { + "Id": 8 + }, + "KeyedForeignKeyAssociate": { + "Id": 9, + "EntityId": 1 + }, + "ForeignKeyAssociateCollection": [ + { + }, + { + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": 12, + "EntityId": 1 + }, + { + "Id": 13, + "EntityId": 1 + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var result = await query.SingleAsync(); + Assert.Equal(1, result.ForeignKeyAssociateCollection.First().Id); + Assert.Equal(2, result.ForeignKeyAssociateCollection.Last().Id); + } + + [Theory, InlineData(true), InlineData(false)] + public async Task KeyedForeignKeyAssociateCollection_null_primary_key_throws(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "Id": 1, + "id": "1", + "Associate": { + "Id": 2 + }, + "KeyedAssociate": {}, + "AssociateCollection": [ + { + "Id": 4 + }, + { + "Id": 5 + } + ], + "KeyedAssociateCollection": [ + { + "Id": 6, + "EntityId": 1 + }, + { + "Id": 7, + "EntityId": 1 + } + ], + "ForeignKeyAssociate": { + "Id": 8 + }, + "KeyedForeignKeyAssociate": { + "Id": 9, + "EntityId": 1 + }, + "ForeignKeyAssociateCollection": [ + { + "Id": 10 + }, + { + "Id": 11 + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": null, + "EntityId": 1 + }, + { + "Id": null, + "EntityId": 1 + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var ex = await Assert.ThrowsAsync(() => query.SingleOrDefaultAsync()); + Assert.Equal(CoreStrings.InvalidKeyValue("Entity.KeyedForeignKeyAssociateCollection#ForeignKeyAssociateEntity", "Id"), ex.Message); + } + + [Theory, InlineData(true), InlineData(false)] + public async Task KeyedForeignKeyAssociateCollection_missing_primary_key_throws(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "Id": 1, + "id": "1", + "Associate": { + "Id": 2 + }, + "KeyedAssociate": {}, + "AssociateCollection": [ + { + "Id": 4 + }, + { + "Id": 5 + } + ], + "KeyedAssociateCollection": [ + { + "Id": 6, + "EntityId": 1 + }, + { + "Id": 7, + "EntityId": 1 + } + ], + "ForeignKeyAssociate": { + "Id": 8 + }, + "KeyedForeignKeyAssociate": { + "Id": 9, + "EntityId": 1 + }, + "ForeignKeyAssociateCollection": [ + { + "Id": 10 + }, + { + "Id": 11 + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "EntityId": 1 + }, + { + "EntityId": 1 + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var ex = await Assert.ThrowsAsync(() => query.SingleOrDefaultAsync()); + Assert.Equal(CoreStrings.InvalidKeyValue("Entity.KeyedForeignKeyAssociateCollection#ForeignKeyAssociateEntity", "Id"), ex.Message); + } + + [Theory, InlineData(true), InlineData(false)] + public async Task KeyedForeignKeyAssociateCollection_null_foreign_key_doesnt_throw(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "Id": 1, + "id": "1", + "Associate": { + "Id": 2 + }, + "KeyedAssociate": {}, + "AssociateCollection": [ + { + "Id": 4 + }, + { + "Id": 5 + } + ], + "KeyedAssociateCollection": [ + { + "Id": 6, + "EntityId": 1 + }, + { + "Id": 7, + "EntityId": 1 + } + ], + "ForeignKeyAssociate": { + "Id": 8 + }, + "KeyedForeignKeyAssociate": { + "Id": 9, + "EntityId": 1 + }, + "ForeignKeyAssociateCollection": [ + { + "Id": 10 + }, + { + "Id": 11 + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": 12, + "EntityId": null + }, + { + "Id": 13, + "EntityId": null + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var result = await query.SingleAsync(); + if (tracking) + { + Assert.Empty(result.KeyedForeignKeyAssociateCollection); + } + else + { + foreach (var item in result.KeyedForeignKeyAssociateCollection) + { + Assert.Equal(0, item.EntityId); + } + } + } + + [Theory, InlineData(true), InlineData(false)] + public async Task KeyedForeignKeyAssociateCollection_missing_foreign_key_doesnt_throw(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "Id": 1, + "id": "1", + "Associate": { + "Id": 2 + }, + "KeyedAssociate": {}, + "AssociateCollection": [ + { + "Id": 4 + }, + { + "Id": 5 + } + ], + "KeyedAssociateCollection": [ + { + "Id": 6, + "EntityId": 1 + }, + { + "Id": 7, + "EntityId": 1 + } + ], + "ForeignKeyAssociate": { + "Id": 8 + }, + "KeyedForeignKeyAssociate": { + "Id": 9, + "EntityId": 1 + }, + "ForeignKeyAssociateCollection": [ + { + "Id": 10 + }, + { + "Id": 11 + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": 12 + }, + { + "Id": 13 + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var result = await query.SingleAsync(); + if (tracking) + { + Assert.Empty(result.KeyedForeignKeyAssociateCollection); + } + else + { + foreach (var item in result.KeyedForeignKeyAssociateCollection) + { + Assert.Equal(0, item.EntityId); + } + } + } + + [Theory, InlineData(true), InlineData(false)] + public async Task KeyedForeignKeyAssociateCollection_incorrect_foreign_key_doesnt_throw(bool tracking) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: async context => + { + var client = context.Database.GetCosmosClient(); + var container = client.GetContainer(context.Database.GetCosmosDatabaseId(), containerId: "Entities"); + + var entity = """ +{ + "$type": "Entity", + "Id": 1, + "id": "1", + "Associate": { + "Id": 2 + }, + "KeyedAssociate": {}, + "AssociateCollection": [ + { + "Id": 4 + }, + { + "Id": 5 + } + ], + "KeyedAssociateCollection": [ + { + "Id": 6, + "EntityId": 1 + }, + { + "Id": 7, + "EntityId": 1 + } + ], + "ForeignKeyAssociate": { + "Id": 8 + }, + "KeyedForeignKeyAssociate": { + "Id": 9, + "EntityId": 1 + }, + "ForeignKeyAssociateCollection": [ + { + "Id": 10 + }, + { + "Id": 11 + } + ], + "KeyedForeignKeyAssociateCollection": [ + { + "Id": 12, + "EntityId": 99 + }, + { + "Id": 13, + "EntityId": 99 + } + ] +} +"""; + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + container, + entity, + CancellationToken.None); + }); + + using var context = contextFactory.CreateDbContext(); + IQueryable query = context.Entities; + if (!tracking) + { + query = query.AsNoTracking(); + } + + var result = await query.SingleAsync(); + if (tracking) + { + Assert.Empty(result.KeyedForeignKeyAssociateCollection); + } + else + { + foreach (var item in result.KeyedForeignKeyAssociateCollection) + { + Assert.Equal(99, item.EntityId); + } + } + } + + private class ContextBadPrimaryKeyJsonProperties : DbContext + { + public ContextBadPrimaryKeyJsonProperties(DbContextOptions options) + : base(options) + { + } + + public DbSet Entities { get; set; } = null!; + + public class Entity + { + public int Id { get; set; } + + public AssociateEntity Associate { get; set; } = null!; + public AssociateEntity KeyedAssociate { get; set; } = null!; + + public List AssociateCollection { get; set; } = new(); + public List KeyedAssociateCollection { get; set; } = new(); + + public ForeignKeyAssociateEntity ForeignKeyAssociate { get; set; } = null!; + public ForeignKeyAssociateEntity KeyedForeignKeyAssociate { get; set; } = null!; + + public List ForeignKeyAssociateCollection { get; set; } = new(); + public List KeyedForeignKeyAssociateCollection { get; set; } = new(); + } + + public class AssociateEntity + { + public int Id { get; set; } + } + + public class ForeignKeyAssociateEntity + { + public int Id { get; set; } + + public int EntityId { get; set; } + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + var entity = modelBuilder.Entity(); + entity.ToContainer("Entities"); + entity.OwnsOne(x => x.Associate); + entity.OwnsOne(x => x.KeyedAssociate, b => b.HasKey(x => x.Id)); + entity.OwnsMany(x => x.AssociateCollection); + entity.OwnsMany(x => x.KeyedAssociateCollection, b => b.HasKey(x => x.Id)); + + entity.OwnsOne(x => x.ForeignKeyAssociate); + entity.OwnsOne(x => x.KeyedForeignKeyAssociate, b => b.HasKey(x => x.Id)); + entity.OwnsMany(x => x.ForeignKeyAssociateCollection); + entity.OwnsMany(x => x.KeyedForeignKeyAssociateCollection, b => b.HasKey(x => x.Id)); + } + } + + #endregion + +#nullable restore + + #endregion + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.InMemory.FunctionalTests/Query/AdHocManyToManyQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/AdHocManyToManyQueryInMemoryTest.cs deleted file mode 100644 index e2c8078fe73..00000000000 --- a/test/EFCore.InMemory.FunctionalTests/Query/AdHocManyToManyQueryInMemoryTest.cs +++ /dev/null @@ -1,10 +0,0 @@ -// 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 AdHocManyToManyQueryInMemoryTest(NonSharedFixture fixture) : AdHocManyToManyQueryTestBase(fixture) -{ - protected override ITestStoreFactory NonSharedTestStoreFactory - => InMemoryTestStoreFactory.Instance; -} diff --git a/test/EFCore.Relational.Specification.Tests/Query/AdHocComplexTypeQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/AdHocComplexTypeQueryRelationalTestBase.cs deleted file mode 100644 index 9c4b95146ce..00000000000 --- a/test/EFCore.Relational.Specification.Tests/Query/AdHocComplexTypeQueryRelationalTestBase.cs +++ /dev/null @@ -1,240 +0,0 @@ -// 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 abstract class AdHocComplexTypeQueryRelationalTestBase(NonSharedFixture fixture) : AdHocComplexTypeQueryTestBase(fixture) -{ - #region 37205 - - [Fact] - public virtual async Task Complex_json_collection_inside_left_join_subquery() - { - var contextFactory = await InitializeNonSharedTest(); - - await using var context = contextFactory.CreateDbContext(); - - _ = await context.Set().Include(p => p.Child).ToListAsync(); - } - - private class Context37205(DbContextOptions options) : DbContext(options) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity(); - modelBuilder.Entity(b => - { - b.ComplexCollection(e => e.CareNeeds, cb => cb.ToJson()); - b.HasQueryFilter(child => child.IsPublic); - }); - } - - public class Parent - { - public int Id { get; set; } - public Child? Child { get; set; } - public int? ChildId { get; set; } - } - - public class Child - { - public int Id { get; set; } - public Parent Parent { get; set; } = null!; - public bool IsPublic { get; set; } - public required List CareNeeds { get; set; } - } - - public record CareNeedAnswer - { - public required string Topic { get; set; } - } - } - - #endregion 37205 - - #region 35025 - - [Fact] - public virtual async Task Select_TPC_base_with_ComplexType() - { - var contextFactory = await InitializeNonSharedTest(); - using var context = contextFactory.CreateDbContext(); - - var count = await context.TpcBases.ToListAsync(); - - // TODO: Seed data and assert materialization as well - // Assert.Equal(0, count); - } - - protected class Context35025(DbContextOptions options) : DbContext(options) - { - public DbSet TpcBases { get; set; } = null!; - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity(builder => - { - builder.ComplexProperty(e => e.BaseComplexProperty); - builder.UseTpcMappingStrategy(); - }); - - modelBuilder.Entity().ComplexProperty(e => e.ChildComplexProperty); - modelBuilder.Entity().ComplexProperty(e => e.ChildComplexProperty); - } - - public abstract class TpcBase - { - public int Id { get; set; } - public required ComplexThing BaseComplexProperty { get; set; } - } - - public class TpcChild1 : TpcBase - { - public required ComplexThing ChildComplexProperty { get; set; } - public int ChildProperty { get; set; } - } - - public class TpcChild2 : TpcBase - { - public required AnotherComplexThing ChildComplexProperty { get; set; } - public required string ChildProperty { get; set; } - } - - public class ComplexThing - { - public int PropertyInsideComplexThing { get; set; } - } - - public class AnotherComplexThing - { - // Another nested property with the same name but a different type. - // We should properly uniquify the projected name coming out of the UNION. - public required string PropertyInsideComplexThing { get; set; } - } - } - - #endregion 35025 - - #region 34706 - - [Fact] - public virtual async Task Complex_type_on_an_entity_mapped_to_view_and_table() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: mb => mb.Entity(eb => eb - .ToTable("Blogs") - .ToView("BlogsView") - .ComplexProperty(b => b.ComplexThing)), - seed: async context => - { - var sqlGenerationHelper = context.GetService(); - await context.Database.ExecuteSqlRawAsync( - $"CREATE VIEW {Q("BlogsView")} AS SELECT {Q("Id")}, {Q("ComplexThing_Prop1")}, {Q("ComplexThing_Prop2")} FROM {Q("Blogs")}"); - - context.Add( - new Context34706.Blog { ComplexThing = new Context34706.ComplexThing { Prop1 = 1, Prop2 = 2 } }); - await context.SaveChangesAsync(); - - string Q(string name) - => sqlGenerationHelper.DelimitIdentifier(name); - }); - - await using var context = contextFactory.CreateDbContext(); - - var entity = await context.Set().SingleAsync(); - - Assert.NotNull(entity.ComplexThing); - Assert.Equal(1, entity.ComplexThing.Prop1); - Assert.Equal(2, entity.ComplexThing.Prop2); - } - - private class Context34706(DbContextOptions options) : DbContext(options) - { - public class Blog - { - public int Id { get; set; } - public required ComplexThing ComplexThing { get; set; } - } - - public class ComplexThing - { - public int Prop1 { get; set; } - public int Prop2 { get; set; } - } - } - - #endregion 34706 - - #region 38077 - - [Fact] - public virtual async Task Complex_property_on_split_entity() - { - var contextFactory = await InitializeNonSharedTest( - seed: async context => - { - context.Set().Add( - new Context38077.Hook - { - CreatedOn = new DateTime(2024, 1, 2, 3, 4, 5, DateTimeKind.Utc), - Number = new Context38077.HookNumber { Raw = "123", Parsed = 123 }, - Weight = 1.25m, - IsTestHook = false - }); - - await context.SaveChangesAsync(); - }); - - await using var context = contextFactory.CreateDbContext(); - - var hooks = await context.Set().ToListAsync(); - - Assert.Single(hooks); - Assert.Equal(new DateTime(2024, 1, 2, 3, 4, 5, DateTimeKind.Utc), hooks[0].CreatedOn); - Assert.Equal("123", hooks[0].Number.Raw); - Assert.Equal(123, hooks[0].Number.Parsed); - Assert.Equal(1.25m, hooks[0].Weight); - Assert.False(hooks[0].IsTestHook); - } - - private class Context38077(DbContextOptions options) : DbContext(options) - { - public class Hook - { - public int Id { get; set; } - public DateTime CreatedOn { get; set; } - public required HookNumber Number { get; set; } - public decimal? Weight { get; set; } - public bool? IsTestHook { get; set; } - } - - public class HookNumber - { - public required string Raw { get; set; } - public long Parsed { get; set; } - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - => modelBuilder.Entity(b => - { - b.ComplexProperty(h => h.Number); - b.Property(h => h.Weight).HasPrecision(18, 6); - b.SplitToTable( - "HookMetadata", - tb => - { - tb.Property(h => h.Id).HasColumnName("HookId"); - tb.Property(h => h.Weight); - tb.Property(h => h.IsTestHook); - }); - }); - } - - #endregion 38077 - - protected TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ListLoggerFactory; - - protected void AssertSql(params string[] expected) - => TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryRelationalTestBase.cs deleted file mode 100644 index cfb0e4df753..00000000000 --- a/test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryRelationalTestBase.cs +++ /dev/null @@ -1,883 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Text.Json; - -namespace Microsoft.EntityFrameworkCore.Query; - -#nullable disable - -public abstract class AdHocJsonQueryRelationalTestBase(NonSharedFixture fixture) : AdHocJsonQueryTestBase(fixture) -{ - protected override void ConfigureWarnings(WarningsConfigurationBuilder builder) - { - base.ConfigureWarnings(builder); - - builder.Ignore(RelationalEventId.OwnedEntityMappedToJsonCollectionWarning); - } - - #region 21006 - - public override async Task Project_missing_required_navigation(bool async) - { - var message = (await Assert.ThrowsAsync(() => base.Project_missing_required_navigation(async))).Message; - - Assert.Equal(RelationalStrings.JsonRequiredEntityWithNullJson(typeof(Context21006.JsonEntityNested).Name), message); - } - - public override async Task Project_null_required_navigation(bool async) - { - var message = (await Assert.ThrowsAsync(() => base.Project_null_required_navigation(async))).Message; - - Assert.Equal(RelationalStrings.JsonRequiredEntityWithNullJson(typeof(Context21006.JsonEntityNested).Name), message); - } - - public override async Task Project_top_level_entity_with_null_value_required_scalars(bool async) - { - var message = (await Assert.ThrowsAsync(() - => base.Project_top_level_entity_with_null_value_required_scalars(async))).Message; - - Assert.Equal("Cannot get the value of a token type 'Null' as a number.", message); - } - - protected override void OnModelCreating21006(ModelBuilder modelBuilder) - { - base.OnModelCreating21006(modelBuilder); - - modelBuilder.Entity(b => - { - b.ToTable("Entities"); - b.OwnsOne(x => x.OptionalReference).ToJson().HasColumnType(JsonColumnType); - b.OwnsOne(x => x.RequiredReference).ToJson().HasColumnType(JsonColumnType); - b.OwnsMany(x => x.Collection).ToJson().HasColumnType(JsonColumnType); - }); - } - - #endregion - - #region 32310 - - protected override void OnModelCreating32310(ModelBuilder modelBuilder) - { - base.OnModelCreating32310(modelBuilder); - - modelBuilder.Entity().OwnsOne(e => e.Visits).ToJson().HasColumnType(JsonColumnType); - } - - #endregion - - #region 29219 - - protected override void OnModelCreating29219(ModelBuilder modelBuilder) - { - base.OnModelCreating29219(modelBuilder); - - modelBuilder.Entity(b => - { - b.ToTable("Entities"); - b.OwnsOne(x => x.Reference).ToJson().HasColumnType(JsonColumnType); - b.OwnsMany(x => x.Collection).ToJson().HasColumnType(JsonColumnType); - }); - } - - #endregion - - #region 30028 - - protected override void OnModelCreating30028(ModelBuilder modelBuilder) - { - base.OnModelCreating30028(modelBuilder); - - modelBuilder.Entity(b => - { - b.ToTable("Entities"); - b.OwnsOne( - x => x.Json, nb => nb.ToJson().HasColumnType(JsonColumnType)); - }); - } - - #endregion - - #region 32939 - - protected override void OnModelCreating32939(ModelBuilder modelBuilder) - { - base.OnModelCreating32939(modelBuilder); - - modelBuilder.Entity().OwnsOne(x => x.Empty, b => b.ToJson().HasColumnType(JsonColumnType)); - modelBuilder.Entity().OwnsOne(x => x.FieldOnly, b => b.ToJson().HasColumnType(JsonColumnType)); - } - - #endregion - - #region 33046 - - protected override void OnModelCreating33046(ModelBuilder modelBuilder) - { - base.OnModelCreating33046(modelBuilder); - - modelBuilder.Entity(b => - { - b.ToTable("Reviews"); - b.OwnsMany( - x => x.Rounds, ownedBuilder => ownedBuilder.ToJson().HasColumnType(JsonColumnType)); - }); - } - - #endregion - - #region 34293 - - [Fact] - public virtual async Task Project_entity_with_optional_json_entity_owned_by_required_json() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreating34293, - seed: ctx => ctx.Seed()); - - using var context = contextFactory.CreateDbContext(); - var entityProjection = await context.Set().ToListAsync(); - - Assert.Equal(3, entityProjection.Count); - } - - [Fact] - public virtual async Task Project_required_json_entity() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreating34293, - seed: ctx => ctx.Seed()); - - using var context = contextFactory.CreateDbContext(); - - var rootProjection = - await context.Set().AsNoTracking().Where(x => x.Id != 3).Select(x => x.Json).ToListAsync(); - Assert.Equal(2, rootProjection.Count); - - var branchProjection = await context.Set().AsNoTracking().Where(x => x.Id != 3).Select(x => x.Json.Required) - .ToListAsync(); - Assert.Equal(2, rootProjection.Count); - - var badRootProjectionMessage = (await Assert.ThrowsAsync(() - => context.Set().AsNoTracking().Where(x => x.Id == 3).Select(x => x.Json).ToListAsync())).Message; - Assert.Equal(RelationalStrings.JsonRequiredEntityWithNullJson(nameof(Context34293.JsonBranch)), badRootProjectionMessage); - - var badBranchProjectionMessage = (await Assert.ThrowsAsync(() - => context.Set().AsNoTracking().Where(x => x.Id == 3).Select(x => x.Json.Required).ToListAsync())).Message; - Assert.Equal(RelationalStrings.JsonRequiredEntityWithNullJson(nameof(Context34293.JsonBranch)), badBranchProjectionMessage); - } - - [Fact] - public virtual async Task Project_optional_json_entity_owned_by_required_json_entity() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreating34293, - seed: ctx => ctx.Seed()); - - using var context = contextFactory.CreateDbContext(); - var leafProjection = await context.Set().AsNoTracking().Select(x => x.Json.Required.Optional).ToListAsync(); - Assert.Equal(3, leafProjection.Count); - } - - protected class Context34293(DbContextOptions options) : DbContext(options) - { - public DbSet Entities { get; set; } - - public class Entity - { - public int Id { get; set; } - public JsonRoot Json { get; set; } - } - - public class JsonRoot - { - public DateTime Date { get; set; } - - public JsonBranch Required { get; set; } - } - - public class JsonBranch - { - public int Number { get; set; } - public JsonLeaf Optional { get; set; } - } - - public class JsonLeaf - { - public string Name { get; set; } - } - - public async Task Seed() - { - // everything - ok - var e1 = new Entity - { - Id = 1, - Json = new JsonRoot - { - Date = new DateTime(2001, 1, 1), - Required = new JsonBranch { Number = 1, Optional = new JsonLeaf { Name = "optional 1" } } - } - }; - - // null leaf - ok (optional nav) - var e2 = new Entity - { - Id = 2, - Json = new JsonRoot { Date = new DateTime(2002, 2, 2), Required = new JsonBranch { Number = 2, Optional = null } } - }; - - // null branch - invalid (required nav) - var e3 = new Entity - { - Id = 3, - Json = new JsonRoot - { - Date = new DateTime(2003, 3, 3), - Required = null, - } - }; - - Entities.AddRange(e1, e2, e3); - await SaveChangesAsync(); - } - } - - protected virtual void OnModelCreating34293(ModelBuilder modelBuilder) - => modelBuilder.Entity(b => - { - b.Property(x => x.Id).ValueGeneratedNever(); - b.OwnsOne( - x => x.Json, b => - { - b.ToJson().HasColumnType(JsonColumnType); - b.OwnsOne( - x => x.Required, bb => - { - bb.OwnsOne(x => x.Optional); - bb.Navigation(x => x.Optional).IsRequired(false); - }); - b.Navigation(x => x.Required).IsRequired(); - }); - b.Navigation(x => x.Json).IsRequired(); - }); - - #endregion - - #region 34960 - - public override async Task Try_project_collection_but_JSON_is_entity() - { - var message = (await Assert.ThrowsAsync(() => base.Try_project_collection_but_JSON_is_entity())).Message; - - Assert.Equal( - CoreStrings.JsonReaderInvalidTokenType(nameof(JsonTokenType.StartObject)), - message); - } - - public override async Task Try_project_reference_but_JSON_is_collection() - { - var message = (await Assert.ThrowsAsync(() => base.Try_project_reference_but_JSON_is_collection())) - .Message; - - Assert.Equal( - CoreStrings.JsonReaderInvalidTokenType(nameof(JsonTokenType.StartArray)), - message); - } - - protected override void OnModelCreating34960(ModelBuilder modelBuilder) - { - base.OnModelCreating34960(modelBuilder); - - modelBuilder.Entity(b => - { - b.ToTable("Entities"); - - b.OwnsOne( - x => x.Reference, b => b.ToJson().HasColumnType(JsonColumnType)); - - b.OwnsMany( - x => x.Collection, b => b.ToJson().HasColumnType(JsonColumnType)); - }); - - modelBuilder.Entity(b => - { - b.ToTable("Junk"); - - b.OwnsOne( - x => x.Reference, b => b.ToJson().HasColumnType(JsonColumnType)); - - b.OwnsMany( - x => x.Collection, b => b.ToJson().HasColumnType(JsonColumnType)); - }); - } - - #endregion - - #region ArrayOfPrimitives - - protected override void OnModelCreatingArrayOfPrimitives(ModelBuilder modelBuilder) - { - base.OnModelCreatingArrayOfPrimitives(modelBuilder); - - modelBuilder.Entity().OwnsOne( - x => x.Reference, b => b.ToJson().HasColumnType(JsonColumnType)); - - modelBuilder.Entity().OwnsMany( - x => x.Collection, b => b.ToJson().HasColumnType(JsonColumnType)); - } - - #endregion - - #region JunkInJson - - protected override void OnModelCreatingJunkInJson(ModelBuilder modelBuilder) - { - base.OnModelCreatingJunkInJson(modelBuilder); - - modelBuilder.Entity(b => - { - b.ToTable("Entities"); - - b.OwnsOne( - x => x.Reference, b => b.ToJson().HasColumnType(JsonColumnType)); - - b.OwnsOne( - x => x.ReferenceWithCtor, b => b.ToJson().HasColumnType(JsonColumnType)); - - b.OwnsMany( - x => x.Collection, b => b.ToJson().HasColumnType(JsonColumnType)); - - b.OwnsMany( - x => x.CollectionWithCtor, b => b.ToJson().HasColumnType(JsonColumnType)); - }); - } - - #endregion - - #region TrickyBuffering - - protected override void OnModelCreatingTrickyBuffering(ModelBuilder modelBuilder) - { - base.OnModelCreatingTrickyBuffering(modelBuilder); - - modelBuilder.Entity(b => - { - b.ToTable("Entities"); - b.OwnsOne( - x => x.Reference, b => b.ToJson().HasColumnType(JsonColumnType)); - }); - } - - #endregion - - #region ShadowProperties - - protected override void OnModelCreatingShadowProperties(ModelBuilder modelBuilder) - { - base.OnModelCreatingShadowProperties(modelBuilder); - - modelBuilder.Entity(b => - { - b.ToTable("Entities"); - - b.OwnsOne( - x => x.Reference, b => b.ToJson().HasColumnType(JsonColumnType)); - - b.OwnsOne( - x => x.ReferenceWithCtor, b => - { - b.ToJson().HasColumnType(JsonColumnType); - b.Property("Shadow_Int").HasJsonPropertyName("ShadowInt"); - }); - - b.OwnsMany( - x => x.Collection, b => b.ToJson().HasColumnType(JsonColumnType)); - - b.OwnsMany( - x => x.CollectionWithCtor, b => b.ToJson().HasColumnType(JsonColumnType)); - }); - } - - #endregion - - #region LazyLoadingProxies - - protected override void OnModelCreatingLazyLoadingProxies(ModelBuilder modelBuilder) - { - base.OnModelCreatingLazyLoadingProxies(modelBuilder); - - modelBuilder.Entity().OwnsOne(x => x.Reference, b => b.ToJson().HasColumnType(JsonColumnType)); - modelBuilder.Entity() - .OwnsMany(x => x.Collection, b => b.ToJson().HasColumnType(JsonColumnType)); - } - - //protected void OnConfiguringLazyLoadingProxies(DbContextOptionsBuilder optionsBuilder) - // => optionsBuilder.UseLazyLoadingProxies(); - - //protected IServiceCollection AddServicesLazyLoadingProxies(IServiceCollection addServices) - // => addServices.AddEntityFrameworkProxies(); - - //private Task SeedLazyLoadingProxies(DbContext ctx) - //{ - // var r1 = new MyJsonEntityLazyLoadingProxiesWithCtor("r1", 1); - // var c11 = new MyJsonEntityLazyLoadingProxies { Name = "c11", Number = 11 }; - // var c12 = new MyJsonEntityLazyLoadingProxies { Name = "c12", Number = 12 }; - // var c13 = new MyJsonEntityLazyLoadingProxies { Name = "c13", Number = 13 }; - - // var r2 = new MyJsonEntityLazyLoadingProxiesWithCtor("r2", 2); - // var c21 = new MyJsonEntityLazyLoadingProxies { Name = "c21", Number = 21 }; - // var c22 = new MyJsonEntityLazyLoadingProxies { Name = "c22", Number = 22 }; - - // var e1 = new MyEntityLazyLoadingProxies - // { - // Id = 1, - // Name = "e1", - // Reference = r1, - // Collection = - // [ - // c11, - // c12, - // c13 - // ] - // }; - - // var e2 = new MyEntityLazyLoadingProxies - // { - // Id = 2, - // Name = "e2", - // Reference = r2, - // Collection = [c21, c22] - // }; - - // ctx.Set().AddRange(e1, e2); - // return ctx.SaveChangesAsync(); - //} - - #endregion - - #region NotICollection - - protected override void OnModelCreatingNotICollection(ModelBuilder modelBuilder) - { - base.OnModelCreatingNotICollection(modelBuilder); - - modelBuilder.Entity(b => - { - b.ToTable("Entities"); - b.OwnsOne( - cr => cr.Json, nb => nb.ToJson().HasColumnType(JsonColumnType)); - }); - } - - #endregion - - #region BadJsonProperties - - public override async Task Bad_json_properties_duplicated_navigations(bool noTracking) - { - // tracking returns different results - see #35807 - if (noTracking) - { - await base.Bad_json_properties_duplicated_navigations(noTracking); - } - } - - public override Task Bad_json_properties_null_navigations(bool noTracking) - => Assert.ThrowsAnyAsync(() => base.Bad_json_properties_null_navigations(noTracking)); - - public override async Task Bad_json_properties_null_scalars(bool noTracking) - { - var message = (await Assert.ThrowsAnyAsync(() => base.Bad_json_properties_null_scalars(noTracking))).Message; - - Assert.StartsWith("'n' is an invalid start of a property name. Expected a '\"'.", message); - } - - protected override void OnModelCreatingBadJsonProperties(ModelBuilder modelBuilder) - { - base.OnModelCreatingBadJsonProperties(modelBuilder); - - modelBuilder.Entity(b => - { - b.ToTable("Entities"); - - b.OwnsOne( - x => x.RequiredReference, b => b.ToJson().HasColumnType(JsonColumnType)); - - b.OwnsOne( - x => x.OptionalReference, b => b.ToJson().HasColumnType(JsonColumnType)); - - b.OwnsMany( - x => x.Collection, b => b.ToJson().HasColumnType(JsonColumnType)); - }); - } - - #endregion - - #region Entity splitting - - [Fact] // #36145 - public virtual async Task Entity_splitting_with_owned_json() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingEntitySplitting, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedEntitySplitting); - - using var context = contextFactory.CreateDbContext(); - var result = await context.Set().SingleAsync(); - - Assert.Equal("split content", result.PropertyInOtherTable); - var json = Assert.Single(result.Json); - Assert.Equal("JSON content", json.Foo); - } - - protected virtual void OnModelCreatingEntitySplitting(ModelBuilder modelBuilder) - => modelBuilder.Entity(b => - { - b.Property(p => p.Id).ValueGeneratedNever(); - b.OwnsMany(p => p.Json, b => b.ToJson()); - b.SplitToTable("OtherTable", b => b.Property(p => p.PropertyInOtherTable)); - }); - - protected virtual async Task SeedEntitySplitting(ContextEntitySplitting context) - { - var e1 = new ContextEntitySplitting.MyEntity - { - Id = 1, - PropertyInOtherTable = "split content", - Json = [new ContextEntitySplitting.JsonEntity { Foo = "JSON content" }] - }; - - context.Add(e1); - await context.SaveChangesAsync(); - } - - protected class ContextEntitySplitting(DbContextOptions options) : DbContext(options) - { - public class MyEntity - { - public int Id { get; set; } - public string PropertyInMainTable { get; set; } // TODO: currently required because of #36171 - public string PropertyInOtherTable { get; set; } - - public List Json { get; set; } - } - - public class JsonEntity - { - public string Foo { get; set; } - } - } - - #endregion - - #region HasJsonPropertyName - - [Fact] - public virtual async Task HasJsonPropertyName() - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - onModelCreating: m => m.Entity().ComplexProperty( - e => e.Json, b => - { - b.ToJson(); - - b.Property(j => j.String).HasJsonPropertyName("string"); - - b.ComplexProperty( - j => j.Nested, b => - { - b.HasJsonPropertyName("nested"); - b.Property(x => x.Int).HasJsonPropertyName("int"); - }); - - b.ComplexCollection( - a => a.NestedCollection, b => - { - b.HasJsonPropertyName("nested_collection"); - b.Property(x => x.Int).HasJsonPropertyName("int"); - }); - }), - seed: context => - { - context.Set().Add( - new Context37009.Entity - { - Json = new Context37009.JsonComplexType - { - String = "foo", - Nested = new Context37009.JsonNestedType { Int = 1 }, - NestedCollection = [new Context37009.JsonNestedType { Int = 2 }] - } - }); - - return context.SaveChangesAsync(); - }); - - await using var context = contextFactory.CreateDbContext(); - - Assert.Equal(1, await context.Set().CountAsync(e => e.Json.String == "foo")); - Assert.Equal(1, await context.Set().CountAsync(e => e.Json.Nested.Int == 1)); - Assert.Equal(1, await context.Set().CountAsync(e => e.Json.NestedCollection.Any(x => x.Int == 2))); - } - - protected class Context37009(DbContextOptions options) : DbContext(options) - { - public DbSet Entities { get; set; } - - public class Entity - { - public int Id { get; set; } - public JsonComplexType Json { get; set; } - } - - public class JsonComplexType - { - public string String { get; set; } - - public JsonNestedType Nested { get; set; } - public List NestedCollection { get; set; } - } - - public class JsonNestedType - { - public int Int { get; set; } - } - } - - #endregion HasJsonPropertyName - - #region 38615 - - [Fact] - public virtual async Task SelectMany_over_primitive_collection_nested_in_complex_collection_inside_json_column() - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - onModelCreating: m => m.Entity(b => - { - b.ToTable("Cars"); - b.HasKey(e => e.CarId); - b.Property(e => e.Vin).IsUnicode(false).HasMaxLength(32); - b.Property(e => e.DealerId).IsUnicode(false).HasMaxLength(32); - b.HasIndex(e => new { e.Vin, e.DealerId }).IsUnique(); - - b.ComplexProperty( - e => e.CarConfiguration, pp => - { - pp.ToJson("CarConfiguration"); - if (JsonColumnType != null) - { - pp.HasColumnType(JsonColumnType); - } - - pp.Property(p => p.CurrentTrim).HasJsonPropertyName("currentTrim"); - - pp.ComplexCollection( - p => p.OptionPackages, op => - { - op.HasJsonPropertyName("optionPackages"); - op.Property(o => o.PackageId).HasJsonPropertyName("packageId"); - op.PrimitiveCollection(o => o.PartNumbers) - .ElementType(e => e.IsUnicode(false).HasMaxLength(32)) - .HasJsonPropertyName("partNumbers"); - }); - }); - }), - seed: context => - { - context.Set().Add( - new Context38615.Car - { - Vin = "1FA6P8TH8J5123456", - DealerId = "DEALER-001", - CarConfiguration = new Context38615.CarConfiguration - { - CurrentTrim = "GT-Line", - OptionPackages = - [ - new Context38615.OptionPackage { PackageId = "PKG-SPORT", PartNumbers = ["SP-100", "SP-101"] }, - new Context38615.OptionPackage { PackageId = "PKG-TOW", PartNumbers = ["TW-200"] } - ] - } - }); - - return context.SaveChangesAsync(); - }); - - await using var context = contextFactory.CreateDbContext(); - - var partNumbers = await context.Set() - .Where(c => c.Vin == "1FA6P8TH8J5123456" && c.DealerId == "DEALER-001") - .SelectMany(c => c.CarConfiguration.OptionPackages) - .Where(op => op.PackageId == "PKG-SPORT") - .SelectMany(op => op.PartNumbers) - .ToListAsync(); - - Assert.Equal(["SP-100", "SP-101"], partNumbers.Order().ToList()); - } - - protected class Context38615(DbContextOptions options) : DbContext(options) - { - public class Car - { - public int CarId { get; set; } - public string Vin { get; set; } - public string DealerId { get; set; } - public CarConfiguration CarConfiguration { get; set; } - } - - public class CarConfiguration - { - public string CurrentTrim { get; set; } - public List OptionPackages { get; set; } - } - - public class OptionPackage - { - public string PackageId { get; set; } - public ICollection PartNumbers { get; set; } - } - } - - #endregion 38615 - - #region Value converter equality null scalar - - [Fact] - public virtual async Task Value_converter_equality_null_scalar() - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - onModelCreating: m => m.Entity().ComplexProperty( - e => e.Json, b => - { - b.ToJson(); - - b.Property(j => j.IntToString).HasConversion(new Context37983_StringToIntConverter()); - }), - seed: context => - { - context.Set() - .Add(new Context37983.Entity { Json = new Context37983.JsonComplexType { IntToString = null, } }); - - return context.SaveChangesAsync(); - }); - - await using var context = contextFactory.CreateDbContext(); - - TestSqlLoggerFactory.Clear(); - - var complexType = new Context37983.JsonComplexType { IntToString = null, }; - - Assert.Equal(1, await context.Set().CountAsync(e => e.Json == complexType)); - } - - protected class Context37983(DbContextOptions options) : DbContext(options) - { - public DbSet Entities { get; set; } - - public class Entity - { - public int Id { get; set; } - public JsonComplexType Json { get; set; } - } - - public class JsonComplexType - { - public int? IntToString { get; set; } - } - } - - protected class Context37983_StringToIntConverter : ValueConverter - { - public Context37983_StringToIntConverter() - : base( - v => v == null ? "" : v.ToString(), - v => int.Parse(v)) - { - } - - public override bool ConvertsNulls - => true; - } - - #endregion - - #region 38315 - - [Fact] - public virtual async Task Filter_on_complex_json_collection_on_entity_mapped_to_view() - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - onModelCreating: m => - { - m.Entity(e => - { - e.ToTable("Persons"); - e.ComplexCollection(p => p.OrderIds, b => b.ToJson()); - }); - m.Entity(e => - { - e.ToView("PersonOrdersView"); - e.ComplexCollection(p => p.OrderIds, b => b.ToJson()); - }); - }, - seed: async context => - { - var sqlGenerationHelper = context.GetService(); - await context.Database.ExecuteSqlRawAsync( - $"CREATE VIEW {Q("PersonOrdersView")} AS SELECT {Q("Id")}, {Q("OrderIds")} FROM {Q("Persons")}"); - - context.Set().Add( - new Context38315.Person { OrderIds = [new Context38315.ValueJson { Value = 3 }] }); - await context.SaveChangesAsync(); - - string Q(string name) - => sqlGenerationHelper.DelimitIdentifier(name); - }); - - await using var context = contextFactory.CreateDbContext(); - - var r = 3; - var result = await context.Set() - .Where(po => po.OrderIds.Any(oId => oId.Value == r)) - .ToListAsync(); - - Assert.Single(result); - } - - protected class Context38315(DbContextOptions options) : DbContext(options) - { - public DbSet Persons { get; set; } - public DbSet PersonOrdersViews { get; set; } - - public class Person - { - public int Id { get; set; } - public List OrderIds { get; set; } = []; - } - - public class PersonOrdersView - { - public int Id { get; set; } - public List OrderIds { get; set; } = []; - } - - public class ValueJson - { - public int Value { get; set; } - } - } - - #endregion - - protected TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ListLoggerFactory; - - protected virtual string JsonColumnType - => null; -} diff --git a/test/EFCore.Relational.Specification.Tests/Query/AdHocManyToManyQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/AdHocManyToManyQueryRelationalTestBase.cs deleted file mode 100644 index fd6b5c64fd4..00000000000 --- a/test/EFCore.Relational.Specification.Tests/Query/AdHocManyToManyQueryRelationalTestBase.cs +++ /dev/null @@ -1,18 +0,0 @@ -// 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; - -#nullable disable - -public abstract class AdHocManyToManyQueryRelationalTestBase(NonSharedFixture fixture) : AdHocManyToManyQueryTestBase(fixture) -{ - protected TestSqlLoggerFactory TestSqlLoggerFactory - => (TestSqlLoggerFactory)ListLoggerFactory; - - protected override void ClearLog() - => TestSqlLoggerFactory.Clear(); - - protected void AssertSql(params string[] expected) - => TestSqlLoggerFactory.AssertBaseline(expected); -} diff --git a/test/EFCore.Relational.Specification.Tests/Query/ComplexTypeQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/ComplexTypeQueryRelationalTestBase.cs index e9cd422bb42..15483d2b7b4 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/ComplexTypeQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/ComplexTypeQueryRelationalTestBase.cs @@ -71,6 +71,242 @@ public override async Task Union_two_different_struct_complex_type(bool async) AssertSql(); } + #region Non-shared test resources + +#nullable enable + + #region 37205 + + [Fact] + public virtual async Task Complex_json_collection_inside_left_join_subquery() + { + var contextFactory = await InitializeNonSharedTest(); + + await using var context = contextFactory.CreateDbContext(); + + _ = await context.Set().Include(p => p.Child).ToListAsync(); + } + + private class Context37205(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(); + modelBuilder.Entity(b => + { + b.ComplexCollection(e => e.CareNeeds, cb => cb.ToJson()); + b.HasQueryFilter(child => child.IsPublic); + }); + } + + public class Parent + { + public int Id { get; set; } + public Child? Child { get; set; } + public int? ChildId { get; set; } + } + + public class Child + { + public int Id { get; set; } + public Parent Parent { get; set; } = null!; + public bool IsPublic { get; set; } + public required List CareNeeds { get; set; } + } + + public record CareNeedAnswer + { + public required string Topic { get; set; } + } + } + + #endregion 37205 + + #region 35025 + + [Fact] + public virtual async Task Select_TPC_base_with_ComplexType() + { + var contextFactory = await InitializeNonSharedTest(); + using var context = contextFactory.CreateDbContext(); + + var entities = await context.TpcBases.ToListAsync(); + Assert.Empty(entities); + } + + protected class Context35025(DbContextOptions options) : DbContext(options) + { + public DbSet TpcBases { get; set; } = null!; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(builder => + { + builder.ComplexProperty(e => e.BaseComplexProperty); + builder.UseTpcMappingStrategy(); + }); + + modelBuilder.Entity().ComplexProperty(e => e.ChildComplexProperty); + modelBuilder.Entity().ComplexProperty(e => e.ChildComplexProperty); + } + + public abstract class TpcBase + { + public int Id { get; set; } + public required ComplexThing BaseComplexProperty { get; set; } + } + + public class TpcChild1 : TpcBase + { + public required ComplexThing ChildComplexProperty { get; set; } + public int ChildProperty { get; set; } + } + + public class TpcChild2 : TpcBase + { + public required AnotherComplexThing ChildComplexProperty { get; set; } + public required string ChildProperty { get; set; } + } + + public class ComplexThing + { + public int PropertyInsideComplexThing { get; set; } + } + + public class AnotherComplexThing + { + // Another nested property with the same name but a different type. + // We should properly uniquify the projected name coming out of the UNION. + public required string PropertyInsideComplexThing { get; set; } + } + } + + #endregion 35025 + + #region 34706 + + [Fact] + public virtual async Task Complex_type_on_an_entity_mapped_to_view_and_table() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: mb => mb.Entity(eb => eb + .ToTable("Blogs") + .ToView("BlogsView") + .ComplexProperty(b => b.ComplexThing)), + seed: async context => + { + var sqlGenerationHelper = context.GetService(); + await context.Database.ExecuteSqlRawAsync( + $"CREATE VIEW {Q("BlogsView")} AS SELECT {Q("Id")}, {Q("ComplexThing_Prop1")}, {Q("ComplexThing_Prop2")} FROM {Q("Blogs")}"); + + context.Add( + new Context34706.Blog { ComplexThing = new Context34706.ComplexThing { Prop1 = 1, Prop2 = 2 } }); + await context.SaveChangesAsync(); + + string Q(string name) + => sqlGenerationHelper.DelimitIdentifier(name); + }); + + await using var context = contextFactory.CreateDbContext(); + + var entity = await context.Set().SingleAsync(); + + Assert.NotNull(entity.ComplexThing); + Assert.Equal(1, entity.ComplexThing.Prop1); + Assert.Equal(2, entity.ComplexThing.Prop2); + } + + private class Context34706(DbContextOptions options) : DbContext(options) + { + public class Blog + { + public int Id { get; set; } + public required ComplexThing ComplexThing { get; set; } + } + + public class ComplexThing + { + public int Prop1 { get; set; } + public int Prop2 { get; set; } + } + } + + #endregion 34706 + + #region 38077 + + [Fact] + public virtual async Task Complex_property_on_split_entity() + { + var contextFactory = await InitializeNonSharedTest( + seed: async context => + { + context.Set().Add( + new Context38077.Hook + { + CreatedOn = new DateTime(2024, 1, 2, 3, 4, 5, DateTimeKind.Utc), + Number = new Context38077.HookNumber { Raw = "123", Parsed = 123 }, + Weight = 1.25m, + IsTestHook = false + }); + + await context.SaveChangesAsync(); + }); + + await using var context = contextFactory.CreateDbContext(); + + var hooks = await context.Set().ToListAsync(); + + Assert.Single(hooks); + Assert.Equal(new DateTime(2024, 1, 2, 3, 4, 5, DateTimeKind.Utc), hooks[0].CreatedOn); + Assert.Equal("123", hooks[0].Number.Raw); + Assert.Equal(123, hooks[0].Number.Parsed); + Assert.Equal(1.25m, hooks[0].Weight); + Assert.False(hooks[0].IsTestHook); + } + + private class Context38077(DbContextOptions options) : DbContext(options) + { + public class Hook + { + public int Id { get; set; } + public DateTime CreatedOn { get; set; } + public required HookNumber Number { get; set; } + public decimal? Weight { get; set; } + public bool? IsTestHook { get; set; } + } + + public class HookNumber + { + public required string Raw { get; set; } + public long Parsed { get; set; } + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.ComplexProperty(h => h.Number); + b.Property(h => h.Weight).HasPrecision(18, 6); + b.SplitToTable( + "HookMetadata", + tb => + { + tb.Property(h => h.Id).HasColumnName("HookId"); + tb.Property(h => h.Weight); + tb.Property(h => h.IsTestHook); + }); + }); + } + + #endregion 38077 + + protected TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + +#nullable disable + + #endregion Non-shared test resources + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryRelationalTestBase.cs index 4b4a4ac0a93..f3180abe0a6 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryRelationalTestBase.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.Text.Json; using Microsoft.EntityFrameworkCore.TestModels.JsonQuery; namespace Microsoft.EntityFrameworkCore.Query; @@ -566,4 +567,885 @@ public virtual async Task Json_projection_second_element_projected_before_owner_ nameof(QueryTrackingBehavior.NoTrackingWithIdentityResolution)), message); } + + #region Non-shared test resources + +#nullable disable + + + protected override void ConfigureWarnings(WarningsConfigurationBuilder builder) + { + base.ConfigureWarnings(builder); + + builder.Ignore(RelationalEventId.OwnedEntityMappedToJsonCollectionWarning); + } + + protected TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected virtual string JsonColumnType + => null; + + #region 21006 + + public override async Task Project_missing_required_navigation(bool async) + { + var message = (await Assert.ThrowsAsync(() => base.Project_missing_required_navigation(async))).Message; + + Assert.Equal(RelationalStrings.JsonRequiredEntityWithNullJson(typeof(Context21006.JsonEntityNested).Name), message); + } + + public override async Task Project_null_required_navigation(bool async) + { + var message = (await Assert.ThrowsAsync(() => base.Project_null_required_navigation(async))).Message; + + Assert.Equal(RelationalStrings.JsonRequiredEntityWithNullJson(typeof(Context21006.JsonEntityNested).Name), message); + } + + public override async Task Project_top_level_entity_with_null_value_required_scalars(bool async) + { + var message = (await Assert.ThrowsAsync(() + => base.Project_top_level_entity_with_null_value_required_scalars(async))).Message; + + Assert.Equal("Cannot get the value of a token type 'Null' as a number.", message); + } + + protected override void OnModelCreating21006(ModelBuilder modelBuilder) + { + base.OnModelCreating21006(modelBuilder); + + modelBuilder.Entity(b => + { + b.ToTable("Entities"); + b.OwnsOne(x => x.OptionalReference).ToJson().HasColumnType(JsonColumnType); + b.OwnsOne(x => x.RequiredReference).ToJson().HasColumnType(JsonColumnType); + b.OwnsMany(x => x.Collection).ToJson().HasColumnType(JsonColumnType); + }); + } + + #endregion + + #region 32310 + + protected override void OnModelCreating32310(ModelBuilder modelBuilder) + { + base.OnModelCreating32310(modelBuilder); + + modelBuilder.Entity().OwnsOne(e => e.Visits).ToJson().HasColumnType(JsonColumnType); + } + + #endregion + + #region 29219 + + protected override void OnModelCreating29219(ModelBuilder modelBuilder) + { + base.OnModelCreating29219(modelBuilder); + + modelBuilder.Entity(b => + { + b.ToTable("Entities"); + b.OwnsOne(x => x.Reference).ToJson().HasColumnType(JsonColumnType); + b.OwnsMany(x => x.Collection).ToJson().HasColumnType(JsonColumnType); + }); + } + + #endregion + + #region 30028 + + protected override void OnModelCreating30028(ModelBuilder modelBuilder) + { + base.OnModelCreating30028(modelBuilder); + + modelBuilder.Entity(b => + { + b.ToTable("Entities"); + b.OwnsOne( + x => x.Json, nb => nb.ToJson().HasColumnType(JsonColumnType)); + }); + } + + #endregion + + #region 32939 + + protected override void OnModelCreating32939(ModelBuilder modelBuilder) + { + base.OnModelCreating32939(modelBuilder); + + modelBuilder.Entity().OwnsOne(x => x.Empty, b => b.ToJson().HasColumnType(JsonColumnType)); + modelBuilder.Entity().OwnsOne(x => x.FieldOnly, b => b.ToJson().HasColumnType(JsonColumnType)); + } + + #endregion + + #region 33046 + + protected override void OnModelCreating33046(ModelBuilder modelBuilder) + { + base.OnModelCreating33046(modelBuilder); + + modelBuilder.Entity(b => + { + b.ToTable("Reviews"); + b.OwnsMany( + x => x.Rounds, ownedBuilder => ownedBuilder.ToJson().HasColumnType(JsonColumnType)); + }); + } + + #endregion + + #region 34293 + + [Fact] + public virtual async Task Project_entity_with_optional_json_entity_owned_by_required_json() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreating34293, + seed: ctx => ctx.Seed()); + + using var context = contextFactory.CreateDbContext(); + var entityProjection = await context.Set().ToListAsync(); + + Assert.Equal(3, entityProjection.Count); + } + + [Fact] + public virtual async Task Project_required_json_entity() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreating34293, + seed: ctx => ctx.Seed()); + + using var context = contextFactory.CreateDbContext(); + + var rootProjection = + await context.Set().AsNoTracking().Where(x => x.Id != 3).Select(x => x.Json).ToListAsync(); + Assert.Equal(2, rootProjection.Count); + + var branchProjection = await context.Set().AsNoTracking().Where(x => x.Id != 3).Select(x => x.Json.Required) + .ToListAsync(); + Assert.Equal(2, rootProjection.Count); + + var badRootProjectionMessage = (await Assert.ThrowsAsync(() + => context.Set().AsNoTracking().Where(x => x.Id == 3).Select(x => x.Json).ToListAsync())).Message; + Assert.Equal(RelationalStrings.JsonRequiredEntityWithNullJson(nameof(Context34293.JsonBranch)), badRootProjectionMessage); + + var badBranchProjectionMessage = (await Assert.ThrowsAsync(() + => context.Set().AsNoTracking().Where(x => x.Id == 3).Select(x => x.Json.Required).ToListAsync())).Message; + Assert.Equal(RelationalStrings.JsonRequiredEntityWithNullJson(nameof(Context34293.JsonBranch)), badBranchProjectionMessage); + } + + [Fact] + public virtual async Task Project_optional_json_entity_owned_by_required_json_entity() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreating34293, + seed: ctx => ctx.Seed()); + + using var context = contextFactory.CreateDbContext(); + var leafProjection = await context.Set().AsNoTracking().Select(x => x.Json.Required.Optional).ToListAsync(); + Assert.Equal(3, leafProjection.Count); + } + + protected class Context34293(DbContextOptions options) : DbContext(options) + { + public DbSet Entities { get; set; } + + public class Entity + { + public int Id { get; set; } + public JsonRoot Json { get; set; } + } + + public class JsonRoot + { + public DateTime Date { get; set; } + + public JsonBranch Required { get; set; } + } + + public class JsonBranch + { + public int Number { get; set; } + public JsonLeaf Optional { get; set; } + } + + public class JsonLeaf + { + public string Name { get; set; } + } + + public async Task Seed() + { + // everything - ok + var e1 = new Entity + { + Id = 1, + Json = new JsonRoot + { + Date = new DateTime(2001, 1, 1), + Required = new JsonBranch { Number = 1, Optional = new JsonLeaf { Name = "optional 1" } } + } + }; + + // null leaf - ok (optional nav) + var e2 = new Entity + { + Id = 2, + Json = new JsonRoot { Date = new DateTime(2002, 2, 2), Required = new JsonBranch { Number = 2, Optional = null } } + }; + + // null branch - invalid (required nav) + var e3 = new Entity + { + Id = 3, + Json = new JsonRoot + { + Date = new DateTime(2003, 3, 3), + Required = null, + } + }; + + Entities.AddRange(e1, e2, e3); + await SaveChangesAsync(); + } + } + + protected virtual void OnModelCreating34293(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.Property(x => x.Id).ValueGeneratedNever(); + b.OwnsOne( + x => x.Json, b => + { + b.ToJson().HasColumnType(JsonColumnType); + b.OwnsOne( + x => x.Required, bb => + { + bb.OwnsOne(x => x.Optional); + bb.Navigation(x => x.Optional).IsRequired(false); + }); + b.Navigation(x => x.Required).IsRequired(); + }); + b.Navigation(x => x.Json).IsRequired(); + }); + + #endregion + + #region 34960 + + public override async Task Try_project_collection_but_JSON_is_entity() + { + var message = (await Assert.ThrowsAsync(() => base.Try_project_collection_but_JSON_is_entity())).Message; + + Assert.Equal( + CoreStrings.JsonReaderInvalidTokenType(nameof(JsonTokenType.StartObject)), + message); + } + + public override async Task Try_project_reference_but_JSON_is_collection() + { + var message = (await Assert.ThrowsAsync(() => base.Try_project_reference_but_JSON_is_collection())) + .Message; + + Assert.Equal( + CoreStrings.JsonReaderInvalidTokenType(nameof(JsonTokenType.StartArray)), + message); + } + + protected override void OnModelCreating34960(ModelBuilder modelBuilder) + { + base.OnModelCreating34960(modelBuilder); + + modelBuilder.Entity(b => + { + b.ToTable("Entities"); + + b.OwnsOne( + x => x.Reference, b => b.ToJson().HasColumnType(JsonColumnType)); + + b.OwnsMany( + x => x.Collection, b => b.ToJson().HasColumnType(JsonColumnType)); + }); + + modelBuilder.Entity(b => + { + b.ToTable("Junk"); + + b.OwnsOne( + x => x.Reference, b => b.ToJson().HasColumnType(JsonColumnType)); + + b.OwnsMany( + x => x.Collection, b => b.ToJson().HasColumnType(JsonColumnType)); + }); + } + + #endregion + + #region ArrayOfPrimitives + + protected override void OnModelCreatingArrayOfPrimitives(ModelBuilder modelBuilder) + { + base.OnModelCreatingArrayOfPrimitives(modelBuilder); + + modelBuilder.Entity().OwnsOne( + x => x.Reference, b => b.ToJson().HasColumnType(JsonColumnType)); + + modelBuilder.Entity().OwnsMany( + x => x.Collection, b => b.ToJson().HasColumnType(JsonColumnType)); + } + + #endregion + + #region JunkInJson + + protected override void OnModelCreatingJunkInJson(ModelBuilder modelBuilder) + { + base.OnModelCreatingJunkInJson(modelBuilder); + + modelBuilder.Entity(b => + { + b.ToTable("Entities"); + + b.OwnsOne( + x => x.Reference, b => b.ToJson().HasColumnType(JsonColumnType)); + + b.OwnsOne( + x => x.ReferenceWithCtor, b => b.ToJson().HasColumnType(JsonColumnType)); + + b.OwnsMany( + x => x.Collection, b => b.ToJson().HasColumnType(JsonColumnType)); + + b.OwnsMany( + x => x.CollectionWithCtor, b => b.ToJson().HasColumnType(JsonColumnType)); + }); + } + + #endregion + + #region TrickyBuffering + + protected override void OnModelCreatingTrickyBuffering(ModelBuilder modelBuilder) + { + base.OnModelCreatingTrickyBuffering(modelBuilder); + + modelBuilder.Entity(b => + { + b.ToTable("Entities"); + b.OwnsOne( + x => x.Reference, b => b.ToJson().HasColumnType(JsonColumnType)); + }); + } + + #endregion + + #region ShadowProperties + + protected override void OnModelCreatingShadowProperties(ModelBuilder modelBuilder) + { + base.OnModelCreatingShadowProperties(modelBuilder); + + modelBuilder.Entity(b => + { + b.ToTable("Entities"); + + b.OwnsOne( + x => x.Reference, b => b.ToJson().HasColumnType(JsonColumnType)); + + b.OwnsOne( + x => x.ReferenceWithCtor, b => + { + b.ToJson().HasColumnType(JsonColumnType); + b.Property("Shadow_Int").HasJsonPropertyName("ShadowInt"); + }); + + b.OwnsMany( + x => x.Collection, b => b.ToJson().HasColumnType(JsonColumnType)); + + b.OwnsMany( + x => x.CollectionWithCtor, b => b.ToJson().HasColumnType(JsonColumnType)); + }); + } + + #endregion + + #region LazyLoadingProxies + + protected override void OnModelCreatingLazyLoadingProxies(ModelBuilder modelBuilder) + { + base.OnModelCreatingLazyLoadingProxies(modelBuilder); + + modelBuilder.Entity().OwnsOne(x => x.Reference, b => b.ToJson().HasColumnType(JsonColumnType)); + modelBuilder.Entity() + .OwnsMany(x => x.Collection, b => b.ToJson().HasColumnType(JsonColumnType)); + } + + //protected void OnConfiguringLazyLoadingProxies(DbContextOptionsBuilder optionsBuilder) + // => optionsBuilder.UseLazyLoadingProxies(); + + //protected IServiceCollection AddServicesLazyLoadingProxies(IServiceCollection addServices) + // => addServices.AddEntityFrameworkProxies(); + + //private Task SeedLazyLoadingProxies(DbContext ctx) + //{ + // var r1 = new MyJsonEntityLazyLoadingProxiesWithCtor("r1", 1); + // var c11 = new MyJsonEntityLazyLoadingProxies { Name = "c11", Number = 11 }; + // var c12 = new MyJsonEntityLazyLoadingProxies { Name = "c12", Number = 12 }; + // var c13 = new MyJsonEntityLazyLoadingProxies { Name = "c13", Number = 13 }; + + // var r2 = new MyJsonEntityLazyLoadingProxiesWithCtor("r2", 2); + // var c21 = new MyJsonEntityLazyLoadingProxies { Name = "c21", Number = 21 }; + // var c22 = new MyJsonEntityLazyLoadingProxies { Name = "c22", Number = 22 }; + + // var e1 = new MyEntityLazyLoadingProxies + // { + // Id = 1, + // Name = "e1", + // Reference = r1, + // Collection = + // [ + // c11, + // c12, + // c13 + // ] + // }; + + // var e2 = new MyEntityLazyLoadingProxies + // { + // Id = 2, + // Name = "e2", + // Reference = r2, + // Collection = [c21, c22] + // }; + + // ctx.Set().AddRange(e1, e2); + // return ctx.SaveChangesAsync(); + //} + + #endregion + + #region NotICollection + + protected override void OnModelCreatingNotICollection(ModelBuilder modelBuilder) + { + base.OnModelCreatingNotICollection(modelBuilder); + + modelBuilder.Entity(b => + { + b.ToTable("Entities"); + b.OwnsOne( + cr => cr.Json, nb => nb.ToJson().HasColumnType(JsonColumnType)); + }); + } + + #endregion + + #region BadJsonProperties + + public override async Task Bad_json_properties_duplicated_navigations(bool noTracking) + { + // tracking returns different results - see #35807 + if (noTracking) + { + await base.Bad_json_properties_duplicated_navigations(noTracking); + } + } + + public override Task Bad_json_properties_null_navigations(bool noTracking) + => Assert.ThrowsAnyAsync(() => base.Bad_json_properties_null_navigations(noTracking)); + + public override async Task Bad_json_properties_null_scalars(bool noTracking) + { + var message = (await Assert.ThrowsAnyAsync(() => base.Bad_json_properties_null_scalars(noTracking))).Message; + + Assert.StartsWith("'n' is an invalid start of a property name. Expected a '\"'.", message); + } + + protected override void OnModelCreatingBadJsonProperties(ModelBuilder modelBuilder) + { + base.OnModelCreatingBadJsonProperties(modelBuilder); + + modelBuilder.Entity(b => + { + b.ToTable("Entities"); + + b.OwnsOne( + x => x.RequiredReference, b => b.ToJson().HasColumnType(JsonColumnType)); + + b.OwnsOne( + x => x.OptionalReference, b => b.ToJson().HasColumnType(JsonColumnType)); + + b.OwnsMany( + x => x.Collection, b => b.ToJson().HasColumnType(JsonColumnType)); + }); + } + + #endregion + + #region Entity splitting + + [Fact] // #36145 + public virtual async Task Entity_splitting_with_owned_json() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingEntitySplitting, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedEntitySplitting); + + using var context = contextFactory.CreateDbContext(); + var result = await context.Set().SingleAsync(); + + Assert.Equal("split content", result.PropertyInOtherTable); + var json = Assert.Single(result.Json); + Assert.Equal("JSON content", json.Foo); + } + + protected virtual void OnModelCreatingEntitySplitting(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.Property(p => p.Id).ValueGeneratedNever(); + b.OwnsMany(p => p.Json, b => b.ToJson()); + b.SplitToTable("OtherTable", b => b.Property(p => p.PropertyInOtherTable)); + }); + + protected virtual async Task SeedEntitySplitting(ContextEntitySplitting context) + { + var e1 = new ContextEntitySplitting.MyEntity + { + Id = 1, + PropertyInOtherTable = "split content", + Json = [new ContextEntitySplitting.JsonEntity { Foo = "JSON content" }] + }; + + context.Add(e1); + await context.SaveChangesAsync(); + } + + protected class ContextEntitySplitting(DbContextOptions options) : DbContext(options) + { + public class MyEntity + { + public int Id { get; set; } + public string PropertyInMainTable { get; set; } // TODO: currently required because of #36171 + public string PropertyInOtherTable { get; set; } + + public List Json { get; set; } + } + + public class JsonEntity + { + public string Foo { get; set; } + } + } + + #endregion + + #region HasJsonPropertyName + + [Fact] + public virtual async Task HasJsonPropertyName() + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: m => m.Entity().ComplexProperty( + e => e.Json, b => + { + b.ToJson(); + + b.Property(j => j.String).HasJsonPropertyName("string"); + + b.ComplexProperty( + j => j.Nested, b => + { + b.HasJsonPropertyName("nested"); + b.Property(x => x.Int).HasJsonPropertyName("int"); + }); + + b.ComplexCollection( + a => a.NestedCollection, b => + { + b.HasJsonPropertyName("nested_collection"); + b.Property(x => x.Int).HasJsonPropertyName("int"); + }); + }), + seed: context => + { + context.Set().Add( + new Context37009.Entity + { + Json = new Context37009.JsonComplexType + { + String = "foo", + Nested = new Context37009.JsonNestedType { Int = 1 }, + NestedCollection = [new Context37009.JsonNestedType { Int = 2 }] + } + }); + + return context.SaveChangesAsync(); + }); + + await using var context = contextFactory.CreateDbContext(); + + Assert.Equal(1, await context.Set().CountAsync(e => e.Json.String == "foo")); + Assert.Equal(1, await context.Set().CountAsync(e => e.Json.Nested.Int == 1)); + Assert.Equal(1, await context.Set().CountAsync(e => e.Json.NestedCollection.Any(x => x.Int == 2))); + } + + protected class Context37009(DbContextOptions options) : DbContext(options) + { + public DbSet Entities { get; set; } + + public class Entity + { + public int Id { get; set; } + public JsonComplexType Json { get; set; } + } + + public class JsonComplexType + { + public string String { get; set; } + + public JsonNestedType Nested { get; set; } + public List NestedCollection { get; set; } + } + + public class JsonNestedType + { + public int Int { get; set; } + } + } + + #endregion HasJsonPropertyName + + #region 38615 + + [Fact] + public virtual async Task SelectMany_over_primitive_collection_nested_in_complex_collection_inside_json_column() + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: m => m.Entity(b => + { + b.ToTable("Cars"); + b.HasKey(e => e.CarId); + b.Property(e => e.Vin).IsUnicode(false).HasMaxLength(32); + b.Property(e => e.DealerId).IsUnicode(false).HasMaxLength(32); + b.HasIndex(e => new { e.Vin, e.DealerId }).IsUnique(); + + b.ComplexProperty( + e => e.CarConfiguration, pp => + { + pp.ToJson("CarConfiguration"); + if (JsonColumnType != null) + { + pp.HasColumnType(JsonColumnType); + } + + pp.Property(p => p.CurrentTrim).HasJsonPropertyName("currentTrim"); + + pp.ComplexCollection( + p => p.OptionPackages, op => + { + op.HasJsonPropertyName("optionPackages"); + op.Property(o => o.PackageId).HasJsonPropertyName("packageId"); + op.PrimitiveCollection(o => o.PartNumbers) + .ElementType(e => e.IsUnicode(false).HasMaxLength(32)) + .HasJsonPropertyName("partNumbers"); + }); + }); + }), + seed: context => + { + context.Set().Add( + new Context38615.Car + { + Vin = "1FA6P8TH8J5123456", + DealerId = "DEALER-001", + CarConfiguration = new Context38615.CarConfiguration + { + CurrentTrim = "GT-Line", + OptionPackages = + [ + new Context38615.OptionPackage { PackageId = "PKG-SPORT", PartNumbers = ["SP-100", "SP-101"] }, + new Context38615.OptionPackage { PackageId = "PKG-TOW", PartNumbers = ["TW-200"] } + ] + } + }); + + return context.SaveChangesAsync(); + }); + + await using var context = contextFactory.CreateDbContext(); + + var partNumbers = await context.Set() + .Where(c => c.Vin == "1FA6P8TH8J5123456" && c.DealerId == "DEALER-001") + .SelectMany(c => c.CarConfiguration.OptionPackages) + .Where(op => op.PackageId == "PKG-SPORT") + .SelectMany(op => op.PartNumbers) + .ToListAsync(); + + Assert.Equal(["SP-100", "SP-101"], partNumbers.Order().ToList()); + } + + protected class Context38615(DbContextOptions options) : DbContext(options) + { + public class Car + { + public int CarId { get; set; } + public string Vin { get; set; } + public string DealerId { get; set; } + public CarConfiguration CarConfiguration { get; set; } + } + + public class CarConfiguration + { + public string CurrentTrim { get; set; } + public List OptionPackages { get; set; } + } + + public class OptionPackage + { + public string PackageId { get; set; } + public ICollection PartNumbers { get; set; } + } + } + + #endregion 38615 + + #region Value converter equality null scalar + + [Fact] + public virtual async Task Value_converter_equality_null_scalar() + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: m => m.Entity().ComplexProperty( + e => e.Json, b => + { + b.ToJson(); + + b.Property(j => j.IntToString).HasConversion(new Context37983_StringToIntConverter()); + }), + seed: context => + { + context.Set() + .Add(new Context37983.Entity { Json = new Context37983.JsonComplexType { IntToString = null, } }); + + return context.SaveChangesAsync(); + }); + + await using var context = contextFactory.CreateDbContext(); + + TestSqlLoggerFactory.Clear(); + + var complexType = new Context37983.JsonComplexType { IntToString = null, }; + + Assert.Equal(1, await context.Set().CountAsync(e => e.Json == complexType)); + } + + protected class Context37983(DbContextOptions options) : DbContext(options) + { + public DbSet Entities { get; set; } + + public class Entity + { + public int Id { get; set; } + public JsonComplexType Json { get; set; } + } + + public class JsonComplexType + { + public int? IntToString { get; set; } + } + } + + protected class Context37983_StringToIntConverter : ValueConverter + { + public Context37983_StringToIntConverter() + : base( + v => v == null ? "" : v.ToString(), + v => int.Parse(v)) + { + } + + public override bool ConvertsNulls + => true; + } + + #endregion + + #region 38315 + + [Fact] + public virtual async Task Filter_on_complex_json_collection_on_entity_mapped_to_view() + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: m => + { + m.Entity(e => + { + e.ToTable("Persons"); + e.ComplexCollection(p => p.OrderIds, b => b.ToJson()); + }); + m.Entity(e => + { + e.ToView("PersonOrdersView"); + e.ComplexCollection(p => p.OrderIds, b => b.ToJson()); + }); + }, + seed: async context => + { + var sqlGenerationHelper = context.GetService(); + await context.Database.ExecuteSqlRawAsync( + $"CREATE VIEW {Q("PersonOrdersView")} AS SELECT {Q("Id")}, {Q("OrderIds")} FROM {Q("Persons")}"); + + context.Set().Add( + new Context38315.Person { OrderIds = [new Context38315.ValueJson { Value = 3 }] }); + await context.SaveChangesAsync(); + + string Q(string name) + => sqlGenerationHelper.DelimitIdentifier(name); + }); + + await using var context = contextFactory.CreateDbContext(); + + var r = 3; + var result = await context.Set() + .Where(po => po.OrderIds.Any(oId => oId.Value == r)) + .ToListAsync(); + + Assert.Single(result); + } + + protected class Context38315(DbContextOptions options) : DbContext(options) + { + public DbSet Persons { get; set; } + public DbSet PersonOrdersViews { get; set; } + + public class Person + { + public int Id { get; set; } + public List OrderIds { get; set; } = []; + } + + public class PersonOrdersView + { + public int Id { get; set; } + public List OrderIds { get; set; } = []; + } + + public class ValueJson + { + public int Value { get; set; } + } + } + + #endregion + +#nullable restore + + #endregion } diff --git a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs deleted file mode 100644 index 6c8c86dd228..00000000000 --- a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs +++ /dev/null @@ -1,868 +0,0 @@ -// 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; - -// ReSharper disable ClassNeverInstantiated.Local -public abstract class AdHocComplexTypeQueryTestBase(NonSharedFixture fixture) - : NonSharedModelTestBase(fixture), IClassFixture -{ - #region 33449 - - [Fact] - public virtual async Task Complex_type_equals_parameter_with_nested_types_with_property_of_same_name() - { - var contextFactory = await InitializeNonSharedTest( - seed: context => - { - context.AddRange( - new Context33449.EntityType - { - Id = 1, - ComplexContainer = new Context33449.ComplexContainer - { - Id = 1, - Containee1 = new Context33449.ComplexContainee1 { Id = 2 }, - Containee2 = new Context33449.ComplexContainee2 { Id = 3 } - } - }); - return context.SaveChangesAsync(); - }); - - await using var context = contextFactory.CreateDbContext(); - - var container = new Context33449.ComplexContainer - { - Id = 1, - Containee1 = new Context33449.ComplexContainee1 { Id = 2 }, - Containee2 = new Context33449.ComplexContainee2 { Id = 3 } - }; - - _ = await context.Set().Where(b => b.ComplexContainer == container).SingleAsync(); - } - - private class Context33449(DbContextOptions options) : DbContext(options) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - => modelBuilder.Entity(b => - { - b.Property(b => b.Id).ValueGeneratedNever(); - b.ComplexProperty( - b => b.ComplexContainer, x => - { - x.ComplexProperty(c => c.Containee1); - x.ComplexProperty(c => c.Containee2); - }); - }); - - public class EntityType - { - public int Id { get; set; } - public ComplexContainer ComplexContainer { get; set; } = null!; - } - - public class ComplexContainer - { - public int Id { get; set; } - - public ComplexContainee1 Containee1 { get; set; } = null!; - public ComplexContainee2 Containee2 { get; set; } = null!; - } - - public class ComplexContainee1 - { - public int Id { get; set; } - } - - public class ComplexContainee2 - { - public int Id { get; set; } - } - } - - #endregion 33449 - - #region 34749 - - [Fact] - public virtual async Task Projecting_complex_property_does_not_auto_include_owned_types() - { - var contextFactory = await InitializeNonSharedTest(); - - await using var context = contextFactory.CreateDbContext(); - - _ = await context.Set().Select(x => x.Complex).ToListAsync(); - } - - private class Context34749(DbContextOptions options) : DbContext(options) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - => modelBuilder.Entity(b => - { - b.Property(b => b.Id).ValueGeneratedNever(); - b.ComplexProperty(x => x.Complex); - b.OwnsOne(x => x.OwnedReference); - }); - - public class EntityType - { - public int Id { get; set; } - public string? Name { get; set; } - public OwnedType OwnedReference { get; set; } = null!; - public ComplexType Complex { get; set; } = null!; - } - - public class ComplexType - { - public int Number { get; set; } - public string? Name { get; set; } - } - - public class OwnedType - { - public string? Foo { get; set; } - public int Bar { get; set; } - } - } - - #endregion - - #region ShadowDiscriminator - - [Fact] - public virtual async Task Optional_complex_type_with_discriminator() - { - var contextFactory = await InitializeNonSharedTest( - seed: context => - { - context.AddRange( - new ContextShadowDiscriminator.EntityType - { - Id = 1, - AllOptionalsComplexType = - new ContextShadowDiscriminator.AllOptionalsComplexType { OptionalProperty = "Non-null" } - }, - new ContextShadowDiscriminator.EntityType - { - Id = 2, - AllOptionalsComplexType = new ContextShadowDiscriminator.AllOptionalsComplexType { OptionalProperty = null } - }, - new ContextShadowDiscriminator.EntityType { Id = 3, AllOptionalsComplexType = null } - ); - return context.SaveChangesAsync(); - }); - - await using (var context = contextFactory.CreateDbContext()) - { - var complexTypeNull = await context.Set() - .SingleAsync(b => b.AllOptionalsComplexType == null); - Assert.Null(complexTypeNull.AllOptionalsComplexType); - - complexTypeNull.AllOptionalsComplexType = - new ContextShadowDiscriminator.AllOptionalsComplexType { OptionalProperty = "New thing" }; - await context.SaveChangesAsync(); - } - - await using (var context = contextFactory.CreateDbContext()) - { - var entities = await context.Set().ToListAsync(); - Assert.Equal(3, entities.Count); - Assert.All(entities, e => Assert.NotNull(e.AllOptionalsComplexType)); - } - } - - private class ContextShadowDiscriminator(DbContextOptions options) : DbContext(options) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - => modelBuilder.Entity(b => - { - b.Property(b => b.Id).ValueGeneratedNever(); - b.ComplexProperty(b => b.AllOptionalsComplexType, x => x.HasDiscriminator()); - }); - - public class EntityType - { - public int Id { get; set; } - public AllOptionalsComplexType? AllOptionalsComplexType { get; set; } - } - - public class AllOptionalsComplexType - { - public string? OptionalProperty { get; set; } - } - } - - #endregion ShadowDiscriminator - - #region 37162 - - [Fact] - public virtual async Task Non_optional_complex_type_with_all_nullable_properties() - { - var contextFactory = await InitializeNonSharedTest( - seed: context => - { - context.Add( - new Context37162.EntityType - { - Id = 1, - NonOptionalComplexType = new Context37162.ComplexTypeWithAllNulls - { - // All properties are null - } - }); - return context.SaveChangesAsync(); - }); - - await using var context = contextFactory.CreateDbContext(); - - var entity = await context.Set().SingleAsync(); - - Assert.NotNull(entity.NonOptionalComplexType); - Assert.Null(entity.NonOptionalComplexType.NullableString); - Assert.Null(entity.NonOptionalComplexType.NullableDateTime); - } - - private class Context37162(DbContextOptions options) : DbContext(options) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - => modelBuilder.Entity(b => - { - b.Property(b => b.Id).ValueGeneratedNever(); - b.ComplexProperty(b => b.NonOptionalComplexType); - }); - - public class EntityType - { - public int Id { get; set; } - public ComplexTypeWithAllNulls NonOptionalComplexType { get; set; } = null!; - } - - public class ComplexTypeWithAllNulls - { - public string? NullableString { get; set; } - public DateTime? NullableDateTime { get; set; } - } - } - - #endregion 37162 - - #region 37304 - - [Fact] - public virtual async Task Non_optional_complex_type_with_all_nullable_properties_via_left_join() - { - var contextFactory = await InitializeNonSharedTest( - seed: context => - { - context.Add( - new Context37304.Parent - { - Id = 1, - Children = - [ - new Context37304.Child { Id = 1, ComplexType = new Context37304.ComplexTypeWithAllNulls() } - ] - }); - return context.SaveChangesAsync(); - }); - - await using var context = contextFactory.CreateDbContext(); - - var parent = await context.Set().Include(p => p.Children).SingleAsync(); - - var child = parent.Children.Single(); - Assert.NotNull(child.ComplexType); - Assert.Null(child.ComplexType.NullableString); - Assert.Null(child.ComplexType.NullableDateTime); - } - - private class Context37304(DbContextOptions options) : DbContext(options) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity(b => b.Property(p => p.Id).ValueGeneratedNever()); - - modelBuilder.Entity(b => - { - b.Property(c => c.Id).ValueGeneratedNever(); - b.HasOne(c => c.Parent).WithMany(p => p.Children).HasForeignKey(c => c.ParentId); - b.ComplexProperty(c => c.ComplexType); - }); - } - - public class Parent - { - public int Id { get; set; } - public List Children { get; set; } = []; - } - - public class Child - { - public int Id { get; set; } - public int ParentId { get; set; } - public Parent Parent { get; set; } = null!; - public ComplexTypeWithAllNulls ComplexType { get; set; } = null!; - } - - public class ComplexTypeWithAllNulls - { - public string? NullableString { get; set; } - public DateTime? NullableDateTime { get; set; } - } - } - - #endregion 37304 - - #region Issue37337 - - private const string Issue37337CreatedByShadowPropertyName = "CreatedBy"; - - [Fact] - public virtual async Task Nullable_complex_type_with_discriminator_and_shadow_property() - { - var contextFactory = await InitializeNonSharedTest( - seed: context => - { - var entity = new Context37337.EntityType - { - Id = Guid.NewGuid(), - Prop = new Context37337.OptionalComplexProperty { OptionalValue = true } - }; - context.Add(entity); - context.Entry(entity).Property(Issue37337CreatedByShadowPropertyName).CurrentValue = "Seeder"; - return context.SaveChangesAsync(); - }); - - await using var context = contextFactory.CreateDbContext(); - - var entities = await context.Set().ToArrayAsync(); - - Assert.Single(entities); - var entity = entities[0]; - Assert.NotNull(entity.Prop); - Assert.True(entity.Prop.OptionalValue); - - var entry = context.Entry(entity); - Assert.Equal("Seeder", entry.Property(Issue37337CreatedByShadowPropertyName).CurrentValue); - } - - protected class Context37337(DbContextOptions options) : DbContext(options) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - var entity = modelBuilder.Entity(); - entity.Property(p => p.Id).ValueGeneratedNever(); - entity.HasKey(p => p.Id); - - var compl = entity.ComplexProperty(p => p.Prop); - compl.Property(p => p.OptionalValue); - compl.HasDiscriminator(); - - // Shadow property added via convention (e.g., audit field) - entity.Property(Issue37337CreatedByShadowPropertyName).IsRequired(false); - } - - public class EntityType - { - public Guid Id { get; set; } - public OptionalComplexProperty? Prop { get; set; } - } - - public class OptionalComplexProperty - { - public bool? OptionalValue { get; set; } - } - } - - #endregion Issue37337 - - #region Issue38119 - - [Fact] - public virtual async Task Nullable_complex_type_with_discriminator_null_to_non_null_roundtrip() - { - var contextFactory = await InitializeNonSharedTest( - seed: context => - { - context.Add(new Context38119.EntityType { Id = Guid.NewGuid() }); - return context.SaveChangesAsync(); - }); - - await using (var context = contextFactory.CreateDbContext()) - { - var entity = await context.Set().SingleAsync(); - Assert.Null(entity.Prop); - - entity.Prop = new Context38119.OptionalComplexProperty { OptionalValue = true }; - await context.SaveChangesAsync(); - } - - await using (var context = contextFactory.CreateDbContext()) - { - var entity = await context.Set().SingleAsync(); - Assert.NotNull(entity.Prop); - Assert.True(entity.Prop.OptionalValue); - } - } - - [Fact] - public virtual async Task Nullable_complex_type_with_discriminator_non_null_to_null_roundtrip() - { - var contextFactory = await InitializeNonSharedTest( - seed: context => - { - context.Add( - new Context38119.EntityType - { - Id = Guid.NewGuid(), - Prop = new Context38119.OptionalComplexProperty { OptionalValue = true } - }); - return context.SaveChangesAsync(); - }); - - await using (var context = contextFactory.CreateDbContext()) - { - var entity = await context.Set().SingleAsync(); - Assert.NotNull(entity.Prop); - - entity.Prop = null; - await context.SaveChangesAsync(); - } - - await using (var context = contextFactory.CreateDbContext()) - { - var entity = await context.Set().SingleAsync(); - Assert.Null(entity.Prop); - } - } - - [Fact] - public virtual async Task Nullable_complex_type_with_discriminator_update_non_null_entity_roundtrip() - { - var contextFactory = await InitializeNonSharedTest( - seed: context => - { - context.Add( - new Context38119.EntityType - { - Id = Guid.NewGuid(), - Prop = new Context38119.OptionalComplexProperty { OptionalValue = true } - }); - return context.SaveChangesAsync(); - }); - - await using (var context = contextFactory.CreateDbContext()) - { - var entity = await context.Set().SingleAsync(); - Assert.NotNull(entity.Prop); - Assert.True(entity.Prop.OptionalValue); - - context.Update(entity); - await context.SaveChangesAsync(); - } - - await using (var context = contextFactory.CreateDbContext()) - { - var entity = await context.Set().SingleAsync(); - Assert.NotNull(entity.Prop); - Assert.True(entity.Prop.OptionalValue); - } - } - - [Fact] - public virtual async Task Nullable_complex_type_with_discriminator_set_to_different_value() - { - var contextFactory = await InitializeNonSharedTest(); - - Guid entityId; - await using (var context = contextFactory.CreateDbContext()) - { - var entity = new Context38119.EntityType - { - Id = Guid.NewGuid(), - Prop = new Context38119.OptionalComplexProperty { OptionalValue = true } - }; - context.Add(entity); - entityId = entity.Id; - - // Override the discriminator value before saving - var discriminatorEntry = context.Entry(entity).ComplexProperty(e => e.Prop).Property("Discriminator"); - Assert.Equal("OptionalComplexProperty", discriminatorEntry.CurrentValue); - discriminatorEntry.CurrentValue = "SomeOtherValue"; - await context.SaveChangesAsync(); - } - - await using (var context = contextFactory.CreateDbContext()) - { - // The discriminator is non-null so the complex property is still materialized - var entity = await context.Set().SingleAsync(e => e.Id == entityId); - Assert.NotNull(entity.Prop); - Assert.True(entity.Prop.OptionalValue); - } - } - - [Fact] - public virtual async Task Nullable_complex_type_with_discriminator_set_to_null() - { - var contextFactory = await InitializeNonSharedTest(); - - Guid entityId; - await using (var context = contextFactory.CreateDbContext()) - { - var entity = new Context38119.EntityType - { - Id = Guid.NewGuid(), - Prop = new Context38119.OptionalComplexProperty { OptionalValue = true } - }; - context.Add(entity); - entityId = entity.Id; - - // Set discriminator to null before saving, which should cause the complex property to be null on reload - var discriminatorEntry = context.Entry(entity).ComplexProperty(e => e.Prop).Property("Discriminator"); - Assert.Equal("OptionalComplexProperty", discriminatorEntry.CurrentValue); - discriminatorEntry.CurrentValue = null; - await context.SaveChangesAsync(); - } - - await using (var context = contextFactory.CreateDbContext()) - { - // With null discriminator, the complex property should be materialized as null - var entity = await context.Set().SingleAsync(e => e.Id == entityId); - Assert.Null(entity.Prop); - } - } - - [Fact] - public virtual async Task Nested_nullable_complex_type_with_discriminator_null_to_non_null_roundtrip() - { - var contextFactory = await InitializeNonSharedTest( - seed: context => - { - context.Add( - new Context38119Nested.EntityType - { - Id = Guid.NewGuid(), - Outer = new Context38119Nested.OuterComplexProperty { Name = "outer" } - }); - return context.SaveChangesAsync(); - }); - - await using (var context = contextFactory.CreateDbContext()) - { - var entity = await context.Set().SingleAsync(); - Assert.NotNull(entity.Outer); - Assert.Null(entity.Outer.Inner); - - entity.Outer.Inner = new Context38119Nested.InnerComplexProperty { Value = 42 }; - await context.SaveChangesAsync(); - } - - await using (var context = contextFactory.CreateDbContext()) - { - var entity = await context.Set().SingleAsync(); - Assert.NotNull(entity.Outer); - Assert.NotNull(entity.Outer.Inner); - Assert.Equal(42, entity.Outer.Inner.Value); - } - } - - protected class Context38119(DbContextOptions options) : DbContext(options) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - var entity = modelBuilder.Entity(); - entity.HasKey(p => p.Id); - entity.Property(p => p.Id).ValueGeneratedNever(); - - var compl = entity.ComplexProperty(p => p.Prop); - compl.HasDiscriminator(); - } - - public class EntityType - { - public Guid Id { get; set; } - public OptionalComplexProperty? Prop { get; set; } - } - - public class OptionalComplexProperty - { - public bool? OptionalValue { get; set; } - } - } - - private class Context38119Nested(DbContextOptions options) : DbContext(options) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - var entity = modelBuilder.Entity(); - entity.HasKey(p => p.Id); - entity.Property(p => p.Id).ValueGeneratedNever(); - - entity.ComplexProperty( - p => p.Outer, outer => outer.ComplexProperty( - p => p.Inner, inner => inner.HasDiscriminator())); - } - - public class EntityType - { - public Guid Id { get; set; } - public OuterComplexProperty Outer { get; set; } = null!; - } - - public class OuterComplexProperty - { - public string? Name { get; set; } - public InnerComplexProperty? Inner { get; set; } - } - - public class InnerComplexProperty - { - public int? Value { get; set; } - } - } - - #endregion Issue38119 - - #region Issue38105 - - [Fact] - public virtual async Task Update_entity_with_nullable_complex_type_and_discriminator_does_not_throw() - { - var contextFactory = await InitializeNonSharedTest( - seed: context => - { - var entity = new Context37337.EntityType - { - Id = Guid.NewGuid(), - Prop = new Context37337.OptionalComplexProperty { OptionalValue = true } - }; - context.Add(entity); - context.Entry(entity).Property(Issue37337CreatedByShadowPropertyName).CurrentValue = "Seeder"; - return context.SaveChangesAsync(); - }); - - await using var context = contextFactory.CreateDbContext(); - - var entity = await context.Set().SingleAsync(); - var id = entity.Id; - context.ChangeTracker.Clear(); - - // Create a new disconnected instance with the same key and Update it. - // The complex type discriminator (shadow property with AfterSaveBehavior.Throw) should not - // be marked as modified by Update(), and SaveChanges should succeed without throwing. - var updatedEntity = new Context37337.EntityType - { - Id = id, - Prop = new Context37337.OptionalComplexProperty { OptionalValue = false } - }; - - context.Update(updatedEntity); - - await context.SaveChangesAsync(); - - context.ChangeTracker.Clear(); - - var reloaded = await context.Set().SingleAsync(); - Assert.Equal(id, reloaded.Id); - Assert.NotNull(reloaded.Prop); - Assert.False(reloaded.Prop.OptionalValue); - } - - #endregion Issue38105 - - #region Issue31246 - - [Fact] - public virtual async Task Can_query_by_complex_type_property_with_index() - { - var contextFactory = await InitializeNonSharedTest( - seed: context => - { - context.AddRange( - new Context31246.Person - { - Id = new Context31246.StronglyTypedId(1), - Address = new Context31246.Address { City = "Seattle", PostalCode = "98101" } - }, - new Context31246.Person - { - Id = new Context31246.StronglyTypedId(2), - Address = new Context31246.Address { City = "Redmond", PostalCode = "98052" } - }); - return context.SaveChangesAsync(); - }); - - await using var context = contextFactory.CreateDbContext(); - - // The primary key Id.Id reorders the entity's complex properties so that the chain containing - // the PK property comes first, and reorders the properties inside the Id complex type so that - // the PK property is listed first. - var personType = context.Model.FindEntityType(typeof(Context31246.Person))!; - Assert.Equal( - [nameof(Context31246.Person.Id), nameof(Context31246.Person.Address)], - personType.GetComplexProperties().Select(p => p.Name)); - var idComplexType = personType.FindComplexProperty(nameof(Context31246.Person.Id))!.ComplexType; - Assert.Equal( - [nameof(Context31246.StronglyTypedId.Id)], - idComplexType.GetProperties().Select(p => p.Name)); - - var found = await context.Set().SingleAsync(p => p.Address.City == "Seattle"); - Assert.Equal(new Context31246.StronglyTypedId(1), found.Id); - Assert.Equal("98101", found.Address.PostalCode); - } - - [Fact] - public virtual async Task Can_update_entity_with_index_on_complex_type_property() - { - var contextFactory = await InitializeNonSharedTest( - seed: context => - { - context.Add( - new Context31246.Person - { - Id = new Context31246.StronglyTypedId(1), - Address = new Context31246.Address { City = "Seattle", PostalCode = "98101" } - }); - return context.SaveChangesAsync(); - }); - - await using (var context = contextFactory.CreateDbContext()) - { - var person = await context.Set().SingleAsync(); - person.Address.PostalCode = "98102"; - await context.SaveChangesAsync(); - } - - await using (var context = contextFactory.CreateDbContext()) - { - var person = await context.Set().SingleAsync(); - Assert.Equal("98102", person.Address.PostalCode); - } - } - - [Fact] - public virtual async Task Can_delete_entity_with_index_on_complex_type_property() - { - var contextFactory = await InitializeNonSharedTest( - seed: context => - { - context.Add( - new Context31246.Person - { - Id = new Context31246.StronglyTypedId(1), - Address = new Context31246.Address { City = "Seattle", PostalCode = "98101" } - }); - return context.SaveChangesAsync(); - }); - - await using (var context = contextFactory.CreateDbContext()) - { - var person = await context.Set().SingleAsync(); - context.Remove(person); - await context.SaveChangesAsync(); - } - - await using (var context = contextFactory.CreateDbContext()) - { - Assert.Equal(0, await context.Set().CountAsync()); - } - } - - [Fact] - public virtual async Task Can_query_by_alternate_key_on_complex_type_property() - { - var contextFactory = await InitializeNonSharedTest( - seed: context => - { - context.AddRange( - new Context31246.Person - { - Id = new Context31246.StronglyTypedId(1), - Address = new Context31246.Address { City = "Seattle", PostalCode = "98101" } - }, - new Context31246.Person - { - Id = new Context31246.StronglyTypedId(2), - Address = new Context31246.Address { City = "Redmond", PostalCode = "98052" } - }); - return context.SaveChangesAsync(); - }); - - await using var context = contextFactory.CreateDbContext(); - - var found = await context.Set().SingleAsync(p => p.Address.City == "Redmond"); - Assert.Equal(new Context31246.StronglyTypedId(2), found.Id); - } - - [Fact] - public virtual async Task Can_save_batch_swapping_alternate_key_values_on_complex_type_property() - { - var contextFactory = await InitializeNonSharedTest( - seed: context => - { - context.AddRange( - new Context31246.Person - { - Id = new Context31246.StronglyTypedId(1), - Address = new Context31246.Address { City = "Seattle", PostalCode = "98101" } - }, - new Context31246.Person - { - Id = new Context31246.StronglyTypedId(2), - Address = new Context31246.Address { City = "Redmond", PostalCode = "98052" } - }); - return context.SaveChangesAsync(); - }); - - // Update non-AK columns on multiple rows in the same batch. The CommandBatchPreparer - // still has to read the alternate-key column values (Address_City) for each command in - // order to build edges in the topological sort (AddUniqueValueEdges). - await using (var context = contextFactory.CreateDbContext()) - { - var people = await context.Set().OrderBy(p => p.Id.Id).ToListAsync(); - people[0].Address.PostalCode = "98103"; - people[1].Address.PostalCode = "98054"; - await context.SaveChangesAsync(); - } - - await using (var context = contextFactory.CreateDbContext()) - { - var people = await context.Set().OrderBy(p => p.Id.Id).ToListAsync(); - Assert.Equal("98103", people[0].Address.PostalCode); - Assert.Equal("98054", people[1].Address.PostalCode); - } - } - - protected class Context31246(DbContextOptions options) : DbContext(options) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - => modelBuilder.Entity(b => - { - b.ComplexProperty(p => p.Id); - b.HasKey(p => p.Id.Id); - b.Property(p => p.Id.Id).ValueGeneratedNever(); - b.HasAlternateKey(p => p.Address.City); - b.HasIndex(p => p.Address.PostalCode); - }); - - public readonly record struct StronglyTypedId(int Id); - - public class Person - { - public StronglyTypedId Id { get; set; } - public Address Address { get; set; } = null!; - } - - public class Address - { - public string City { get; set; } = null!; - public string PostalCode { get; set; } = null!; - } - } - - #endregion Issue31246 - - protected override string NonSharedStoreName - => "AdHocComplexTypeQueryTest"; -} diff --git a/test/EFCore.Specification.Tests/Query/AdHocJsonQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/AdHocJsonQueryTestBase.cs deleted file mode 100644 index d61783f27d9..00000000000 --- a/test/EFCore.Specification.Tests/Query/AdHocJsonQueryTestBase.cs +++ /dev/null @@ -1,1888 +0,0 @@ -// 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; - -#nullable disable - -public abstract class AdHocJsonQueryTestBase(NonSharedFixture fixture) : NonSharedModelTestBase(fixture), IClassFixture -{ - protected override string NonSharedStoreName - => "AdHocJsonQueryTests"; - - protected virtual void ClearLog() - => ListLoggerFactory.Clear(); - - protected virtual void ConfigureWarnings(WarningsConfigurationBuilder builder) - { - } - - #region 21006 - - [Theory, MemberData(nameof(IsAsyncData))] - public virtual async Task Project_root_with_missing_scalars(bool async) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - onModelCreating: OnModelCreating21006, - seed: Seed21006); - - await using var context = contextFactory.CreateDbContext(); - - var query = context.Set().Where(x => x.Id < 4); - - var result = async - ? await query.ToListAsync() - : query.ToList()!; - - var topLevel = result.Single(x => x.Id == 2); - var nested = result.Single(x => x.Id == 3); - - Assert.Equal(default, topLevel.OptionalReference.Number); - Assert.Equal(default, topLevel.RequiredReference.Number); - Assert.True(topLevel.Collection.All(x => x.Number == default)); - - Assert.Equal(default, nested.RequiredReference.NestedRequiredReference.DoB); - Assert.Equal(default, nested.RequiredReference.NestedOptionalReference.DoB); - Assert.Equal(default, nested.OptionalReference.NestedRequiredReference.DoB); - Assert.Equal(default, nested.OptionalReference.NestedOptionalReference.DoB); - Assert.True(nested.Collection.SelectMany(x => x.NestedCollection).All(x => x.DoB == default)); - } - - [Theory, MemberData(nameof(IsAsyncData))] - public virtual async Task Project_top_level_json_entity_with_missing_scalars(bool async) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - onModelCreating: OnModelCreating21006, - seed: Seed21006); - - await using var context = contextFactory.CreateDbContext(); - - var query = context.Set().Where(x => x.Id < 4).Select(x => new - { - x.Id, - x.OptionalReference, - x.RequiredReference, - x.Collection - }).AsNoTracking(); - - var result = async - ? await query.ToListAsync() - : query.ToList(); - - var topLevel = result.Single(x => x.Id == 2); - var nested = result.Single(x => x.Id == 3); - - Assert.Equal(default, topLevel.OptionalReference.Number); - Assert.Equal(default, topLevel.RequiredReference.Number); - Assert.True(topLevel.Collection.All(x => x.Number == default)); - - Assert.Equal(default, nested.RequiredReference.NestedRequiredReference.DoB); - Assert.Equal(default, nested.RequiredReference.NestedOptionalReference.DoB); - Assert.Equal(default, nested.OptionalReference.NestedRequiredReference.DoB); - Assert.Equal(default, nested.OptionalReference.NestedOptionalReference.DoB); - Assert.True(nested.Collection.SelectMany(x => x.NestedCollection).All(x => x.DoB == default)); - } - - [Theory, MemberData(nameof(IsAsyncData))] - public virtual async Task Project_nested_json_entity_with_missing_scalars(bool async) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - onModelCreating: OnModelCreating21006, - seed: Seed21006); - - await using var context = contextFactory.CreateDbContext(); - - var query = context.Set().Where(x => x.Id < 4).Select(x => new - { - x.Id, - x.OptionalReference.NestedOptionalReference, - x.RequiredReference.NestedRequiredReference, - x.Collection[0].NestedCollection - }).AsNoTracking(); - - var result = async - ? await query.ToListAsync() - : query.ToList(); - - var topLevel = result.Single(x => x.Id == 2); - var nested = result.Single(x => x.Id == 3); - - Assert.Equal(default, nested.NestedOptionalReference.DoB); - Assert.Equal(default, nested.NestedRequiredReference.DoB); - Assert.True(nested.NestedCollection.All(x => x.DoB == default)); - } - - [Theory, MemberData(nameof(IsAsyncData))] - public virtual async Task Project_top_level_entity_with_null_value_required_scalars(bool async) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - onModelCreating: OnModelCreating21006, - seed: Seed21006); - - await using var context = contextFactory.CreateDbContext(); - - var query = context.Set().Where(x => x.Id == 4).Select(x => new - { - x.Id, - x.RequiredReference, - }).AsNoTracking(); - - var result = async - ? await query.ToListAsync() - : query.ToList(); - - var nullScalars = result.Single(); - - Assert.Equal(default, nullScalars.RequiredReference.Number); - } - - [Theory, MemberData(nameof(IsAsyncData))] - public virtual async Task Project_root_entity_with_missing_required_navigation(bool async) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - onModelCreating: OnModelCreating21006, - seed: Seed21006); - - await using var context = contextFactory.CreateDbContext(); - - var query = context.Set().Where(x => x.Id == 5).AsNoTracking(); - - var result = async - ? await query.ToListAsync() - : query.ToList(); - - var missingRequiredNav = result.Single(); - - Assert.Equal(default, missingRequiredNav.RequiredReference.NestedRequiredReference); - Assert.Equal(default, missingRequiredNav.OptionalReference.NestedRequiredReference); - Assert.True(missingRequiredNav.Collection.All(x => x.NestedRequiredReference == default)); - } - - [Theory, MemberData(nameof(IsAsyncData))] - public virtual async Task Project_missing_required_navigation(bool async) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - onModelCreating: OnModelCreating21006, - seed: Seed21006); - - await using var context = contextFactory.CreateDbContext(); - - var query = context.Set().Where(x => x.Id == 5).Select(x => x.RequiredReference.NestedRequiredReference) - .AsNoTracking(); - - var result = async - ? await query.ToListAsync() - : query.ToList(); - - var missingRequiredNav = result.Single(); - - Assert.Equal(default, missingRequiredNav); - } - - [Theory, MemberData(nameof(IsAsyncData))] - public virtual async Task Project_root_entity_with_null_required_navigation(bool async) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - onModelCreating: OnModelCreating21006, - seed: Seed21006); - - await using var context = contextFactory.CreateDbContext(); - - var query = context.Set().Where(x => x.Id == 6).AsNoTracking(); - - var result = async - ? await query.ToListAsync() - : query.ToList(); - - var nullRequiredNav = result.Single(); - - Assert.Equal(default, nullRequiredNav.RequiredReference.NestedRequiredReference); - Assert.Equal(default, nullRequiredNav.OptionalReference.NestedRequiredReference); - Assert.True(nullRequiredNav.Collection.All(x => x.NestedRequiredReference == default)); - } - - [Theory, MemberData(nameof(IsAsyncData))] - public virtual async Task Project_null_required_navigation(bool async) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - onModelCreating: OnModelCreating21006, - seed: Seed21006); - - await using var context = contextFactory.CreateDbContext(); - - var query = context.Set().Where(x => x.Id == 6).Select(x => x.RequiredReference).AsNoTracking(); - - var result = async - ? await query.ToListAsync() - : query.ToList(); - - var nullRequiredNav = result.Single(); - - Assert.Equal(default, nullRequiredNav.NestedRequiredReference); - } - - [Theory, MemberData(nameof(IsAsyncData))] - public virtual async Task Project_missing_required_scalar(bool async) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - onModelCreating: OnModelCreating21006, - seed: Seed21006); - - await using var context = contextFactory.CreateDbContext(); - - var query = context.Set() - .Where(x => x.Id == 2) - .Select(x => new { x.Id, Number = (double?)x.RequiredReference.Number }); - - var result = async - ? await query.ToListAsync() - : query.ToList(); - - Assert.Null(result.Single().Number); - } - - [Theory, MemberData(nameof(IsAsyncData))] - public virtual async Task Project_null_required_scalar(bool async) - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - onModelCreating: OnModelCreating21006, - seed: Seed21006); - - await using var context = contextFactory.CreateDbContext(); - - var query = context.Set() - .Where(x => x.Id == 4) - .Select(x => new - { - x.Id, - Number = (double?)x.RequiredReference.Number, - }); - - var result = async - ? await query.ToListAsync() - : query.ToList(); - - Assert.Null(result.Single().Number); - } - - protected virtual void OnModelCreating21006(ModelBuilder modelBuilder) - => modelBuilder.Entity(b => - { - b.Property(x => x.Id).ValueGeneratedNever(); - b.OwnsOne( - x => x.OptionalReference, bb => - { - bb.OwnsOne(x => x.NestedOptionalReference); - bb.OwnsOne(x => x.NestedRequiredReference); - bb.Navigation(x => x.NestedRequiredReference).IsRequired(); - bb.OwnsMany(x => x.NestedCollection); - }); - b.OwnsOne( - x => x.RequiredReference, bb => - { - bb.OwnsOne(x => x.NestedOptionalReference); - bb.OwnsOne(x => x.NestedRequiredReference); - bb.Navigation(x => x.NestedRequiredReference).IsRequired(); - bb.OwnsMany(x => x.NestedCollection); - }); - b.Navigation(x => x.RequiredReference).IsRequired(); - b.OwnsMany( - x => x.Collection, bb => - { - bb.OwnsOne(x => x.NestedOptionalReference); - bb.OwnsOne(x => x.NestedRequiredReference); - bb.Navigation(x => x.NestedRequiredReference).IsRequired(); - bb.OwnsMany(x => x.NestedCollection); - }); - }); - - protected virtual async Task Seed21006(Context21006 context) - { - // everything - var e1 = new Context21006.Entity - { - Id = 1, - Name = "e1", - OptionalReference = new Context21006.JsonEntity - { - Number = 7, - Text = "e1 or", - NestedOptionalReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 or nor" }, - NestedRequiredReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 or nrr" }, - NestedCollection = - [ - new() { DoB = new DateTime(2000, 1, 1), Text = "e1 or c1" }, - new() { DoB = new DateTime(2000, 1, 1), Text = "e1 or c2" } - ] - }, - RequiredReference = new Context21006.JsonEntity - { - Number = 7, - Text = "e1 rr", - NestedOptionalReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 rr nor" }, - NestedRequiredReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 rr nrr" }, - NestedCollection = - [ - new() { DoB = new DateTime(2000, 1, 1), Text = "e1 rr c1" }, - new() { DoB = new DateTime(2000, 1, 1), Text = "e1 rr c2" } - ] - }, - Collection = - [ - new() - { - Number = 7, - Text = "e1 c1", - NestedOptionalReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 c1 nor" }, - NestedRequiredReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 c1 nrr" }, - NestedCollection = - [ - new() { DoB = new DateTime(2000, 1, 1), Text = "e1 c1 c1" }, - new() { DoB = new DateTime(2000, 1, 1), Text = "e1 c1 c2" } - ] - }, - - new() - { - Number = 7, - Text = "e1 c2", - NestedOptionalReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 c2 nor" }, - NestedRequiredReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 c2 nrr" }, - NestedCollection = - [ - new() { DoB = new DateTime(2000, 1, 1), Text = "e1 c2 c1" }, - new() { DoB = new DateTime(2000, 1, 1), Text = "e1 c2 c2" } - ] - } - ] - }; - - context.Add(e1); - await context.SaveChangesAsync(); - } - - protected class Context21006(DbContextOptions options) : DbContext(options) - { - public DbSet Entities { get; set; } - - public class Entity - { - public int Id { get; set; } - public string Name { get; set; } - public JsonEntity OptionalReference { get; set; } - public JsonEntity RequiredReference { get; set; } - public List Collection { get; set; } - } - - public class JsonEntity - { - public string Text { get; set; } - public double Number { get; set; } - - public JsonEntityNested NestedOptionalReference { get; set; } - public JsonEntityNested NestedRequiredReference { get; set; } - public List NestedCollection { get; set; } - } - - public class JsonEntityNested - { - public DateTime DoB { get; set; } - public string Text { get; set; } - } - } - - #endregion - - #region 29219 - - [Fact] - public virtual async Task Optional_json_properties_materialized_as_null_when_the_element_in_json_is_not_present() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreating29219, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: Seed29219); - - using var context = contextFactory.CreateDbContext(); - var query = context.Set().Where(x => x.Id == 3); - var result = await query.SingleAsync(); - - Assert.Equal(3, result.Id); - Assert.Null(result.Reference.NullableScalar); - Assert.Null(result.Collection[0].NullableScalar); - } - - [Fact] - public virtual async Task Can_project_nullable_json_property_when_the_element_in_json_is_not_present() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreating29219, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: Seed29219); - - using var context = contextFactory.CreateDbContext(); - - var query = context.Set().OrderBy(x => x.Id).Select(x => x.Reference.NullableScalar); - var result = await query.ToListAsync(); - - Assert.Equal(3, result.Count); - Assert.Equal(11, result[0]); - Assert.Null(result[1]); - Assert.Null(result[2]); - } - - protected virtual void OnModelCreating29219(ModelBuilder modelBuilder) - => modelBuilder.Entity(b => - { - b.Property(x => x.Id).ValueGeneratedNever(); - b.OwnsOne(x => x.Reference); - b.OwnsMany(x => x.Collection); - }); - - protected virtual async Task Seed29219(DbContext ctx) - { - var entity1 = new Context29219.MyEntity - { - Id = 1, - Reference = new Context29219.MyJsonEntity { NonNullableScalar = 10, NullableScalar = 11 }, - Collection = - [ - new Context29219.MyJsonEntity { NonNullableScalar = 100, NullableScalar = 101 }, - new Context29219.MyJsonEntity { NonNullableScalar = 200, NullableScalar = 201 }, - new Context29219.MyJsonEntity { NonNullableScalar = 300, NullableScalar = null } - ] - }; - - var entity2 = new Context29219.MyEntity - { - Id = 2, - Reference = new Context29219.MyJsonEntity { NonNullableScalar = 20, NullableScalar = null }, - Collection = [new Context29219.MyJsonEntity { NonNullableScalar = 1001, NullableScalar = null }] - }; - - ctx.AddRange(entity1, entity2); - await ctx.SaveChangesAsync(); - } - - protected class Context29219(DbContextOptions options) : DbContext(options) - { - public class MyEntity - { - public int Id { get; set; } - public MyJsonEntity Reference { get; set; } - public List Collection { get; set; } - } - - public class MyJsonEntity - { - public int NonNullableScalar { get; set; } - public int? NullableScalar { get; set; } - } - } - - #endregion - - #region 30028 - - [Fact] - public virtual async Task Accessing_missing_navigation_works() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreating30028, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: Seed30028); - - using var context = contextFactory.CreateDbContext(); - var result = await context.Set().OrderBy(x => x.Id).ToListAsync(); - Assert.Equal(4, result.Count); - Assert.NotNull(result[0].Json.Collection); - Assert.NotNull(result[0].Json.OptionalReference); - Assert.NotNull(result[0].Json.RequiredReference); - - Assert.Null(result[1].Json.Collection); - Assert.NotNull(result[1].Json.OptionalReference); - Assert.NotNull(result[1].Json.RequiredReference); - - Assert.NotNull(result[2].Json.Collection); - Assert.Null(result[2].Json.OptionalReference); - Assert.NotNull(result[2].Json.RequiredReference); - - Assert.NotNull(result[3].Json.Collection); - Assert.NotNull(result[3].Json.OptionalReference); - Assert.Null(result[3].Json.RequiredReference); - } - - [Theory, MemberData(nameof(IsAsyncData))] - public virtual async Task Missing_navigation_works_with_deduplication(bool async) - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreating30028, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: Seed30028); - - using var context = contextFactory.CreateDbContext(); - var queryable = context.Set().OrderBy(x => x.Id).Select(x => new - { - x, - x.Json, - x.Json.OptionalReference, - x.Json.RequiredReference, - NestedOptional = x.Json.OptionalReference.Nested, - NestedRequired = x.Json.RequiredReference.Nested, - x.Json.Collection, - }).AsNoTracking(); - - var result = async ? await queryable.ToListAsync() : queryable.ToList(); - - Assert.Equal(4, result.Count); - Assert.NotNull(result[0].OptionalReference); - Assert.NotNull(result[0].RequiredReference); - Assert.NotNull(result[0].NestedOptional); - Assert.NotNull(result[0].NestedRequired); - Assert.NotNull(result[0].Collection); - - Assert.NotNull(result[1].OptionalReference); - Assert.NotNull(result[1].RequiredReference); - Assert.NotNull(result[1].NestedOptional); - Assert.NotNull(result[1].NestedRequired); - Assert.Null(result[1].Collection); - - Assert.Null(result[2].OptionalReference); - Assert.NotNull(result[2].RequiredReference); - Assert.Null(result[2].NestedOptional); - Assert.NotNull(result[2].NestedRequired); - Assert.NotNull(result[2].Collection); - - Assert.NotNull(result[3].OptionalReference); - Assert.Null(result[3].RequiredReference); - Assert.NotNull(result[3].NestedOptional); - Assert.Null(result[3].NestedRequired); - Assert.NotNull(result[3].Collection); - } - - protected virtual void OnModelCreating30028(ModelBuilder modelBuilder) - => modelBuilder.Entity(b => - { - b.Property(x => x.Id).ValueGeneratedNever(); - b.OwnsOne( - x => x.Json, nb => - { - nb.OwnsMany(x => x.Collection, nnb => nnb.OwnsOne(x => x.Nested)); - nb.OwnsOne(x => x.OptionalReference, nnb => nnb.OwnsOne(x => x.Nested)); - nb.OwnsOne(x => x.RequiredReference, nnb => nnb.OwnsOne(x => x.Nested)); - nb.Navigation(x => x.RequiredReference).IsRequired(); - }); - }); - - protected abstract Task Seed30028(DbContext ctx); - - protected class Context30028(DbContextOptions options) : DbContext(options) - { - public class MyEntity - { - public int Id { get; set; } - public MyJsonRootEntity Json { get; set; } - } - - public class MyJsonRootEntity - { - public string RootName { get; set; } - public MyJsonBranchEntity RequiredReference { get; set; } - public MyJsonBranchEntity OptionalReference { get; set; } - public List Collection { get; set; } - } - - public class MyJsonBranchEntity - { - public string BranchName { get; set; } - public MyJsonLeafEntity Nested { get; set; } - } - - public class MyJsonLeafEntity - { - public string LeafName { get; set; } - } - } - - #endregion - - #region 32310 - - [Fact] - public virtual async Task Contains_on_nested_collection_with_init_only_navigation() - { - var contextFactory = await InitializeNonSharedTest( - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - onModelCreating: OnModelCreating32310, - seed: Seed32310); - - await using var context = contextFactory.CreateDbContext(); - - var query = context.Set() - .Where(u => u.Visits.DaysVisited.Contains(new DateOnly(2023, 1, 1))); - - var result = await query.FirstOrDefaultAsync(); - - Assert.Equal("FBI", result.Name); - Assert.Equal(new DateOnly(2023, 1, 1), result.Visits.DaysVisited.Single()); - } - - protected virtual void OnModelCreating32310(ModelBuilder modelBuilder) - => modelBuilder.Entity().OwnsOne(e => e.Visits); - - protected virtual async Task Seed32310(DbContext context) - { - var user = new Context32310.Pub - { - Name = "FBI", - Visits = new Context32310.Visits { LocationTag = "tag", DaysVisited = [new DateOnly(2023, 1, 1)] } - }; - - context.Add(user); - await context.SaveChangesAsync(); - } - - protected class Context32310(DbContextOptions options) : DbContext(options) - { - public class Pub - { - public int Id { get; set; } - public required string Name { get; set; } - public Visits Visits { get; set; } = null!; - } - - public class Visits - { - public string LocationTag { get; set; } - public required List DaysVisited { get; init; } - } - } - - #endregion - - #region 32939 - - [Fact] - public virtual async Task Project_json_with_no_properties() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreating32939, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: Seed32939); - - using var context = contextFactory.CreateDbContext(); - await context.Set().ToListAsync(); - } - - protected virtual void OnModelCreating32939(ModelBuilder modelBuilder) - { - modelBuilder.Entity().Property(x => x.Id).ValueGeneratedNever(); - modelBuilder.Entity().OwnsOne(x => x.Empty); - modelBuilder.Entity().OwnsOne(x => x.FieldOnly); - } - - protected Task Seed32939(DbContext ctx) - { - var entity = new Context32939.Entity { Empty = new Context32939.JsonEmpty(), FieldOnly = new Context32939.JsonFieldOnly() }; - - ctx.Add(entity); - return ctx.SaveChangesAsync(); - } - - protected class Context32939(DbContextOptions options) : DbContext(options) - { - public class Entity - { - public int Id { get; set; } - public JsonEmpty Empty { get; set; } - public JsonFieldOnly FieldOnly { get; set; } - } - - public class JsonEmpty - { - } - - public class JsonFieldOnly - { - public int Field; - } - } - - #endregion - - #region 33046 - - [Fact] - public virtual async Task Query_with_nested_json_collection_mapped_to_private_field_via_IReadOnlyList() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreating33046, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: Seed33046); - - using var context = contextFactory.CreateDbContext(); - var query = await context.Set().ToListAsync(); - Assert.Equal(1, query.Count); - } - - protected virtual void OnModelCreating33046(ModelBuilder modelBuilder) - => modelBuilder.Entity(b => - { - b.Property(x => x.Id).ValueGeneratedNever(); - b.OwnsMany( - x => x.Rounds, ownedBuilder => ownedBuilder.OwnsMany(r => r.SubRounds)); - }); - - protected abstract Task Seed33046(DbContext ctx); - - protected class Context33046(DbContextOptions options) : DbContext(options) - { - public class Review - { - public int Id { get; set; } - -#pragma warning disable IDE0044 // Add readonly modifier - private List _rounds = []; -#pragma warning restore IDE0044 // Add readonly modifier - public IReadOnlyList Rounds - => _rounds.AsReadOnly(); - } - - public class ReviewRound - { - public int RoundNumber { get; set; } - -#pragma warning disable IDE0044 // Add readonly modifier - private readonly List _subRounds = []; -#pragma warning restore IDE0044 // Add readonly modifier - public IReadOnlyList SubRounds - => _subRounds.AsReadOnly(); - } - - public class SubRound - { - public int SubRoundNumber { get; set; } - } - } - - #endregion - - #region 34960 - - [Fact] - public virtual async Task Project_entity_with_json_null_values() - { - var contextFactory = await InitializeNonSharedTest( - seed: Seed34960, onModelCreating: OnModelCreating34960, onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings)); - - using var context = contextFactory.CreateDbContext(); - var query = await context.Entities.ToListAsync(); - } - - [Fact] - public virtual async Task Try_project_collection_but_JSON_is_entity() - { - var contextFactory = await InitializeNonSharedTest( - seed: Seed34960, onModelCreating: OnModelCreating34960, onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings)); - using var context = contextFactory.CreateDbContext(); - - await context.Junk.AsNoTracking().Where(x => x.Id == 1).Select(x => x.Collection).FirstOrDefaultAsync(); - } - - [Fact] - public virtual async Task Try_project_reference_but_JSON_is_collection() - { - var contextFactory = await InitializeNonSharedTest( - seed: Seed34960, onModelCreating: OnModelCreating34960, onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings)); - using var context = contextFactory.CreateDbContext(); - - await context.Junk.AsNoTracking().Where(x => x.Id == 2).Select(x => x.Reference).FirstOrDefaultAsync(); - } - - protected class Context34960(DbContextOptions options) : DbContext(options) - { - public DbSet Entities { get; set; } - public DbSet Junk { get; set; } - - public class Entity - { - public int Id { get; set; } - public JsonEntity Reference { get; set; } - public List Collection { get; set; } - } - - public class JsonEntity - { - public string Name { get; set; } - public double Number { get; set; } - - public JsonEntityNested NestedReference { get; set; } - public List NestedCollection { get; set; } - } - - public class JsonEntityNested - { - public DateTime DoB { get; set; } - public string Text { get; set; } - } - - public class JunkEntity - { - public int Id { get; set; } - public JsonEntity Reference { get; set; } - public List Collection { get; set; } - } - } - - protected virtual void OnModelCreating34960(ModelBuilder modelBuilder) - { - modelBuilder.Entity(b => - { - b.Property(x => x.Id).ValueGeneratedNever(); - - b.OwnsOne( - x => x.Reference, b => - { - b.OwnsOne(x => x.NestedReference); - b.OwnsMany(x => x.NestedCollection); - }); - - b.OwnsMany( - x => x.Collection, b => - { - b.OwnsOne(x => x.NestedReference); - b.OwnsMany(x => x.NestedCollection); - }); - }); - - modelBuilder.Entity(b => - { - b.Property(x => x.Id).ValueGeneratedNever(); - - b.OwnsOne( - x => x.Reference, b => - { - b.Ignore(x => x.NestedReference); - b.Ignore(x => x.NestedCollection); - }); - - b.OwnsMany( - x => x.Collection, b => - { - b.Ignore(x => x.NestedReference); - b.Ignore(x => x.NestedCollection); - }); - }); - } - - protected virtual async Task Seed34960(Context34960 ctx) - { - // everything - var e1 = new Context34960.Entity - { - Id = 1, - Reference = new Context34960.JsonEntity - { - Name = "ref1", - Number = 1.5f, - NestedReference = new Context34960.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "nested ref 1" }, - NestedCollection = - [ - new Context34960.JsonEntityNested { DoB = new DateTime(2001, 1, 1), Text = "nested col 1 1" }, - new Context34960.JsonEntityNested { DoB = new DateTime(2001, 2, 2), Text = "nested col 1 2" }, - ], - }, - Collection = - [ - new Context34960.JsonEntity - { - Name = "col 1 1", - Number = 2.5f, - NestedReference = new Context34960.JsonEntityNested { DoB = new DateTime(2010, 1, 1), Text = "nested col 1 1 ref 1" }, - NestedCollection = - [ - new Context34960.JsonEntityNested { DoB = new DateTime(2011, 1, 1), Text = "nested col 1 1 col 1 1" }, - new Context34960.JsonEntityNested { DoB = new DateTime(2011, 2, 2), Text = "nested col 1 1 col 1 2" }, - ], - }, - new Context34960.JsonEntity - { - Name = "col 1 2", - Number = 2.5f, - NestedReference = new Context34960.JsonEntityNested { DoB = new DateTime(2020, 1, 1), Text = "nested col 1 2 ref 1" }, - NestedCollection = - [ - new Context34960.JsonEntityNested { DoB = new DateTime(2021, 1, 1), Text = "nested col 1 2 col 1 1" }, - new Context34960.JsonEntityNested { DoB = new DateTime(2021, 2, 2), Text = "nested col 1 2 col 1 2" }, - ], - }, - ], - }; - - // relational nulls - var e2 = new Context34960.Entity - { - Id = 2, - Reference = null, - Collection = null - }; - - // nested relational nulls - var e3 = new Context34960.Entity - { - Id = 3, - Reference = new Context34960.JsonEntity - { - Name = "ref3", - Number = 3.5f, - NestedReference = null, - NestedCollection = null - }, - Collection = - [ - new Context34960.JsonEntity - { - Name = "col 3 1", - Number = 32.5f, - NestedReference = null, - NestedCollection = null, - }, - new Context34960.JsonEntity - { - Name = "col 3 2", - Number = 33.5f, - NestedReference = null, - NestedCollection = null, - }, - ], - }; - - ctx.Entities.AddRange(e1, e2, e3); - await ctx.SaveChangesAsync(); - } - - #endregion - - #region ArrayOfPrimitives - - [Fact] - public virtual async Task Project_json_array_of_primitives_on_reference() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingArrayOfPrimitives, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedArrayOfPrimitives); - - using var context = contextFactory.CreateDbContext(); - var query = context.Set().OrderBy(x => x.Id) - .Select(x => new { x.Reference.IntArray, x.Reference.ListOfString }); - - var result = await query.ToListAsync(); - - Assert.Equal(2, result.Count); - Assert.Equal(3, result[0].IntArray.Length); - Assert.Equal(3, result[0].ListOfString.Count); - Assert.Equal(3, result[1].IntArray.Length); - Assert.Equal(3, result[1].ListOfString.Count); - } - - [Fact(Skip = "Issue #32611")] - public virtual async Task Project_json_array_of_primitives_on_collection() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingArrayOfPrimitives, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedArrayOfPrimitives); - - using var context = contextFactory.CreateDbContext(); - var query = context.Set().OrderBy(x => x.Id) - .Select(x => new { x.Collection[0].IntArray, x.Collection[1].ListOfString }); - - var result = await query.ToListAsync(); - - Assert.Equal(2, result.Count); - Assert.Equal(3, result[0].IntArray.Length); - Assert.Equal(2, result[0].ListOfString.Count); - Assert.Equal(3, result[1].IntArray.Length); - Assert.Equal(2, result[1].ListOfString.Count); - } - - [Fact] - public virtual async Task Project_element_of_json_array_of_primitives() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingArrayOfPrimitives, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedArrayOfPrimitives); - - using var context = contextFactory.CreateDbContext(); - var query = context.Set().OrderBy(x => x.Id).Select(x - => new { ArrayElement = x.Reference.IntArray[0], ListElement = x.Reference.ListOfString[1] }); - var result = await query.ToListAsync(); - } - - [Fact] - public virtual async Task Predicate_based_on_element_of_json_array_of_primitives1() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingArrayOfPrimitives, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedArrayOfPrimitives); - - using var context = contextFactory.CreateDbContext(); - var query = context.Set().Where(x => x.Reference.IntArray[0] == 1); - var result = await query.ToListAsync(); - - Assert.Equal(1, result.Count); - Assert.Equal(1, result[0].Reference.IntArray[0]); - } - - [Fact] - public virtual async Task Predicate_based_on_element_of_json_array_of_primitives2() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingArrayOfPrimitives, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedArrayOfPrimitives); - - using var context = contextFactory.CreateDbContext(); - var query = context.Set().Where(x => x.Reference.ListOfString[1] == "Bar"); - var result = await query.ToListAsync(); - - Assert.Equal(1, result.Count); - Assert.Equal("Bar", result[0].Reference.ListOfString[1]); - } - - [Fact, MemberData(nameof(IsAsyncData))] - public virtual async Task Predicate_based_on_element_of_json_array_of_primitives3() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingArrayOfPrimitives, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedArrayOfPrimitives); - - using var context = contextFactory.CreateDbContext(); - var query = context.Set() - .Where(x => x.Reference.IntArray.AsQueryable().ElementAt(0) == 1 - || x.Reference.ListOfString.AsQueryable().ElementAt(1) == "Bar") - .OrderBy(e => e.Id); - var result = await query.ToListAsync(); - - Assert.Equal(1, result.Count); - Assert.Equal(1, result[0].Reference.IntArray[0]); - Assert.Equal("Bar", result[0].Reference.ListOfString[1]); - } - - protected Task SeedArrayOfPrimitives(DbContext ctx) - { - var entity1 = new ContextArrayOfPrimitives.MyEntity - { - Id = 1, - Reference = new ContextArrayOfPrimitives.MyJsonEntity - { - IntArray = [1, 2, 3], - ListOfString = - [ - "Foo", - "Bar", - "Baz" - ] - }, - Collection = - [ - new ContextArrayOfPrimitives.MyJsonEntity { IntArray = [111, 112, 113], ListOfString = ["Foo11", "Bar11"] }, - new ContextArrayOfPrimitives.MyJsonEntity { IntArray = [211, 212, 213], ListOfString = ["Foo12", "Bar12"] } - ] - }; - - var entity2 = new ContextArrayOfPrimitives.MyEntity - { - Id = 2, - Reference = new ContextArrayOfPrimitives.MyJsonEntity - { - IntArray = [10, 20, 30], - ListOfString = - [ - "A", - "B", - "C" - ] - }, - Collection = - [ - new ContextArrayOfPrimitives.MyJsonEntity { IntArray = [110, 120, 130], ListOfString = ["A1", "Z1"] }, - new ContextArrayOfPrimitives.MyJsonEntity { IntArray = [210, 220, 230], ListOfString = ["A2", "Z2"] } - ] - }; - - ctx.Set().AddRange(entity1, entity2); - - return ctx.SaveChangesAsync(); - } - - protected virtual void OnModelCreatingArrayOfPrimitives(ModelBuilder modelBuilder) - { - modelBuilder.Entity().Property(x => x.Id).ValueGeneratedNever(); - modelBuilder.Entity().OwnsOne(x => x.Reference); - modelBuilder.Entity().OwnsMany(x => x.Collection); - } - - protected class ContextArrayOfPrimitives(DbContextOptions options) : DbContext(options) - { - public class MyEntity - { - public int Id { get; set; } - public MyJsonEntity Reference { get; set; } - public List Collection { get; set; } - } - - public class MyJsonEntity - { - public int[] IntArray { get; set; } - public List ListOfString { get; set; } - } - } - - #endregion - - #region JunkInJson - - [Fact] - public virtual async Task Junk_in_json_basic_tracking() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingJunkInJson, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedJunkInJson); - - using var context = contextFactory.CreateDbContext(); - var query = context.Set(); - var result = await query.ToListAsync(); - - Assert.Equal(1, result.Count); - Assert.Equal(2, result[0].Collection.Count); - Assert.Equal(2, result[0].CollectionWithCtor.Count); - Assert.Equal(2, result[0].Reference.NestedCollection.Count); - Assert.NotNull(result[0].Reference.NestedReference); - Assert.Equal(2, result[0].ReferenceWithCtor.NestedCollection.Count); - Assert.NotNull(result[0].ReferenceWithCtor.NestedReference); - } - - [Fact] - public virtual async Task Junk_in_json_basic_no_tracking() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingJunkInJson, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedJunkInJson); - - using var context = contextFactory.CreateDbContext(); - var query = context.Set().AsNoTracking(); - var result = await query.ToListAsync(); - - Assert.Equal(1, result.Count); - Assert.Equal(2, result[0].Collection.Count); - Assert.Equal(2, result[0].CollectionWithCtor.Count); - Assert.Equal(2, result[0].Reference.NestedCollection.Count); - Assert.NotNull(result[0].Reference.NestedReference); - Assert.Equal(2, result[0].ReferenceWithCtor.NestedCollection.Count); - Assert.NotNull(result[0].ReferenceWithCtor.NestedReference); - } - - protected abstract Task SeedJunkInJson(DbContext ctx); - - protected virtual void OnModelCreatingJunkInJson(ModelBuilder modelBuilder) - => modelBuilder.Entity(b => - { - b.Property(x => x.Id).ValueGeneratedNever(); - - b.OwnsOne( - x => x.Reference, b => - { - b.OwnsOne(x => x.NestedReference); - b.OwnsMany(x => x.NestedCollection); - }); - - b.OwnsOne( - x => x.ReferenceWithCtor, b => - { - b.OwnsOne(x => x.NestedReference); - b.OwnsMany(x => x.NestedCollection); - }); - - b.OwnsMany( - x => x.Collection, b => - { - b.OwnsOne(x => x.NestedReference); - b.OwnsMany(x => x.NestedCollection); - }); - - b.OwnsMany( - x => x.CollectionWithCtor, b => - { - b.OwnsOne(x => x.NestedReference); - b.OwnsMany(x => x.NestedCollection); - }); - }); - - protected class ContextJunkInJson(DbContextOptions options) : DbContext(options) - { - public class MyEntity - { - public int Id { get; set; } - public MyJsonEntity Reference { get; set; } - public MyJsonEntityWithCtor ReferenceWithCtor { get; set; } - public List Collection { get; set; } - public List CollectionWithCtor { get; set; } - } - - public class MyJsonEntity - { - public string Name { get; set; } - public double Number { get; set; } - - public MyJsonEntityNested NestedReference { get; set; } - public List NestedCollection { get; set; } - } - - public class MyJsonEntityNested - { - public DateTime DoB { get; set; } - } - - public class MyJsonEntityWithCtor(bool myBool, string name) - { - public bool MyBool { get; set; } = myBool; - public string Name { get; set; } = name; - - public MyJsonEntityWithCtorNested NestedReference { get; set; } - public List NestedCollection { get; set; } - } - - public class MyJsonEntityWithCtorNested(DateTime doB) - { - public DateTime DoB { get; set; } = doB; - } - } - - #endregion - - #region TrickyBuffering - - [Fact] - public virtual async Task Tricky_buffering_basic() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingTrickyBuffering, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedTrickyBuffering); - - using var context = contextFactory.CreateDbContext(); - var query = context.Set(); - var result = await query.ToListAsync(); - - Assert.Equal(1, result.Count); - Assert.Equal("r1", result[0].Reference.Name); - Assert.Equal(7, result[0].Reference.Number); - Assert.Equal(new DateTime(2000, 1, 1), result[0].Reference.NestedReference.DoB); - Assert.Equal(2, result[0].Reference.NestedCollection.Count); - } - - protected abstract Task SeedTrickyBuffering(DbContext ctx); - - protected virtual void OnModelCreatingTrickyBuffering(ModelBuilder modelBuilder) - => modelBuilder.Entity(b => - { - b.Property(x => x.Id).ValueGeneratedNever(); - b.OwnsOne( - x => x.Reference, b => - { - b.OwnsOne(x => x.NestedReference); - b.OwnsMany(x => x.NestedCollection); - }); - }); - - protected class ContextTrickyBuffering(DbContextOptions options) : DbContext(options) - { - public class MyEntity - { - public int Id { get; set; } - public MyJsonEntity Reference { get; set; } - } - - public class MyJsonEntity - { - public string Name { get; set; } - public int Number { get; set; } - public MyJsonEntityNested NestedReference { get; set; } - public List NestedCollection { get; set; } - } - - public class MyJsonEntityNested - { - public DateTime DoB { get; set; } - } - } - - #endregion - - #region ShadowProperties - - [Fact] - public virtual async Task Shadow_properties_basic_tracking() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingShadowProperties, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedShadowProperties); - - using var context = contextFactory.CreateDbContext(); - var query = context.Set(); - var result = await query.ToListAsync(); - - Assert.Equal(1, result.Count); - Assert.Equal(2, result[0].Collection.Count); - Assert.Equal(2, result[0].CollectionWithCtor.Count); - Assert.NotNull(result[0].Reference); - Assert.NotNull(result[0].ReferenceWithCtor); - - var referenceEntry = context.ChangeTracker.Entries().Single(x => x.Entity == result[0].Reference); - Assert.Equal("Foo", referenceEntry.Property("ShadowString").CurrentValue); - - var referenceCtorEntry = context.ChangeTracker.Entries().Single(x => x.Entity == result[0].ReferenceWithCtor); - Assert.Equal(143, referenceCtorEntry.Property("Shadow_Int").CurrentValue); - - var collectionEntry1 = context.ChangeTracker.Entries().Single(x => x.Entity == result[0].Collection[0]); - var collectionEntry2 = context.ChangeTracker.Entries().Single(x => x.Entity == result[0].Collection[1]); - Assert.Equal(5.5, collectionEntry1.Property("ShadowDouble").CurrentValue); - Assert.Equal(20.5, collectionEntry2.Property("ShadowDouble").CurrentValue); - - var collectionCtorEntry1 = context.ChangeTracker.Entries().Single(x => x.Entity == result[0].CollectionWithCtor[0]); - var collectionCtorEntry2 = context.ChangeTracker.Entries().Single(x => x.Entity == result[0].CollectionWithCtor[1]); - Assert.Equal((byte)6, collectionCtorEntry1.Property("ShadowNullableByte").CurrentValue); - Assert.Null(collectionCtorEntry2.Property("ShadowNullableByte").CurrentValue); - } - - [Fact] - public virtual async Task Shadow_properties_basic_no_tracking() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingShadowProperties, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedShadowProperties); - - using var context = contextFactory.CreateDbContext(); - var query = context.Set().AsNoTracking(); - var result = await query.ToListAsync(); - - Assert.Equal(1, result.Count); - Assert.Equal(2, result[0].Collection.Count); - Assert.Equal(2, result[0].CollectionWithCtor.Count); - Assert.NotNull(result[0].Reference); - Assert.NotNull(result[0].ReferenceWithCtor); - } - - [Fact] - public virtual async Task Project_shadow_properties_from_json_entity() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingShadowProperties, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedShadowProperties); - - using var context = contextFactory.CreateDbContext(); - var query = context.Set().Select(x => new - { - ShadowString = EF.Property(x.Reference, "ShadowString"), - ShadowInt = EF.Property(x.ReferenceWithCtor, "Shadow_Int"), - }); - - var result = await query.ToListAsync(); - - Assert.Equal(1, result.Count); - Assert.Equal("Foo", result[0].ShadowString); - Assert.Equal(143, result[0].ShadowInt); - } - - protected abstract Task SeedShadowProperties(DbContext ctx); - - protected virtual void OnModelCreatingShadowProperties(ModelBuilder modelBuilder) - => modelBuilder.Entity(b => - { - b.Property(x => x.Id).ValueGeneratedNever(); - - b.OwnsOne( - x => x.Reference, b => b.Property("ShadowString")); - - b.OwnsOne( - x => x.ReferenceWithCtor, b => b.Property("Shadow_Int")); - - b.OwnsMany( - x => x.Collection, b => b.Property("ShadowDouble")); - - b.OwnsMany( - x => x.CollectionWithCtor, b => b.Property("ShadowNullableByte")); - }); - - protected class ContextShadowProperties(DbContextOptions options) : DbContext(options) - { - public class MyEntity - { - public int Id { get; set; } - public string Name { get; set; } - - public MyJsonEntity Reference { get; set; } - public List Collection { get; set; } - public MyJsonEntityWithCtor ReferenceWithCtor { get; set; } - public List CollectionWithCtor { get; set; } - } - - public class MyJsonEntity - { - public string Name { get; set; } - } - - public class MyJsonEntityWithCtor(string name) - { - public string Name { get; set; } = name; - } - } - - #endregion - - #region LazyLoadingProxies - - [Fact] - public virtual async Task Project_proxies_entity_with_json() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingLazyLoadingProxies, - seed: SeedLazyLoadingProxies, - onConfiguring: b => - { - b = b.ConfigureWarnings(ConfigureWarnings); - OnConfiguringLazyLoadingProxies(b); - }, - addServices: AddServicesLazyLoadingProxies); - - using var context = contextFactory.CreateDbContext(); - var query = context.Set(); - var result = await query.ToListAsync(); - - Assert.Equal(2, result.Count); - } - - [Fact] // Issue #38466 - public virtual async Task Project_proxies_entity_with_json_with_primitive_collection() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingLazyLoadingProxies, - seed: SeedLazyLoadingProxies, - onConfiguring: b => - { - b = b.ConfigureWarnings(ConfigureWarnings); - OnConfiguringLazyLoadingProxies(b); - }, - addServices: AddServicesLazyLoadingProxies); - - using var context = contextFactory.CreateDbContext(); - var result = await context.Set().OrderBy(x => x.Id).ToListAsync(); - - Assert.Equal(2, result.Count); - - var e1Collection = result[0].Collection.OrderBy(x => x.Number).ToList(); - Assert.Equal(3, e1Collection.Count); - Assert.Equal(new long[] { 110, 111 }, e1Collection[0].Ints); - Assert.Equal(new long[] { 120, 121, 122 }, e1Collection[1].Ints); - Assert.Empty(e1Collection[2].Ints); - - var e2Collection = result[1].Collection.OrderBy(x => x.Number).ToList(); - Assert.Equal(2, e2Collection.Count); - Assert.Equal(new long[] { 210 }, e2Collection[0].Ints); - Assert.Equal(new long[] { 220, 221 }, e2Collection[1].Ints); - } - - protected void OnConfiguringLazyLoadingProxies(DbContextOptionsBuilder optionsBuilder) - => optionsBuilder.UseLazyLoadingProxies(); - - protected IServiceCollection AddServicesLazyLoadingProxies(IServiceCollection addServices) - => addServices.AddEntityFrameworkProxies(); - - private Task SeedLazyLoadingProxies(DbContext ctx) - { - var r1 = new ContextLazyLoadingProxies.MyJsonEntityWithCtor("r1", 1); - var c11 = new ContextLazyLoadingProxies.MyJsonEntity - { - Name = "c11", - Number = 11, - Ints = [110, 111] - }; - var c12 = new ContextLazyLoadingProxies.MyJsonEntity - { - Name = "c12", - Number = 12, - Ints = [120, 121, 122] - }; - var c13 = new ContextLazyLoadingProxies.MyJsonEntity - { - Name = "c13", - Number = 13, - Ints = [] - }; - - var r2 = new ContextLazyLoadingProxies.MyJsonEntityWithCtor("r2", 2); - var c21 = new ContextLazyLoadingProxies.MyJsonEntity - { - Name = "c21", - Number = 21, - Ints = [210] - }; - var c22 = new ContextLazyLoadingProxies.MyJsonEntity - { - Name = "c22", - Number = 22, - Ints = [220, 221] - }; - - var e1 = new ContextLazyLoadingProxies.MyEntity - { - Id = 1, - Name = "e1", - Reference = r1, - Collection = - [ - c11, - c12, - c13 - ] - }; - - var e2 = new ContextLazyLoadingProxies.MyEntity - { - Id = 2, - Name = "e2", - Reference = r2, - Collection = [c21, c22] - }; - - ctx.Set().AddRange(e1, e2); - return ctx.SaveChangesAsync(); - } - - protected virtual void OnModelCreatingLazyLoadingProxies(ModelBuilder modelBuilder) - { - modelBuilder.Entity().Property(x => x.Id).ValueGeneratedNever(); - modelBuilder.Entity().OwnsOne(x => x.Reference); - modelBuilder.Entity().OwnsMany(x => x.Collection); - } - - public class ContextLazyLoadingProxies(DbContextOptions options) : DbContext(options) - { - public class MyEntity - { - public int Id { get; set; } - public string Name { get; set; } - - public virtual MyJsonEntityWithCtor Reference { get; set; } - public virtual List Collection { get; set; } - } - - public class MyJsonEntityWithCtor(string name, int number) - { - public string Name { get; set; } = name; - public int Number { get; set; } = number; - } - - public class MyJsonEntity - { - public string Name { get; set; } - public int Number { get; set; } - public IList Ints { get; set; } - } - } - - #endregion - - #region NotICollection - - [Fact] - public virtual async Task Not_ICollection_basic_projection() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingNotICollection, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedNotICollection); - - using var context = contextFactory.CreateDbContext(); - var query = context.Set(); - var result = await query.ToListAsync(); - - Assert.Equal(2, result.Count); - } - - protected abstract Task SeedNotICollection(DbContext ctx); - - protected virtual void OnModelCreatingNotICollection(ModelBuilder modelBuilder) - => modelBuilder.Entity(b => - { - b.Property(x => x.Id).ValueGeneratedNever(); - b.OwnsOne( - cr => cr.Json, nb => nb.OwnsMany(x => x.Collection)); - }); - - protected class ContextNotICollection(DbContextOptions options) : DbContext(options) - { - public class MyEntity - { - public int Id { get; set; } - - public MyJsonEntity Json { get; set; } - } - - public class MyJsonEntity - { - private readonly List _collection = []; - - public IEnumerable Collection - => _collection.AsReadOnly(); - } - - public class MyJsonNestedEntity - { - public string Foo { get; set; } - public int Bar { get; set; } - } - } - - #endregion - - #region BadJsonProperties - - [Theory, InlineData(true), InlineData(false)] - public virtual async Task Bad_json_properties_duplicated_navigations(bool noTracking) - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingBadJsonProperties, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedBadJsonProperties); - - using var context = contextFactory.CreateDbContext(); - var query = noTracking ? context.Entities.AsNoTracking() : context.Entities; - var baseline = await query.SingleAsync(x => x.Scenario == "baseline"); - var dupNavs = await query.SingleAsync(x => x.Scenario == "duplicated navigations"); - - // for no tracking, last one wins - Assert.Equal(baseline.RequiredReference.NestedOptional.Text + " dupnav", dupNavs.RequiredReference.NestedOptional.Text); - Assert.Equal(baseline.RequiredReference.NestedRequired.Text + " dupnav", dupNavs.RequiredReference.NestedRequired.Text); - Assert.Equal(baseline.RequiredReference.NestedCollection[0].Text + " dupnav", dupNavs.RequiredReference.NestedCollection[0].Text); - Assert.Equal(baseline.RequiredReference.NestedCollection[1].Text + " dupnav", dupNavs.RequiredReference.NestedCollection[1].Text); - - Assert.Equal(baseline.OptionalReference.NestedOptional.Text + " dupnav", dupNavs.OptionalReference.NestedOptional.Text); - Assert.Equal(baseline.OptionalReference.NestedRequired.Text + " dupnav", dupNavs.OptionalReference.NestedRequired.Text); - Assert.Equal(baseline.OptionalReference.NestedCollection[0].Text + " dupnav", dupNavs.OptionalReference.NestedCollection[0].Text); - Assert.Equal(baseline.OptionalReference.NestedCollection[1].Text + " dupnav", dupNavs.OptionalReference.NestedCollection[1].Text); - - Assert.Equal(baseline.Collection[0].NestedOptional.Text + " dupnav", dupNavs.Collection[0].NestedOptional.Text); - Assert.Equal(baseline.Collection[0].NestedRequired.Text + " dupnav", dupNavs.Collection[0].NestedRequired.Text); - Assert.Equal(baseline.Collection[0].NestedCollection[0].Text + " dupnav", dupNavs.Collection[0].NestedCollection[0].Text); - Assert.Equal(baseline.Collection[0].NestedCollection[1].Text + " dupnav", dupNavs.Collection[0].NestedCollection[1].Text); - - Assert.Equal(baseline.Collection[1].NestedOptional.Text + " dupnav", dupNavs.Collection[1].NestedOptional.Text); - Assert.Equal(baseline.Collection[1].NestedRequired.Text + " dupnav", dupNavs.Collection[1].NestedRequired.Text); - Assert.Equal(baseline.Collection[1].NestedCollection[0].Text + " dupnav", dupNavs.Collection[1].NestedCollection[0].Text); - Assert.Equal(baseline.Collection[1].NestedCollection[1].Text + " dupnav", dupNavs.Collection[1].NestedCollection[1].Text); - } - - [Theory, InlineData(true), InlineData(false)] - public virtual async Task Bad_json_properties_duplicated_scalars(bool noTracking) - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingBadJsonProperties, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedBadJsonProperties); - - using var context = contextFactory.CreateDbContext(); - var query = noTracking ? context.Entities.AsNoTracking() : context.Entities; - - var baseline = await query.SingleAsync(x => x.Scenario == "baseline"); - var dupProps = await query.SingleAsync(x => x.Scenario == "duplicated scalars"); - - Assert.Equal(baseline.RequiredReference.NestedOptional.Text + " dupprop", dupProps.RequiredReference.NestedOptional.Text); - Assert.Equal(baseline.RequiredReference.NestedRequired.Text + " dupprop", dupProps.RequiredReference.NestedRequired.Text); - Assert.Equal(baseline.RequiredReference.NestedCollection[0].Text + " dupprop", dupProps.RequiredReference.NestedCollection[0].Text); - Assert.Equal(baseline.RequiredReference.NestedCollection[1].Text + " dupprop", dupProps.RequiredReference.NestedCollection[1].Text); - - Assert.Equal(baseline.OptionalReference.NestedOptional.Text + " dupprop", dupProps.OptionalReference.NestedOptional.Text); - Assert.Equal(baseline.OptionalReference.NestedRequired.Text + " dupprop", dupProps.OptionalReference.NestedRequired.Text); - Assert.Equal(baseline.OptionalReference.NestedCollection[0].Text + " dupprop", dupProps.OptionalReference.NestedCollection[0].Text); - Assert.Equal(baseline.OptionalReference.NestedCollection[1].Text + " dupprop", dupProps.OptionalReference.NestedCollection[1].Text); - - Assert.Equal(baseline.Collection[0].NestedOptional.Text + " dupprop", dupProps.Collection[0].NestedOptional.Text); - Assert.Equal(baseline.Collection[0].NestedRequired.Text + " dupprop", dupProps.Collection[0].NestedRequired.Text); - Assert.Equal(baseline.Collection[0].NestedCollection[0].Text + " dupprop", dupProps.Collection[0].NestedCollection[0].Text); - Assert.Equal(baseline.Collection[0].NestedCollection[1].Text + " dupprop", dupProps.Collection[0].NestedCollection[1].Text); - - Assert.Equal(baseline.Collection[1].NestedOptional.Text + " dupprop", dupProps.Collection[1].NestedOptional.Text); - Assert.Equal(baseline.Collection[1].NestedRequired.Text + " dupprop", dupProps.Collection[1].NestedRequired.Text); - Assert.Equal(baseline.Collection[1].NestedCollection[0].Text + " dupprop", dupProps.Collection[1].NestedCollection[0].Text); - Assert.Equal(baseline.Collection[1].NestedCollection[1].Text + " dupprop", dupProps.Collection[1].NestedCollection[1].Text); - } - - [Theory, InlineData(true), InlineData(false)] - public virtual async Task Bad_json_properties_empty_navigations(bool noTracking) - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingBadJsonProperties, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedBadJsonProperties); - - using var context = contextFactory.CreateDbContext(); - var query = noTracking ? context.Entities.AsNoTracking() : context.Entities; - var emptyNavs = await query.SingleAsync(x => x.Scenario == "empty navigation property names"); - - Assert.Null(emptyNavs.RequiredReference.NestedOptional); - Assert.Null(emptyNavs.RequiredReference.NestedRequired); - Assert.Null(emptyNavs.RequiredReference.NestedCollection); - - Assert.Null(emptyNavs.OptionalReference.NestedOptional); - Assert.Null(emptyNavs.OptionalReference.NestedRequired); - Assert.Null(emptyNavs.OptionalReference.NestedCollection); - - Assert.Null(emptyNavs.Collection[0].NestedOptional); - Assert.Null(emptyNavs.Collection[0].NestedRequired); - Assert.Null(emptyNavs.Collection[0].NestedCollection); - - Assert.Null(emptyNavs.Collection[1].NestedOptional); - Assert.Null(emptyNavs.Collection[1].NestedRequired); - Assert.Null(emptyNavs.Collection[1].NestedCollection); - } - - [Theory, InlineData(true), InlineData(false)] - public virtual async Task Bad_json_properties_empty_scalars(bool noTracking) - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingBadJsonProperties, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedBadJsonProperties); - - using var context = contextFactory.CreateDbContext(); - var query = noTracking ? context.Entities.AsNoTracking() : context.Entities; - var emptyNavs = await query.SingleAsync(x => x.Scenario == "empty scalar property names"); - - Assert.Null(emptyNavs.RequiredReference.NestedOptional.Text); - Assert.Null(emptyNavs.RequiredReference.NestedRequired.Text); - Assert.Null(emptyNavs.RequiredReference.NestedCollection[0].Text); - Assert.Null(emptyNavs.RequiredReference.NestedCollection[1].Text); - - Assert.Null(emptyNavs.OptionalReference.NestedOptional.Text); - Assert.Null(emptyNavs.OptionalReference.NestedRequired.Text); - Assert.Null(emptyNavs.OptionalReference.NestedCollection[0].Text); - Assert.Null(emptyNavs.OptionalReference.NestedCollection[1].Text); - - Assert.Null(emptyNavs.Collection[0].NestedOptional.Text); - Assert.Null(emptyNavs.Collection[0].NestedRequired.Text); - Assert.Null(emptyNavs.Collection[0].NestedCollection[0].Text); - Assert.Null(emptyNavs.Collection[0].NestedCollection[1].Text); - - Assert.Null(emptyNavs.Collection[1].NestedOptional.Text); - Assert.Null(emptyNavs.Collection[1].NestedRequired.Text); - Assert.Null(emptyNavs.Collection[1].NestedCollection[0].Text); - Assert.Null(emptyNavs.Collection[1].NestedCollection[1].Text); - } - - [Theory, InlineData(true), InlineData(false)] - public virtual async Task Bad_json_properties_null_navigations(bool noTracking) - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingBadJsonProperties, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedBadJsonProperties); - - using var context = contextFactory.CreateDbContext(); - var query = noTracking ? context.Entities.AsNoTracking() : context.Entities; - var _ = await query.SingleAsync(x => x.Scenario == "null navigation property names"); - } - - [Theory, InlineData(true), InlineData(false)] - public virtual async Task Bad_json_properties_null_scalars(bool noTracking) - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingBadJsonProperties, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedBadJsonProperties); - - using var context = contextFactory.CreateDbContext(); - var query = noTracking ? context.Entities.AsNoTracking() : context.Entities; - var _ = await query.SingleAsync(x => x.Scenario == "null scalar property names"); - } - - protected abstract Task SeedBadJsonProperties(ContextBadJsonProperties ctx); - - protected virtual void OnModelCreatingBadJsonProperties(ModelBuilder modelBuilder) - => modelBuilder.Entity(b => - { - b.Property(x => x.Id).ValueGeneratedNever(); - - b.OwnsOne( - x => x.RequiredReference, b => - { - b.OwnsOne(x => x.NestedOptional); - b.OwnsOne(x => x.NestedRequired); - b.OwnsMany(x => x.NestedCollection); - }); - - b.OwnsOne( - x => x.OptionalReference, b => - { - b.OwnsOne(x => x.NestedOptional); - b.OwnsOne(x => x.NestedRequired); - b.OwnsMany(x => x.NestedCollection); - }); - - b.OwnsMany( - x => x.Collection, b => - { - b.OwnsOne(x => x.NestedOptional); - b.OwnsOne(x => x.NestedRequired); - b.OwnsMany(x => x.NestedCollection); - }); - }); - - protected class ContextBadJsonProperties(DbContextOptions options) : DbContext(options) - { - public DbSet Entities { get; set; } - - public class Entity - { - public int Id { get; set; } - public string Scenario { get; set; } - public JsonRoot OptionalReference { get; set; } - public JsonRoot RequiredReference { get; set; } - public List Collection { get; set; } - } - - public class JsonRoot - { - public JsonBranch NestedRequired { get; set; } - public JsonBranch NestedOptional { get; set; } - public List NestedCollection { get; set; } - } - - public class JsonBranch - { - public string Text { get; set; } - } - } - - #endregion -} diff --git a/test/EFCore.Specification.Tests/Query/AdHocManyToManyQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/AdHocManyToManyQueryTestBase.cs deleted file mode 100644 index 0ce1ccbb62d..00000000000 --- a/test/EFCore.Specification.Tests/Query/AdHocManyToManyQueryTestBase.cs +++ /dev/null @@ -1,183 +0,0 @@ -// 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; - -#nullable disable - -public abstract class AdHocManyToManyQueryTestBase(NonSharedFixture fixture) - : NonSharedModelTestBase(fixture), IClassFixture -{ - protected override string NonSharedStoreName - => "AdHocManyToManyQueryTests"; - - protected virtual void ClearLog() - => ListLoggerFactory.Clear(); - - #region 7973 - - [Fact] - public virtual async Task SelectMany_with_collection_selector_having_subquery() - { - var contextFactory = await InitializeNonSharedTest(seed: c => c.SeedAsync()); - using var context = contextFactory.CreateDbContext(); - var users = (from user in context.Users - from organisation in context.Organisations.Where(o => o.OrganisationUsers.Any()).DefaultIfEmpty() - select new { UserId = user.Id, OrgId = organisation.Id }).ToList(); - - Assert.Equal(2, users.Count); - } - - // Protected so that it can be used by inheriting tests, and so that things like unused setters are not removed. - protected class MyContext7973(DbContextOptions options) : DbContext(options) - { - public DbSet Users { get; set; } - public DbSet Organisations { get; set; } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.Entity().HasKey(ou => new { ou.OrganisationId, ou.UserId }); - modelBuilder.Entity().HasOne(ou => ou.Organisation).WithMany(o => o.OrganisationUsers) - .HasForeignKey(ou => ou.OrganisationId); - modelBuilder.Entity().HasOne(ou => ou.User).WithMany(u => u.OrganisationUsers) - .HasForeignKey(ou => ou.UserId); - } - - public Task SeedAsync() - { - AddRange( - new OrganisationUser { Organisation = new Organisation(), User = new User() }, - new Organisation(), - new User()); - - return SaveChangesAsync(); - } - - public class User - { - public int Id { get; set; } - public List OrganisationUsers { get; set; } - } - - public class Organisation - { - public int Id { get; set; } - public List OrganisationUsers { get; set; } - } - - public class OrganisationUser - { - public int OrganisationId { get; set; } - public Organisation Organisation { get; set; } - - public int UserId { get; set; } - public User User { get; set; } - } - } - - #endregion - - #region 20277 - - [Theory, MemberData(nameof(IsAsyncData))] - public virtual async Task Many_to_many_load_works_when_join_entity_has_custom_key(bool async) - { - var contextFactory = await InitializeNonSharedTest(); - - int id; - using (var context = contextFactory.CreateDbContext()) - { - var m = new ManyM_DB(); - var n = new ManyN_DB(); - context.AddRange(m, n); - m.ManyN_DB = [n]; - - context.SaveChanges(); - - id = m.Id; - } - - ClearLog(); - - using (var context = contextFactory.CreateDbContext()) - { - var m = context.Find(id); - - if (async) - { - await context.Entry(m).Collection(x => x.ManyN_DB).LoadAsync(); - } - else - { - context.Entry(m).Collection(x => x.ManyN_DB).Load(); - } - - Assert.Equal(3, context.ChangeTracker.Entries().Count()); - Assert.Equal(1, m.ManyN_DB.Count); - Assert.Equal(1, m.ManyN_DB.Single().ManyM_DB.Count); - Assert.Equal(1, m.ManyNM_DB.Count); - Assert.Equal(1, m.ManyN_DB.Single().ManyNM_DB.Count); - - id = m.ManyN_DB.Single().Id; - } - - using (var context = contextFactory.CreateDbContext()) - { - var n = context.Find(id); - - if (async) - { - await context.Entry(n).Collection(x => x.ManyM_DB).LoadAsync(); - } - else - { - context.Entry(n).Collection(x => x.ManyM_DB).Load(); - } - - Assert.Equal(3, context.ChangeTracker.Entries().Count()); - Assert.Equal(1, n.ManyM_DB.Count); - Assert.Equal(1, n.ManyM_DB.Single().ManyN_DB.Count); - Assert.Equal(1, n.ManyNM_DB.Count); - Assert.Equal(1, n.ManyM_DB.Single().ManyNM_DB.Count); - } - } - - protected class Context20277(DbContextOptions options) : DbContext(options) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - => modelBuilder.Entity() - .HasMany(e => e.ManyN_DB) - .WithMany(e => e.ManyM_DB) - .UsingEntity( - r => r.HasOne(e => e.ManyN_DB).WithMany(e => e.ManyNM_DB).HasForeignKey(e => e.ManyN_Id), - l => l.HasOne(e => e.ManyM_DB).WithMany(e => e.ManyNM_DB).HasForeignKey(e => e.ManyM_Id), - b => b.HasKey(e => e.Id)); - } - - public class ManyM_DB - { - public int Id { get; set; } - public ICollection ManyN_DB { get; set; } - public ICollection ManyNM_DB { get; set; } - } - - public class ManyN_DB - { - public int Id { get; set; } - public ICollection ManyM_DB { get; set; } - public ICollection ManyNM_DB { get; set; } - } - - public sealed class ManyMN_DB - { - public int Id { get; set; } - - public int ManyM_Id { get; set; } - public ManyM_DB ManyM_DB { get; set; } - - public int? ManyN_Id { get; set; } - public ManyN_DB ManyN_DB { get; set; } - } - - #endregion -} diff --git a/test/EFCore.Specification.Tests/Query/ComplexTypeQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/ComplexTypeQueryTestBase.cs index de3bc89ad34..e00ffb36177 100644 --- a/test/EFCore.Specification.Tests/Query/ComplexTypeQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/ComplexTypeQueryTestBase.cs @@ -807,4 +807,864 @@ public virtual Task Project_entity_with_complex_type_pushdown_and_then_left_join protected DbContext CreateContext() => Fixture.CreateContext(); + + #region Non-shared test resources + + #region 33449 + + [Fact] + public virtual async Task Complex_type_equals_parameter_with_nested_types_with_property_of_same_name() + { + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + context.AddRange( + new Context33449.EntityType + { + Id = 1, + ComplexContainer = new Context33449.ComplexContainer + { + Id = 1, + Containee1 = new Context33449.ComplexContainee1 { Id = 2 }, + Containee2 = new Context33449.ComplexContainee2 { Id = 3 } + } + }); + return context.SaveChangesAsync(); + }); + + await using var context = contextFactory.CreateDbContext(); + + var container = new Context33449.ComplexContainer + { + Id = 1, + Containee1 = new Context33449.ComplexContainee1 { Id = 2 }, + Containee2 = new Context33449.ComplexContainee2 { Id = 3 } + }; + + _ = await context.Set().Where(b => b.ComplexContainer == container).SingleAsync(); + } + + private class Context33449(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.Property(b => b.Id).ValueGeneratedNever(); + b.ComplexProperty( + b => b.ComplexContainer, x => + { + x.ComplexProperty(c => c.Containee1); + x.ComplexProperty(c => c.Containee2); + }); + }); + + public class EntityType + { + public int Id { get; set; } + public ComplexContainer ComplexContainer { get; set; } = null!; + } + + public class ComplexContainer + { + public int Id { get; set; } + + public ComplexContainee1 Containee1 { get; set; } = null!; + public ComplexContainee2 Containee2 { get; set; } = null!; + } + + public class ComplexContainee1 + { + public int Id { get; set; } + } + + public class ComplexContainee2 + { + public int Id { get; set; } + } + } + + #endregion 33449 + + #region 34749 + + [Fact] + public virtual async Task Projecting_complex_property_does_not_auto_include_owned_types() + { + var contextFactory = await InitializeNonSharedTest(); + + await using var context = contextFactory.CreateDbContext(); + + _ = await context.Set().Select(x => x.Complex).ToListAsync(); + } + + private class Context34749(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.Property(b => b.Id).ValueGeneratedNever(); + b.ComplexProperty(x => x.Complex); + b.OwnsOne(x => x.OwnedReference); + }); + + public class EntityType + { + public int Id { get; set; } + public string? Name { get; set; } + public OwnedType OwnedReference { get; set; } = null!; + public ComplexType Complex { get; set; } = null!; + } + + public class ComplexType + { + public int Number { get; set; } + public string? Name { get; set; } + } + + public class OwnedType + { + public string? Foo { get; set; } + public int Bar { get; set; } + } + } + + #endregion + + #region ShadowDiscriminator + + [Fact] + public virtual async Task Optional_complex_type_with_discriminator() + { + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + context.AddRange( + new ContextShadowDiscriminator.EntityType + { + Id = 1, + AllOptionalsComplexType = + new ContextShadowDiscriminator.AllOptionalsComplexType { OptionalProperty = "Non-null" } + }, + new ContextShadowDiscriminator.EntityType + { + Id = 2, + AllOptionalsComplexType = new ContextShadowDiscriminator.AllOptionalsComplexType { OptionalProperty = null } + }, + new ContextShadowDiscriminator.EntityType { Id = 3, AllOptionalsComplexType = null } + ); + return context.SaveChangesAsync(); + }); + + await using (var context = contextFactory.CreateDbContext()) + { + var complexTypeNull = await context.Set() + .SingleAsync(b => b.AllOptionalsComplexType == null); + Assert.Null(complexTypeNull.AllOptionalsComplexType); + + complexTypeNull.AllOptionalsComplexType = + new ContextShadowDiscriminator.AllOptionalsComplexType { OptionalProperty = "New thing" }; + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateDbContext()) + { + var entities = await context.Set().ToListAsync(); + Assert.Equal(3, entities.Count); + Assert.All(entities, e => Assert.NotNull(e.AllOptionalsComplexType)); + } + } + + private class ContextShadowDiscriminator(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.Property(b => b.Id).ValueGeneratedNever(); + b.ComplexProperty(b => b.AllOptionalsComplexType, x => x.HasDiscriminator()); + }); + + public class EntityType + { + public int Id { get; set; } + public AllOptionalsComplexType? AllOptionalsComplexType { get; set; } + } + + public class AllOptionalsComplexType + { + public string? OptionalProperty { get; set; } + } + } + + #endregion ShadowDiscriminator + + #region 37162 + + [Fact] + public virtual async Task Non_optional_complex_type_with_all_nullable_properties() + { + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + context.Add( + new Context37162.EntityType + { + Id = 1, + NonOptionalComplexType = new Context37162.ComplexTypeWithAllNulls + { + // All properties are null + } + }); + return context.SaveChangesAsync(); + }); + + await using var context = contextFactory.CreateDbContext(); + + var entity = await context.Set().SingleAsync(); + + Assert.NotNull(entity.NonOptionalComplexType); + Assert.Null(entity.NonOptionalComplexType.NullableString); + Assert.Null(entity.NonOptionalComplexType.NullableDateTime); + } + + private class Context37162(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.Property(b => b.Id).ValueGeneratedNever(); + b.ComplexProperty(b => b.NonOptionalComplexType); + }); + + public class EntityType + { + public int Id { get; set; } + public ComplexTypeWithAllNulls NonOptionalComplexType { get; set; } = null!; + } + + public class ComplexTypeWithAllNulls + { + public string? NullableString { get; set; } + public DateTime? NullableDateTime { get; set; } + } + } + + #endregion 37162 + + #region 37304 + + [Fact] + public virtual async Task Non_optional_complex_type_with_all_nullable_properties_via_left_join() + { + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + context.Add( + new Context37304.Parent + { + Id = 1, + Children = + [ + new Context37304.Child { Id = 1, ComplexType = new Context37304.ComplexTypeWithAllNulls() } + ] + }); + return context.SaveChangesAsync(); + }); + + await using var context = contextFactory.CreateDbContext(); + + var parent = await context.Set().Include(p => p.Children).SingleAsync(); + + var child = parent.Children.Single(); + Assert.NotNull(child.ComplexType); + Assert.Null(child.ComplexType.NullableString); + Assert.Null(child.ComplexType.NullableDateTime); + } + + private class Context37304(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(b => b.Property(p => p.Id).ValueGeneratedNever()); + + modelBuilder.Entity(b => + { + b.Property(c => c.Id).ValueGeneratedNever(); + b.HasOne(c => c.Parent).WithMany(p => p.Children).HasForeignKey(c => c.ParentId); + b.ComplexProperty(c => c.ComplexType); + }); + } + + public class Parent + { + public int Id { get; set; } + public List Children { get; set; } = []; + } + + public class Child + { + public int Id { get; set; } + public int ParentId { get; set; } + public Parent Parent { get; set; } = null!; + public ComplexTypeWithAllNulls ComplexType { get; set; } = null!; + } + + public class ComplexTypeWithAllNulls + { + public string? NullableString { get; set; } + public DateTime? NullableDateTime { get; set; } + } + } + + #endregion 37304 + + #region Issue37337 + + private const string Issue37337CreatedByShadowPropertyName = "CreatedBy"; + + [Fact] + public virtual async Task Nullable_complex_type_with_discriminator_and_shadow_property() + { + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + var entity = new Context37337.EntityType + { + Id = Guid.NewGuid(), + Prop = new Context37337.OptionalComplexProperty { OptionalValue = true } + }; + context.Add(entity); + context.Entry(entity).Property(Issue37337CreatedByShadowPropertyName).CurrentValue = "Seeder"; + return context.SaveChangesAsync(); + }); + + await using var context = contextFactory.CreateDbContext(); + + var entities = await context.Set().ToArrayAsync(); + + Assert.Single(entities); + var entity = entities[0]; + Assert.NotNull(entity.Prop); + Assert.True(entity.Prop.OptionalValue); + + var entry = context.Entry(entity); + Assert.Equal("Seeder", entry.Property(Issue37337CreatedByShadowPropertyName).CurrentValue); + } + + protected class Context37337(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + var entity = modelBuilder.Entity(); + entity.Property(p => p.Id).ValueGeneratedNever(); + entity.HasKey(p => p.Id); + + var compl = entity.ComplexProperty(p => p.Prop); + compl.Property(p => p.OptionalValue); + compl.HasDiscriminator(); + + // Shadow property added via convention (e.g., audit field) + entity.Property(Issue37337CreatedByShadowPropertyName).IsRequired(false); + } + + public class EntityType + { + public Guid Id { get; set; } + public OptionalComplexProperty? Prop { get; set; } + } + + public class OptionalComplexProperty + { + public bool? OptionalValue { get; set; } + } + } + + #endregion Issue37337 + + #region Issue38119 + + [Fact] + public virtual async Task Nullable_complex_type_with_discriminator_null_to_non_null_roundtrip() + { + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + context.Add(new Context38119.EntityType { Id = Guid.NewGuid() }); + return context.SaveChangesAsync(); + }); + + await using (var context = contextFactory.CreateDbContext()) + { + var entity = await context.Set().SingleAsync(); + Assert.Null(entity.Prop); + + entity.Prop = new Context38119.OptionalComplexProperty { OptionalValue = true }; + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateDbContext()) + { + var entity = await context.Set().SingleAsync(); + Assert.NotNull(entity.Prop); + Assert.True(entity.Prop.OptionalValue); + } + } + + [Fact] + public virtual async Task Nullable_complex_type_with_discriminator_non_null_to_null_roundtrip() + { + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + context.Add( + new Context38119.EntityType + { + Id = Guid.NewGuid(), + Prop = new Context38119.OptionalComplexProperty { OptionalValue = true } + }); + return context.SaveChangesAsync(); + }); + + await using (var context = contextFactory.CreateDbContext()) + { + var entity = await context.Set().SingleAsync(); + Assert.NotNull(entity.Prop); + + entity.Prop = null; + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateDbContext()) + { + var entity = await context.Set().SingleAsync(); + Assert.Null(entity.Prop); + } + } + + [Fact] + public virtual async Task Nullable_complex_type_with_discriminator_update_non_null_entity_roundtrip() + { + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + context.Add( + new Context38119.EntityType + { + Id = Guid.NewGuid(), + Prop = new Context38119.OptionalComplexProperty { OptionalValue = true } + }); + return context.SaveChangesAsync(); + }); + + await using (var context = contextFactory.CreateDbContext()) + { + var entity = await context.Set().SingleAsync(); + Assert.NotNull(entity.Prop); + Assert.True(entity.Prop.OptionalValue); + + context.Update(entity); + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateDbContext()) + { + var entity = await context.Set().SingleAsync(); + Assert.NotNull(entity.Prop); + Assert.True(entity.Prop.OptionalValue); + } + } + + [Fact] + public virtual async Task Nullable_complex_type_with_discriminator_set_to_different_value() + { + var contextFactory = await InitializeNonSharedTest(); + + Guid entityId; + await using (var context = contextFactory.CreateDbContext()) + { + var entity = new Context38119.EntityType + { + Id = Guid.NewGuid(), + Prop = new Context38119.OptionalComplexProperty { OptionalValue = true } + }; + context.Add(entity); + entityId = entity.Id; + + // Override the discriminator value before saving + var discriminatorEntry = context.Entry(entity).ComplexProperty(e => e.Prop).Property("Discriminator"); + Assert.Equal("OptionalComplexProperty", discriminatorEntry.CurrentValue); + discriminatorEntry.CurrentValue = "SomeOtherValue"; + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateDbContext()) + { + // The discriminator is non-null so the complex property is still materialized + var entity = await context.Set().SingleAsync(e => e.Id == entityId); + Assert.NotNull(entity.Prop); + Assert.True(entity.Prop.OptionalValue); + } + } + + [Fact] + public virtual async Task Nullable_complex_type_with_discriminator_set_to_null() + { + var contextFactory = await InitializeNonSharedTest(); + + Guid entityId; + await using (var context = contextFactory.CreateDbContext()) + { + var entity = new Context38119.EntityType + { + Id = Guid.NewGuid(), + Prop = new Context38119.OptionalComplexProperty { OptionalValue = true } + }; + context.Add(entity); + entityId = entity.Id; + + // Set discriminator to null before saving, which should cause the complex property to be null on reload + var discriminatorEntry = context.Entry(entity).ComplexProperty(e => e.Prop).Property("Discriminator"); + Assert.Equal("OptionalComplexProperty", discriminatorEntry.CurrentValue); + discriminatorEntry.CurrentValue = null; + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateDbContext()) + { + // With null discriminator, the complex property should be materialized as null + var entity = await context.Set().SingleAsync(e => e.Id == entityId); + Assert.Null(entity.Prop); + } + } + + [Fact] + public virtual async Task Nested_nullable_complex_type_with_discriminator_null_to_non_null_roundtrip() + { + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + context.Add( + new Context38119Nested.EntityType + { + Id = Guid.NewGuid(), + Outer = new Context38119Nested.OuterComplexProperty { Name = "outer" } + }); + return context.SaveChangesAsync(); + }); + + await using (var context = contextFactory.CreateDbContext()) + { + var entity = await context.Set().SingleAsync(); + Assert.NotNull(entity.Outer); + Assert.Null(entity.Outer.Inner); + + entity.Outer.Inner = new Context38119Nested.InnerComplexProperty { Value = 42 }; + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateDbContext()) + { + var entity = await context.Set().SingleAsync(); + Assert.NotNull(entity.Outer); + Assert.NotNull(entity.Outer.Inner); + Assert.Equal(42, entity.Outer.Inner.Value); + } + } + + protected class Context38119(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + var entity = modelBuilder.Entity(); + entity.HasKey(p => p.Id); + entity.Property(p => p.Id).ValueGeneratedNever(); + + var compl = entity.ComplexProperty(p => p.Prop); + compl.HasDiscriminator(); + } + + public class EntityType + { + public Guid Id { get; set; } + public OptionalComplexProperty? Prop { get; set; } + } + + public class OptionalComplexProperty + { + public bool? OptionalValue { get; set; } + } + } + + private class Context38119Nested(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + var entity = modelBuilder.Entity(); + entity.HasKey(p => p.Id); + entity.Property(p => p.Id).ValueGeneratedNever(); + + entity.ComplexProperty( + p => p.Outer, outer => outer.ComplexProperty( + p => p.Inner, inner => inner.HasDiscriminator())); + } + + public class EntityType + { + public Guid Id { get; set; } + public OuterComplexProperty Outer { get; set; } = null!; + } + + public class OuterComplexProperty + { + public string? Name { get; set; } + public InnerComplexProperty? Inner { get; set; } + } + + public class InnerComplexProperty + { + public int? Value { get; set; } + } + } + + #endregion Issue38119 + + #region Issue38105 + + [Fact] + public virtual async Task Update_entity_with_nullable_complex_type_and_discriminator_does_not_throw() + { + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + var entity = new Context37337.EntityType + { + Id = Guid.NewGuid(), + Prop = new Context37337.OptionalComplexProperty { OptionalValue = true } + }; + context.Add(entity); + context.Entry(entity).Property(Issue37337CreatedByShadowPropertyName).CurrentValue = "Seeder"; + return context.SaveChangesAsync(); + }); + + await using var context = contextFactory.CreateDbContext(); + + var entity = await context.Set().SingleAsync(); + var id = entity.Id; + context.ChangeTracker.Clear(); + + // Create a new disconnected instance with the same key and Update it. + // The complex type discriminator (shadow property with AfterSaveBehavior.Throw) should not + // be marked as modified by Update(), and SaveChanges should succeed without throwing. + var updatedEntity = new Context37337.EntityType + { + Id = id, + Prop = new Context37337.OptionalComplexProperty { OptionalValue = false } + }; + + context.Update(updatedEntity); + + await context.SaveChangesAsync(); + + context.ChangeTracker.Clear(); + + var reloaded = await context.Set().SingleAsync(); + Assert.Equal(id, reloaded.Id); + Assert.NotNull(reloaded.Prop); + Assert.False(reloaded.Prop.OptionalValue); + } + + #endregion Issue38105 + + #region Issue31246 + + [Fact] + public virtual async Task Can_query_by_complex_type_property_with_index() + { + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + context.AddRange( + new Context31246.Person + { + Id = new Context31246.StronglyTypedId(1), + Address = new Context31246.Address { City = "Seattle", PostalCode = "98101" } + }, + new Context31246.Person + { + Id = new Context31246.StronglyTypedId(2), + Address = new Context31246.Address { City = "Redmond", PostalCode = "98052" } + }); + return context.SaveChangesAsync(); + }); + + await using var context = contextFactory.CreateDbContext(); + + // The primary key Id.Id reorders the entity's complex properties so that the chain containing + // the PK property comes first, and reorders the properties inside the Id complex type so that + // the PK property is listed first. + var personType = context.Model.FindEntityType(typeof(Context31246.Person))!; + Assert.Equal( + [nameof(Context31246.Person.Id), nameof(Context31246.Person.Address)], + personType.GetComplexProperties().Select(p => p.Name)); + var idComplexType = personType.FindComplexProperty(nameof(Context31246.Person.Id))!.ComplexType; + Assert.Equal( + [nameof(Context31246.StronglyTypedId.Id)], + idComplexType.GetProperties().Select(p => p.Name)); + + var found = await context.Set().SingleAsync(p => p.Address.City == "Seattle"); + Assert.Equal(new Context31246.StronglyTypedId(1), found.Id); + Assert.Equal("98101", found.Address.PostalCode); + } + + [Fact] + public virtual async Task Can_update_entity_with_index_on_complex_type_property() + { + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + context.Add( + new Context31246.Person + { + Id = new Context31246.StronglyTypedId(1), + Address = new Context31246.Address { City = "Seattle", PostalCode = "98101" } + }); + return context.SaveChangesAsync(); + }); + + await using (var context = contextFactory.CreateDbContext()) + { + var person = await context.Set().SingleAsync(); + person.Address.PostalCode = "98102"; + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateDbContext()) + { + var person = await context.Set().SingleAsync(); + Assert.Equal("98102", person.Address.PostalCode); + } + } + + [Fact] + public virtual async Task Can_delete_entity_with_index_on_complex_type_property() + { + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + context.Add( + new Context31246.Person + { + Id = new Context31246.StronglyTypedId(1), + Address = new Context31246.Address { City = "Seattle", PostalCode = "98101" } + }); + return context.SaveChangesAsync(); + }); + + await using (var context = contextFactory.CreateDbContext()) + { + var person = await context.Set().SingleAsync(); + context.Remove(person); + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateDbContext()) + { + Assert.Equal(0, await context.Set().CountAsync()); + } + } + + [Fact] + public virtual async Task Can_query_by_alternate_key_on_complex_type_property() + { + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + context.AddRange( + new Context31246.Person + { + Id = new Context31246.StronglyTypedId(1), + Address = new Context31246.Address { City = "Seattle", PostalCode = "98101" } + }, + new Context31246.Person + { + Id = new Context31246.StronglyTypedId(2), + Address = new Context31246.Address { City = "Redmond", PostalCode = "98052" } + }); + return context.SaveChangesAsync(); + }); + + await using var context = contextFactory.CreateDbContext(); + + var found = await context.Set().SingleAsync(p => p.Address.City == "Redmond"); + Assert.Equal(new Context31246.StronglyTypedId(2), found.Id); + } + + [Fact] + public virtual async Task Can_save_batch_swapping_alternate_key_values_on_complex_type_property() + { + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + context.AddRange( + new Context31246.Person + { + Id = new Context31246.StronglyTypedId(1), + Address = new Context31246.Address { City = "Seattle", PostalCode = "98101" } + }, + new Context31246.Person + { + Id = new Context31246.StronglyTypedId(2), + Address = new Context31246.Address { City = "Redmond", PostalCode = "98052" } + }); + return context.SaveChangesAsync(); + }); + + // Update non-AK columns on multiple rows in the same batch. The CommandBatchPreparer + // still has to read the alternate-key column values (Address_City) for each command in + // order to build edges in the topological sort (AddUniqueValueEdges). + await using (var context = contextFactory.CreateDbContext()) + { + var people = await context.Set().OrderBy(p => p.Id.Id).ToListAsync(); + people[0].Address.PostalCode = "98103"; + people[1].Address.PostalCode = "98054"; + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateDbContext()) + { + var people = await context.Set().OrderBy(p => p.Id.Id).ToListAsync(); + Assert.Equal("98103", people[0].Address.PostalCode); + Assert.Equal("98054", people[1].Address.PostalCode); + } + } + + protected class Context31246(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.ComplexProperty(p => p.Id); + b.HasKey(p => p.Id.Id); + b.Property(p => p.Id.Id).ValueGeneratedNever(); + b.HasAlternateKey(p => p.Address.City); + b.HasIndex(p => p.Address.PostalCode); + }); + + public readonly record struct StronglyTypedId(int Id); + + public class Person + { + public StronglyTypedId Id { get; set; } + public Address Address { get; set; } = null!; + } + + public class Address + { + public string City { get; set; } = null!; + public string PostalCode { get; set; } = null!; + } + } + + #endregion Issue31246 + + #endregion Non-shared test resources } diff --git a/test/EFCore.Specification.Tests/Query/JsonQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/JsonQueryTestBase.cs index afbc517fb11..23c3bcaf48a 100644 --- a/test/EFCore.Specification.Tests/Query/JsonQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/JsonQueryTestBase.cs @@ -2389,4 +2389,1886 @@ public virtual Task Project_json_entity_in_tracking_query_fails_even_when_owner_ x.OwnedReferenceRoot, x.OwnedCollectionRoot })); + + #region Non-shared test resources + + #region 21006 + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual async Task Project_root_with_missing_scalars(bool async) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateDbContext(); + + var query = context.Set().Where(x => x.Id < 4); + + var result = async + ? await query.ToListAsync() + : query.ToList()!; + + var topLevel = result.Single(x => x.Id == 2); + var nested = result.Single(x => x.Id == 3); + + Assert.Equal(default, topLevel.OptionalReference.Number); + Assert.Equal(default, topLevel.RequiredReference.Number); + Assert.True(topLevel.Collection.All(x => x.Number == default)); + + Assert.Equal(default, nested.RequiredReference.NestedRequiredReference.DoB); + Assert.Equal(default, nested.RequiredReference.NestedOptionalReference.DoB); + Assert.Equal(default, nested.OptionalReference.NestedRequiredReference.DoB); + Assert.Equal(default, nested.OptionalReference.NestedOptionalReference.DoB); + Assert.True(nested.Collection.SelectMany(x => x.NestedCollection).All(x => x.DoB == default)); + } + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual async Task Project_top_level_json_entity_with_missing_scalars(bool async) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateDbContext(); + + var query = context.Set().Where(x => x.Id < 4).Select(x => new + { + x.Id, + x.OptionalReference, + x.RequiredReference, + x.Collection + }).AsNoTracking(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + var topLevel = result.Single(x => x.Id == 2); + var nested = result.Single(x => x.Id == 3); + + Assert.Equal(default, topLevel.OptionalReference.Number); + Assert.Equal(default, topLevel.RequiredReference.Number); + Assert.True(topLevel.Collection.All(x => x.Number == default)); + + Assert.Equal(default, nested.RequiredReference.NestedRequiredReference.DoB); + Assert.Equal(default, nested.RequiredReference.NestedOptionalReference.DoB); + Assert.Equal(default, nested.OptionalReference.NestedRequiredReference.DoB); + Assert.Equal(default, nested.OptionalReference.NestedOptionalReference.DoB); + Assert.True(nested.Collection.SelectMany(x => x.NestedCollection).All(x => x.DoB == default)); + } + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual async Task Project_nested_json_entity_with_missing_scalars(bool async) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateDbContext(); + + var query = context.Set().Where(x => x.Id < 4).Select(x => new + { + x.Id, + x.OptionalReference.NestedOptionalReference, + x.RequiredReference.NestedRequiredReference, + x.Collection[0].NestedCollection + }).AsNoTracking(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + var topLevel = result.Single(x => x.Id == 2); + var nested = result.Single(x => x.Id == 3); + + Assert.Equal(default, nested.NestedOptionalReference.DoB); + Assert.Equal(default, nested.NestedRequiredReference.DoB); + Assert.True(nested.NestedCollection.All(x => x.DoB == default)); + } + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual async Task Project_top_level_entity_with_null_value_required_scalars(bool async) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateDbContext(); + + var query = context.Set().Where(x => x.Id == 4).Select(x => new + { + x.Id, + x.RequiredReference, + }).AsNoTracking(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + var nullScalars = result.Single(); + + Assert.Equal(default, nullScalars.RequiredReference.Number); + } + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual async Task Project_root_entity_with_missing_required_navigation(bool async) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateDbContext(); + + var query = context.Set().Where(x => x.Id == 5).AsNoTracking(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + var missingRequiredNav = result.Single(); + + Assert.Equal(default, missingRequiredNav.RequiredReference.NestedRequiredReference); + Assert.Equal(default, missingRequiredNav.OptionalReference.NestedRequiredReference); + Assert.True(missingRequiredNav.Collection.All(x => x.NestedRequiredReference == default)); + } + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual async Task Project_missing_required_navigation(bool async) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateDbContext(); + + var query = context.Set().Where(x => x.Id == 5).Select(x => x.RequiredReference.NestedRequiredReference) + .AsNoTracking(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + var missingRequiredNav = result.Single(); + + Assert.Equal(default, missingRequiredNav); + } + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual async Task Project_root_entity_with_null_required_navigation(bool async) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateDbContext(); + + var query = context.Set().Where(x => x.Id == 6).AsNoTracking(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + var nullRequiredNav = result.Single(); + + Assert.Equal(default, nullRequiredNav.RequiredReference.NestedRequiredReference); + Assert.Equal(default, nullRequiredNav.OptionalReference.NestedRequiredReference); + Assert.True(nullRequiredNav.Collection.All(x => x.NestedRequiredReference == default)); + } + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual async Task Project_null_required_navigation(bool async) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateDbContext(); + + var query = context.Set().Where(x => x.Id == 6).Select(x => x.RequiredReference).AsNoTracking(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + var nullRequiredNav = result.Single(); + + Assert.Equal(default, nullRequiredNav.NestedRequiredReference); + } + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual async Task Project_missing_required_scalar(bool async) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateDbContext(); + + var query = context.Set() + .Where(x => x.Id == 2) + .Select(x => new { x.Id, Number = (double?)x.RequiredReference.Number }); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Null(result.Single().Number); + } + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual async Task Project_null_required_scalar(bool async) + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating21006, + seed: Seed21006); + + await using var context = contextFactory.CreateDbContext(); + + var query = context.Set() + .Where(x => x.Id == 4) + .Select(x => new + { + x.Id, + Number = (double?)x.RequiredReference.Number, + }); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Null(result.Single().Number); + } + + protected virtual void OnModelCreating21006(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.Property(x => x.Id).ValueGeneratedNever(); + b.OwnsOne( + x => x.OptionalReference, bb => + { + bb.OwnsOne(x => x.NestedOptionalReference); + bb.OwnsOne(x => x.NestedRequiredReference); + bb.Navigation(x => x.NestedRequiredReference).IsRequired(); + bb.OwnsMany(x => x.NestedCollection); + }); + b.OwnsOne( + x => x.RequiredReference, bb => + { + bb.OwnsOne(x => x.NestedOptionalReference); + bb.OwnsOne(x => x.NestedRequiredReference); + bb.Navigation(x => x.NestedRequiredReference).IsRequired(); + bb.OwnsMany(x => x.NestedCollection); + }); + b.Navigation(x => x.RequiredReference).IsRequired(); + b.OwnsMany( + x => x.Collection, bb => + { + bb.OwnsOne(x => x.NestedOptionalReference); + bb.OwnsOne(x => x.NestedRequiredReference); + bb.Navigation(x => x.NestedRequiredReference).IsRequired(); + bb.OwnsMany(x => x.NestedCollection); + }); + }); + + protected virtual async Task Seed21006(Context21006 context) + { + // everything + var e1 = new Context21006.Entity + { + Id = 1, + Name = "e1", + OptionalReference = new Context21006.JsonEntity + { + Number = 7, + Text = "e1 or", + NestedOptionalReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 or nor" }, + NestedRequiredReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 or nrr" }, + NestedCollection = + [ + new() { DoB = new DateTime(2000, 1, 1), Text = "e1 or c1" }, + new() { DoB = new DateTime(2000, 1, 1), Text = "e1 or c2" } + ] + }, + RequiredReference = new Context21006.JsonEntity + { + Number = 7, + Text = "e1 rr", + NestedOptionalReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 rr nor" }, + NestedRequiredReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 rr nrr" }, + NestedCollection = + [ + new() { DoB = new DateTime(2000, 1, 1), Text = "e1 rr c1" }, + new() { DoB = new DateTime(2000, 1, 1), Text = "e1 rr c2" } + ] + }, + Collection = + [ + new() + { + Number = 7, + Text = "e1 c1", + NestedOptionalReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 c1 nor" }, + NestedRequiredReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 c1 nrr" }, + NestedCollection = + [ + new() { DoB = new DateTime(2000, 1, 1), Text = "e1 c1 c1" }, + new() { DoB = new DateTime(2000, 1, 1), Text = "e1 c1 c2" } + ] + }, + + new() + { + Number = 7, + Text = "e1 c2", + NestedOptionalReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 c2 nor" }, + NestedRequiredReference = new Context21006.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "e1 c2 nrr" }, + NestedCollection = + [ + new() { DoB = new DateTime(2000, 1, 1), Text = "e1 c2 c1" }, + new() { DoB = new DateTime(2000, 1, 1), Text = "e1 c2 c2" } + ] + } + ] + }; + + context.Add(e1); + await context.SaveChangesAsync(); + } + + protected class Context21006(DbContextOptions options) : DbContext(options) + { + public DbSet Entities { get; set; } + + public class Entity + { + public int Id { get; set; } + public string Name { get; set; } + public JsonEntity OptionalReference { get; set; } + public JsonEntity RequiredReference { get; set; } + public List Collection { get; set; } + } + + public class JsonEntity + { + public string Text { get; set; } + public double Number { get; set; } + + public JsonEntityNested NestedOptionalReference { get; set; } + public JsonEntityNested NestedRequiredReference { get; set; } + public List NestedCollection { get; set; } + } + + public class JsonEntityNested + { + public DateTime DoB { get; set; } + public string Text { get; set; } + } + } + + #endregion + + #region 29219 + + [Fact] + public virtual async Task Optional_json_properties_materialized_as_null_when_the_element_in_json_is_not_present() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreating29219, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: Seed29219); + + using var context = contextFactory.CreateDbContext(); + var query = context.Set().Where(x => x.Id == 3); + var result = await query.SingleAsync(); + + Assert.Equal(3, result.Id); + Assert.Null(result.Reference.NullableScalar); + Assert.Null(result.Collection[0].NullableScalar); + } + + [Fact] + public virtual async Task Can_project_nullable_json_property_when_the_element_in_json_is_not_present() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreating29219, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: Seed29219); + + using var context = contextFactory.CreateDbContext(); + + var query = context.Set().OrderBy(x => x.Id).Select(x => x.Reference.NullableScalar); + var result = await query.ToListAsync(); + + Assert.Equal(3, result.Count); + Assert.Equal(11, result[0]); + Assert.Null(result[1]); + Assert.Null(result[2]); + } + + protected virtual void OnModelCreating29219(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.Property(x => x.Id).ValueGeneratedNever(); + b.OwnsOne(x => x.Reference); + b.OwnsMany(x => x.Collection); + }); + + protected virtual async Task Seed29219(DbContext ctx) + { + var entity1 = new Context29219.MyEntity + { + Id = 1, + Reference = new Context29219.MyJsonEntity { NonNullableScalar = 10, NullableScalar = 11 }, + Collection = + [ + new Context29219.MyJsonEntity { NonNullableScalar = 100, NullableScalar = 101 }, + new Context29219.MyJsonEntity { NonNullableScalar = 200, NullableScalar = 201 }, + new Context29219.MyJsonEntity { NonNullableScalar = 300, NullableScalar = null } + ] + }; + + var entity2 = new Context29219.MyEntity + { + Id = 2, + Reference = new Context29219.MyJsonEntity { NonNullableScalar = 20, NullableScalar = null }, + Collection = [new Context29219.MyJsonEntity { NonNullableScalar = 1001, NullableScalar = null }] + }; + + ctx.AddRange(entity1, entity2); + await ctx.SaveChangesAsync(); + } + + protected class Context29219(DbContextOptions options) : DbContext(options) + { + public class MyEntity + { + public int Id { get; set; } + public MyJsonEntity Reference { get; set; } + public List Collection { get; set; } + } + + public class MyJsonEntity + { + public int NonNullableScalar { get; set; } + public int? NullableScalar { get; set; } + } + } + + #endregion + + #region 30028 + + [Fact] + public virtual async Task Accessing_missing_navigation_works() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreating30028, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: Seed30028); + + using var context = contextFactory.CreateDbContext(); + var result = await context.Set().OrderBy(x => x.Id).ToListAsync(); + Assert.Equal(4, result.Count); + Assert.NotNull(result[0].Json.Collection); + Assert.NotNull(result[0].Json.OptionalReference); + Assert.NotNull(result[0].Json.RequiredReference); + + Assert.Null(result[1].Json.Collection); + Assert.NotNull(result[1].Json.OptionalReference); + Assert.NotNull(result[1].Json.RequiredReference); + + Assert.NotNull(result[2].Json.Collection); + Assert.Null(result[2].Json.OptionalReference); + Assert.NotNull(result[2].Json.RequiredReference); + + Assert.NotNull(result[3].Json.Collection); + Assert.NotNull(result[3].Json.OptionalReference); + Assert.Null(result[3].Json.RequiredReference); + } + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual async Task Missing_navigation_works_with_deduplication(bool async) + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreating30028, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: Seed30028); + + using var context = contextFactory.CreateDbContext(); + var queryable = context.Set().OrderBy(x => x.Id).Select(x => new + { + x, + x.Json, + x.Json.OptionalReference, + x.Json.RequiredReference, + NestedOptional = x.Json.OptionalReference.Nested, + NestedRequired = x.Json.RequiredReference.Nested, + x.Json.Collection, + }).AsNoTracking(); + + var result = async ? await queryable.ToListAsync() : queryable.ToList(); + + Assert.Equal(4, result.Count); + Assert.NotNull(result[0].OptionalReference); + Assert.NotNull(result[0].RequiredReference); + Assert.NotNull(result[0].NestedOptional); + Assert.NotNull(result[0].NestedRequired); + Assert.NotNull(result[0].Collection); + + Assert.NotNull(result[1].OptionalReference); + Assert.NotNull(result[1].RequiredReference); + Assert.NotNull(result[1].NestedOptional); + Assert.NotNull(result[1].NestedRequired); + Assert.Null(result[1].Collection); + + Assert.Null(result[2].OptionalReference); + Assert.NotNull(result[2].RequiredReference); + Assert.Null(result[2].NestedOptional); + Assert.NotNull(result[2].NestedRequired); + Assert.NotNull(result[2].Collection); + + Assert.NotNull(result[3].OptionalReference); + Assert.Null(result[3].RequiredReference); + Assert.NotNull(result[3].NestedOptional); + Assert.Null(result[3].NestedRequired); + Assert.NotNull(result[3].Collection); + } + + protected virtual void OnModelCreating30028(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.Property(x => x.Id).ValueGeneratedNever(); + b.OwnsOne( + x => x.Json, nb => + { + nb.OwnsMany(x => x.Collection, nnb => nnb.OwnsOne(x => x.Nested)); + nb.OwnsOne(x => x.OptionalReference, nnb => nnb.OwnsOne(x => x.Nested)); + nb.OwnsOne(x => x.RequiredReference, nnb => nnb.OwnsOne(x => x.Nested)); + nb.Navigation(x => x.RequiredReference).IsRequired(); + }); + }); + + protected abstract Task Seed30028(DbContext ctx); + + protected class Context30028(DbContextOptions options) : DbContext(options) + { + public class MyEntity + { + public int Id { get; set; } + public MyJsonRootEntity Json { get; set; } + } + + public class MyJsonRootEntity + { + public string RootName { get; set; } + public MyJsonBranchEntity RequiredReference { get; set; } + public MyJsonBranchEntity OptionalReference { get; set; } + public List Collection { get; set; } + } + + public class MyJsonBranchEntity + { + public string BranchName { get; set; } + public MyJsonLeafEntity Nested { get; set; } + } + + public class MyJsonLeafEntity + { + public string LeafName { get; set; } + } + } + + #endregion + + #region 32310 + + [Fact] + public virtual async Task Contains_on_nested_collection_with_init_only_navigation() + { + var contextFactory = await InitializeNonSharedTest( + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + onModelCreating: OnModelCreating32310, + seed: Seed32310); + + await using var context = contextFactory.CreateDbContext(); + + var query = context.Set() + .Where(u => u.Visits.DaysVisited.Contains(new DateOnly(2023, 1, 1))); + + var result = await query.FirstOrDefaultAsync(); + + Assert.Equal("FBI", result.Name); + Assert.Equal(new DateOnly(2023, 1, 1), result.Visits.DaysVisited.Single()); + } + + protected virtual void OnModelCreating32310(ModelBuilder modelBuilder) + => modelBuilder.Entity().OwnsOne(e => e.Visits); + + protected virtual async Task Seed32310(DbContext context) + { + var user = new Context32310.Pub + { + Name = "FBI", + Visits = new Context32310.Visits { LocationTag = "tag", DaysVisited = [new DateOnly(2023, 1, 1)] } + }; + + context.Add(user); + await context.SaveChangesAsync(); + } + + protected class Context32310(DbContextOptions options) : DbContext(options) + { + public class Pub + { + public int Id { get; set; } + public required string Name { get; set; } + public Visits Visits { get; set; } = null!; + } + + public class Visits + { + public string LocationTag { get; set; } + public required List DaysVisited { get; init; } + } + } + + #endregion + + #region 32939 + + [Fact] + public virtual async Task Project_json_with_no_properties() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreating32939, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: Seed32939); + + using var context = contextFactory.CreateDbContext(); + await context.Set().ToListAsync(); + } + + protected virtual void OnModelCreating32939(ModelBuilder modelBuilder) + { + modelBuilder.Entity().Property(x => x.Id).ValueGeneratedNever(); + modelBuilder.Entity().OwnsOne(x => x.Empty); + modelBuilder.Entity().OwnsOne(x => x.FieldOnly); + } + + protected Task Seed32939(DbContext ctx) + { + var entity = new Context32939.Entity { Empty = new Context32939.JsonEmpty(), FieldOnly = new Context32939.JsonFieldOnly() }; + + ctx.Add(entity); + return ctx.SaveChangesAsync(); + } + + protected class Context32939(DbContextOptions options) : DbContext(options) + { + public class Entity + { + public int Id { get; set; } + public JsonEmpty Empty { get; set; } + public JsonFieldOnly FieldOnly { get; set; } + } + + public class JsonEmpty + { + } + + public class JsonFieldOnly + { + public int Field; + } + } + + #endregion + + #region 33046 + + [Fact] + public virtual async Task Query_with_nested_json_collection_mapped_to_private_field_via_IReadOnlyList() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreating33046, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: Seed33046); + + using var context = contextFactory.CreateDbContext(); + var query = await context.Set().ToListAsync(); + Assert.Equal(1, query.Count); + } + + protected virtual void OnModelCreating33046(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.Property(x => x.Id).ValueGeneratedNever(); + b.OwnsMany( + x => x.Rounds, ownedBuilder => ownedBuilder.OwnsMany(r => r.SubRounds)); + }); + + protected abstract Task Seed33046(DbContext ctx); + + protected class Context33046(DbContextOptions options) : DbContext(options) + { + public class Review + { + public int Id { get; set; } + +#pragma warning disable IDE0044 // Add readonly modifier + private List _rounds = []; +#pragma warning restore IDE0044 // Add readonly modifier + public IReadOnlyList Rounds + => _rounds.AsReadOnly(); + } + + public class ReviewRound + { + public int RoundNumber { get; set; } + +#pragma warning disable IDE0044 // Add readonly modifier + private readonly List _subRounds = []; +#pragma warning restore IDE0044 // Add readonly modifier + public IReadOnlyList SubRounds + => _subRounds.AsReadOnly(); + } + + public class SubRound + { + public int SubRoundNumber { get; set; } + } + } + + #endregion + + #region 34960 + + [Fact] + public virtual async Task Project_entity_with_json_null_values() + { + var contextFactory = await InitializeNonSharedTest( + seed: Seed34960, onModelCreating: OnModelCreating34960, onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings)); + + using var context = contextFactory.CreateDbContext(); + var query = await context.Entities.ToListAsync(); + } + + [Fact] + public virtual async Task Try_project_collection_but_JSON_is_entity() + { + var contextFactory = await InitializeNonSharedTest( + seed: Seed34960, onModelCreating: OnModelCreating34960, onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings)); + using var context = contextFactory.CreateDbContext(); + + await context.Junk.AsNoTracking().Where(x => x.Id == 1).Select(x => x.Collection).FirstOrDefaultAsync(); + } + + [Fact] + public virtual async Task Try_project_reference_but_JSON_is_collection() + { + var contextFactory = await InitializeNonSharedTest( + seed: Seed34960, onModelCreating: OnModelCreating34960, onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings)); + using var context = contextFactory.CreateDbContext(); + + await context.Junk.AsNoTracking().Where(x => x.Id == 2).Select(x => x.Reference).FirstOrDefaultAsync(); + } + + protected class Context34960(DbContextOptions options) : DbContext(options) + { + public DbSet Entities { get; set; } + public DbSet Junk { get; set; } + + public class Entity + { + public int Id { get; set; } + public JsonEntity Reference { get; set; } + public List Collection { get; set; } + } + + public class JsonEntity + { + public string Name { get; set; } + public double Number { get; set; } + + public JsonEntityNested NestedReference { get; set; } + public List NestedCollection { get; set; } + } + + public class JsonEntityNested + { + public DateTime DoB { get; set; } + public string Text { get; set; } + } + + public class JunkEntity + { + public int Id { get; set; } + public JsonEntity Reference { get; set; } + public List Collection { get; set; } + } + } + + protected virtual void OnModelCreating34960(ModelBuilder modelBuilder) + { + modelBuilder.Entity(b => + { + b.Property(x => x.Id).ValueGeneratedNever(); + + b.OwnsOne( + x => x.Reference, b => + { + b.OwnsOne(x => x.NestedReference); + b.OwnsMany(x => x.NestedCollection); + }); + + b.OwnsMany( + x => x.Collection, b => + { + b.OwnsOne(x => x.NestedReference); + b.OwnsMany(x => x.NestedCollection); + }); + }); + + modelBuilder.Entity(b => + { + b.Property(x => x.Id).ValueGeneratedNever(); + + b.OwnsOne( + x => x.Reference, b => + { + b.Ignore(x => x.NestedReference); + b.Ignore(x => x.NestedCollection); + }); + + b.OwnsMany( + x => x.Collection, b => + { + b.Ignore(x => x.NestedReference); + b.Ignore(x => x.NestedCollection); + }); + }); + } + + protected virtual async Task Seed34960(Context34960 ctx) + { + // everything + var e1 = new Context34960.Entity + { + Id = 1, + Reference = new Context34960.JsonEntity + { + Name = "ref1", + Number = 1.5f, + NestedReference = new Context34960.JsonEntityNested { DoB = new DateTime(2000, 1, 1), Text = "nested ref 1" }, + NestedCollection = + [ + new Context34960.JsonEntityNested { DoB = new DateTime(2001, 1, 1), Text = "nested col 1 1" }, + new Context34960.JsonEntityNested { DoB = new DateTime(2001, 2, 2), Text = "nested col 1 2" }, + ], + }, + Collection = + [ + new Context34960.JsonEntity + { + Name = "col 1 1", + Number = 2.5f, + NestedReference = new Context34960.JsonEntityNested { DoB = new DateTime(2010, 1, 1), Text = "nested col 1 1 ref 1" }, + NestedCollection = + [ + new Context34960.JsonEntityNested { DoB = new DateTime(2011, 1, 1), Text = "nested col 1 1 col 1 1" }, + new Context34960.JsonEntityNested { DoB = new DateTime(2011, 2, 2), Text = "nested col 1 1 col 1 2" }, + ], + }, + new Context34960.JsonEntity + { + Name = "col 1 2", + Number = 2.5f, + NestedReference = new Context34960.JsonEntityNested { DoB = new DateTime(2020, 1, 1), Text = "nested col 1 2 ref 1" }, + NestedCollection = + [ + new Context34960.JsonEntityNested { DoB = new DateTime(2021, 1, 1), Text = "nested col 1 2 col 1 1" }, + new Context34960.JsonEntityNested { DoB = new DateTime(2021, 2, 2), Text = "nested col 1 2 col 1 2" }, + ], + }, + ], + }; + + // relational nulls + var e2 = new Context34960.Entity + { + Id = 2, + Reference = null, + Collection = null + }; + + // nested relational nulls + var e3 = new Context34960.Entity + { + Id = 3, + Reference = new Context34960.JsonEntity + { + Name = "ref3", + Number = 3.5f, + NestedReference = null, + NestedCollection = null + }, + Collection = + [ + new Context34960.JsonEntity + { + Name = "col 3 1", + Number = 32.5f, + NestedReference = null, + NestedCollection = null, + }, + new Context34960.JsonEntity + { + Name = "col 3 2", + Number = 33.5f, + NestedReference = null, + NestedCollection = null, + }, + ], + }; + + ctx.Entities.AddRange(e1, e2, e3); + await ctx.SaveChangesAsync(); + } + + #endregion + + #region ArrayOfPrimitives + + [Fact] + public virtual async Task Project_json_array_of_primitives_on_reference() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingArrayOfPrimitives, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedArrayOfPrimitives); + + using var context = contextFactory.CreateDbContext(); + var query = context.Set().OrderBy(x => x.Id) + .Select(x => new { x.Reference.IntArray, x.Reference.ListOfString }); + + var result = await query.ToListAsync(); + + Assert.Equal(2, result.Count); + Assert.Equal(3, result[0].IntArray.Length); + Assert.Equal(3, result[0].ListOfString.Count); + Assert.Equal(3, result[1].IntArray.Length); + Assert.Equal(3, result[1].ListOfString.Count); + } + + [Fact(Skip = "Issue #32611")] + public virtual async Task Project_json_array_of_primitives_on_collection() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingArrayOfPrimitives, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedArrayOfPrimitives); + + using var context = contextFactory.CreateDbContext(); + var query = context.Set().OrderBy(x => x.Id) + .Select(x => new { x.Collection[0].IntArray, x.Collection[1].ListOfString }); + + var result = await query.ToListAsync(); + + Assert.Equal(2, result.Count); + Assert.Equal(3, result[0].IntArray.Length); + Assert.Equal(2, result[0].ListOfString.Count); + Assert.Equal(3, result[1].IntArray.Length); + Assert.Equal(2, result[1].ListOfString.Count); + } + + [Fact] + public virtual async Task Project_element_of_json_array_of_primitives() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingArrayOfPrimitives, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedArrayOfPrimitives); + + using var context = contextFactory.CreateDbContext(); + var query = context.Set().OrderBy(x => x.Id).Select(x + => new { ArrayElement = x.Reference.IntArray[0], ListElement = x.Reference.ListOfString[1] }); + var result = await query.ToListAsync(); + } + + [Fact] + public virtual async Task Predicate_based_on_element_of_json_array_of_primitives1() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingArrayOfPrimitives, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedArrayOfPrimitives); + + using var context = contextFactory.CreateDbContext(); + var query = context.Set().Where(x => x.Reference.IntArray[0] == 1); + var result = await query.ToListAsync(); + + Assert.Equal(1, result.Count); + Assert.Equal(1, result[0].Reference.IntArray[0]); + } + + [Fact] + public virtual async Task Predicate_based_on_element_of_json_array_of_primitives2() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingArrayOfPrimitives, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedArrayOfPrimitives); + + using var context = contextFactory.CreateDbContext(); + var query = context.Set().Where(x => x.Reference.ListOfString[1] == "Bar"); + var result = await query.ToListAsync(); + + Assert.Equal(1, result.Count); + Assert.Equal("Bar", result[0].Reference.ListOfString[1]); + } + + [Fact, MemberData(nameof(IsAsyncData))] + public virtual async Task Predicate_based_on_element_of_json_array_of_primitives3() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingArrayOfPrimitives, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedArrayOfPrimitives); + + using var context = contextFactory.CreateDbContext(); + var query = context.Set() + .Where(x => x.Reference.IntArray.AsQueryable().ElementAt(0) == 1 + || x.Reference.ListOfString.AsQueryable().ElementAt(1) == "Bar") + .OrderBy(e => e.Id); + var result = await query.ToListAsync(); + + Assert.Equal(1, result.Count); + Assert.Equal(1, result[0].Reference.IntArray[0]); + Assert.Equal("Bar", result[0].Reference.ListOfString[1]); + } + + protected Task SeedArrayOfPrimitives(DbContext ctx) + { + var entity1 = new ContextArrayOfPrimitives.MyEntity + { + Id = 1, + Reference = new ContextArrayOfPrimitives.MyJsonEntity + { + IntArray = [1, 2, 3], + ListOfString = + [ + "Foo", + "Bar", + "Baz" + ] + }, + Collection = + [ + new ContextArrayOfPrimitives.MyJsonEntity { IntArray = [111, 112, 113], ListOfString = ["Foo11", "Bar11"] }, + new ContextArrayOfPrimitives.MyJsonEntity { IntArray = [211, 212, 213], ListOfString = ["Foo12", "Bar12"] } + ] + }; + + var entity2 = new ContextArrayOfPrimitives.MyEntity + { + Id = 2, + Reference = new ContextArrayOfPrimitives.MyJsonEntity + { + IntArray = [10, 20, 30], + ListOfString = + [ + "A", + "B", + "C" + ] + }, + Collection = + [ + new ContextArrayOfPrimitives.MyJsonEntity { IntArray = [110, 120, 130], ListOfString = ["A1", "Z1"] }, + new ContextArrayOfPrimitives.MyJsonEntity { IntArray = [210, 220, 230], ListOfString = ["A2", "Z2"] } + ] + }; + + ctx.Set().AddRange(entity1, entity2); + + return ctx.SaveChangesAsync(); + } + + protected virtual void OnModelCreatingArrayOfPrimitives(ModelBuilder modelBuilder) + { + modelBuilder.Entity().Property(x => x.Id).ValueGeneratedNever(); + modelBuilder.Entity().OwnsOne(x => x.Reference); + modelBuilder.Entity().OwnsMany(x => x.Collection); + } + + protected class ContextArrayOfPrimitives(DbContextOptions options) : DbContext(options) + { + public class MyEntity + { + public int Id { get; set; } + public MyJsonEntity Reference { get; set; } + public List Collection { get; set; } + } + + public class MyJsonEntity + { + public int[] IntArray { get; set; } + public List ListOfString { get; set; } + } + } + + #endregion + + #region JunkInJson + + [Fact] + public virtual async Task Junk_in_json_basic_tracking() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingJunkInJson, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedJunkInJson); + + using var context = contextFactory.CreateDbContext(); + var query = context.Set(); + var result = await query.ToListAsync(); + + Assert.Equal(1, result.Count); + Assert.Equal(2, result[0].Collection.Count); + Assert.Equal(2, result[0].CollectionWithCtor.Count); + Assert.Equal(2, result[0].Reference.NestedCollection.Count); + Assert.NotNull(result[0].Reference.NestedReference); + Assert.Equal(2, result[0].ReferenceWithCtor.NestedCollection.Count); + Assert.NotNull(result[0].ReferenceWithCtor.NestedReference); + } + + [Fact] + public virtual async Task Junk_in_json_basic_no_tracking() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingJunkInJson, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedJunkInJson); + + using var context = contextFactory.CreateDbContext(); + var query = context.Set().AsNoTracking(); + var result = await query.ToListAsync(); + + Assert.Equal(1, result.Count); + Assert.Equal(2, result[0].Collection.Count); + Assert.Equal(2, result[0].CollectionWithCtor.Count); + Assert.Equal(2, result[0].Reference.NestedCollection.Count); + Assert.NotNull(result[0].Reference.NestedReference); + Assert.Equal(2, result[0].ReferenceWithCtor.NestedCollection.Count); + Assert.NotNull(result[0].ReferenceWithCtor.NestedReference); + } + + protected abstract Task SeedJunkInJson(DbContext ctx); + + protected virtual void OnModelCreatingJunkInJson(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.Property(x => x.Id).ValueGeneratedNever(); + + b.OwnsOne( + x => x.Reference, b => + { + b.OwnsOne(x => x.NestedReference); + b.OwnsMany(x => x.NestedCollection); + }); + + b.OwnsOne( + x => x.ReferenceWithCtor, b => + { + b.OwnsOne(x => x.NestedReference); + b.OwnsMany(x => x.NestedCollection); + }); + + b.OwnsMany( + x => x.Collection, b => + { + b.OwnsOne(x => x.NestedReference); + b.OwnsMany(x => x.NestedCollection); + }); + + b.OwnsMany( + x => x.CollectionWithCtor, b => + { + b.OwnsOne(x => x.NestedReference); + b.OwnsMany(x => x.NestedCollection); + }); + }); + + protected class ContextJunkInJson(DbContextOptions options) : DbContext(options) + { + public class MyEntity + { + public int Id { get; set; } + public MyJsonEntity Reference { get; set; } + public MyJsonEntityWithCtor ReferenceWithCtor { get; set; } + public List Collection { get; set; } + public List CollectionWithCtor { get; set; } + } + + public class MyJsonEntity + { + public string Name { get; set; } + public double Number { get; set; } + + public MyJsonEntityNested NestedReference { get; set; } + public List NestedCollection { get; set; } + } + + public class MyJsonEntityNested + { + public DateTime DoB { get; set; } + } + + public class MyJsonEntityWithCtor(bool myBool, string name) + { + public bool MyBool { get; set; } = myBool; + public string Name { get; set; } = name; + + public MyJsonEntityWithCtorNested NestedReference { get; set; } + public List NestedCollection { get; set; } + } + + public class MyJsonEntityWithCtorNested(DateTime doB) + { + public DateTime DoB { get; set; } = doB; + } + } + + #endregion + + #region TrickyBuffering + + [Fact] + public virtual async Task Tricky_buffering_basic() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingTrickyBuffering, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedTrickyBuffering); + + using var context = contextFactory.CreateDbContext(); + var query = context.Set(); + var result = await query.ToListAsync(); + + Assert.Equal(1, result.Count); + Assert.Equal("r1", result[0].Reference.Name); + Assert.Equal(7, result[0].Reference.Number); + Assert.Equal(new DateTime(2000, 1, 1), result[0].Reference.NestedReference.DoB); + Assert.Equal(2, result[0].Reference.NestedCollection.Count); + } + + protected abstract Task SeedTrickyBuffering(DbContext ctx); + + protected virtual void OnModelCreatingTrickyBuffering(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.Property(x => x.Id).ValueGeneratedNever(); + b.OwnsOne( + x => x.Reference, b => + { + b.OwnsOne(x => x.NestedReference); + b.OwnsMany(x => x.NestedCollection); + }); + }); + + protected class ContextTrickyBuffering(DbContextOptions options) : DbContext(options) + { + public class MyEntity + { + public int Id { get; set; } + public MyJsonEntity Reference { get; set; } + } + + public class MyJsonEntity + { + public string Name { get; set; } + public int Number { get; set; } + public MyJsonEntityNested NestedReference { get; set; } + public List NestedCollection { get; set; } + } + + public class MyJsonEntityNested + { + public DateTime DoB { get; set; } + } + } + + #endregion + + #region ShadowProperties + + [Fact] + public virtual async Task Shadow_properties_basic_tracking() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingShadowProperties, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedShadowProperties); + + using var context = contextFactory.CreateDbContext(); + var query = context.Set(); + var result = await query.ToListAsync(); + + Assert.Equal(1, result.Count); + Assert.Equal(2, result[0].Collection.Count); + Assert.Equal(2, result[0].CollectionWithCtor.Count); + Assert.NotNull(result[0].Reference); + Assert.NotNull(result[0].ReferenceWithCtor); + + var referenceEntry = context.ChangeTracker.Entries().Single(x => x.Entity == result[0].Reference); + Assert.Equal("Foo", referenceEntry.Property("ShadowString").CurrentValue); + + var referenceCtorEntry = context.ChangeTracker.Entries().Single(x => x.Entity == result[0].ReferenceWithCtor); + Assert.Equal(143, referenceCtorEntry.Property("Shadow_Int").CurrentValue); + + var collectionEntry1 = context.ChangeTracker.Entries().Single(x => x.Entity == result[0].Collection[0]); + var collectionEntry2 = context.ChangeTracker.Entries().Single(x => x.Entity == result[0].Collection[1]); + Assert.Equal(5.5, collectionEntry1.Property("ShadowDouble").CurrentValue); + Assert.Equal(20.5, collectionEntry2.Property("ShadowDouble").CurrentValue); + + var collectionCtorEntry1 = context.ChangeTracker.Entries().Single(x => x.Entity == result[0].CollectionWithCtor[0]); + var collectionCtorEntry2 = context.ChangeTracker.Entries().Single(x => x.Entity == result[0].CollectionWithCtor[1]); + Assert.Equal((byte)6, collectionCtorEntry1.Property("ShadowNullableByte").CurrentValue); + Assert.Null(collectionCtorEntry2.Property("ShadowNullableByte").CurrentValue); + } + + [Fact] + public virtual async Task Shadow_properties_basic_no_tracking() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingShadowProperties, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedShadowProperties); + + using var context = contextFactory.CreateDbContext(); + var query = context.Set().AsNoTracking(); + var result = await query.ToListAsync(); + + Assert.Equal(1, result.Count); + Assert.Equal(2, result[0].Collection.Count); + Assert.Equal(2, result[0].CollectionWithCtor.Count); + Assert.NotNull(result[0].Reference); + Assert.NotNull(result[0].ReferenceWithCtor); + } + + [Fact] + public virtual async Task Project_shadow_properties_from_json_entity() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingShadowProperties, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedShadowProperties); + + using var context = contextFactory.CreateDbContext(); + var query = context.Set().Select(x => new + { + ShadowString = EF.Property(x.Reference, "ShadowString"), + ShadowInt = EF.Property(x.ReferenceWithCtor, "Shadow_Int"), + }); + + var result = await query.ToListAsync(); + + Assert.Equal(1, result.Count); + Assert.Equal("Foo", result[0].ShadowString); + Assert.Equal(143, result[0].ShadowInt); + } + + protected abstract Task SeedShadowProperties(DbContext ctx); + + protected virtual void OnModelCreatingShadowProperties(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.Property(x => x.Id).ValueGeneratedNever(); + + b.OwnsOne( + x => x.Reference, b => b.Property("ShadowString")); + + b.OwnsOne( + x => x.ReferenceWithCtor, b => b.Property("Shadow_Int")); + + b.OwnsMany( + x => x.Collection, b => b.Property("ShadowDouble")); + + b.OwnsMany( + x => x.CollectionWithCtor, b => b.Property("ShadowNullableByte")); + }); + + protected class ContextShadowProperties(DbContextOptions options) : DbContext(options) + { + public class MyEntity + { + public int Id { get; set; } + public string Name { get; set; } + + public MyJsonEntity Reference { get; set; } + public List Collection { get; set; } + public MyJsonEntityWithCtor ReferenceWithCtor { get; set; } + public List CollectionWithCtor { get; set; } + } + + public class MyJsonEntity + { + public string Name { get; set; } + } + + public class MyJsonEntityWithCtor(string name) + { + public string Name { get; set; } = name; + } + } + + #endregion + + #region LazyLoadingProxies + + [Fact] + public virtual async Task Project_proxies_entity_with_json() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingLazyLoadingProxies, + seed: SeedLazyLoadingProxies, + onConfiguring: b => + { + b = b.ConfigureWarnings(ConfigureWarnings); + OnConfiguringLazyLoadingProxies(b); + }, + addServices: AddServicesLazyLoadingProxies); + + using var context = contextFactory.CreateDbContext(); + var query = context.Set(); + var result = await query.ToListAsync(); + + Assert.Equal(2, result.Count); + } + + [Fact] // Issue #38466 + public virtual async Task Project_proxies_entity_with_json_with_primitive_collection() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingLazyLoadingProxies, + seed: SeedLazyLoadingProxies, + onConfiguring: b => + { + b = b.ConfigureWarnings(ConfigureWarnings); + OnConfiguringLazyLoadingProxies(b); + }, + addServices: AddServicesLazyLoadingProxies); + + using var context = contextFactory.CreateDbContext(); + var result = await context.Set().OrderBy(x => x.Id).ToListAsync(); + + Assert.Equal(2, result.Count); + + var e1Collection = result[0].Collection.OrderBy(x => x.Number).ToList(); + Assert.Equal(3, e1Collection.Count); + Assert.Equal(new long[] { 110, 111 }, e1Collection[0].Ints); + Assert.Equal(new long[] { 120, 121, 122 }, e1Collection[1].Ints); + Assert.Empty(e1Collection[2].Ints); + + var e2Collection = result[1].Collection.OrderBy(x => x.Number).ToList(); + Assert.Equal(2, e2Collection.Count); + Assert.Equal(new long[] { 210 }, e2Collection[0].Ints); + Assert.Equal(new long[] { 220, 221 }, e2Collection[1].Ints); + } + + protected void OnConfiguringLazyLoadingProxies(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder.UseLazyLoadingProxies(); + + protected IServiceCollection AddServicesLazyLoadingProxies(IServiceCollection addServices) + => addServices.AddEntityFrameworkProxies(); + + private Task SeedLazyLoadingProxies(DbContext ctx) + { + var r1 = new ContextLazyLoadingProxies.MyJsonEntityWithCtor("r1", 1); + var c11 = new ContextLazyLoadingProxies.MyJsonEntity + { + Name = "c11", + Number = 11, + Ints = [110, 111] + }; + var c12 = new ContextLazyLoadingProxies.MyJsonEntity + { + Name = "c12", + Number = 12, + Ints = [120, 121, 122] + }; + var c13 = new ContextLazyLoadingProxies.MyJsonEntity + { + Name = "c13", + Number = 13, + Ints = [] + }; + + var r2 = new ContextLazyLoadingProxies.MyJsonEntityWithCtor("r2", 2); + var c21 = new ContextLazyLoadingProxies.MyJsonEntity + { + Name = "c21", + Number = 21, + Ints = [210] + }; + var c22 = new ContextLazyLoadingProxies.MyJsonEntity + { + Name = "c22", + Number = 22, + Ints = [220, 221] + }; + + var e1 = new ContextLazyLoadingProxies.MyEntity + { + Id = 1, + Name = "e1", + Reference = r1, + Collection = + [ + c11, + c12, + c13 + ] + }; + + var e2 = new ContextLazyLoadingProxies.MyEntity + { + Id = 2, + Name = "e2", + Reference = r2, + Collection = [c21, c22] + }; + + ctx.Set().AddRange(e1, e2); + return ctx.SaveChangesAsync(); + } + + protected virtual void OnModelCreatingLazyLoadingProxies(ModelBuilder modelBuilder) + { + modelBuilder.Entity().Property(x => x.Id).ValueGeneratedNever(); + modelBuilder.Entity().OwnsOne(x => x.Reference); + modelBuilder.Entity().OwnsMany(x => x.Collection); + } + + public class ContextLazyLoadingProxies(DbContextOptions options) : DbContext(options) + { + public class MyEntity + { + public int Id { get; set; } + public string Name { get; set; } + + public virtual MyJsonEntityWithCtor Reference { get; set; } + public virtual List Collection { get; set; } + } + + public class MyJsonEntityWithCtor(string name, int number) + { + public string Name { get; set; } = name; + public int Number { get; set; } = number; + } + + public class MyJsonEntity + { + public string Name { get; set; } + public int Number { get; set; } + public IList Ints { get; set; } + } + } + + #endregion + + #region NotICollection + + [Fact] + public virtual async Task Not_ICollection_basic_projection() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingNotICollection, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedNotICollection); + + using var context = contextFactory.CreateDbContext(); + var query = context.Set(); + var result = await query.ToListAsync(); + + Assert.Equal(2, result.Count); + } + + protected abstract Task SeedNotICollection(DbContext ctx); + + protected virtual void OnModelCreatingNotICollection(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.Property(x => x.Id).ValueGeneratedNever(); + b.OwnsOne( + cr => cr.Json, nb => nb.OwnsMany(x => x.Collection)); + }); + + protected class ContextNotICollection(DbContextOptions options) : DbContext(options) + { + public class MyEntity + { + public int Id { get; set; } + + public MyJsonEntity Json { get; set; } + } + + public class MyJsonEntity + { + private readonly List _collection = []; + + public IEnumerable Collection + => _collection.AsReadOnly(); + } + + public class MyJsonNestedEntity + { + public string Foo { get; set; } + public int Bar { get; set; } + } + } + + #endregion + + #region BadJsonProperties + + [Theory, InlineData(true), InlineData(false)] + public virtual async Task Bad_json_properties_duplicated_navigations(bool noTracking) + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingBadJsonProperties, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedBadJsonProperties); + + using var context = contextFactory.CreateDbContext(); + var query = noTracking ? context.Entities.AsNoTracking() : context.Entities; + var baseline = await query.SingleAsync(x => x.Scenario == "baseline"); + var dupNavs = await query.SingleAsync(x => x.Scenario == "duplicated navigations"); + + // for no tracking, last one wins + Assert.Equal(baseline.RequiredReference.NestedOptional.Text + " dupnav", dupNavs.RequiredReference.NestedOptional.Text); + Assert.Equal(baseline.RequiredReference.NestedRequired.Text + " dupnav", dupNavs.RequiredReference.NestedRequired.Text); + Assert.Equal(baseline.RequiredReference.NestedCollection[0].Text + " dupnav", dupNavs.RequiredReference.NestedCollection[0].Text); + Assert.Equal(baseline.RequiredReference.NestedCollection[1].Text + " dupnav", dupNavs.RequiredReference.NestedCollection[1].Text); + + Assert.Equal(baseline.OptionalReference.NestedOptional.Text + " dupnav", dupNavs.OptionalReference.NestedOptional.Text); + Assert.Equal(baseline.OptionalReference.NestedRequired.Text + " dupnav", dupNavs.OptionalReference.NestedRequired.Text); + Assert.Equal(baseline.OptionalReference.NestedCollection[0].Text + " dupnav", dupNavs.OptionalReference.NestedCollection[0].Text); + Assert.Equal(baseline.OptionalReference.NestedCollection[1].Text + " dupnav", dupNavs.OptionalReference.NestedCollection[1].Text); + + Assert.Equal(baseline.Collection[0].NestedOptional.Text + " dupnav", dupNavs.Collection[0].NestedOptional.Text); + Assert.Equal(baseline.Collection[0].NestedRequired.Text + " dupnav", dupNavs.Collection[0].NestedRequired.Text); + Assert.Equal(baseline.Collection[0].NestedCollection[0].Text + " dupnav", dupNavs.Collection[0].NestedCollection[0].Text); + Assert.Equal(baseline.Collection[0].NestedCollection[1].Text + " dupnav", dupNavs.Collection[0].NestedCollection[1].Text); + + Assert.Equal(baseline.Collection[1].NestedOptional.Text + " dupnav", dupNavs.Collection[1].NestedOptional.Text); + Assert.Equal(baseline.Collection[1].NestedRequired.Text + " dupnav", dupNavs.Collection[1].NestedRequired.Text); + Assert.Equal(baseline.Collection[1].NestedCollection[0].Text + " dupnav", dupNavs.Collection[1].NestedCollection[0].Text); + Assert.Equal(baseline.Collection[1].NestedCollection[1].Text + " dupnav", dupNavs.Collection[1].NestedCollection[1].Text); + } + + [Theory, InlineData(true), InlineData(false)] + public virtual async Task Bad_json_properties_duplicated_scalars(bool noTracking) + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingBadJsonProperties, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedBadJsonProperties); + + using var context = contextFactory.CreateDbContext(); + var query = noTracking ? context.Entities.AsNoTracking() : context.Entities; + + var baseline = await query.SingleAsync(x => x.Scenario == "baseline"); + var dupProps = await query.SingleAsync(x => x.Scenario == "duplicated scalars"); + + Assert.Equal(baseline.RequiredReference.NestedOptional.Text + " dupprop", dupProps.RequiredReference.NestedOptional.Text); + Assert.Equal(baseline.RequiredReference.NestedRequired.Text + " dupprop", dupProps.RequiredReference.NestedRequired.Text); + Assert.Equal(baseline.RequiredReference.NestedCollection[0].Text + " dupprop", dupProps.RequiredReference.NestedCollection[0].Text); + Assert.Equal(baseline.RequiredReference.NestedCollection[1].Text + " dupprop", dupProps.RequiredReference.NestedCollection[1].Text); + + Assert.Equal(baseline.OptionalReference.NestedOptional.Text + " dupprop", dupProps.OptionalReference.NestedOptional.Text); + Assert.Equal(baseline.OptionalReference.NestedRequired.Text + " dupprop", dupProps.OptionalReference.NestedRequired.Text); + Assert.Equal(baseline.OptionalReference.NestedCollection[0].Text + " dupprop", dupProps.OptionalReference.NestedCollection[0].Text); + Assert.Equal(baseline.OptionalReference.NestedCollection[1].Text + " dupprop", dupProps.OptionalReference.NestedCollection[1].Text); + + Assert.Equal(baseline.Collection[0].NestedOptional.Text + " dupprop", dupProps.Collection[0].NestedOptional.Text); + Assert.Equal(baseline.Collection[0].NestedRequired.Text + " dupprop", dupProps.Collection[0].NestedRequired.Text); + Assert.Equal(baseline.Collection[0].NestedCollection[0].Text + " dupprop", dupProps.Collection[0].NestedCollection[0].Text); + Assert.Equal(baseline.Collection[0].NestedCollection[1].Text + " dupprop", dupProps.Collection[0].NestedCollection[1].Text); + + Assert.Equal(baseline.Collection[1].NestedOptional.Text + " dupprop", dupProps.Collection[1].NestedOptional.Text); + Assert.Equal(baseline.Collection[1].NestedRequired.Text + " dupprop", dupProps.Collection[1].NestedRequired.Text); + Assert.Equal(baseline.Collection[1].NestedCollection[0].Text + " dupprop", dupProps.Collection[1].NestedCollection[0].Text); + Assert.Equal(baseline.Collection[1].NestedCollection[1].Text + " dupprop", dupProps.Collection[1].NestedCollection[1].Text); + } + + [Theory, InlineData(true), InlineData(false)] + public virtual async Task Bad_json_properties_empty_navigations(bool noTracking) + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingBadJsonProperties, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedBadJsonProperties); + + using var context = contextFactory.CreateDbContext(); + var query = noTracking ? context.Entities.AsNoTracking() : context.Entities; + var emptyNavs = await query.SingleAsync(x => x.Scenario == "empty navigation property names"); + + Assert.Null(emptyNavs.RequiredReference.NestedOptional); + Assert.Null(emptyNavs.RequiredReference.NestedRequired); + Assert.Null(emptyNavs.RequiredReference.NestedCollection); + + Assert.Null(emptyNavs.OptionalReference.NestedOptional); + Assert.Null(emptyNavs.OptionalReference.NestedRequired); + Assert.Null(emptyNavs.OptionalReference.NestedCollection); + + Assert.Null(emptyNavs.Collection[0].NestedOptional); + Assert.Null(emptyNavs.Collection[0].NestedRequired); + Assert.Null(emptyNavs.Collection[0].NestedCollection); + + Assert.Null(emptyNavs.Collection[1].NestedOptional); + Assert.Null(emptyNavs.Collection[1].NestedRequired); + Assert.Null(emptyNavs.Collection[1].NestedCollection); + } + + [Theory, InlineData(true), InlineData(false)] + public virtual async Task Bad_json_properties_empty_scalars(bool noTracking) + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingBadJsonProperties, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedBadJsonProperties); + + using var context = contextFactory.CreateDbContext(); + var query = noTracking ? context.Entities.AsNoTracking() : context.Entities; + var emptyNavs = await query.SingleAsync(x => x.Scenario == "empty scalar property names"); + + Assert.Null(emptyNavs.RequiredReference.NestedOptional.Text); + Assert.Null(emptyNavs.RequiredReference.NestedRequired.Text); + Assert.Null(emptyNavs.RequiredReference.NestedCollection[0].Text); + Assert.Null(emptyNavs.RequiredReference.NestedCollection[1].Text); + + Assert.Null(emptyNavs.OptionalReference.NestedOptional.Text); + Assert.Null(emptyNavs.OptionalReference.NestedRequired.Text); + Assert.Null(emptyNavs.OptionalReference.NestedCollection[0].Text); + Assert.Null(emptyNavs.OptionalReference.NestedCollection[1].Text); + + Assert.Null(emptyNavs.Collection[0].NestedOptional.Text); + Assert.Null(emptyNavs.Collection[0].NestedRequired.Text); + Assert.Null(emptyNavs.Collection[0].NestedCollection[0].Text); + Assert.Null(emptyNavs.Collection[0].NestedCollection[1].Text); + + Assert.Null(emptyNavs.Collection[1].NestedOptional.Text); + Assert.Null(emptyNavs.Collection[1].NestedRequired.Text); + Assert.Null(emptyNavs.Collection[1].NestedCollection[0].Text); + Assert.Null(emptyNavs.Collection[1].NestedCollection[1].Text); + } + + [Theory, InlineData(true), InlineData(false)] + public virtual async Task Bad_json_properties_null_navigations(bool noTracking) + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingBadJsonProperties, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedBadJsonProperties); + + using var context = contextFactory.CreateDbContext(); + var query = noTracking ? context.Entities.AsNoTracking() : context.Entities; + var _ = await query.SingleAsync(x => x.Scenario == "null navigation property names"); + } + + [Theory, InlineData(true), InlineData(false)] + public virtual async Task Bad_json_properties_null_scalars(bool noTracking) + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingBadJsonProperties, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedBadJsonProperties); + + using var context = contextFactory.CreateDbContext(); + var query = noTracking ? context.Entities.AsNoTracking() : context.Entities; + var _ = await query.SingleAsync(x => x.Scenario == "null scalar property names"); + } + + protected abstract Task SeedBadJsonProperties(ContextBadJsonProperties ctx); + + protected virtual void OnModelCreatingBadJsonProperties(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.Property(x => x.Id).ValueGeneratedNever(); + + b.OwnsOne( + x => x.RequiredReference, b => + { + b.OwnsOne(x => x.NestedOptional); + b.OwnsOne(x => x.NestedRequired); + b.OwnsMany(x => x.NestedCollection); + }); + + b.OwnsOne( + x => x.OptionalReference, b => + { + b.OwnsOne(x => x.NestedOptional); + b.OwnsOne(x => x.NestedRequired); + b.OwnsMany(x => x.NestedCollection); + }); + + b.OwnsMany( + x => x.Collection, b => + { + b.OwnsOne(x => x.NestedOptional); + b.OwnsOne(x => x.NestedRequired); + b.OwnsMany(x => x.NestedCollection); + }); + }); + + protected class ContextBadJsonProperties(DbContextOptions options) : DbContext(options) + { + public DbSet Entities { get; set; } + + public class Entity + { + public int Id { get; set; } + public string Scenario { get; set; } + public JsonRoot OptionalReference { get; set; } + public JsonRoot RequiredReference { get; set; } + public List Collection { get; set; } + } + + public class JsonRoot + { + public JsonBranch NestedRequired { get; set; } + public JsonBranch NestedOptional { get; set; } + public List NestedCollection { get; set; } + } + + public class JsonBranch + { + public string Text { get; set; } + } + } + + #endregion + + #endregion + + protected virtual void ClearLog() + { + } + + protected virtual void ConfigureWarnings(WarningsConfigurationBuilder builder) + { + } + } diff --git a/test/EFCore.Specification.Tests/Query/ManyToManyQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/ManyToManyQueryTestBase.cs index 57f12e0ad97..1354f413ab0 100644 --- a/test/EFCore.Specification.Tests/Query/ManyToManyQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/ManyToManyQueryTestBase.cs @@ -986,5 +986,177 @@ protected ManyToManyContext CreateContext() protected virtual void ClearLog() { + ListLoggerFactory.Clear(); } + + #region Non-shared test resources + + #region 7973 + + [Fact] + public virtual async Task SelectMany_with_collection_selector_having_subquery() + { + var contextFactory = await InitializeNonSharedTest(seed: c => c.SeedAsync()); + using var context = contextFactory.CreateDbContext(); + var users = (from user in context.Users + from organisation in context.Organisations.Where(o => o.OrganisationUsers.Any()).DefaultIfEmpty() + select new { UserId = user.Id, OrgId = organisation.Id }).ToList(); + + Assert.Equal(2, users.Count); + } + + // Protected so that it can be used by inheriting tests, and so that things like unused setters are not removed. + protected class MyContext7973(DbContextOptions options) : DbContext(options) + { + public DbSet Users { get; set; } + public DbSet Organisations { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasKey(ou => new { ou.OrganisationId, ou.UserId }); + modelBuilder.Entity().HasOne(ou => ou.Organisation).WithMany(o => o.OrganisationUsers) + .HasForeignKey(ou => ou.OrganisationId); + modelBuilder.Entity().HasOne(ou => ou.User).WithMany(u => u.OrganisationUsers) + .HasForeignKey(ou => ou.UserId); + } + + public Task SeedAsync() + { + AddRange( + new OrganisationUser { Organisation = new Organisation(), User = new User() }, + new Organisation(), + new User()); + + return SaveChangesAsync(); + } + + public class User + { + public int Id { get; set; } + public List OrganisationUsers { get; set; } + } + + public class Organisation + { + public int Id { get; set; } + public List OrganisationUsers { get; set; } + } + + public class OrganisationUser + { + public int OrganisationId { get; set; } + public Organisation Organisation { get; set; } + + public int UserId { get; set; } + public User User { get; set; } + } + } + + #endregion 7973 + + #region 20277 + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual async Task Many_to_many_load_works_when_join_entity_has_custom_key(bool async) + { + var contextFactory = await InitializeNonSharedTest(); + + int id; + using (var context = contextFactory.CreateDbContext()) + { + var m = new ManyM_DB(); + var n = new ManyN_DB(); + context.AddRange(m, n); + m.ManyN_DB = [n]; + + context.SaveChanges(); + + id = m.Id; + } + + ClearLog(); + + using (var context = contextFactory.CreateDbContext()) + { + var m = context.Find(id); + + if (async) + { + await context.Entry(m).Collection(x => x.ManyN_DB).LoadAsync(); + } + else + { + context.Entry(m).Collection(x => x.ManyN_DB).Load(); + } + + Assert.Equal(3, context.ChangeTracker.Entries().Count()); + Assert.Equal(1, m.ManyN_DB.Count); + Assert.Equal(1, m.ManyN_DB.Single().ManyM_DB.Count); + Assert.Equal(1, m.ManyNM_DB.Count); + Assert.Equal(1, m.ManyN_DB.Single().ManyNM_DB.Count); + + id = m.ManyN_DB.Single().Id; + } + + using (var context = contextFactory.CreateDbContext()) + { + var n = context.Find(id); + + if (async) + { + await context.Entry(n).Collection(x => x.ManyM_DB).LoadAsync(); + } + else + { + context.Entry(n).Collection(x => x.ManyM_DB).Load(); + } + + Assert.Equal(3, context.ChangeTracker.Entries().Count()); + Assert.Equal(1, n.ManyM_DB.Count); + Assert.Equal(1, n.ManyM_DB.Single().ManyN_DB.Count); + Assert.Equal(1, n.ManyNM_DB.Count); + Assert.Equal(1, n.ManyM_DB.Single().ManyNM_DB.Count); + } + } + + protected class Context20277(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity() + .HasMany(e => e.ManyN_DB) + .WithMany(e => e.ManyM_DB) + .UsingEntity( + r => r.HasOne(e => e.ManyN_DB).WithMany(e => e.ManyNM_DB).HasForeignKey(e => e.ManyN_Id), + l => l.HasOne(e => e.ManyM_DB).WithMany(e => e.ManyNM_DB).HasForeignKey(e => e.ManyM_Id), + b => b.HasKey(e => e.Id)); + } + + public class ManyM_DB + { + public int Id { get; set; } + public ICollection ManyN_DB { get; set; } + public ICollection ManyNM_DB { get; set; } + } + + public class ManyN_DB + { + public int Id { get; set; } + public ICollection ManyM_DB { get; set; } + public ICollection ManyNM_DB { get; set; } + } + + public sealed class ManyMN_DB + { + public int Id { get; set; } + + public int ManyM_Id { get; set; } + public ManyM_DB ManyM_DB { get; set; } + + public int? ManyN_Id { get; set; } + public ManyN_DB ManyN_DB { get; set; } + } + + #endregion 20277 + + #endregion Non-shared test resources } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocComplexTypeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocComplexTypeQuerySqlServerTest.cs deleted file mode 100644 index f10d60d0483..00000000000 --- a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocComplexTypeQuerySqlServerTest.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.ComponentModel.DataAnnotations.Schema; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class AdHocComplexTypeQuerySqlServerTest(NonSharedFixture fixture) : AdHocComplexTypeQueryRelationalTestBase(fixture) -{ - 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' (Nullable = true) -@entity_equality_container_Containee1_Id='2' (Nullable = true) -@entity_equality_container_Containee2_Id='3' (Nullable = true) - -SELECT TOP(2) [e].[Id], [e].[ComplexContainer_Id], [e].[ComplexContainer_Containee1_Id], [e].[ComplexContainer_Containee2_Id] -FROM [EntityType] AS [e] -WHERE [e].[ComplexContainer_Id] = @entity_equality_container_Id AND [e].[ComplexContainer_Containee1_Id] = @entity_equality_container_Containee1_Id AND [e].[ComplexContainer_Containee2_Id] = @entity_equality_container_Containee2_Id -"""); - } - - public override async Task Projecting_complex_property_does_not_auto_include_owned_types() - { - await base.Projecting_complex_property_does_not_auto_include_owned_types(); - - AssertSql( - """ -SELECT [e].[Complex_Name], [e].[Complex_Number] -FROM [EntityType] AS [e] -"""); - } - - #region 36837 - - #region 35025 - - public override async Task Select_TPC_base_with_ComplexType() - { - await base.Select_TPC_base_with_ComplexType(); - - AssertSql( - """ -SELECT [t].[Id], [t].[ChildProperty], NULL AS [ChildProperty1], [t].[PropertyInsideComplexThing], [t].[ChildComplexProperty_PropertyInsideComplexThing], NULL AS [ChildComplexProperty_PropertyInsideComplexThing1], N'TpcChild1' AS [Discriminator] -FROM [TpcChild1] AS [t] -UNION ALL -SELECT [t0].[Id], NULL AS [ChildProperty], [t0].[ChildProperty] AS [ChildProperty1], [t0].[PropertyInsideComplexThing], NULL AS [ChildComplexProperty_PropertyInsideComplexThing], [t0].[ChildComplexProperty_PropertyInsideComplexThing] AS [ChildComplexProperty_PropertyInsideComplexThing1], N'TpcChild2' AS [Discriminator] -FROM [TpcChild2] AS [t0] -"""); - } - - #endregion 35025 - - #region 34706 - - public override async Task Complex_type_on_an_entity_mapped_to_view_and_table() - { - await base.Complex_type_on_an_entity_mapped_to_view_and_table(); - - AssertSql( - """ -SELECT TOP(2) [b].[Id], [b].[ComplexThing_Prop1], [b].[ComplexThing_Prop2] -FROM [BlogsView] AS [b] -"""); - } - - #endregion 34706 - - #region 38077 - - public override async Task Complex_property_on_split_entity() - { - await base.Complex_property_on_split_entity(); - - AssertSql( - """ -SELECT [h].[Id], [h].[CreatedOn], [h0].[IsTestHook], [h0].[Weight], [h].[Number_Parsed], [h].[Number_Raw] -FROM [Hook] AS [h] -INNER JOIN [HookMetadata] AS [h0] ON [h].[Id] = [h0].[HookId] -"""); - } - - #endregion 38077 - - [Fact] - public virtual async Task Complex_type_equality_with_non_default_type_mapping() - { - var contextFactory = await InitializeNonSharedTest( - seed: context => - { - context.AddRange( - new Context36837.EntityType { ComplexThing = new Context36837.ComplexThing { DateTime = new DateTime(2020, 1, 1) } }); - return context.SaveChangesAsync(); - }); - - await using var context = contextFactory.CreateDbContext(); - - var count = await context.Set() - .CountAsync(b => b.ComplexThing == new Context36837.ComplexThing { DateTime = new DateTime(2020, 1, 1, 1, 1, 1, 999, 999) }); - Assert.Equal(0, count); - - AssertSql( - """ -SELECT COUNT(*) -FROM [EntityType] AS [e] -WHERE [e].[ComplexThing_DateTime] = '2020-01-01T01:01:01.999' -"""); - } - - private class Context36837(DbContextOptions options) : DbContext(options) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - => modelBuilder.Entity().ComplexProperty(b => b.ComplexThing); - - public class EntityType - { - public int Id { get; set; } - public ComplexThing ComplexThing { get; set; } = null!; - } - - public class ComplexThing - { - [Column(TypeName = "datetime")] // Non-default type mapping - public DateTime DateTime { get; set; } - } - } - - #endregion 36837 - - protected override ITestStoreFactory NonSharedTestStoreFactory - => SqlServerTestStoreFactory.Instance; -} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerJsonTypeTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerJsonTypeTest.cs deleted file mode 100644 index 646d9e60bc5..00000000000 --- a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerJsonTypeTest.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#nullable disable - -using Microsoft.Data.SqlClient; -using Xunit.Sdk; - -namespace Microsoft.EntityFrameworkCore.Query; - -[ConditionalClass(typeof(SqlServerTestEnvironment), nameof(SqlServerTestEnvironment.IsJsonTypeSupported))] -public class AdHocJsonQuerySqlServerJsonTypeTest(NonSharedFixture fixture) : AdHocJsonQuerySqlServerTestBase(fixture) -{ - #region BadJsonProperties - - // When using the SQL Server JSON data type, insertion of the bad data fails thanks to SQL Server validation, - // unlike with tests mapping to nvarchar(max) where the bad JSON data is inserted correctly and then read. - - public override Task Bad_json_properties_duplicated_navigations(bool noTracking) - => Task.CompletedTask; - - public override Task Bad_json_properties_duplicated_scalars(bool noTracking) - => Task.CompletedTask; - - public override Task Bad_json_properties_empty_navigations(bool noTracking) - => Task.CompletedTask; - - public override Task Bad_json_properties_empty_scalars(bool noTracking) - => Task.CompletedTask; - - public override Task Bad_json_properties_null_navigations(bool noTracking) - => Task.CompletedTask; - - public override Task Bad_json_properties_null_scalars(bool noTracking) - => Task.CompletedTask; - - #endregion BadJsonProperties - - // SQL Server 2025 (CTP 2.1) does not support casting JSON scalar strings to json - // (CAST('8' AS json) and CAST('null' AS json) fail with "JSON text is not properly formatted"). - public override Task Project_entity_with_json_null_values() - => Assert.ThrowsAsync(base.Project_entity_with_json_null_values); - - // SQL Server 2025 (CTP 2.1) does not support casting JSON scalar strings to json - // (CAST('8' AS json) and CAST('null' AS json) fail with "JSON text is not properly formatted"). - // The base implementation expects a different exception. - public override Task Try_project_collection_but_JSON_is_entity() - => Assert.ThrowsAsync(base.Try_project_collection_but_JSON_is_entity); - - // SQL Server 2025 (CTP 2.1) does not support casting JSON scalar strings to json - // (CAST('8' AS json) and CAST('null' AS json) fail with "JSON text is not properly formatted"). - // The base implementation expects a different exception. - public override Task Try_project_reference_but_JSON_is_collection() - => Assert.ThrowsAsync(base.Try_project_reference_but_JSON_is_collection); - - // When using the SQL Server json type, we use JSON_VALUE() with the RETURNING clause to parse the JSON property value - // into the correct type (int in this case); since we (currently) use lax mode, if the format isn't correct as here - // (text instead of int), we get NULL out (strict mode would throw, see #36802 about possibly switching to that). - // And since the enum properties are required in the model, this causes "Nullable object must have a value" to be thrown. - public override Task Read_enum_property_with_legacy_values(bool async) - => Assert.ThrowsAsync(() => base.Read_enum_property_with_legacy_values_core(async)); - - protected override string NonSharedStoreName - => "AdHocJsonQueryJsonTypeTest"; - - protected override string JsonColumnType - => "json"; -} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerTest.cs deleted file mode 100644 index 7e4c68c67a6..00000000000 --- a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerTest.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Data.SqlClient; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class AdHocJsonQuerySqlServerTest(NonSharedFixture fixture) : AdHocJsonQuerySqlServerTestBase(fixture) -{ - public override async Task Read_enum_property_with_legacy_values(bool async) - { - var exception = await Assert.ThrowsAsync(() => base.Read_enum_property_with_legacy_values_core(async)); - - // When using legacy nvarchar(max) to store JSON, we add a CAST() node to convert the text coming out of JSON_VALUE() - // to the appropriate type (int in this case); if the format isn't correct as here (text instead of int), - // we get: Conversion failed when converting the nvarchar value '...' to data type int - Assert.Equal(245, exception.Number); - } - - protected override string JsonColumnType - => "nvarchar(max)"; -} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerTestBase.cs b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerTestBase.cs deleted file mode 100644 index 127f0855ac1..00000000000 --- a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerTestBase.cs +++ /dev/null @@ -1,1047 +0,0 @@ -// 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.Diagnostics.Internal; -using Microsoft.EntityFrameworkCore.SqlServer.Diagnostics.Internal; - -namespace Microsoft.EntityFrameworkCore.Query; - -#nullable disable - -public abstract class AdHocJsonQuerySqlServerTestBase(NonSharedFixture fixture) : AdHocJsonQueryRelationalTestBase(fixture) -{ - protected override ITestStoreFactory NonSharedTestStoreFactory - => SqlServerTestStoreFactory.Instance; - - protected void AssertSql(params string[] expected) - => TestSqlLoggerFactory.AssertBaseline(expected); - - protected override void ConfigureWarnings(WarningsConfigurationBuilder builder) - { - base.ConfigureWarnings(builder); - - builder.Log(CoreEventId.StringEnumValueInJson); - } - - public override async Task Project_root_with_missing_scalars(bool async) - { - await base.Project_root_with_missing_scalars(async); - - AssertSql( - """ -SELECT [e].[Id], [e].[Name], [e].[Collection], [e].[OptionalReference], [e].[RequiredReference] -FROM [Entities] AS [e] -WHERE [e].[Id] < 4 -"""); - } - - public override async Task Project_top_level_json_entity_with_missing_scalars(bool async) - { - await base.Project_top_level_json_entity_with_missing_scalars(async); - - AssertSql( - """ -SELECT [e].[Id], [e].[OptionalReference], [e].[RequiredReference], [e].[Collection] -FROM [Entities] AS [e] -WHERE [e].[Id] < 4 -"""); - } - - public override async Task Project_nested_json_entity_with_missing_scalars(bool async) - { - await base.Project_nested_json_entity_with_missing_scalars(async); - - AssertSql( - """ -SELECT [e].[Id], JSON_QUERY([e].[OptionalReference], '$.NestedOptionalReference'), JSON_QUERY([e].[RequiredReference], '$.NestedRequiredReference'), JSON_QUERY([e].[Collection], '$[0].NestedCollection') -FROM [Entities] AS [e] -WHERE [e].[Id] < 4 -"""); - } - - public override async Task Project_root_entity_with_missing_required_navigation(bool async) - { - await base.Project_root_entity_with_missing_required_navigation(async); - - AssertSql( - """ -SELECT [e].[Id], [e].[Name], [e].[Collection], [e].[OptionalReference], [e].[RequiredReference] -FROM [Entities] AS [e] -WHERE [e].[Id] = 5 -"""); - } - - public override async Task Project_missing_required_navigation(bool async) - { - await base.Project_missing_required_navigation(async); - - AssertSql( - """ -SELECT JSON_QUERY([e].[RequiredReference], '$.NestedRequiredReference'), [e].[Id] -FROM [Entities] AS [e] -WHERE [e].[Id] = 5 -"""); - } - - public override async Task Project_root_entity_with_null_required_navigation(bool async) - { - await base.Project_root_entity_with_null_required_navigation(async); - - AssertSql( - """ -SELECT [e].[Id], [e].[Name], [e].[Collection], [e].[OptionalReference], [e].[RequiredReference] -FROM [Entities] AS [e] -WHERE [e].[Id] = 6 -"""); - } - - public override async Task Project_null_required_navigation(bool async) - { - await base.Project_null_required_navigation(async); - - AssertSql( - """ -SELECT [e].[RequiredReference], [e].[Id] -FROM [Entities] AS [e] -WHERE [e].[Id] = 6 -"""); - } - - public override async Task Project_missing_required_scalar(bool async) - { - await base.Project_missing_required_scalar(async); - - switch (JsonColumnType) - { - case "json": - AssertSql( - """ -SELECT [e].[Id], JSON_VALUE([e].[RequiredReference], '$.Number' RETURNING float) AS [Number] -FROM [Entities] AS [e] -WHERE [e].[Id] = 2 -"""); - break; - case "nvarchar(max)": - AssertSql( - """ -SELECT [e].[Id], CAST(JSON_VALUE([e].[RequiredReference], '$.Number') AS float) AS [Number] -FROM [Entities] AS [e] -WHERE [e].[Id] = 2 -"""); - break; - default: - throw new UnreachableException(); - } - } - - public override async Task Project_null_required_scalar(bool async) - { - await base.Project_null_required_scalar(async); - - switch (JsonColumnType) - { - case "json": - AssertSql( - """ -SELECT [e].[Id], JSON_VALUE([e].[RequiredReference], '$.Number' RETURNING float) AS [Number] -FROM [Entities] AS [e] -WHERE [e].[Id] = 4 -"""); - break; - case "nvarchar(max)": - AssertSql( - """ -SELECT [e].[Id], CAST(JSON_VALUE([e].[RequiredReference], '$.Number') AS float) AS [Number] -FROM [Entities] AS [e] -WHERE [e].[Id] = 4 -"""); - break; - default: - throw new UnreachableException(); - } - } - - protected override async Task Seed21006(Context21006 context) - { - await base.Seed21006(context); - - // missing scalar on top level - await context.Database.ExecuteSqlAsync( - $$$""" -INSERT INTO [Entities] ([Collection], [OptionalReference], [RequiredReference], [Id], [Name]) -VALUES ( -'[{"Text":"e2 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 nrr"}},{"Text":"e2 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 nrr"}}]', -'{"Text":"e2 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 or nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 or nrr"}}', -'{"Text":"e2 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 rr nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 rr nrr"}}', -2, -'e2') -"""); - - // missing scalar on nested level - await context.Database.ExecuteSqlAsync( - $$$""" -INSERT INTO [Entities] ([Collection], [OptionalReference], [RequiredReference], [Id], [Name]) -VALUES ( -'[{"Number":7,"Text":"e3 c1","NestedCollection":[{"Text":"e3 c1 c1"},{"Text":"e3 c1 c2"}],"NestedOptionalReference":{"Text":"e3 c1 nor"},"NestedRequiredReference":{"Text":"e3 c1 nrr"}},{"Number":7,"Text":"e3 c2","NestedCollection":[{"Text":"e3 c2 c1"},{"Text":"e3 c2 c2"}],"NestedOptionalReference":{"Text":"e3 c2 nor"},"NestedRequiredReference":{"Text":"e3 c2 nrr"}}]', -'{"Number":7,"Text":"e3 or","NestedCollection":[{"Text":"e3 or c1"},{"Text":"e3 or c2"}],"NestedOptionalReference":{"Text":"e3 or nor"},"NestedRequiredReference":{"Text":"e3 or nrr"}}', -'{"Number":7,"Text":"e3 rr","NestedCollection":[{"Text":"e3 rr c1"},{"Text":"e3 rr c2"}],"NestedOptionalReference":{"Text":"e3 rr nor"},"NestedRequiredReference":{"Text":"e3 rr nrr"}}', -3, -'e3') -"""); - - // null scalar on top level - await context.Database.ExecuteSqlAsync( - $$$""" -INSERT INTO [Entities] ([Collection], [OptionalReference], [RequiredReference], [Id], [Name]) -VALUES ( -'[{"Number":null,"Text":"e4 c1","NestedCollection":[{"Text":"e4 c1 c1"},{"Text":"e4 c1 c2"}],"NestedOptionalReference":{"Text":"e4 c1 nor"},"NestedRequiredReference":{"Text":"e4 c1 nrr"}},{"Number":null,"Text":"e4 c2","NestedCollection":[{"Text":"e4 c2 c1"},{"Text":"e4 c2 c2"}],"NestedOptionalReference":{"Text":"e4 c2 nor"},"NestedRequiredReference":{"Text":"e4 c2 nrr"}}]', -'{"Number":null,"Text":"e4 or","NestedCollection":[{"Text":"e4 or c1"},{"Text":"e4 or c2"}],"NestedOptionalReference":{"Text":"e4 or nor"},"NestedRequiredReference":{"Text":"e4 or nrr"}}', -'{"Number":null,"Text":"e4 rr","NestedCollection":[{"Text":"e4 rr c1"},{"Text":"e4 rr c2"}],"NestedOptionalReference":{"Text":"e4 rr nor"},"NestedRequiredReference":{"Text":"e4 rr nrr"}}', -4, -'e4') -"""); - - // missing required navigation - await context.Database.ExecuteSqlAsync( - $$$""" -INSERT INTO [Entities] ([Collection], [OptionalReference], [RequiredReference], [Id], [Name]) -VALUES ( -'[{"Number":7,"Text":"e5 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 nor"}},{"Number":7,"Text":"e5 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 nor"}}]', -'{"Number":7,"Text":"e5 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 or nor"}}', -'{"Number":7,"Text":"e5 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 rr nor"}}', -5, -'e5') -"""); - - // null required navigation - await context.Database.ExecuteSqlAsync( - $$$""" -INSERT INTO [Entities] ([Collection], [OptionalReference], [RequiredReference], [Id], [Name]) -VALUES ( -'[{"Number":7,"Text":"e6 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 nor"},"NestedRequiredReference":null},{"Number":7,"Text":"e6 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 nor"},"NestedRequiredReference":null}]', -'{"Number":7,"Text":"e6 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 or nor"},"NestedRequiredReference":null}', -'{"Number":7,"Text":"e6 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 rr nor"},"NestedRequiredReference":null}', -6, -'e6') -"""); - } - - protected override async Task Seed29219(DbContext ctx) - { - await base.Seed29219(ctx); - - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO [Entities] ([Id], [Reference], [Collection]) -VALUES(3, '{ "NonNullableScalar" : 30 }', '[{ "NonNullableScalar" : 10001 }]') -"""); - } - - protected override async Task Seed30028(DbContext ctx) - { - // complete - await ctx.Database.ExecuteSqlAsync( - $$$$""" -INSERT INTO [Entities] ([Id], [Json]) -VALUES( -1, -'{"RootName":"e1","Collection":[{"BranchName":"e1 c1","Nested":{"LeafName":"e1 c1 l"}},{"BranchName":"e1 c2","Nested":{"LeafName":"e1 c2 l"}}],"OptionalReference":{"BranchName":"e1 or","Nested":{"LeafName":"e1 or l"}},"RequiredReference":{"BranchName":"e1 rr","Nested":{"LeafName":"e1 rr l"}}}') -"""); - - // missing collection - await ctx.Database.ExecuteSqlAsync( - $$$$""" -INSERT INTO [Entities] ([Id], [Json]) -VALUES( -2, -'{"RootName":"e2","OptionalReference":{"BranchName":"e2 or","Nested":{"LeafName":"e2 or l"}},"RequiredReference":{"BranchName":"e2 rr","Nested":{"LeafName":"e2 rr l"}}}') -"""); - - // missing optional reference - await ctx.Database.ExecuteSqlAsync( - $$$$""" -INSERT INTO [Entities] ([Id], [Json]) -VALUES( -3, -'{"RootName":"e3","Collection":[{"BranchName":"e3 c1","Nested":{"LeafName":"e3 c1 l"}},{"BranchName":"e3 c2","Nested":{"LeafName":"e3 c2 l"}}],"RequiredReference":{"BranchName":"e3 rr","Nested":{"LeafName":"e3 rr l"}}}') -"""); - - // missing required reference - await ctx.Database.ExecuteSqlAsync( - $$$$""" -INSERT INTO [Entities] ([Id], [Json]) -VALUES( -4, -'{"RootName":"e4","Collection":[{"BranchName":"e4 c1","Nested":{"LeafName":"e4 c1 l"}},{"BranchName":"e4 c2","Nested":{"LeafName":"e4 c2 l"}}],"OptionalReference":{"BranchName":"e4 or","Nested":{"LeafName":"e4 or l"}}}') -"""); - } - - protected override Task Seed33046(DbContext ctx) - => ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO [Reviews] ([Rounds], [Id]) -VALUES('[{"RoundNumber":11,"SubRounds":[{"SubRoundNumber":111},{"SubRoundNumber":112}]}]', 1) -"""); - - protected override async Task Seed34960(Context34960 ctx) - { - await base.Seed34960(ctx); - - // JSON nulls - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO [Entities] ([Collection], [Reference], [Id]) -VALUES( -'null', -'null', -4) -"""); - - // JSON object where collection should be - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO [Junk] ([Collection], [Reference], [Id]) -VALUES( -'{ "DoB":"2000-01-01T00:00:00","Text":"junk" }', -NULL, -1) -"""); - - // JSON array where entity should be - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO [Junk] ([Collection], [Reference], [Id]) -VALUES( -NULL, -'[{ "DoB":"2000-01-01T00:00:00","Text":"junk" }]', -2) -"""); - } - - protected override Task SeedJunkInJson(DbContext ctx) - => ctx.Database.ExecuteSqlAsync( - $$$$""" -INSERT INTO [Entities] ([Collection], [CollectionWithCtor], [Reference], [ReferenceWithCtor], [Id]) -VALUES( -'[{"JunkReference":{"Something":"SomeValue" },"Name":"c11","JunkProperty1":50,"Number":11.5,"JunkCollection1":[],"JunkCollection2":[{"Foo":"junk value"}],"NestedCollection":[{"DoB":"2002-04-01T00:00:00","DummyProp":"Dummy value"},{"DoB":"2002-04-02T00:00:00","DummyReference":{"Foo":5}}],"NestedReference":{"DoB":"2002-03-01T00:00:00"}},{"Name":"c12","Number":12.5,"NestedCollection":[{"DoB":"2002-06-01T00:00:00"},{"DoB":"2002-06-02T00:00:00"}],"NestedDummy":59,"NestedReference":{"DoB":"2002-05-01T00:00:00"}}]', -'[{"MyBool":true,"Name":"c11 ctor","JunkReference":{"Something":"SomeValue","JunkCollection":[{"Foo":"junk value"}]},"NestedCollection":[{"DoB":"2002-08-01T00:00:00"},{"DoB":"2002-08-02T00:00:00"}],"NestedReference":{"DoB":"2002-07-01T00:00:00"}},{"MyBool":false,"Name":"c12 ctor","NestedCollection":[{"DoB":"2002-10-01T00:00:00"},{"DoB":"2002-10-02T00:00:00"}],"JunkCollection":[{"Foo":"junk value"}],"NestedReference":{"DoB":"2002-09-01T00:00:00"}}]', -'{"Name":"r1","JunkCollection":[{"Foo":"junk value"}],"JunkReference":{"Something":"SomeValue" },"Number":1.5,"NestedCollection":[{"DoB":"2000-02-01T00:00:00","JunkReference":{"Something":"SomeValue"}},{"DoB":"2000-02-02T00:00:00"}],"NestedReference":{"DoB":"2000-01-01T00:00:00"}}', -'{"MyBool":true,"JunkCollection":[{"Foo":"junk value"}],"Name":"r1 ctor","JunkReference":{"Something":"SomeValue" },"NestedCollection":[{"DoB":"2001-02-01T00:00:00"},{"DoB":"2001-02-02T00:00:00"}],"NestedReference":{"JunkCollection":[{"Foo":"junk value"}],"DoB":"2001-01-01T00:00:00"}}', -1) -"""); - - protected override Task SeedTrickyBuffering(DbContext ctx) - => ctx.Database.ExecuteSqlAsync( - $$$""" -INSERT INTO [Entities] ([Reference], [Id]) -VALUES( -'{"Name": "r1", "Number": 7, "JunkReference":{"Something": "SomeValue" }, "JunkCollection": [{"Foo": "junk value"}], "NestedReference": {"DoB": "2000-01-01T00:00:00"}, "NestedCollection": [{"DoB": "2000-02-01T00:00:00", "JunkReference": {"Something": "SomeValue"}}, {"DoB": "2000-02-02T00:00:00"}]}',1) -"""); - - protected override Task SeedShadowProperties(DbContext ctx) - => ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO [Entities] ([Collection], [CollectionWithCtor], [Reference], [ReferenceWithCtor], [Id], [Name]) -VALUES( -'[{"Name":"e1_c1","ShadowDouble":5.5},{"ShadowDouble":20.5,"Name":"e1_c2"}]', -'[{"Name":"e1_c1 ctor","ShadowNullableByte":6},{"ShadowNullableByte":null,"Name":"e1_c2 ctor"}]', -'{"Name":"e1_r", "ShadowString":"Foo"}', -'{"ShadowInt":143,"Name":"e1_r ctor"}', -1, -'e1') -"""); - - protected override async Task SeedNotICollection(DbContext ctx) - { - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO [Entities] ([Json], [Id]) -VALUES( -'{"Collection":[{"Bar":11,"Foo":"c11"},{"Bar":12,"Foo":"c12"},{"Bar":13,"Foo":"c13"}]}', -1) -"""); - - await ctx.Database.ExecuteSqlAsync( - $$$""" -INSERT INTO [Entities] ([Json], [Id]) -VALUES( -'{"Collection":[{"Bar":21,"Foo":"c21"},{"Bar":22,"Foo":"c22"}]}', -2) -"""); - } - - protected override async Task SeedBadJsonProperties(ContextBadJsonProperties ctx) - { - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO [Entities] ([Id], [Scenario], [OptionalReference], [RequiredReference], [Collection]) -VALUES( -1, -'baseline', -'{"NestedOptional": { "Text":"or no" }, "NestedRequired": { "Text":"or nr" }, "NestedCollection": [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ] }', -'{"NestedOptional": { "Text":"rr no" }, "NestedRequired": { "Text":"rr nr" }, "NestedCollection": [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ] }', -'[ -{"NestedOptional": { "Text":"c 1 no" }, "NestedRequired": { "Text":"c 1 nr" }, "NestedCollection": [ { "Text":"c 1 nc 1" }, { "Text":"c 1 nc 2" } ] }, -{"NestedOptional": { "Text":"c 2 no" }, "NestedRequired": { "Text":"c 2 nr" }, "NestedCollection": [ { "Text":"c 2 nc 1" }, { "Text":"c 2 nc 2" } ] } -]') -"""); - - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO [Entities] ([Id], [Scenario], [OptionalReference], [RequiredReference], [Collection]) -VALUES( -2, -'duplicated navigations', -'{"NestedOptional": { "Text":"or no" }, "NestedOptional": { "Text":"or no dupnav" }, "NestedRequired": { "Text":"or nr" }, "NestedCollection": [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ], "NestedCollection": [ { "Text":"or nc 1 dupnav" }, { "Text":"or nc 2 dupnav" } ], "NestedRequired": { "Text":"or nr dupnav" } }', -'{"NestedOptional": { "Text":"rr no" }, "NestedOptional": { "Text":"rr no dupnav" }, "NestedRequired": { "Text":"rr nr" }, "NestedCollection": [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ], "NestedCollection": [ { "Text":"rr nc 1 dupnav" }, { "Text":"rr nc 2 dupnav" } ], "NestedRequired": { "Text":"rr nr dupnav" } }', -'[ -{"NestedOptional": { "Text":"c 1 no" }, "NestedOptional": { "Text":"c 1 no dupnav" }, "NestedRequired": { "Text":"c 1 nr" }, "NestedCollection": [ { "Text":"c 1 nc 1" }, { "Text":"c 1 nc 2" } ], "NestedCollection": [ { "Text":"c 1 nc 1 dupnav" }, { "Text":"c 1 nc 2 dupnav" } ], "NestedRequired": { "Text":"c 1 nr dupnav" } }, -{"NestedOptional": { "Text":"c 2 no" }, "NestedOptional": { "Text":"c 2 no dupnav" }, "NestedRequired": { "Text":"c 2 nr" }, "NestedCollection": [ { "Text":"c 2 nc 1" }, { "Text":"c 2 nc 2" } ], "NestedCollection": [ { "Text":"c 2 nc 1 dupnav" }, { "Text":"c 2 nc 2 dupnav" } ], "NestedRequired": { "Text":"c 2 nr dupnav" } } -]') -"""); - - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO [Entities] ([Id], [Scenario], [OptionalReference], [RequiredReference], [Collection]) -VALUES( -3, -'duplicated scalars', -'{"NestedOptional": { "Text":"or no", "Text":"or no dupprop" }, "NestedRequired": { "Text":"or nr", "Text":"or nr dupprop" }, "NestedCollection": [ { "Text":"or nc 1", "Text":"or nc 1 dupprop" }, { "Text":"or nc 2", "Text":"or nc 2 dupprop" } ] }', -'{"NestedOptional": { "Text":"rr no", "Text":"rr no dupprop" }, "NestedRequired": { "Text":"rr nr", "Text":"rr nr dupprop" }, "NestedCollection": [ { "Text":"rr nc 1", "Text":"rr nc 1 dupprop" }, { "Text":"rr nc 2", "Text":"rr nc 2 dupprop" } ] }', -'[ -{"NestedOptional": { "Text":"c 1 no", "Text":"c 1 no dupprop" }, "NestedRequired": { "Text":"c 1 nr", "Text":"c 1 nr dupprop" }, "NestedCollection": [ { "Text":"c 1 nc 1", "Text":"c 1 nc 1 dupprop" }, { "Text":"c 1 nc 2", "Text":"c 1 nc 2 dupprop" } ] }, -{"NestedOptional": { "Text":"c 2 no", "Text":"c 2 no dupprop" }, "NestedRequired": { "Text":"c 2 nr", "Text":"c 2 nr dupprop" }, "NestedCollection": [ { "Text":"c 2 nc 1", "Text":"c 2 nc 1 dupprop" }, { "Text":"c 2 nc 2", "Text":"c 2 nc 2 dupprop" } ] } -]') -"""); - - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO [Entities] ([Id], [Scenario], [OptionalReference], [RequiredReference], [Collection]) -VALUES( -4, -'empty navigation property names', -'{"": { "Text":"or no" }, "": { "Text":"or nr" }, "": [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ] }', -'{"": { "Text":"rr no" }, "": { "Text":"rr nr" }, "": [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ] }', -'[ -{"": { "Text":"c 1 no" }, "": { "Text":"c 1 nr" }, "": [ { "Text":"c 1 nc 1" }, { "Text":"c 1 nc 2" } ] }, -{"": { "Text":"c 2 no" }, "": { "Text":"c 2 nr" }, "": [ { "Text":"c 2 nc 1" }, { "Text":"c 2 nc 2" } ] } -]') -"""); - - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO [Entities] ([Id], [Scenario], [OptionalReference], [RequiredReference], [Collection]) -VALUES( -5, -'empty scalar property names', -'{"NestedOptional": { "":"or no" }, "NestedRequired": { "":"or nr" }, "NestedCollection": [ { "":"or nc 1" }, { "":"or nc 2" } ] }', -'{"NestedOptional": { "":"rr no" }, "NestedRequired": { "":"rr nr" }, "NestedCollection": [ { "":"rr nc 1" }, { "":"rr nc 2" } ] }', -'[ -{"NestedOptional": { "":"c 1 no" }, "NestedRequired": { "":"c 1 nr" }, "NestedCollection": [ { "":"c 1 nc 1" }, { "":"c 1 nc 2" } ] }, -{"NestedOptional": { "":"c 2 no" }, "NestedRequired": { "":"c 2 nr" }, "NestedCollection": [ { "":"c 2 nc 1" }, { "":"c 2 nc 2" } ] } -]') -"""); - - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO [Entities] ([Id], [Scenario], [OptionalReference], [RequiredReference], [Collection]) -VALUES( -10, -'null navigation property names', -'{null: { "Text":"or no" }, null: { "Text":"or nr" }, null: [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ] }', -'{null: { "Text":"rr no" }, null: { "Text":"rr nr" }, null: [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ] }', -'[ -{null: { "Text":"c 1 no" }, null: { "Text":"c 1 nr" }, null: [ { "Text":"c 1 nc 1" }, { "Text":"c 1 nc 2" } ] }, -{null: { "Text":"c 2 no" }, null: { "Text":"c 2 nr" }, null: [ { "Text":"c 2 nc 1" }, { "Text":"c 2 nc 2" } ] } -]') -"""); - - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO [Entities] ([Id], [Scenario], [OptionalReference], [RequiredReference], [Collection]) -VALUES( -11, -'null scalar property names', -'{"NestedOptional": { null:"or no", "Text":"or no nonnull" }, "NestedRequired": { null:"or nr", "Text":"or nr nonnull" }, "NestedCollection": [ { null:"or nc 1", "Text":"or nc 1 nonnull" }, { null:"or nc 2", "Text":"or nc 2 nonnull" } ] }', -'{"NestedOptional": { null:"rr no", "Text":"rr no nonnull" }, "NestedRequired": { null:"rr nr", "Text":"rr nr nonnull" }, "NestedCollection": [ { null:"rr nc 1", "Text":"rr nc 1 nonnull" }, { null:"rr nc 2", "Text":"rr nc 2 nonnull" } ] }', -'[ -{"NestedOptional": { null:"c 1 no", "Text":"c 1 no nonnull" }, "NestedRequired": { null:"c 1 nr", "Text":"c 1 nr nonnull" }, "NestedCollection": [ { null:"c 1 nc 1", "Text":"c 1 nc 1 nonnull" }, { null:"c 1 nc 2", "Text":"c 1 nc 2 nonnull" } ] }, -{"NestedOptional": { null:"c 2 no", "Text":"c 2 no nonnull" }, "NestedRequired": { null:"c 2 nr", "Text":"c 2 nr nonnull" }, "NestedCollection": [ { null:"c 2 nc 1", "Text":"c 2 nc 1 nonnull" }, { null:"c 2 nc 2", "Text":"c 2 nc 2 nonnull" } ] } -]') -"""); - } - - #region PrimitiveCollectionInColumn - - [Fact] - public virtual async Task Materialize_json_null_primitive_collection_mapped_to_column_is_null() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingPrimitiveCollectionInColumn, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedPrimitiveCollectionInColumn); - - using var context = contextFactory.CreateDbContext(); - - // The primitive collection is mapped to its own column via CollectionToJsonStringConverter (which does NOT - // handle the JSON 'null' token). Legacy/external data may store the JSON 'null' token (the literal string - // "null", which Utf8JsonReader tokenizes as JsonTokenType.Null) rather than a SQL NULL. The materializer peeks - // the first token and short-circuits to null instead of letting JsonCollectionOfReferencesReaderWriter throw - // "Invalid token type: 'Null'". See issues #34881 and #38454. - var result = await context.Set() - .Where(x => x.Id < 4).OrderBy(x => x.Id).ToListAsync(); - - Assert.Equal(3, result.Count); - Assert.Equal(["a", "b"], result[0].Tags); - Assert.Null(result[1].Tags); // JSON 'null' token - Assert.Null(result[2].Tags); // SQL NULL - } - - [Fact] - public virtual async Task Materialize_empty_json_primitive_collection_mapped_to_column_throws() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingPrimitiveCollectionInColumn, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedPrimitiveCollectionInColumn); - - using var context = contextFactory.CreateDbContext(); - - // An empty/whitespace JSON string in the column isn't valid JSON; the diagnostics here match the converter - // path (JsonValueReaderWriter.FromJsonString), which rejects it with CoreStrings.EmptyJsonString. - var exception = await Assert.ThrowsAsync(() - => context.Set().Where(x => x.Id == 4).ToListAsync()); - - Assert.Equal(CoreStrings.EmptyJsonString, exception.Message); - } - - [Fact] - public virtual async Task Materialize_json_null_required_primitive_collection_mapped_to_column_throws() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingPrimitiveCollectionInColumn, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedPrimitiveCollectionInColumn); - - using var context = contextFactory.CreateDbContext(); - - // The required primitive collection column holds the JSON 'null' token. Since the property is required, the - // materializer throws a clear, property-named error rather than silently materializing null. See #34881. - var exception = await Assert.ThrowsAsync(() - => context.Set().Where(x => x.Id == 5).ToListAsync()); - - Assert.Equal(RelationalStrings.NullValueInRequiredJsonProperty("RequiredTags"), exception.Message); - } - - [Fact] - public virtual async Task Project_json_null_primitive_collection_mapped_to_column_is_null() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingPrimitiveCollectionInColumn, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedPrimitiveCollectionInColumn); - - using var context = contextFactory.CreateDbContext(); - - // Projecting the collection column directly reaches the materializer without an IProperty. The JSON 'null' - // token (and SQL NULL) must still be materialized as null rather than throwing "Invalid token type: 'Null'". - var tags = await context.Set() - .Where(x => x.Id < 4).OrderBy(x => x.Id).Select(x => x.Tags).ToListAsync(); - - Assert.Equal(3, tags.Count); - Assert.Equal(["a", "b"], tags[0]); - Assert.Null(tags[1]); // JSON 'null' token - Assert.Null(tags[2]); // SQL NULL - } - - protected virtual async Task SeedPrimitiveCollectionInColumn(DbContext ctx) - { - // primitive collection column contains a JSON array - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO [Entities] ([Id], [Tags], [RequiredTags]) -VALUES(1, N'["a","b"]', N'[]') -"""); - - // primitive collection column contains a JSON null token (the literal string "null", not a SQL NULL) - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO [Entities] ([Id], [Tags], [RequiredTags]) -VALUES(2, N'null', N'[]') -"""); - - // primitive collection column is SQL NULL - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO [Entities] ([Id], [Tags], [RequiredTags]) -VALUES(3, NULL, N'[]') -"""); - - // primitive collection column contains an empty (invalid) JSON string - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO [Entities] ([Id], [Tags], [RequiredTags]) -VALUES(4, N'', N'[]') -"""); - - // required primitive collection column contains a JSON null token - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO [Entities] ([Id], [Tags], [RequiredTags]) -VALUES(5, N'["a","b"]', N'null') -"""); - } - - protected virtual void OnModelCreatingPrimitiveCollectionInColumn(ModelBuilder modelBuilder) - => modelBuilder.Entity(b => - { - b.ToTable("Entities"); - b.Property(x => x.Id).ValueGeneratedNever(); - b.PrimitiveCollection(x => x.Tags); - b.PrimitiveCollection(x => x.RequiredTags).IsRequired(); - }); - - protected class ContextPrimitiveCollectionInColumn(DbContextOptions options) : DbContext(options) - { - public class MyEntity - { - public int Id { get; set; } - - // IList matches the legacy mapping reported in #34881. - public IList Tags { get; set; } - - public IList RequiredTags { get; set; } - } - } - - #endregion - - #region JsonPropertyWithConverters - - [Fact] - public virtual async Task Materialize_json_null_property_with_converters() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingJsonPropertyWithConverters, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedJsonPropertyWithConverters); - - using var context = contextFactory.CreateDbContext(); - var result = await context.Set().SingleAsync(x => x.Id == 1); - - Assert.Equal("e1", result.Reference.Name); - - // Both properties have a JSON 'null' token value. The streaming JSON shaper's null guard - // (CreateReadJsonPropertyValueExpression) routes the null based on the converter's ConvertsNulls flag: - // - ConvertsNulls == true -> ConvertFromProvider(default) maps DB null to the "FROM_DB_NULL" sentinel. - // - ConvertsNulls == false -> returns default(string) (null) without invoking the converter. - Assert.Equal("FROM_DB_NULL", result.Reference.ConvertedHandlingNulls); - Assert.Null(result.Reference.ConvertedNotHandlingNulls); - } - - protected virtual async Task SeedJsonPropertyWithConverters(DbContext ctx) - => await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO [Entities] ([Id], [Reference]) -VALUES(1, '{"Name":"e1","ConvertedHandlingNulls":null,"ConvertedNotHandlingNulls":null}') -"""); - - protected virtual void OnModelCreatingJsonPropertyWithConverters(ModelBuilder modelBuilder) - => modelBuilder.Entity(b => - { - b.ToTable("Entities"); - b.Property(x => x.Id).ValueGeneratedNever(); - b.OwnsOne( - x => x.Reference, b => - { - b.ToJson().HasColumnType(JsonColumnType); - b.Property(x => x.ConvertedHandlingNulls).HasConversion( - new ValueConverter( - v => v, - v => v ?? "FROM_DB_NULL", - convertsNulls: true)); - b.Property(x => x.ConvertedNotHandlingNulls).HasConversion( - new ValueConverter( - v => v, - v => "FROM_CONVERTER:" + v, - convertsNulls: false)); - }); - }); - - protected class ContextJsonPropertyWithConverters(DbContextOptions options) : DbContext(options) - { - public class MyEntity - { - public int Id { get; set; } - public MyJsonEntity Reference { get; set; } - } - - public class MyJsonEntity - { - public string Name { get; set; } - public string ConvertedHandlingNulls { get; set; } - public string ConvertedNotHandlingNulls { get; set; } - } - } - - #endregion - - #region PrimitiveCollectionInJson - - [Fact] - public virtual async Task Materialize_json_null_optional_primitive_collection_in_json_is_null() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingPrimitiveCollectionInJson, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedPrimitiveCollectionInJson); - - using var context = contextFactory.CreateDbContext(); - var result = await context.Set() - .Where(x => x.Id < 3).OrderBy(x => x.Id).ToListAsync(); - - Assert.Equal(2, result.Count); - - // Id == 1: JSON has "NullableStrings": null -> materialized as null. - Assert.Null(result[0].Reference.NullableStrings); - Assert.Equal(["a", "b"], result[0].Reference.RequiredStrings); - - // Id == 2: JSON has "NullableStrings": ["x", "y"]. - Assert.Equal(["x", "y"], result[1].Reference.NullableStrings); - Assert.Equal(["c", "d"], result[1].Reference.RequiredStrings); - } - - [Fact] - public virtual async Task Materialize_json_null_required_primitive_collection_in_json_throws() - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: OnModelCreatingPrimitiveCollectionInJson, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedPrimitiveCollectionInJson); - - using var context = contextFactory.CreateDbContext(); - - // A required primitive collection nested in a JSON document whose value is a 'null' token yields a clear, - // property-named error rather than the cryptic reader/writer "Invalid token type: 'Null'". - var exception = await Assert.ThrowsAsync(() - => context.Set().Where(x => x.Id == 3).ToListAsync()); - - Assert.Equal(RelationalStrings.NullValueInRequiredJsonProperty("RequiredStrings"), exception.Message); - } - - protected virtual async Task SeedPrimitiveCollectionInJson(DbContext ctx) - { - // optional collection is JSON null - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO [Entities] ([Id], [Reference]) -VALUES(1, '{"NullableStrings":null,"RequiredStrings":["a","b"]}') -"""); - - // optional collection has a value - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO [Entities] ([Id], [Reference]) -VALUES(2, '{"NullableStrings":["x","y"],"RequiredStrings":["c","d"]}') -"""); - - // required collection is JSON null (materialization throws) - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO [Entities] ([Id], [Reference]) -VALUES(3, '{"NullableStrings":["x","y"],"RequiredStrings":null}') -"""); - } - - protected virtual void OnModelCreatingPrimitiveCollectionInJson(ModelBuilder modelBuilder) - => modelBuilder.Entity(b => - { - b.ToTable("Entities"); - b.Property(x => x.Id).ValueGeneratedNever(); - b.OwnsOne( - x => x.Reference, b => - { - b.ToJson().HasColumnType(JsonColumnType); - b.PrimitiveCollection(x => x.NullableStrings).IsRequired(false); - b.PrimitiveCollection(x => x.RequiredStrings).IsRequired(); - }); - }); - - protected class ContextPrimitiveCollectionInJson(DbContextOptions options) : DbContext(options) - { - public class MyEntity - { - public int Id { get; set; } - public MyJsonEntity Reference { get; set; } - } - - public class MyJsonEntity - { - public List NullableStrings { get; set; } - public List RequiredStrings { get; set; } - } - } - - #endregion - - #region EnumLegacyValues - - [Theory, MemberData(nameof(IsAsyncData))] - public abstract Task Read_enum_property_with_legacy_values(bool async); - - protected virtual async Task Read_enum_property_with_legacy_values_core(bool async) - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: BuildModelEnumLegacyValues, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedEnumLegacyValues); - - using var context = contextFactory.CreateDbContext(); - - var query = context.Set().Select(x => new - { - x.Reference.IntEnum, - x.Reference.ByteEnum, - x.Reference.LongEnum, - x.Reference.NullableEnum - }); - - if (async) - { - await query.ToListAsync(); - } - else - { - query.ToList(); - } - } - - [Theory, MemberData(nameof(IsAsyncData))] - public virtual async Task Read_json_entity_with_enum_properties_with_legacy_values(bool async) - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: BuildModelEnumLegacyValues, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedEnumLegacyValues, - shouldLogCategory: c => c == DbLoggerCategory.Query.Name); - - using (var context = contextFactory.CreateDbContext()) - { - var query = context.Set().Select(x => x.Reference).AsNoTracking(); - - var result = async - ? await query.ToListAsync() - : query.ToList(); - - Assert.Equal(1, result.Count); - Assert.Equal(ByteEnumLegacyValues.Redmond, result[0].ByteEnum); - Assert.Equal(IntEnumLegacyValues.Foo, result[0].IntEnum); - Assert.Equal(LongEnumLegacyValues.Three, result[0].LongEnum); - Assert.Equal(ULongEnumLegacyValues.Three, result[0].ULongEnum); - Assert.Equal(IntEnumLegacyValues.Bar, result[0].NullableEnum); - } - - var testLogger = new TestLogger(); - Assert.Single( - ListLoggerFactory.Log, - l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(ByteEnumLegacyValues))); - Assert.Single( - ListLoggerFactory.Log, - l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(IntEnumLegacyValues))); - Assert.Single( - ListLoggerFactory.Log, - l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(LongEnumLegacyValues))); - Assert.Single( - ListLoggerFactory.Log, - l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(ULongEnumLegacyValues))); - } - - [Theory, MemberData(nameof(IsAsyncData))] - public virtual async Task Read_json_entity_collection_with_enum_properties_with_legacy_values(bool async) - { - var contextFactory = await InitializeNonSharedTest( - onModelCreating: BuildModelEnumLegacyValues, - onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), - seed: SeedEnumLegacyValues, - shouldLogCategory: c => c == DbLoggerCategory.Query.Name); - - using (var context = contextFactory.CreateDbContext()) - { - var query = context.Set().Select(x => x.Collection).AsNoTracking(); - - var result = async - ? await query.ToListAsync() - : query.ToList(); - - Assert.Equal(1, result.Count); - Assert.Equal(2, result[0].Count); - Assert.Equal(ByteEnumLegacyValues.Bellevue, result[0][0].ByteEnum); - Assert.Equal(IntEnumLegacyValues.Foo, result[0][0].IntEnum); - Assert.Equal(LongEnumLegacyValues.One, result[0][0].LongEnum); - Assert.Equal(ULongEnumLegacyValues.One, result[0][0].ULongEnum); - Assert.Equal(IntEnumLegacyValues.Bar, result[0][0].NullableEnum); - Assert.Equal(ByteEnumLegacyValues.Seattle, result[0][1].ByteEnum); - Assert.Equal(IntEnumLegacyValues.Baz, result[0][1].IntEnum); - Assert.Equal(LongEnumLegacyValues.Two, result[0][1].LongEnum); - Assert.Equal(ULongEnumLegacyValues.Two, result[0][1].ULongEnum); - Assert.Null(result[0][1].NullableEnum); - } - - var testLogger = new TestLogger(); - Assert.Single( - ListLoggerFactory.Log, - l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(ByteEnumLegacyValues))); - Assert.Single( - ListLoggerFactory.Log, - l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(IntEnumLegacyValues))); - Assert.Single( - ListLoggerFactory.Log, - l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(LongEnumLegacyValues))); - Assert.Single( - ListLoggerFactory.Log, - l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(ULongEnumLegacyValues))); - } - - private Task SeedEnumLegacyValues(DbContext ctx) - => ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO [Entities] ([Collection], [Reference], [Id], [Name]) -VALUES( -N'[{"ByteEnum":"Bellevue","IntEnum":"Foo","LongEnum":"One","ULongEnum":"One","Name":"e1_c1","NullableEnum":"Bar"},{"ByteEnum":"Seattle","IntEnum":"Baz","LongEnum":"Two","ULongEnum":"Two","Name":"e1_c2","NullableEnum":null}]', -N'{"ByteEnum":"Redmond","IntEnum":"Foo","LongEnum":"Three","ULongEnum":"Three","Name":"e1_r","NullableEnum":"Bar"}', -1, -N'e1') -"""); - - protected virtual void BuildModelEnumLegacyValues(ModelBuilder modelBuilder) - => modelBuilder.Entity(b => - { - b.ToTable("Entities"); - b.Property(x => x.Id).ValueGeneratedNever(); - b.OwnsOne(x => x.Reference, b => b.ToJson().HasColumnType(JsonColumnType)); - b.OwnsMany(x => x.Collection, b => b.ToJson().HasColumnType(JsonColumnType)); - }); - - private class MyEntityEnumLegacyValues - { - public int Id { get; set; } - public string Name { get; set; } - - public MyJsonEntityEnumLegacyValues Reference { get; set; } - public List Collection { get; set; } - } - - private class MyJsonEntityEnumLegacyValues - { - public string Name { get; set; } - - // ReSharper disable once UnusedAutoPropertyAccessor.Local - public IntEnumLegacyValues IntEnum { get; set; } - - // ReSharper disable once UnusedAutoPropertyAccessor.Local - public ByteEnumLegacyValues ByteEnum { get; set; } - - // ReSharper disable once UnusedAutoPropertyAccessor.Local - public LongEnumLegacyValues LongEnum { get; set; } - - // ReSharper disable once UnusedAutoPropertyAccessor.Local - public ULongEnumLegacyValues ULongEnum { get; set; } - - // ReSharper disable once UnusedAutoPropertyAccessor.Local - public IntEnumLegacyValues? NullableEnum { get; set; } - } - - private enum IntEnumLegacyValues - { - Foo = int.MinValue, - Bar, - Baz = int.MaxValue, - } - - private enum ByteEnumLegacyValues : byte - { - Seattle, - Redmond, - Bellevue = 255, - } - - private enum LongEnumLegacyValues : long - { - One = long.MinValue, - Two = 1, - Three = long.MaxValue, - } - - private enum ULongEnumLegacyValues : ulong - { - One = ulong.MinValue, - Two = 1, - Three = ulong.MaxValue, - } - - #endregion - - public override async Task Entity_splitting_with_owned_json() - { - await base.Entity_splitting_with_owned_json(); - - AssertSql( - """ -SELECT TOP(2) [m].[Id], [m].[PropertyInMainTable], [o].[PropertyInOtherTable], [m].[Json] -FROM [MyEntity] AS [m] -INNER JOIN [OtherTable] AS [o] ON [m].[Id] = [o].[Id] -"""); - } - - public override async Task SelectMany_over_primitive_collection_nested_in_complex_collection_inside_json_column() - { - await base.SelectMany_over_primitive_collection_nested_in_complex_collection_inside_json_column(); - - switch (JsonColumnType) - { - case "json": - AssertSql( - """ -SELECT [p].[value] -FROM [Cars] AS [c] -CROSS APPLY OPENJSON([c].[CarConfiguration], '$.optionPackages') WITH ( - [packageId] nvarchar(max) '$.packageId', - [partNumbers] nvarchar(max) '$.partNumbers' AS JSON -) AS [o] -CROSS APPLY OPENJSON([o].[partNumbers]) WITH ([value] varchar(32) '$') AS [p] -WHERE [c].[Vin] = '1FA6P8TH8J5123456' AND [c].[DealerId] = 'DEALER-001' AND [o].[packageId] = N'PKG-SPORT' -"""); - break; - case "nvarchar(max)": - AssertSql( - """ -SELECT [p].[value] -FROM [Cars] AS [c] -CROSS APPLY OPENJSON([c].[CarConfiguration], '$.optionPackages') WITH ( - [packageId] nvarchar(max) '$.packageId', - [partNumbers] nvarchar(max) '$.partNumbers' AS JSON -) AS [o] -CROSS APPLY OPENJSON([o].[partNumbers]) WITH ([value] varchar(32) '$') AS [p] -WHERE [c].[Vin] = '1FA6P8TH8J5123456' AND [c].[DealerId] = 'DEALER-001' AND [o].[packageId] = N'PKG-SPORT' -"""); - break; - - default: - throw new UnreachableException(); - } - } - - public override async Task Value_converter_equality_null_scalar() - { - await base.Value_converter_equality_null_scalar(); - - AssertSql( - """ -@entity_equality_complexType='{"IntToString":"\u003Cnull\u003E"}' (Size = 34) - -SELECT COUNT(*) -FROM [Entities] AS [e] -WHERE [e].[Json] = @entity_equality_complexType -"""); - } -} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocManyToManyQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocManyToManyQuerySqlServerTest.cs deleted file mode 100644 index c7a5aada59c..00000000000 --- a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocManyToManyQuerySqlServerTest.cs +++ /dev/null @@ -1,105 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -// ReSharper disable InconsistentNaming - -namespace Microsoft.EntityFrameworkCore.Query; - -#nullable disable - -public class AdHocManyToManyQuerySqlServerTest(NonSharedFixture fixture) : AdHocManyToManyQueryRelationalTestBase(fixture) -{ - protected override ITestStoreFactory NonSharedTestStoreFactory - => SqlServerTestStoreFactory.Instance; - - public override async Task SelectMany_with_collection_selector_having_subquery() - { - await base.SelectMany_with_collection_selector_having_subquery(); - - AssertSql( - """ -SELECT [u].[Id] AS [UserId], [s].[Id] AS [OrgId] -FROM [Users] AS [u] -CROSS JOIN ( - SELECT [o1].[Id] - FROM ( - SELECT 1 AS empty - ) AS [e] - LEFT JOIN ( - SELECT [o].[Id] - FROM [Organisations] AS [o] - WHERE EXISTS ( - SELECT 1 - FROM [OrganisationUser] AS [o0] - WHERE [o].[Id] = [o0].[OrganisationId]) - ) AS [o1] ON 1 = 1 -) AS [s] -"""); - } - - public override async Task Many_to_many_load_works_when_join_entity_has_custom_key(bool async) - { - await base.Many_to_many_load_works_when_join_entity_has_custom_key(async); - - AssertSql( - """ -@p='1' - -SELECT TOP(1) [m].[Id] -FROM [ManyM_DB] AS [m] -WHERE [m].[Id] = @p -""", - // - """ -@p='1' - -SELECT [s].[Id], [m].[Id], [s].[Id0], [s0].[Id], [s0].[ManyM_Id], [s0].[ManyN_Id], [s0].[Id0] -FROM [ManyM_DB] AS [m] -INNER JOIN ( - SELECT [m1].[Id], [m0].[Id] AS [Id0], [m0].[ManyM_Id] - FROM [ManyMN_DB] AS [m0] - LEFT JOIN [ManyN_DB] AS [m1] ON [m0].[ManyN_Id] = [m1].[Id] -) AS [s] ON [m].[Id] = [s].[ManyM_Id] -LEFT JOIN ( - SELECT [m2].[Id], [m2].[ManyM_Id], [m2].[ManyN_Id], [m3].[Id] AS [Id0] - FROM [ManyMN_DB] AS [m2] - INNER JOIN [ManyM_DB] AS [m3] ON [m2].[ManyM_Id] = [m3].[Id] - WHERE [m3].[Id] = @p -) AS [s0] ON [s].[Id] = [s0].[ManyN_Id] -WHERE [m].[Id] = @p -ORDER BY [m].[Id], [s].[Id0] -""", - // - """ -@p='1' - -SELECT TOP(1) [m].[Id] -FROM [ManyN_DB] AS [m] -WHERE [m].[Id] = @p -""", - // - """ -@p='1' - -SELECT [s].[Id], [m].[Id], [s].[Id0], [s0].[Id], [s0].[ManyM_Id], [s0].[ManyN_Id], [s0].[Id0] -FROM [ManyN_DB] AS [m] -INNER JOIN ( - SELECT [m1].[Id], [m0].[Id] AS [Id0], [m0].[ManyN_Id] - FROM [ManyMN_DB] AS [m0] - INNER JOIN [ManyM_DB] AS [m1] ON [m0].[ManyM_Id] = [m1].[Id] -) AS [s] ON [m].[Id] = [s].[ManyN_Id] -LEFT JOIN ( - SELECT [m2].[Id], [m2].[ManyM_Id], [m2].[ManyN_Id], [m3].[Id] AS [Id0] - FROM [ManyMN_DB] AS [m2] - INNER JOIN [ManyN_DB] AS [m3] ON [m2].[ManyN_Id] = [m3].[Id] - WHERE [m3].[Id] = @p -) AS [s0] ON [s].[Id] = [s0].[ManyM_Id] -WHERE [m].[Id] = @p -ORDER BY [m].[Id], [s].[Id0] -"""); - } - - [Fact] - public virtual void Check_all_tests_overridden() - => TestHelpers.AssertAllMethodsOverridden(GetType()); -} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexTypeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexTypeQuerySqlServerTest.cs index 7a670105efd..7010ccbb1d1 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexTypeQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexTypeQuerySqlServerTest.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.ComponentModel.DataAnnotations.Schema; using Microsoft.EntityFrameworkCore.TestModels.ComplexTypeModel; namespace Microsoft.EntityFrameworkCore.Query; @@ -1271,6 +1272,362 @@ ORDER BY [c1].[Id] DESC """); } + #region Non-shared test resources + + 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' (Nullable = true) +@entity_equality_container_Containee1_Id='2' (Nullable = true) +@entity_equality_container_Containee2_Id='3' (Nullable = true) + +SELECT TOP(2) [e].[Id], [e].[ComplexContainer_Id], [e].[ComplexContainer_Containee1_Id], [e].[ComplexContainer_Containee2_Id] +FROM [EntityType] AS [e] +WHERE [e].[ComplexContainer_Id] = @entity_equality_container_Id AND [e].[ComplexContainer_Containee1_Id] = @entity_equality_container_Containee1_Id AND [e].[ComplexContainer_Containee2_Id] = @entity_equality_container_Containee2_Id +"""); + } + + public override async Task Projecting_complex_property_does_not_auto_include_owned_types() + { + await base.Projecting_complex_property_does_not_auto_include_owned_types(); + + AssertSql( + """ +SELECT [e].[Complex_Name], [e].[Complex_Number] +FROM [EntityType] AS [e] +"""); + } + + #region 36837 + + #region 35025 + + public override async Task Select_TPC_base_with_ComplexType() + { + await base.Select_TPC_base_with_ComplexType(); + + AssertSql( + """ +SELECT [t].[Id], [t].[ChildProperty], NULL AS [ChildProperty1], [t].[PropertyInsideComplexThing], [t].[ChildComplexProperty_PropertyInsideComplexThing], NULL AS [ChildComplexProperty_PropertyInsideComplexThing1], N'TpcChild1' AS [Discriminator] +FROM [TpcChild1] AS [t] +UNION ALL +SELECT [t0].[Id], NULL AS [ChildProperty], [t0].[ChildProperty] AS [ChildProperty1], [t0].[PropertyInsideComplexThing], NULL AS [ChildComplexProperty_PropertyInsideComplexThing], [t0].[ChildComplexProperty_PropertyInsideComplexThing] AS [ChildComplexProperty_PropertyInsideComplexThing1], N'TpcChild2' AS [Discriminator] +FROM [TpcChild2] AS [t0] +"""); + } + + #endregion 35025 + + #region 34706 + + public override async Task Complex_type_on_an_entity_mapped_to_view_and_table() + { + await base.Complex_type_on_an_entity_mapped_to_view_and_table(); + + AssertSql( + """ +SELECT TOP(2) [b].[Id], [b].[ComplexThing_Prop1], [b].[ComplexThing_Prop2] +FROM [BlogsView] AS [b] +"""); + } + + #endregion 34706 + + #region 38077 + + public override async Task Complex_property_on_split_entity() + { + await base.Complex_property_on_split_entity(); + + AssertSql( + """ +SELECT [h].[Id], [h].[CreatedOn], [h0].[IsTestHook], [h0].[Weight], [h].[Number_Parsed], [h].[Number_Raw] +FROM [Hook] AS [h] +INNER JOIN [HookMetadata] AS [h0] ON [h].[Id] = [h0].[HookId] +"""); + } + + #endregion 38077 + + [Fact] + public virtual async Task Complex_type_equality_with_non_default_type_mapping() + { + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + context.AddRange( + new Context36837.EntityType { ComplexThing = new Context36837.ComplexThing { DateTime = new DateTime(2020, 1, 1) } }); + return context.SaveChangesAsync(); + }); + + await using var context = contextFactory.CreateDbContext(); + + var count = await context.Set() + .CountAsync(b => b.ComplexThing == new Context36837.ComplexThing { DateTime = new DateTime(2020, 1, 1, 1, 1, 1, 999, 999) }); + Assert.Equal(0, count); + + AssertSql( + """ +SELECT COUNT(*) +FROM [EntityType] AS [e] +WHERE [e].[ComplexThing_DateTime] = '2020-01-01T01:01:01.999' +"""); + } + + private class Context36837(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity().ComplexProperty(b => b.ComplexThing); + + public class EntityType + { + public int Id { get; set; } + public ComplexThing ComplexThing { get; set; } = null!; + } + + public class ComplexThing + { + [Column(TypeName = "datetime")] // Non-default type mapping + public DateTime DateTime { get; set; } + } + } + + #endregion 36837 + + + public override async Task Optional_complex_type_with_discriminator() + { + await base.Optional_complex_type_with_discriminator(); + + AssertSql( + """ +SELECT TOP(2) [e].[Id], [e].[AllOptionalsComplexType_Discriminator], [e].[AllOptionalsComplexType_OptionalProperty] +FROM [EntityType] AS [e] +WHERE [e].[AllOptionalsComplexType_Discriminator] IS NULL +""", + // + """ +@p2='3' +@p0='AllOptionalsComplexType' (Size = 4000) +@p1='New thing' (Size = 4000) + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [EntityType] SET [AllOptionalsComplexType_Discriminator] = @p0, [AllOptionalsComplexType_OptionalProperty] = @p1 +OUTPUT 1 +WHERE [Id] = @p2; +""", + // + """ +SELECT [e].[Id], [e].[AllOptionalsComplexType_Discriminator], [e].[AllOptionalsComplexType_OptionalProperty] +FROM [EntityType] AS [e] +"""); + } + + public override async Task Non_optional_complex_type_with_all_nullable_properties() + { + await base.Non_optional_complex_type_with_all_nullable_properties(); + + AssertSql( + """ +SELECT TOP(2) [e].[Id], [e].[NonOptionalComplexType_NullableDateTime], [e].[NonOptionalComplexType_NullableString] +FROM [EntityType] AS [e] +"""); + } + + public override async Task Non_optional_complex_type_with_all_nullable_properties_via_left_join() + { + await base.Non_optional_complex_type_with_all_nullable_properties_via_left_join(); + + AssertSql( + """ +SELECT [p0].[Id], [c].[Id], [c].[ParentId], [c].[ComplexType_NullableDateTime], [c].[ComplexType_NullableString] +FROM ( + SELECT TOP(2) [p].[Id] + FROM [Parent] AS [p] +) AS [p0] +LEFT JOIN [Child] AS [c] ON [p0].[Id] = [c].[ParentId] +ORDER BY [p0].[Id] +"""); + } + + public override async Task Nullable_complex_type_with_discriminator_and_shadow_property() + { + await base.Nullable_complex_type_with_discriminator_and_shadow_property(); + + AssertSql( + """ +SELECT [e].[Id], [e].[CreatedBy], [e].[Prop_Discriminator], [e].[Prop_OptionalValue] +FROM [EntityType] AS [e] +"""); + } + + public override async Task Nullable_complex_type_with_discriminator_null_to_non_null_roundtrip() + { + await base.Nullable_complex_type_with_discriminator_null_to_non_null_roundtrip(); + } + + public override async Task Nullable_complex_type_with_discriminator_non_null_to_null_roundtrip() + { + await base.Nullable_complex_type_with_discriminator_non_null_to_null_roundtrip(); + } + + public override async Task Nullable_complex_type_with_discriminator_update_non_null_entity_roundtrip() + { + await base.Nullable_complex_type_with_discriminator_update_non_null_entity_roundtrip(); + } + + public override async Task Nullable_complex_type_with_discriminator_set_to_different_value() + { + await base.Nullable_complex_type_with_discriminator_set_to_different_value(); + } + + public override async Task Nullable_complex_type_with_discriminator_set_to_null() + { + await base.Nullable_complex_type_with_discriminator_set_to_null(); + } + + public override async Task Nested_nullable_complex_type_with_discriminator_null_to_non_null_roundtrip() + { + await base.Nested_nullable_complex_type_with_discriminator_null_to_non_null_roundtrip(); + } + + public override async Task Update_entity_with_nullable_complex_type_and_discriminator_does_not_throw() + { + await base.Update_entity_with_nullable_complex_type_and_discriminator_does_not_throw(); + } + + public override async Task Can_query_by_complex_type_property_with_index() + { + await base.Can_query_by_complex_type_property_with_index(); + + AssertSql( + """ +SELECT TOP(2) [p].[Id_Id], [p].[Address_City], [p].[Address_PostalCode] +FROM [Person] AS [p] +WHERE [p].[Address_City] = N'Seattle' +"""); + } + + public override async Task Can_update_entity_with_index_on_complex_type_property() + { + await base.Can_update_entity_with_index_on_complex_type_property(); + + AssertSql( + """ +SELECT TOP(2) [p].[Id_Id], [p].[Address_City], [p].[Address_PostalCode] +FROM [Person] AS [p] +""", + // + """ +@p1='1' +@p0='98102' (Nullable = false) (Size = 450) + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +UPDATE [Person] SET [Address_PostalCode] = @p0 +OUTPUT 1 +WHERE [Id_Id] = @p1; +""", + // + """ +SELECT TOP(2) [p].[Id_Id], [p].[Address_City], [p].[Address_PostalCode] +FROM [Person] AS [p] +"""); + } + + public override async Task Can_delete_entity_with_index_on_complex_type_property() + { + await base.Can_delete_entity_with_index_on_complex_type_property(); + + AssertSql( + """ +SELECT TOP(2) [p].[Id_Id], [p].[Address_City], [p].[Address_PostalCode] +FROM [Person] AS [p] +""", + // + """ +@p0='1' + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +DELETE FROM [Person] +OUTPUT 1 +WHERE [Id_Id] = @p0; +""", + // + """ +SELECT COUNT(*) +FROM [Person] AS [p] +"""); + } + + public override async Task Can_query_by_alternate_key_on_complex_type_property() + { + await base.Can_query_by_alternate_key_on_complex_type_property(); + + AssertSql( + """ +SELECT TOP(2) [p].[Id_Id], [p].[Address_City], [p].[Address_PostalCode] +FROM [Person] AS [p] +WHERE [p].[Address_City] = N'Redmond' +"""); + } + + public override async Task Can_save_batch_swapping_alternate_key_values_on_complex_type_property() + { + await base.Can_save_batch_swapping_alternate_key_values_on_complex_type_property(); + + AssertSql( + """ +SELECT [p].[Id_Id], [p].[Address_City], [p].[Address_PostalCode] +FROM [Person] AS [p] +ORDER BY [p].[Id_Id] +""", + // + """ +@p1='1' +@p0='98103' (Nullable = false) (Size = 450) +@p3='2' +@p2='98054' (Nullable = false) (Size = 450) + +SET NOCOUNT ON; +UPDATE [Person] SET [Address_PostalCode] = @p0 +OUTPUT 1 +WHERE [Id_Id] = @p1; +UPDATE [Person] SET [Address_PostalCode] = @p2 +OUTPUT 1 +WHERE [Id_Id] = @p3; +""", + // + """ +SELECT [p].[Id_Id], [p].[Address_City], [p].[Address_PostalCode] +FROM [Person] AS [p] +ORDER BY [p].[Id_Id] +"""); + } + + public override async Task Complex_json_collection_inside_left_join_subquery() + { + await base.Complex_json_collection_inside_left_join_subquery(); + + AssertSql( + """ +SELECT [p].[Id], [p].[ChildId], [c0].[Id], [c0].[IsPublic], [c0].[c] +FROM [Parent] AS [p] +LEFT JOIN ( + SELECT [c].[Id], [c].[IsPublic], [c].[CareNeeds] AS [c] + FROM [Child] AS [c] + WHERE [c].[IsPublic] = CAST(1 AS bit) +) AS [c0] ON [p].[ChildId] = [c0].[Id] +"""); + } + + #endregion Non-shared test resources + [Fact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPCManyToManyNoTrackingQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPCManyToManyNoTrackingQuerySqlServerTest.cs index 9f9b8eaed08..39e6d6a9185 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPCManyToManyNoTrackingQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPCManyToManyNoTrackingQuerySqlServerTest.cs @@ -2781,6 +2781,93 @@ FROM [UnidirectionalLeaves] AS [u1] """); } + public override async Task SelectMany_with_collection_selector_having_subquery() + { + await base.SelectMany_with_collection_selector_having_subquery(); + + AssertSql( + """ +SELECT [u].[Id] AS [UserId], [s].[Id] AS [OrgId] +FROM [Users] AS [u] +CROSS JOIN ( + SELECT [o1].[Id] + FROM ( + SELECT 1 AS empty + ) AS [e] + LEFT JOIN ( + SELECT [o].[Id] + FROM [Organisations] AS [o] + WHERE EXISTS ( + SELECT 1 + FROM [OrganisationUser] AS [o0] + WHERE [o].[Id] = [o0].[OrganisationId]) + ) AS [o1] ON 1 = 1 +) AS [s] +"""); + } + + public override async Task Many_to_many_load_works_when_join_entity_has_custom_key(bool async) + { + await base.Many_to_many_load_works_when_join_entity_has_custom_key(async); + + AssertSql( + """ +@p='1' + +SELECT TOP(1) [m].[Id] +FROM [ManyM_DB] AS [m] +WHERE [m].[Id] = @p +""", + // + """ +@p='1' + +SELECT [s].[Id], [m].[Id], [s].[Id0], [s0].[Id], [s0].[ManyM_Id], [s0].[ManyN_Id], [s0].[Id0] +FROM [ManyM_DB] AS [m] +INNER JOIN ( + SELECT [m1].[Id], [m0].[Id] AS [Id0], [m0].[ManyM_Id] + FROM [ManyMN_DB] AS [m0] + LEFT JOIN [ManyN_DB] AS [m1] ON [m0].[ManyN_Id] = [m1].[Id] +) AS [s] ON [m].[Id] = [s].[ManyM_Id] +LEFT JOIN ( + SELECT [m2].[Id], [m2].[ManyM_Id], [m2].[ManyN_Id], [m3].[Id] AS [Id0] + FROM [ManyMN_DB] AS [m2] + INNER JOIN [ManyM_DB] AS [m3] ON [m2].[ManyM_Id] = [m3].[Id] + WHERE [m3].[Id] = @p +) AS [s0] ON [s].[Id] = [s0].[ManyN_Id] +WHERE [m].[Id] = @p +ORDER BY [m].[Id], [s].[Id0] +""", + // + """ +@p='1' + +SELECT TOP(1) [m].[Id] +FROM [ManyN_DB] AS [m] +WHERE [m].[Id] = @p +""", + // + """ +@p='1' + +SELECT [s].[Id], [m].[Id], [s].[Id0], [s0].[Id], [s0].[ManyM_Id], [s0].[ManyN_Id], [s0].[Id0] +FROM [ManyN_DB] AS [m] +INNER JOIN ( + SELECT [m1].[Id], [m0].[Id] AS [Id0], [m0].[ManyN_Id] + FROM [ManyMN_DB] AS [m0] + INNER JOIN [ManyM_DB] AS [m1] ON [m0].[ManyM_Id] = [m1].[Id] +) AS [s] ON [m].[Id] = [s].[ManyN_Id] +LEFT JOIN ( + SELECT [m2].[Id], [m2].[ManyM_Id], [m2].[ManyN_Id], [m3].[Id] AS [Id0] + FROM [ManyMN_DB] AS [m2] + INNER JOIN [ManyN_DB] AS [m3] ON [m2].[ManyN_Id] = [m3].[Id] + WHERE [m3].[Id] = @p +) AS [s0] ON [s].[Id] = [s0].[ManyM_Id] +WHERE [m].[Id] = @p +ORDER BY [m].[Id], [s].[Id0] +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPCManyToManyQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPCManyToManyQuerySqlServerTest.cs index 78bc611dc0b..3508a93722a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPCManyToManyQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPCManyToManyQuerySqlServerTest.cs @@ -2805,6 +2805,93 @@ FROM [UnidirectionalLeaves] AS [u1] """); } + public override async Task SelectMany_with_collection_selector_having_subquery() + { + await base.SelectMany_with_collection_selector_having_subquery(); + + AssertSql( + """ +SELECT [u].[Id] AS [UserId], [s].[Id] AS [OrgId] +FROM [Users] AS [u] +CROSS JOIN ( + SELECT [o1].[Id] + FROM ( + SELECT 1 AS empty + ) AS [e] + LEFT JOIN ( + SELECT [o].[Id] + FROM [Organisations] AS [o] + WHERE EXISTS ( + SELECT 1 + FROM [OrganisationUser] AS [o0] + WHERE [o].[Id] = [o0].[OrganisationId]) + ) AS [o1] ON 1 = 1 +) AS [s] +"""); + } + + public override async Task Many_to_many_load_works_when_join_entity_has_custom_key(bool async) + { + await base.Many_to_many_load_works_when_join_entity_has_custom_key(async); + + AssertSql( + """ +@p='1' + +SELECT TOP(1) [m].[Id] +FROM [ManyM_DB] AS [m] +WHERE [m].[Id] = @p +""", + // + """ +@p='1' + +SELECT [s].[Id], [m].[Id], [s].[Id0], [s0].[Id], [s0].[ManyM_Id], [s0].[ManyN_Id], [s0].[Id0] +FROM [ManyM_DB] AS [m] +INNER JOIN ( + SELECT [m1].[Id], [m0].[Id] AS [Id0], [m0].[ManyM_Id] + FROM [ManyMN_DB] AS [m0] + LEFT JOIN [ManyN_DB] AS [m1] ON [m0].[ManyN_Id] = [m1].[Id] +) AS [s] ON [m].[Id] = [s].[ManyM_Id] +LEFT JOIN ( + SELECT [m2].[Id], [m2].[ManyM_Id], [m2].[ManyN_Id], [m3].[Id] AS [Id0] + FROM [ManyMN_DB] AS [m2] + INNER JOIN [ManyM_DB] AS [m3] ON [m2].[ManyM_Id] = [m3].[Id] + WHERE [m3].[Id] = @p +) AS [s0] ON [s].[Id] = [s0].[ManyN_Id] +WHERE [m].[Id] = @p +ORDER BY [m].[Id], [s].[Id0] +""", + // + """ +@p='1' + +SELECT TOP(1) [m].[Id] +FROM [ManyN_DB] AS [m] +WHERE [m].[Id] = @p +""", + // + """ +@p='1' + +SELECT [s].[Id], [m].[Id], [s].[Id0], [s0].[Id], [s0].[ManyM_Id], [s0].[ManyN_Id], [s0].[Id0] +FROM [ManyN_DB] AS [m] +INNER JOIN ( + SELECT [m1].[Id], [m0].[Id] AS [Id0], [m0].[ManyN_Id] + FROM [ManyMN_DB] AS [m0] + INNER JOIN [ManyM_DB] AS [m1] ON [m0].[ManyM_Id] = [m1].[Id] +) AS [s] ON [m].[Id] = [s].[ManyN_Id] +LEFT JOIN ( + SELECT [m2].[Id], [m2].[ManyM_Id], [m2].[ManyN_Id], [m3].[Id] AS [Id0] + FROM [ManyMN_DB] AS [m2] + INNER JOIN [ManyN_DB] AS [m3] ON [m2].[ManyN_Id] = [m3].[Id] + WHERE [m3].[Id] = @p +) AS [s0] ON [s].[Id] = [s0].[ManyM_Id] +WHERE [m].[Id] = @p +ORDER BY [m].[Id], [s].[Id0] +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPTManyToManyNoTrackingQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPTManyToManyNoTrackingQuerySqlServerTest.cs index 711037a3094..62a379284ec 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPTManyToManyNoTrackingQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPTManyToManyNoTrackingQuerySqlServerTest.cs @@ -2729,6 +2729,93 @@ FROM [UnidirectionalRoots] AS [u] """); } + public override async Task SelectMany_with_collection_selector_having_subquery() + { + await base.SelectMany_with_collection_selector_having_subquery(); + + AssertSql( + """ +SELECT [u].[Id] AS [UserId], [s].[Id] AS [OrgId] +FROM [Users] AS [u] +CROSS JOIN ( + SELECT [o1].[Id] + FROM ( + SELECT 1 AS empty + ) AS [e] + LEFT JOIN ( + SELECT [o].[Id] + FROM [Organisations] AS [o] + WHERE EXISTS ( + SELECT 1 + FROM [OrganisationUser] AS [o0] + WHERE [o].[Id] = [o0].[OrganisationId]) + ) AS [o1] ON 1 = 1 +) AS [s] +"""); + } + + public override async Task Many_to_many_load_works_when_join_entity_has_custom_key(bool async) + { + await base.Many_to_many_load_works_when_join_entity_has_custom_key(async); + + AssertSql( + """ +@p='1' + +SELECT TOP(1) [m].[Id] +FROM [ManyM_DB] AS [m] +WHERE [m].[Id] = @p +""", + // + """ +@p='1' + +SELECT [s].[Id], [m].[Id], [s].[Id0], [s0].[Id], [s0].[ManyM_Id], [s0].[ManyN_Id], [s0].[Id0] +FROM [ManyM_DB] AS [m] +INNER JOIN ( + SELECT [m1].[Id], [m0].[Id] AS [Id0], [m0].[ManyM_Id] + FROM [ManyMN_DB] AS [m0] + LEFT JOIN [ManyN_DB] AS [m1] ON [m0].[ManyN_Id] = [m1].[Id] +) AS [s] ON [m].[Id] = [s].[ManyM_Id] +LEFT JOIN ( + SELECT [m2].[Id], [m2].[ManyM_Id], [m2].[ManyN_Id], [m3].[Id] AS [Id0] + FROM [ManyMN_DB] AS [m2] + INNER JOIN [ManyM_DB] AS [m3] ON [m2].[ManyM_Id] = [m3].[Id] + WHERE [m3].[Id] = @p +) AS [s0] ON [s].[Id] = [s0].[ManyN_Id] +WHERE [m].[Id] = @p +ORDER BY [m].[Id], [s].[Id0] +""", + // + """ +@p='1' + +SELECT TOP(1) [m].[Id] +FROM [ManyN_DB] AS [m] +WHERE [m].[Id] = @p +""", + // + """ +@p='1' + +SELECT [s].[Id], [m].[Id], [s].[Id0], [s0].[Id], [s0].[ManyM_Id], [s0].[ManyN_Id], [s0].[Id0] +FROM [ManyN_DB] AS [m] +INNER JOIN ( + SELECT [m1].[Id], [m0].[Id] AS [Id0], [m0].[ManyN_Id] + FROM [ManyMN_DB] AS [m0] + INNER JOIN [ManyM_DB] AS [m1] ON [m0].[ManyM_Id] = [m1].[Id] +) AS [s] ON [m].[Id] = [s].[ManyN_Id] +LEFT JOIN ( + SELECT [m2].[Id], [m2].[ManyM_Id], [m2].[ManyN_Id], [m3].[Id] AS [Id0] + FROM [ManyMN_DB] AS [m2] + INNER JOIN [ManyN_DB] AS [m3] ON [m2].[ManyN_Id] = [m3].[Id] + WHERE [m3].[Id] = @p +) AS [s0] ON [s].[Id] = [s0].[ManyM_Id] +WHERE [m].[Id] = @p +ORDER BY [m].[Id], [s].[Id0] +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPTManyToManyQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPTManyToManyQuerySqlServerTest.cs index 34c2d81cef1..a8133a47440 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPTManyToManyQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPTManyToManyQuerySqlServerTest.cs @@ -2752,6 +2752,93 @@ FROM [UnidirectionalRoots] AS [u] """); } + public override async Task SelectMany_with_collection_selector_having_subquery() + { + await base.SelectMany_with_collection_selector_having_subquery(); + + AssertSql( + """ +SELECT [u].[Id] AS [UserId], [s].[Id] AS [OrgId] +FROM [Users] AS [u] +CROSS JOIN ( + SELECT [o1].[Id] + FROM ( + SELECT 1 AS empty + ) AS [e] + LEFT JOIN ( + SELECT [o].[Id] + FROM [Organisations] AS [o] + WHERE EXISTS ( + SELECT 1 + FROM [OrganisationUser] AS [o0] + WHERE [o].[Id] = [o0].[OrganisationId]) + ) AS [o1] ON 1 = 1 +) AS [s] +"""); + } + + public override async Task Many_to_many_load_works_when_join_entity_has_custom_key(bool async) + { + await base.Many_to_many_load_works_when_join_entity_has_custom_key(async); + + AssertSql( + """ +@p='1' + +SELECT TOP(1) [m].[Id] +FROM [ManyM_DB] AS [m] +WHERE [m].[Id] = @p +""", + // + """ +@p='1' + +SELECT [s].[Id], [m].[Id], [s].[Id0], [s0].[Id], [s0].[ManyM_Id], [s0].[ManyN_Id], [s0].[Id0] +FROM [ManyM_DB] AS [m] +INNER JOIN ( + SELECT [m1].[Id], [m0].[Id] AS [Id0], [m0].[ManyM_Id] + FROM [ManyMN_DB] AS [m0] + LEFT JOIN [ManyN_DB] AS [m1] ON [m0].[ManyN_Id] = [m1].[Id] +) AS [s] ON [m].[Id] = [s].[ManyM_Id] +LEFT JOIN ( + SELECT [m2].[Id], [m2].[ManyM_Id], [m2].[ManyN_Id], [m3].[Id] AS [Id0] + FROM [ManyMN_DB] AS [m2] + INNER JOIN [ManyM_DB] AS [m3] ON [m2].[ManyM_Id] = [m3].[Id] + WHERE [m3].[Id] = @p +) AS [s0] ON [s].[Id] = [s0].[ManyN_Id] +WHERE [m].[Id] = @p +ORDER BY [m].[Id], [s].[Id0] +""", + // + """ +@p='1' + +SELECT TOP(1) [m].[Id] +FROM [ManyN_DB] AS [m] +WHERE [m].[Id] = @p +""", + // + """ +@p='1' + +SELECT [s].[Id], [m].[Id], [s].[Id0], [s0].[Id], [s0].[ManyM_Id], [s0].[ManyN_Id], [s0].[Id0] +FROM [ManyN_DB] AS [m] +INNER JOIN ( + SELECT [m1].[Id], [m0].[Id] AS [Id0], [m0].[ManyN_Id] + FROM [ManyMN_DB] AS [m0] + INNER JOIN [ManyM_DB] AS [m1] ON [m0].[ManyM_Id] = [m1].[Id] +) AS [s] ON [m].[Id] = [s].[ManyN_Id] +LEFT JOIN ( + SELECT [m2].[Id], [m2].[ManyM_Id], [m2].[ManyN_Id], [m3].[Id] AS [Id0] + FROM [ManyMN_DB] AS [m2] + INNER JOIN [ManyN_DB] AS [m3] ON [m2].[ManyN_Id] = [m3].[Id] + WHERE [m3].[Id] = @p +) AS [s0] ON [s].[Id] = [s0].[ManyM_Id] +WHERE [m].[Id] = @p +ORDER BY [m].[Id], [s].[Id0] +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryJsonTypeSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryJsonTypeSqlServerTest.cs index 2bd932d56d0..4eebafe5734 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryJsonTypeSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryJsonTypeSqlServerTest.cs @@ -3,6 +3,8 @@ #nullable disable using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore.Diagnostics.Internal; +using Microsoft.EntityFrameworkCore.SqlServer.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.TestModels.JsonQuery; using Xunit.Sdk; @@ -3463,4 +3465,943 @@ FROM [JsonEntitiesBasic] AS [j] private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + #region Non-shared test resources + + protected override void ConfigureWarnings(WarningsConfigurationBuilder builder) + { + base.ConfigureWarnings(builder); + + builder.Log(CoreEventId.StringEnumValueInJson); + } + + public override async Task Project_root_with_missing_scalars(bool async) + { + await base.Project_root_with_missing_scalars(async); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], [e].[Collection], [e].[OptionalReference], [e].[RequiredReference] +FROM [Entities] AS [e] +WHERE [e].[Id] < 4 +"""); + } + + public override async Task Project_top_level_json_entity_with_missing_scalars(bool async) + { + await base.Project_top_level_json_entity_with_missing_scalars(async); + + AssertSql( + """ +SELECT [e].[Id], [e].[OptionalReference], [e].[RequiredReference], [e].[Collection] +FROM [Entities] AS [e] +WHERE [e].[Id] < 4 +"""); + } + + public override async Task Project_nested_json_entity_with_missing_scalars(bool async) + { + await base.Project_nested_json_entity_with_missing_scalars(async); + + AssertSql( + """ +SELECT [e].[Id], JSON_QUERY([e].[OptionalReference], '$.NestedOptionalReference'), JSON_QUERY([e].[RequiredReference], '$.NestedRequiredReference'), JSON_QUERY([e].[Collection], '$[0].NestedCollection') +FROM [Entities] AS [e] +WHERE [e].[Id] < 4 +"""); + } + + public override async Task Project_root_entity_with_missing_required_navigation(bool async) + { + await base.Project_root_entity_with_missing_required_navigation(async); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], [e].[Collection], [e].[OptionalReference], [e].[RequiredReference] +FROM [Entities] AS [e] +WHERE [e].[Id] = 5 +"""); + } + + public override async Task Project_missing_required_navigation(bool async) + { + await base.Project_missing_required_navigation(async); + + AssertSql( + """ +SELECT JSON_QUERY([e].[RequiredReference], '$.NestedRequiredReference'), [e].[Id] +FROM [Entities] AS [e] +WHERE [e].[Id] = 5 +"""); + } + + public override async Task Project_root_entity_with_null_required_navigation(bool async) + { + await base.Project_root_entity_with_null_required_navigation(async); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], [e].[Collection], [e].[OptionalReference], [e].[RequiredReference] +FROM [Entities] AS [e] +WHERE [e].[Id] = 6 +"""); + } + + public override async Task Project_null_required_navigation(bool async) + { + await base.Project_null_required_navigation(async); + + AssertSql( + """ +SELECT [e].[RequiredReference], [e].[Id] +FROM [Entities] AS [e] +WHERE [e].[Id] = 6 +"""); + } + + public override async Task Project_missing_required_scalar(bool async) + { + await base.Project_missing_required_scalar(async); + + switch (JsonColumnType) + { + case "json": + AssertSql( + """ +SELECT [e].[Id], JSON_VALUE([e].[RequiredReference], '$.Number' RETURNING float) AS [Number] +FROM [Entities] AS [e] +WHERE [e].[Id] = 2 +"""); + break; + case "nvarchar(max)": + AssertSql( + """ +SELECT [e].[Id], CAST(JSON_VALUE([e].[RequiredReference], '$.Number') AS float) AS [Number] +FROM [Entities] AS [e] +WHERE [e].[Id] = 2 +"""); + break; + default: + throw new UnreachableException(); + } + } + + public override async Task Project_null_required_scalar(bool async) + { + await base.Project_null_required_scalar(async); + + switch (JsonColumnType) + { + case "json": + AssertSql( + """ +SELECT [e].[Id], JSON_VALUE([e].[RequiredReference], '$.Number' RETURNING float) AS [Number] +FROM [Entities] AS [e] +WHERE [e].[Id] = 4 +"""); + break; + case "nvarchar(max)": + AssertSql( + """ +SELECT [e].[Id], CAST(JSON_VALUE([e].[RequiredReference], '$.Number') AS float) AS [Number] +FROM [Entities] AS [e] +WHERE [e].[Id] = 4 +"""); + break; + default: + throw new UnreachableException(); + } + } + + protected override async Task Seed21006(Context21006 context) + { + await base.Seed21006(context); + + // missing scalar on top level + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ([Collection], [OptionalReference], [RequiredReference], [Id], [Name]) +VALUES ( +'[{"Text":"e2 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 nrr"}},{"Text":"e2 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 nrr"}}]', +'{"Text":"e2 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 or nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 or nrr"}}', +'{"Text":"e2 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 rr nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 rr nrr"}}', +2, +'e2') +"""); + + // missing scalar on nested level + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ([Collection], [OptionalReference], [RequiredReference], [Id], [Name]) +VALUES ( +'[{"Number":7,"Text":"e3 c1","NestedCollection":[{"Text":"e3 c1 c1"},{"Text":"e3 c1 c2"}],"NestedOptionalReference":{"Text":"e3 c1 nor"},"NestedRequiredReference":{"Text":"e3 c1 nrr"}},{"Number":7,"Text":"e3 c2","NestedCollection":[{"Text":"e3 c2 c1"},{"Text":"e3 c2 c2"}],"NestedOptionalReference":{"Text":"e3 c2 nor"},"NestedRequiredReference":{"Text":"e3 c2 nrr"}}]', +'{"Number":7,"Text":"e3 or","NestedCollection":[{"Text":"e3 or c1"},{"Text":"e3 or c2"}],"NestedOptionalReference":{"Text":"e3 or nor"},"NestedRequiredReference":{"Text":"e3 or nrr"}}', +'{"Number":7,"Text":"e3 rr","NestedCollection":[{"Text":"e3 rr c1"},{"Text":"e3 rr c2"}],"NestedOptionalReference":{"Text":"e3 rr nor"},"NestedRequiredReference":{"Text":"e3 rr nrr"}}', +3, +'e3') +"""); + + // null scalar on top level + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ([Collection], [OptionalReference], [RequiredReference], [Id], [Name]) +VALUES ( +'[{"Number":null,"Text":"e4 c1","NestedCollection":[{"Text":"e4 c1 c1"},{"Text":"e4 c1 c2"}],"NestedOptionalReference":{"Text":"e4 c1 nor"},"NestedRequiredReference":{"Text":"e4 c1 nrr"}},{"Number":null,"Text":"e4 c2","NestedCollection":[{"Text":"e4 c2 c1"},{"Text":"e4 c2 c2"}],"NestedOptionalReference":{"Text":"e4 c2 nor"},"NestedRequiredReference":{"Text":"e4 c2 nrr"}}]', +'{"Number":null,"Text":"e4 or","NestedCollection":[{"Text":"e4 or c1"},{"Text":"e4 or c2"}],"NestedOptionalReference":{"Text":"e4 or nor"},"NestedRequiredReference":{"Text":"e4 or nrr"}}', +'{"Number":null,"Text":"e4 rr","NestedCollection":[{"Text":"e4 rr c1"},{"Text":"e4 rr c2"}],"NestedOptionalReference":{"Text":"e4 rr nor"},"NestedRequiredReference":{"Text":"e4 rr nrr"}}', +4, +'e4') +"""); + + // missing required navigation + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ([Collection], [OptionalReference], [RequiredReference], [Id], [Name]) +VALUES ( +'[{"Number":7,"Text":"e5 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 nor"}},{"Number":7,"Text":"e5 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 nor"}}]', +'{"Number":7,"Text":"e5 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 or nor"}}', +'{"Number":7,"Text":"e5 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 rr nor"}}', +5, +'e5') +"""); + + // null required navigation + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ([Collection], [OptionalReference], [RequiredReference], [Id], [Name]) +VALUES ( +'[{"Number":7,"Text":"e6 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 nor"},"NestedRequiredReference":null},{"Number":7,"Text":"e6 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 nor"},"NestedRequiredReference":null}]', +'{"Number":7,"Text":"e6 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 or nor"},"NestedRequiredReference":null}', +'{"Number":7,"Text":"e6 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 rr nor"},"NestedRequiredReference":null}', +6, +'e6') +"""); + } + + protected override async Task Seed29219(DbContext ctx) + { + await base.Seed29219(ctx); + + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Reference], [Collection]) +VALUES(3, '{ "NonNullableScalar" : 30 }', '[{ "NonNullableScalar" : 10001 }]') +"""); + } + + protected override async Task Seed30028(DbContext ctx) + { + // complete + await ctx.Database.ExecuteSqlAsync( + $$$$""" +INSERT INTO [Entities] ([Id], [Json]) +VALUES( +1, +'{"RootName":"e1","Collection":[{"BranchName":"e1 c1","Nested":{"LeafName":"e1 c1 l"}},{"BranchName":"e1 c2","Nested":{"LeafName":"e1 c2 l"}}],"OptionalReference":{"BranchName":"e1 or","Nested":{"LeafName":"e1 or l"}},"RequiredReference":{"BranchName":"e1 rr","Nested":{"LeafName":"e1 rr l"}}}') +"""); + + // missing collection + await ctx.Database.ExecuteSqlAsync( + $$$$""" +INSERT INTO [Entities] ([Id], [Json]) +VALUES( +2, +'{"RootName":"e2","OptionalReference":{"BranchName":"e2 or","Nested":{"LeafName":"e2 or l"}},"RequiredReference":{"BranchName":"e2 rr","Nested":{"LeafName":"e2 rr l"}}}') +"""); + + // missing optional reference + await ctx.Database.ExecuteSqlAsync( + $$$$""" +INSERT INTO [Entities] ([Id], [Json]) +VALUES( +3, +'{"RootName":"e3","Collection":[{"BranchName":"e3 c1","Nested":{"LeafName":"e3 c1 l"}},{"BranchName":"e3 c2","Nested":{"LeafName":"e3 c2 l"}}],"RequiredReference":{"BranchName":"e3 rr","Nested":{"LeafName":"e3 rr l"}}}') +"""); + + // missing required reference + await ctx.Database.ExecuteSqlAsync( + $$$$""" +INSERT INTO [Entities] ([Id], [Json]) +VALUES( +4, +'{"RootName":"e4","Collection":[{"BranchName":"e4 c1","Nested":{"LeafName":"e4 c1 l"}},{"BranchName":"e4 c2","Nested":{"LeafName":"e4 c2 l"}}],"OptionalReference":{"BranchName":"e4 or","Nested":{"LeafName":"e4 or l"}}}') +"""); + } + + protected override Task Seed33046(DbContext ctx) + => ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Reviews] ([Rounds], [Id]) +VALUES('[{"RoundNumber":11,"SubRounds":[{"SubRoundNumber":111},{"SubRoundNumber":112}]}]', 1) +"""); + + protected override async Task Seed34960(Context34960 ctx) + { + await base.Seed34960(ctx); + + // JSON nulls + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Collection], [Reference], [Id]) +VALUES( +'null', +'null', +4) +"""); + + // JSON object where collection should be + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Junk] ([Collection], [Reference], [Id]) +VALUES( +'{ "DoB":"2000-01-01T00:00:00","Text":"junk" }', +NULL, +1) +"""); + + // JSON array where entity should be + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Junk] ([Collection], [Reference], [Id]) +VALUES( +NULL, +'[{ "DoB":"2000-01-01T00:00:00","Text":"junk" }]', +2) +"""); + } + + protected override Task SeedJunkInJson(DbContext ctx) + => ctx.Database.ExecuteSqlAsync( + $$$$""" +INSERT INTO [Entities] ([Collection], [CollectionWithCtor], [Reference], [ReferenceWithCtor], [Id]) +VALUES( +'[{"JunkReference":{"Something":"SomeValue" },"Name":"c11","JunkProperty1":50,"Number":11.5,"JunkCollection1":[],"JunkCollection2":[{"Foo":"junk value"}],"NestedCollection":[{"DoB":"2002-04-01T00:00:00","DummyProp":"Dummy value"},{"DoB":"2002-04-02T00:00:00","DummyReference":{"Foo":5}}],"NestedReference":{"DoB":"2002-03-01T00:00:00"}},{"Name":"c12","Number":12.5,"NestedCollection":[{"DoB":"2002-06-01T00:00:00"},{"DoB":"2002-06-02T00:00:00"}],"NestedDummy":59,"NestedReference":{"DoB":"2002-05-01T00:00:00"}}]', +'[{"MyBool":true,"Name":"c11 ctor","JunkReference":{"Something":"SomeValue","JunkCollection":[{"Foo":"junk value"}]},"NestedCollection":[{"DoB":"2002-08-01T00:00:00"},{"DoB":"2002-08-02T00:00:00"}],"NestedReference":{"DoB":"2002-07-01T00:00:00"}},{"MyBool":false,"Name":"c12 ctor","NestedCollection":[{"DoB":"2002-10-01T00:00:00"},{"DoB":"2002-10-02T00:00:00"}],"JunkCollection":[{"Foo":"junk value"}],"NestedReference":{"DoB":"2002-09-01T00:00:00"}}]', +'{"Name":"r1","JunkCollection":[{"Foo":"junk value"}],"JunkReference":{"Something":"SomeValue" },"Number":1.5,"NestedCollection":[{"DoB":"2000-02-01T00:00:00","JunkReference":{"Something":"SomeValue"}},{"DoB":"2000-02-02T00:00:00"}],"NestedReference":{"DoB":"2000-01-01T00:00:00"}}', +'{"MyBool":true,"JunkCollection":[{"Foo":"junk value"}],"Name":"r1 ctor","JunkReference":{"Something":"SomeValue" },"NestedCollection":[{"DoB":"2001-02-01T00:00:00"},{"DoB":"2001-02-02T00:00:00"}],"NestedReference":{"JunkCollection":[{"Foo":"junk value"}],"DoB":"2001-01-01T00:00:00"}}', +1) +"""); + + protected override Task SeedTrickyBuffering(DbContext ctx) + => ctx.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ([Reference], [Id]) +VALUES( +'{"Name": "r1", "Number": 7, "JunkReference":{"Something": "SomeValue" }, "JunkCollection": [{"Foo": "junk value"}], "NestedReference": {"DoB": "2000-01-01T00:00:00"}, "NestedCollection": [{"DoB": "2000-02-01T00:00:00", "JunkReference": {"Something": "SomeValue"}}, {"DoB": "2000-02-02T00:00:00"}]}',1) +"""); + + protected override Task SeedShadowProperties(DbContext ctx) + => ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Collection], [CollectionWithCtor], [Reference], [ReferenceWithCtor], [Id], [Name]) +VALUES( +'[{"Name":"e1_c1","ShadowDouble":5.5},{"ShadowDouble":20.5,"Name":"e1_c2"}]', +'[{"Name":"e1_c1 ctor","ShadowNullableByte":6},{"ShadowNullableByte":null,"Name":"e1_c2 ctor"}]', +'{"Name":"e1_r", "ShadowString":"Foo"}', +'{"ShadowInt":143,"Name":"e1_r ctor"}', +1, +'e1') +"""); + + protected override async Task SeedNotICollection(DbContext ctx) + { + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Json], [Id]) +VALUES( +'{"Collection":[{"Bar":11,"Foo":"c11"},{"Bar":12,"Foo":"c12"},{"Bar":13,"Foo":"c13"}]}', +1) +"""); + + await ctx.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ([Json], [Id]) +VALUES( +'{"Collection":[{"Bar":21,"Foo":"c21"},{"Bar":22,"Foo":"c22"}]}', +2) +"""); + } + + protected override async Task SeedBadJsonProperties(ContextBadJsonProperties ctx) + { + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Scenario], [OptionalReference], [RequiredReference], [Collection]) +VALUES( +1, +'baseline', +'{"NestedOptional": { "Text":"or no" }, "NestedRequired": { "Text":"or nr" }, "NestedCollection": [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ] }', +'{"NestedOptional": { "Text":"rr no" }, "NestedRequired": { "Text":"rr nr" }, "NestedCollection": [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ] }', +'[ +{"NestedOptional": { "Text":"c 1 no" }, "NestedRequired": { "Text":"c 1 nr" }, "NestedCollection": [ { "Text":"c 1 nc 1" }, { "Text":"c 1 nc 2" } ] }, +{"NestedOptional": { "Text":"c 2 no" }, "NestedRequired": { "Text":"c 2 nr" }, "NestedCollection": [ { "Text":"c 2 nc 1" }, { "Text":"c 2 nc 2" } ] } +]') +"""); + + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Scenario], [OptionalReference], [RequiredReference], [Collection]) +VALUES( +2, +'duplicated navigations', +'{"NestedOptional": { "Text":"or no" }, "NestedOptional": { "Text":"or no dupnav" }, "NestedRequired": { "Text":"or nr" }, "NestedCollection": [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ], "NestedCollection": [ { "Text":"or nc 1 dupnav" }, { "Text":"or nc 2 dupnav" } ], "NestedRequired": { "Text":"or nr dupnav" } }', +'{"NestedOptional": { "Text":"rr no" }, "NestedOptional": { "Text":"rr no dupnav" }, "NestedRequired": { "Text":"rr nr" }, "NestedCollection": [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ], "NestedCollection": [ { "Text":"rr nc 1 dupnav" }, { "Text":"rr nc 2 dupnav" } ], "NestedRequired": { "Text":"rr nr dupnav" } }', +'[ +{"NestedOptional": { "Text":"c 1 no" }, "NestedOptional": { "Text":"c 1 no dupnav" }, "NestedRequired": { "Text":"c 1 nr" }, "NestedCollection": [ { "Text":"c 1 nc 1" }, { "Text":"c 1 nc 2" } ], "NestedCollection": [ { "Text":"c 1 nc 1 dupnav" }, { "Text":"c 1 nc 2 dupnav" } ], "NestedRequired": { "Text":"c 1 nr dupnav" } }, +{"NestedOptional": { "Text":"c 2 no" }, "NestedOptional": { "Text":"c 2 no dupnav" }, "NestedRequired": { "Text":"c 2 nr" }, "NestedCollection": [ { "Text":"c 2 nc 1" }, { "Text":"c 2 nc 2" } ], "NestedCollection": [ { "Text":"c 2 nc 1 dupnav" }, { "Text":"c 2 nc 2 dupnav" } ], "NestedRequired": { "Text":"c 2 nr dupnav" } } +]') +"""); + + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Scenario], [OptionalReference], [RequiredReference], [Collection]) +VALUES( +3, +'duplicated scalars', +'{"NestedOptional": { "Text":"or no", "Text":"or no dupprop" }, "NestedRequired": { "Text":"or nr", "Text":"or nr dupprop" }, "NestedCollection": [ { "Text":"or nc 1", "Text":"or nc 1 dupprop" }, { "Text":"or nc 2", "Text":"or nc 2 dupprop" } ] }', +'{"NestedOptional": { "Text":"rr no", "Text":"rr no dupprop" }, "NestedRequired": { "Text":"rr nr", "Text":"rr nr dupprop" }, "NestedCollection": [ { "Text":"rr nc 1", "Text":"rr nc 1 dupprop" }, { "Text":"rr nc 2", "Text":"rr nc 2 dupprop" } ] }', +'[ +{"NestedOptional": { "Text":"c 1 no", "Text":"c 1 no dupprop" }, "NestedRequired": { "Text":"c 1 nr", "Text":"c 1 nr dupprop" }, "NestedCollection": [ { "Text":"c 1 nc 1", "Text":"c 1 nc 1 dupprop" }, { "Text":"c 1 nc 2", "Text":"c 1 nc 2 dupprop" } ] }, +{"NestedOptional": { "Text":"c 2 no", "Text":"c 2 no dupprop" }, "NestedRequired": { "Text":"c 2 nr", "Text":"c 2 nr dupprop" }, "NestedCollection": [ { "Text":"c 2 nc 1", "Text":"c 2 nc 1 dupprop" }, { "Text":"c 2 nc 2", "Text":"c 2 nc 2 dupprop" } ] } +]') +"""); + + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Scenario], [OptionalReference], [RequiredReference], [Collection]) +VALUES( +4, +'empty navigation property names', +'{"": { "Text":"or no" }, "": { "Text":"or nr" }, "": [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ] }', +'{"": { "Text":"rr no" }, "": { "Text":"rr nr" }, "": [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ] }', +'[ +{"": { "Text":"c 1 no" }, "": { "Text":"c 1 nr" }, "": [ { "Text":"c 1 nc 1" }, { "Text":"c 1 nc 2" } ] }, +{"": { "Text":"c 2 no" }, "": { "Text":"c 2 nr" }, "": [ { "Text":"c 2 nc 1" }, { "Text":"c 2 nc 2" } ] } +]') +"""); + + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Scenario], [OptionalReference], [RequiredReference], [Collection]) +VALUES( +5, +'empty scalar property names', +'{"NestedOptional": { "":"or no" }, "NestedRequired": { "":"or nr" }, "NestedCollection": [ { "":"or nc 1" }, { "":"or nc 2" } ] }', +'{"NestedOptional": { "":"rr no" }, "NestedRequired": { "":"rr nr" }, "NestedCollection": [ { "":"rr nc 1" }, { "":"rr nc 2" } ] }', +'[ +{"NestedOptional": { "":"c 1 no" }, "NestedRequired": { "":"c 1 nr" }, "NestedCollection": [ { "":"c 1 nc 1" }, { "":"c 1 nc 2" } ] }, +{"NestedOptional": { "":"c 2 no" }, "NestedRequired": { "":"c 2 nr" }, "NestedCollection": [ { "":"c 2 nc 1" }, { "":"c 2 nc 2" } ] } +]') +"""); + + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Scenario], [OptionalReference], [RequiredReference], [Collection]) +VALUES( +10, +'null navigation property names', +'{null: { "Text":"or no" }, null: { "Text":"or nr" }, null: [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ] }', +'{null: { "Text":"rr no" }, null: { "Text":"rr nr" }, null: [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ] }', +'[ +{null: { "Text":"c 1 no" }, null: { "Text":"c 1 nr" }, null: [ { "Text":"c 1 nc 1" }, { "Text":"c 1 nc 2" } ] }, +{null: { "Text":"c 2 no" }, null: { "Text":"c 2 nr" }, null: [ { "Text":"c 2 nc 1" }, { "Text":"c 2 nc 2" } ] } +]') +"""); + + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Scenario], [OptionalReference], [RequiredReference], [Collection]) +VALUES( +11, +'null scalar property names', +'{"NestedOptional": { null:"or no", "Text":"or no nonnull" }, "NestedRequired": { null:"or nr", "Text":"or nr nonnull" }, "NestedCollection": [ { null:"or nc 1", "Text":"or nc 1 nonnull" }, { null:"or nc 2", "Text":"or nc 2 nonnull" } ] }', +'{"NestedOptional": { null:"rr no", "Text":"rr no nonnull" }, "NestedRequired": { null:"rr nr", "Text":"rr nr nonnull" }, "NestedCollection": [ { null:"rr nc 1", "Text":"rr nc 1 nonnull" }, { null:"rr nc 2", "Text":"rr nc 2 nonnull" } ] }', +'[ +{"NestedOptional": { null:"c 1 no", "Text":"c 1 no nonnull" }, "NestedRequired": { null:"c 1 nr", "Text":"c 1 nr nonnull" }, "NestedCollection": [ { null:"c 1 nc 1", "Text":"c 1 nc 1 nonnull" }, { null:"c 1 nc 2", "Text":"c 1 nc 2 nonnull" } ] }, +{"NestedOptional": { null:"c 2 no", "Text":"c 2 no nonnull" }, "NestedRequired": { null:"c 2 nr", "Text":"c 2 nr nonnull" }, "NestedCollection": [ { null:"c 2 nc 1", "Text":"c 2 nc 1 nonnull" }, { null:"c 2 nc 2", "Text":"c 2 nc 2 nonnull" } ] } +]') +"""); + } + + #region JsonPropertyWithConverters + + [Fact] + public virtual async Task Materialize_json_null_property_with_converters() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingJsonPropertyWithConverters, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedJsonPropertyWithConverters); + + using var context = contextFactory.CreateDbContext(); + var result = await context.Set().SingleAsync(x => x.Id == 1); + + Assert.Equal("e1", result.Reference.Name); + + // Both properties have a JSON 'null' token value. The streaming JSON shaper's null guard + // (CreateReadJsonPropertyValueExpression) routes the null based on the converter's ConvertsNulls flag: + // - ConvertsNulls == true -> ConvertFromProvider(default) maps DB null to the "FROM_DB_NULL" sentinel. + // - ConvertsNulls == false -> returns default(string) (null) without invoking the converter. + Assert.Equal("FROM_DB_NULL", result.Reference.ConvertedHandlingNulls); + Assert.Null(result.Reference.ConvertedNotHandlingNulls); + } + + protected virtual async Task SeedJsonPropertyWithConverters(DbContext ctx) + => await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Reference]) +VALUES(1, '{"Name":"e1","ConvertedHandlingNulls":null,"ConvertedNotHandlingNulls":null}') +"""); + + protected virtual void OnModelCreatingJsonPropertyWithConverters(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.ToTable("Entities"); + b.Property(x => x.Id).ValueGeneratedNever(); + b.OwnsOne( + x => x.Reference, b => + { + b.ToJson().HasColumnType(JsonColumnType); + b.Property(x => x.ConvertedHandlingNulls).HasConversion( + new ValueConverter( + v => v, + v => v ?? "FROM_DB_NULL", + convertsNulls: true)); + b.Property(x => x.ConvertedNotHandlingNulls).HasConversion( + new ValueConverter( + v => v, + v => "FROM_CONVERTER:" + v, + convertsNulls: false)); + }); + }); + + protected class ContextJsonPropertyWithConverters(DbContextOptions options) : DbContext(options) + { + public class MyEntity + { + public int Id { get; set; } + public MyJsonEntity Reference { get; set; } + } + + public class MyJsonEntity + { + public string Name { get; set; } + public string ConvertedHandlingNulls { get; set; } + public string ConvertedNotHandlingNulls { get; set; } + } + } + + #endregion + + #region PrimitiveCollectionInJson + + [Fact] + public virtual async Task Materialize_json_null_optional_primitive_collection_in_json_is_null() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingPrimitiveCollectionInJson, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedPrimitiveCollectionInJson); + + using var context = contextFactory.CreateDbContext(); + var result = await context.Set() + .Where(x => x.Id < 3).OrderBy(x => x.Id).ToListAsync(); + + Assert.Equal(2, result.Count); + + // Id == 1: JSON has "NullableStrings": null -> materialized as null. + Assert.Null(result[0].Reference.NullableStrings); + Assert.Equal(["a", "b"], result[0].Reference.RequiredStrings); + + // Id == 2: JSON has "NullableStrings": ["x", "y"]. + Assert.Equal(["x", "y"], result[1].Reference.NullableStrings); + Assert.Equal(["c", "d"], result[1].Reference.RequiredStrings); + } + + [Fact] + public virtual async Task Materialize_json_null_required_primitive_collection_in_json_throws() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingPrimitiveCollectionInJson, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedPrimitiveCollectionInJson); + + using var context = contextFactory.CreateDbContext(); + + // A required primitive collection nested in a JSON document whose value is a 'null' token yields a clear, + // property-named error rather than the cryptic reader/writer "Invalid token type: 'Null'". + var exception = await Assert.ThrowsAsync(() + => context.Set().Where(x => x.Id == 3).ToListAsync()); + + Assert.Equal(RelationalStrings.NullValueInRequiredJsonProperty("RequiredStrings"), exception.Message); + } + + protected virtual async Task SeedPrimitiveCollectionInJson(DbContext ctx) + { + // optional collection is JSON null + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Reference]) +VALUES(1, '{"NullableStrings":null,"RequiredStrings":["a","b"]}') +"""); + + // optional collection has a value + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Reference]) +VALUES(2, '{"NullableStrings":["x","y"],"RequiredStrings":["c","d"]}') +"""); + + // required collection is JSON null (materialization throws) + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Reference]) +VALUES(3, '{"NullableStrings":["x","y"],"RequiredStrings":null}') +"""); + } + + protected virtual void OnModelCreatingPrimitiveCollectionInJson(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.ToTable("Entities"); + b.Property(x => x.Id).ValueGeneratedNever(); + b.OwnsOne( + x => x.Reference, b => + { + b.ToJson().HasColumnType(JsonColumnType); + b.PrimitiveCollection(x => x.NullableStrings).IsRequired(false); + b.PrimitiveCollection(x => x.RequiredStrings).IsRequired(); + }); + }); + + protected class ContextPrimitiveCollectionInJson(DbContextOptions options) : DbContext(options) + { + public class MyEntity + { + public int Id { get; set; } + public MyJsonEntity Reference { get; set; } + } + + public class MyJsonEntity + { + public List NullableStrings { get; set; } + public List RequiredStrings { get; set; } + } + } + + #endregion + + #region EnumLegacyValues + + protected virtual async Task Read_enum_property_with_legacy_values_core(bool async) + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: BuildModelEnumLegacyValues, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedEnumLegacyValues); + + using var context = contextFactory.CreateDbContext(); + + var query = context.Set().Select(x => new + { + x.Reference.IntEnum, + x.Reference.ByteEnum, + x.Reference.LongEnum, + x.Reference.NullableEnum + }); + + if (async) + { + await query.ToListAsync(); + } + else + { + query.ToList(); + } + } + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual async Task Read_json_entity_with_enum_properties_with_legacy_values(bool async) + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: BuildModelEnumLegacyValues, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedEnumLegacyValues, + shouldLogCategory: c => c == DbLoggerCategory.Query.Name); + + using (var context = contextFactory.CreateDbContext()) + { + var query = context.Set().Select(x => x.Reference).AsNoTracking(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Equal(1, result.Count); + Assert.Equal(ByteEnumLegacyValues.Redmond, result[0].ByteEnum); + Assert.Equal(IntEnumLegacyValues.Foo, result[0].IntEnum); + Assert.Equal(LongEnumLegacyValues.Three, result[0].LongEnum); + Assert.Equal(ULongEnumLegacyValues.Three, result[0].ULongEnum); + Assert.Equal(IntEnumLegacyValues.Bar, result[0].NullableEnum); + } + + var testLogger = new TestLogger(); + Assert.Single( + ListLoggerFactory.Log, + l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(ByteEnumLegacyValues))); + Assert.Single( + ListLoggerFactory.Log, + l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(IntEnumLegacyValues))); + Assert.Single( + ListLoggerFactory.Log, + l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(LongEnumLegacyValues))); + Assert.Single( + ListLoggerFactory.Log, + l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(ULongEnumLegacyValues))); + } + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual async Task Read_json_entity_collection_with_enum_properties_with_legacy_values(bool async) + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: BuildModelEnumLegacyValues, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedEnumLegacyValues, + shouldLogCategory: c => c == DbLoggerCategory.Query.Name); + + using (var context = contextFactory.CreateDbContext()) + { + var query = context.Set().Select(x => x.Collection).AsNoTracking(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Equal(1, result.Count); + Assert.Equal(2, result[0].Count); + Assert.Equal(ByteEnumLegacyValues.Bellevue, result[0][0].ByteEnum); + Assert.Equal(IntEnumLegacyValues.Foo, result[0][0].IntEnum); + Assert.Equal(LongEnumLegacyValues.One, result[0][0].LongEnum); + Assert.Equal(ULongEnumLegacyValues.One, result[0][0].ULongEnum); + Assert.Equal(IntEnumLegacyValues.Bar, result[0][0].NullableEnum); + Assert.Equal(ByteEnumLegacyValues.Seattle, result[0][1].ByteEnum); + Assert.Equal(IntEnumLegacyValues.Baz, result[0][1].IntEnum); + Assert.Equal(LongEnumLegacyValues.Two, result[0][1].LongEnum); + Assert.Equal(ULongEnumLegacyValues.Two, result[0][1].ULongEnum); + Assert.Null(result[0][1].NullableEnum); + } + + var testLogger = new TestLogger(); + Assert.Single( + ListLoggerFactory.Log, + l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(ByteEnumLegacyValues))); + Assert.Single( + ListLoggerFactory.Log, + l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(IntEnumLegacyValues))); + Assert.Single( + ListLoggerFactory.Log, + l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(LongEnumLegacyValues))); + Assert.Single( + ListLoggerFactory.Log, + l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(ULongEnumLegacyValues))); + } + + private Task SeedEnumLegacyValues(DbContext ctx) + => ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Collection], [Reference], [Id], [Name]) +VALUES( +N'[{"ByteEnum":"Bellevue","IntEnum":"Foo","LongEnum":"One","ULongEnum":"One","Name":"e1_c1","NullableEnum":"Bar"},{"ByteEnum":"Seattle","IntEnum":"Baz","LongEnum":"Two","ULongEnum":"Two","Name":"e1_c2","NullableEnum":null}]', +N'{"ByteEnum":"Redmond","IntEnum":"Foo","LongEnum":"Three","ULongEnum":"Three","Name":"e1_r","NullableEnum":"Bar"}', +1, +N'e1') +"""); + + protected virtual void BuildModelEnumLegacyValues(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.ToTable("Entities"); + b.Property(x => x.Id).ValueGeneratedNever(); + b.OwnsOne(x => x.Reference, b => b.ToJson().HasColumnType(JsonColumnType)); + b.OwnsMany(x => x.Collection, b => b.ToJson().HasColumnType(JsonColumnType)); + }); + + private class MyEntityEnumLegacyValues + { + public int Id { get; set; } + public string Name { get; set; } + + public MyJsonEntityEnumLegacyValues Reference { get; set; } + public List Collection { get; set; } + } + + private class MyJsonEntityEnumLegacyValues + { + public string Name { get; set; } + + // ReSharper disable once UnusedAutoPropertyAccessor.Local + public IntEnumLegacyValues IntEnum { get; set; } + + // ReSharper disable once UnusedAutoPropertyAccessor.Local + public ByteEnumLegacyValues ByteEnum { get; set; } + + // ReSharper disable once UnusedAutoPropertyAccessor.Local + public LongEnumLegacyValues LongEnum { get; set; } + + // ReSharper disable once UnusedAutoPropertyAccessor.Local + public ULongEnumLegacyValues ULongEnum { get; set; } + + // ReSharper disable once UnusedAutoPropertyAccessor.Local + public IntEnumLegacyValues? NullableEnum { get; set; } + } + + private enum IntEnumLegacyValues + { + Foo = int.MinValue, + Bar, + Baz = int.MaxValue, + } + + private enum ByteEnumLegacyValues : byte + { + Seattle, + Redmond, + Bellevue = 255, + } + + private enum LongEnumLegacyValues : long + { + One = long.MinValue, + Two = 1, + Three = long.MaxValue, + } + + private enum ULongEnumLegacyValues : ulong + { + One = ulong.MinValue, + Two = 1, + Three = ulong.MaxValue, + } + + #endregion + + public override async Task Entity_splitting_with_owned_json() + { + await base.Entity_splitting_with_owned_json(); + + AssertSql( + """ +SELECT TOP(2) [m].[Id], [m].[PropertyInMainTable], [o].[PropertyInOtherTable], [m].[Json] +FROM [MyEntity] AS [m] +INNER JOIN [OtherTable] AS [o] ON [m].[Id] = [o].[Id] +"""); + } + + public override async Task SelectMany_over_primitive_collection_nested_in_complex_collection_inside_json_column() + { + await base.SelectMany_over_primitive_collection_nested_in_complex_collection_inside_json_column(); + + switch (JsonColumnType) + { + case "json": + AssertSql( + """ +SELECT [p].[value] +FROM [Cars] AS [c] +CROSS APPLY OPENJSON([c].[CarConfiguration], '$.optionPackages') WITH ( + [packageId] nvarchar(max) '$.packageId', + [partNumbers] json '$.partNumbers' AS JSON +) AS [o] +CROSS APPLY OPENJSON([o].[partNumbers]) WITH ([value] varchar(32) '$') AS [p] +WHERE [c].[Vin] = '1FA6P8TH8J5123456' AND [c].[DealerId] = 'DEALER-001' AND [o].[packageId] = N'PKG-SPORT' +"""); + break; + case "nvarchar(max)": + AssertSql( + """ +SELECT [p].[value] +FROM [Cars] AS [c] +CROSS APPLY OPENJSON([c].[CarConfiguration], '$.optionPackages') WITH ( + [packageId] nvarchar(max) '$.packageId', + [partNumbers] nvarchar(max) '$.partNumbers' AS JSON +) AS [o] +CROSS APPLY OPENJSON([o].[partNumbers]) WITH ([value] varchar(32) '$') AS [p] +WHERE [c].[Vin] = '1FA6P8TH8J5123456' AND [c].[DealerId] = 'DEALER-001' AND [o].[packageId] = N'PKG-SPORT' +"""); + break; + + default: + throw new UnreachableException(); + } + } + + public override async Task Value_converter_equality_null_scalar() + { + await base.Value_converter_equality_null_scalar(); + + AssertSql( + """ +@entity_equality_complexType='{"IntToString":"\u003Cnull\u003E"}' (Size = 34) + +SELECT COUNT(*) +FROM [Entities] AS [e] +WHERE CAST([e].[Json] AS nvarchar(max)) = CAST(@entity_equality_complexType AS nvarchar(max)) +"""); + } + + #region BadJsonProperties + + // When using the SQL Server JSON data type, insertion of the bad data fails thanks to SQL Server validation, + // unlike with tests mapping to nvarchar(max) where the bad JSON data is inserted correctly and then read. + + public override Task Bad_json_properties_duplicated_navigations(bool noTracking) + => Task.CompletedTask; + + public override Task Bad_json_properties_duplicated_scalars(bool noTracking) + => Task.CompletedTask; + + public override Task Bad_json_properties_empty_navigations(bool noTracking) + => Task.CompletedTask; + + public override Task Bad_json_properties_empty_scalars(bool noTracking) + => Task.CompletedTask; + + public override Task Bad_json_properties_null_navigations(bool noTracking) + => Task.CompletedTask; + + public override Task Bad_json_properties_null_scalars(bool noTracking) + => Task.CompletedTask; + + #endregion BadJsonProperties + + // SQL Server 2025 (CTP 2.1) does not support casting JSON scalar strings to json + // (CAST('8' AS json) and CAST('null' AS json) fail with "JSON text is not properly formatted"). + public override Task Project_entity_with_json_null_values() + => Assert.ThrowsAsync(base.Project_entity_with_json_null_values); + + // SQL Server 2025 (CTP 2.1) does not support casting JSON scalar strings to json + // (CAST('8' AS json) and CAST('null' AS json) fail with "JSON text is not properly formatted"). + // The base implementation expects a different exception. + public override Task Try_project_collection_but_JSON_is_entity() + => Assert.ThrowsAsync(base.Try_project_collection_but_JSON_is_entity); + + // SQL Server 2025 (CTP 2.1) does not support casting JSON scalar strings to json + // (CAST('8' AS json) and CAST('null' AS json) fail with "JSON text is not properly formatted"). + // The base implementation expects a different exception. + public override Task Try_project_reference_but_JSON_is_collection() + => Assert.ThrowsAsync(base.Try_project_reference_but_JSON_is_collection); + + // When using the SQL Server json type, we use JSON_VALUE() with the RETURNING clause to parse the JSON property value + // into the correct type (int in this case); since we (currently) use lax mode, if the format isn't correct as here + // (text instead of int), we get NULL out (strict mode would throw, see #36802 about possibly switching to that). + // And since the enum properties are required in the model, this causes "Nullable object must have a value" to be thrown. + [Theory, MemberData(nameof(IsAsyncData))] + public virtual Task Read_enum_property_with_legacy_values(bool async) + => Assert.ThrowsAsync(() => Read_enum_property_with_legacy_values_core(async)); + + protected override string JsonColumnType + => "json"; + + #endregion + } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerFixture.cs b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerFixture.cs index ad9fafff05c..07a5c54edb2 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerFixture.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerFixture.cs @@ -12,6 +12,9 @@ public class JsonQuerySqlServerFixture : JsonQueryRelationalFixture protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; + protected override bool ShouldLogCategory(string logCategory) + => logCategory == DbLoggerCategory.Query.Name; + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) { base.OnModelCreating(modelBuilder, context); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs index 0e3b19d980f..7e88c33ac6b 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore.Diagnostics.Internal; +using Microsoft.EntityFrameworkCore.SqlServer.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.TestModels.JsonQuery; using Xunit.Sdk; @@ -3247,4 +3249,1049 @@ FROM [JsonEntitiesBasic] AS [j] private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + #region Non-shared test resources + + protected override void ConfigureWarnings(WarningsConfigurationBuilder builder) + { + base.ConfigureWarnings(builder); + + builder.Log(CoreEventId.StringEnumValueInJson); + } + + public override async Task Project_root_with_missing_scalars(bool async) + { + await base.Project_root_with_missing_scalars(async); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], [e].[Collection], [e].[OptionalReference], [e].[RequiredReference] +FROM [Entities] AS [e] +WHERE [e].[Id] < 4 +"""); + } + + public override async Task Project_top_level_json_entity_with_missing_scalars(bool async) + { + await base.Project_top_level_json_entity_with_missing_scalars(async); + + AssertSql( + """ +SELECT [e].[Id], [e].[OptionalReference], [e].[RequiredReference], [e].[Collection] +FROM [Entities] AS [e] +WHERE [e].[Id] < 4 +"""); + } + + public override async Task Project_nested_json_entity_with_missing_scalars(bool async) + { + await base.Project_nested_json_entity_with_missing_scalars(async); + + AssertSql( + """ +SELECT [e].[Id], JSON_QUERY([e].[OptionalReference], '$.NestedOptionalReference'), JSON_QUERY([e].[RequiredReference], '$.NestedRequiredReference'), JSON_QUERY([e].[Collection], '$[0].NestedCollection') +FROM [Entities] AS [e] +WHERE [e].[Id] < 4 +"""); + } + + public override async Task Project_root_entity_with_missing_required_navigation(bool async) + { + await base.Project_root_entity_with_missing_required_navigation(async); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], [e].[Collection], [e].[OptionalReference], [e].[RequiredReference] +FROM [Entities] AS [e] +WHERE [e].[Id] = 5 +"""); + } + + public override async Task Project_missing_required_navigation(bool async) + { + await base.Project_missing_required_navigation(async); + + AssertSql( + """ +SELECT JSON_QUERY([e].[RequiredReference], '$.NestedRequiredReference'), [e].[Id] +FROM [Entities] AS [e] +WHERE [e].[Id] = 5 +"""); + } + + public override async Task Project_root_entity_with_null_required_navigation(bool async) + { + await base.Project_root_entity_with_null_required_navigation(async); + + AssertSql( + """ +SELECT [e].[Id], [e].[Name], [e].[Collection], [e].[OptionalReference], [e].[RequiredReference] +FROM [Entities] AS [e] +WHERE [e].[Id] = 6 +"""); + } + + public override async Task Project_null_required_navigation(bool async) + { + await base.Project_null_required_navigation(async); + + AssertSql( + """ +SELECT [e].[RequiredReference], [e].[Id] +FROM [Entities] AS [e] +WHERE [e].[Id] = 6 +"""); + } + + public override async Task Project_missing_required_scalar(bool async) + { + await base.Project_missing_required_scalar(async); + + switch (JsonColumnType) + { + case "json": + AssertSql( + """ +SELECT [e].[Id], JSON_VALUE([e].[RequiredReference], '$.Number' RETURNING float) AS [Number] +FROM [Entities] AS [e] +WHERE [e].[Id] = 2 +"""); + break; + case "nvarchar(max)": + AssertSql( + """ +SELECT [e].[Id], CAST(JSON_VALUE([e].[RequiredReference], '$.Number') AS float) AS [Number] +FROM [Entities] AS [e] +WHERE [e].[Id] = 2 +"""); + break; + default: + throw new UnreachableException(); + } + } + + public override async Task Project_null_required_scalar(bool async) + { + await base.Project_null_required_scalar(async); + + switch (JsonColumnType) + { + case "json": + AssertSql( + """ +SELECT [e].[Id], JSON_VALUE([e].[RequiredReference], '$.Number' RETURNING float) AS [Number] +FROM [Entities] AS [e] +WHERE [e].[Id] = 4 +"""); + break; + case "nvarchar(max)": + AssertSql( + """ +SELECT [e].[Id], CAST(JSON_VALUE([e].[RequiredReference], '$.Number') AS float) AS [Number] +FROM [Entities] AS [e] +WHERE [e].[Id] = 4 +"""); + break; + default: + throw new UnreachableException(); + } + } + + protected override async Task Seed21006(Context21006 context) + { + await base.Seed21006(context); + + // missing scalar on top level + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ([Collection], [OptionalReference], [RequiredReference], [Id], [Name]) +VALUES ( +'[{"Text":"e2 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 nrr"}},{"Text":"e2 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 nrr"}}]', +'{"Text":"e2 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 or nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 or nrr"}}', +'{"Text":"e2 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 rr nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 rr nrr"}}', +2, +'e2') +"""); + + // missing scalar on nested level + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ([Collection], [OptionalReference], [RequiredReference], [Id], [Name]) +VALUES ( +'[{"Number":7,"Text":"e3 c1","NestedCollection":[{"Text":"e3 c1 c1"},{"Text":"e3 c1 c2"}],"NestedOptionalReference":{"Text":"e3 c1 nor"},"NestedRequiredReference":{"Text":"e3 c1 nrr"}},{"Number":7,"Text":"e3 c2","NestedCollection":[{"Text":"e3 c2 c1"},{"Text":"e3 c2 c2"}],"NestedOptionalReference":{"Text":"e3 c2 nor"},"NestedRequiredReference":{"Text":"e3 c2 nrr"}}]', +'{"Number":7,"Text":"e3 or","NestedCollection":[{"Text":"e3 or c1"},{"Text":"e3 or c2"}],"NestedOptionalReference":{"Text":"e3 or nor"},"NestedRequiredReference":{"Text":"e3 or nrr"}}', +'{"Number":7,"Text":"e3 rr","NestedCollection":[{"Text":"e3 rr c1"},{"Text":"e3 rr c2"}],"NestedOptionalReference":{"Text":"e3 rr nor"},"NestedRequiredReference":{"Text":"e3 rr nrr"}}', +3, +'e3') +"""); + + // null scalar on top level + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ([Collection], [OptionalReference], [RequiredReference], [Id], [Name]) +VALUES ( +'[{"Number":null,"Text":"e4 c1","NestedCollection":[{"Text":"e4 c1 c1"},{"Text":"e4 c1 c2"}],"NestedOptionalReference":{"Text":"e4 c1 nor"},"NestedRequiredReference":{"Text":"e4 c1 nrr"}},{"Number":null,"Text":"e4 c2","NestedCollection":[{"Text":"e4 c2 c1"},{"Text":"e4 c2 c2"}],"NestedOptionalReference":{"Text":"e4 c2 nor"},"NestedRequiredReference":{"Text":"e4 c2 nrr"}}]', +'{"Number":null,"Text":"e4 or","NestedCollection":[{"Text":"e4 or c1"},{"Text":"e4 or c2"}],"NestedOptionalReference":{"Text":"e4 or nor"},"NestedRequiredReference":{"Text":"e4 or nrr"}}', +'{"Number":null,"Text":"e4 rr","NestedCollection":[{"Text":"e4 rr c1"},{"Text":"e4 rr c2"}],"NestedOptionalReference":{"Text":"e4 rr nor"},"NestedRequiredReference":{"Text":"e4 rr nrr"}}', +4, +'e4') +"""); + + // missing required navigation + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ([Collection], [OptionalReference], [RequiredReference], [Id], [Name]) +VALUES ( +'[{"Number":7,"Text":"e5 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 nor"}},{"Number":7,"Text":"e5 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 nor"}}]', +'{"Number":7,"Text":"e5 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 or nor"}}', +'{"Number":7,"Text":"e5 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 rr nor"}}', +5, +'e5') +"""); + + // null required navigation + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ([Collection], [OptionalReference], [RequiredReference], [Id], [Name]) +VALUES ( +'[{"Number":7,"Text":"e6 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 nor"},"NestedRequiredReference":null},{"Number":7,"Text":"e6 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 nor"},"NestedRequiredReference":null}]', +'{"Number":7,"Text":"e6 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 or nor"},"NestedRequiredReference":null}', +'{"Number":7,"Text":"e6 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 rr nor"},"NestedRequiredReference":null}', +6, +'e6') +"""); + } + + protected override async Task Seed29219(DbContext ctx) + { + await base.Seed29219(ctx); + + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Reference], [Collection]) +VALUES(3, '{ "NonNullableScalar" : 30 }', '[{ "NonNullableScalar" : 10001 }]') +"""); + } + + protected override async Task Seed30028(DbContext ctx) + { + // complete + await ctx.Database.ExecuteSqlAsync( + $$$$""" +INSERT INTO [Entities] ([Id], [Json]) +VALUES( +1, +'{"RootName":"e1","Collection":[{"BranchName":"e1 c1","Nested":{"LeafName":"e1 c1 l"}},{"BranchName":"e1 c2","Nested":{"LeafName":"e1 c2 l"}}],"OptionalReference":{"BranchName":"e1 or","Nested":{"LeafName":"e1 or l"}},"RequiredReference":{"BranchName":"e1 rr","Nested":{"LeafName":"e1 rr l"}}}') +"""); + + // missing collection + await ctx.Database.ExecuteSqlAsync( + $$$$""" +INSERT INTO [Entities] ([Id], [Json]) +VALUES( +2, +'{"RootName":"e2","OptionalReference":{"BranchName":"e2 or","Nested":{"LeafName":"e2 or l"}},"RequiredReference":{"BranchName":"e2 rr","Nested":{"LeafName":"e2 rr l"}}}') +"""); + + // missing optional reference + await ctx.Database.ExecuteSqlAsync( + $$$$""" +INSERT INTO [Entities] ([Id], [Json]) +VALUES( +3, +'{"RootName":"e3","Collection":[{"BranchName":"e3 c1","Nested":{"LeafName":"e3 c1 l"}},{"BranchName":"e3 c2","Nested":{"LeafName":"e3 c2 l"}}],"RequiredReference":{"BranchName":"e3 rr","Nested":{"LeafName":"e3 rr l"}}}') +"""); + + // missing required reference + await ctx.Database.ExecuteSqlAsync( + $$$$""" +INSERT INTO [Entities] ([Id], [Json]) +VALUES( +4, +'{"RootName":"e4","Collection":[{"BranchName":"e4 c1","Nested":{"LeafName":"e4 c1 l"}},{"BranchName":"e4 c2","Nested":{"LeafName":"e4 c2 l"}}],"OptionalReference":{"BranchName":"e4 or","Nested":{"LeafName":"e4 or l"}}}') +"""); + } + + protected override Task Seed33046(DbContext ctx) + => ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Reviews] ([Rounds], [Id]) +VALUES('[{"RoundNumber":11,"SubRounds":[{"SubRoundNumber":111},{"SubRoundNumber":112}]}]', 1) +"""); + + protected override async Task Seed34960(Context34960 ctx) + { + await base.Seed34960(ctx); + + // JSON nulls + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Collection], [Reference], [Id]) +VALUES( +'null', +'null', +4) +"""); + + // JSON object where collection should be + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Junk] ([Collection], [Reference], [Id]) +VALUES( +'{ "DoB":"2000-01-01T00:00:00","Text":"junk" }', +NULL, +1) +"""); + + // JSON array where entity should be + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Junk] ([Collection], [Reference], [Id]) +VALUES( +NULL, +'[{ "DoB":"2000-01-01T00:00:00","Text":"junk" }]', +2) +"""); + } + + protected override Task SeedJunkInJson(DbContext ctx) + => ctx.Database.ExecuteSqlAsync( + $$$$""" +INSERT INTO [Entities] ([Collection], [CollectionWithCtor], [Reference], [ReferenceWithCtor], [Id]) +VALUES( +'[{"JunkReference":{"Something":"SomeValue" },"Name":"c11","JunkProperty1":50,"Number":11.5,"JunkCollection1":[],"JunkCollection2":[{"Foo":"junk value"}],"NestedCollection":[{"DoB":"2002-04-01T00:00:00","DummyProp":"Dummy value"},{"DoB":"2002-04-02T00:00:00","DummyReference":{"Foo":5}}],"NestedReference":{"DoB":"2002-03-01T00:00:00"}},{"Name":"c12","Number":12.5,"NestedCollection":[{"DoB":"2002-06-01T00:00:00"},{"DoB":"2002-06-02T00:00:00"}],"NestedDummy":59,"NestedReference":{"DoB":"2002-05-01T00:00:00"}}]', +'[{"MyBool":true,"Name":"c11 ctor","JunkReference":{"Something":"SomeValue","JunkCollection":[{"Foo":"junk value"}]},"NestedCollection":[{"DoB":"2002-08-01T00:00:00"},{"DoB":"2002-08-02T00:00:00"}],"NestedReference":{"DoB":"2002-07-01T00:00:00"}},{"MyBool":false,"Name":"c12 ctor","NestedCollection":[{"DoB":"2002-10-01T00:00:00"},{"DoB":"2002-10-02T00:00:00"}],"JunkCollection":[{"Foo":"junk value"}],"NestedReference":{"DoB":"2002-09-01T00:00:00"}}]', +'{"Name":"r1","JunkCollection":[{"Foo":"junk value"}],"JunkReference":{"Something":"SomeValue" },"Number":1.5,"NestedCollection":[{"DoB":"2000-02-01T00:00:00","JunkReference":{"Something":"SomeValue"}},{"DoB":"2000-02-02T00:00:00"}],"NestedReference":{"DoB":"2000-01-01T00:00:00"}}', +'{"MyBool":true,"JunkCollection":[{"Foo":"junk value"}],"Name":"r1 ctor","JunkReference":{"Something":"SomeValue" },"NestedCollection":[{"DoB":"2001-02-01T00:00:00"},{"DoB":"2001-02-02T00:00:00"}],"NestedReference":{"JunkCollection":[{"Foo":"junk value"}],"DoB":"2001-01-01T00:00:00"}}', +1) +"""); + + protected override Task SeedTrickyBuffering(DbContext ctx) + => ctx.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ([Reference], [Id]) +VALUES( +'{"Name": "r1", "Number": 7, "JunkReference":{"Something": "SomeValue" }, "JunkCollection": [{"Foo": "junk value"}], "NestedReference": {"DoB": "2000-01-01T00:00:00"}, "NestedCollection": [{"DoB": "2000-02-01T00:00:00", "JunkReference": {"Something": "SomeValue"}}, {"DoB": "2000-02-02T00:00:00"}]}',1) +"""); + + protected override Task SeedShadowProperties(DbContext ctx) + => ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Collection], [CollectionWithCtor], [Reference], [ReferenceWithCtor], [Id], [Name]) +VALUES( +'[{"Name":"e1_c1","ShadowDouble":5.5},{"ShadowDouble":20.5,"Name":"e1_c2"}]', +'[{"Name":"e1_c1 ctor","ShadowNullableByte":6},{"ShadowNullableByte":null,"Name":"e1_c2 ctor"}]', +'{"Name":"e1_r", "ShadowString":"Foo"}', +'{"ShadowInt":143,"Name":"e1_r ctor"}', +1, +'e1') +"""); + + protected override async Task SeedNotICollection(DbContext ctx) + { + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Json], [Id]) +VALUES( +'{"Collection":[{"Bar":11,"Foo":"c11"},{"Bar":12,"Foo":"c12"},{"Bar":13,"Foo":"c13"}]}', +1) +"""); + + await ctx.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ([Json], [Id]) +VALUES( +'{"Collection":[{"Bar":21,"Foo":"c21"},{"Bar":22,"Foo":"c22"}]}', +2) +"""); + } + + protected override async Task SeedBadJsonProperties(ContextBadJsonProperties ctx) + { + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Scenario], [OptionalReference], [RequiredReference], [Collection]) +VALUES( +1, +'baseline', +'{"NestedOptional": { "Text":"or no" }, "NestedRequired": { "Text":"or nr" }, "NestedCollection": [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ] }', +'{"NestedOptional": { "Text":"rr no" }, "NestedRequired": { "Text":"rr nr" }, "NestedCollection": [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ] }', +'[ +{"NestedOptional": { "Text":"c 1 no" }, "NestedRequired": { "Text":"c 1 nr" }, "NestedCollection": [ { "Text":"c 1 nc 1" }, { "Text":"c 1 nc 2" } ] }, +{"NestedOptional": { "Text":"c 2 no" }, "NestedRequired": { "Text":"c 2 nr" }, "NestedCollection": [ { "Text":"c 2 nc 1" }, { "Text":"c 2 nc 2" } ] } +]') +"""); + + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Scenario], [OptionalReference], [RequiredReference], [Collection]) +VALUES( +2, +'duplicated navigations', +'{"NestedOptional": { "Text":"or no" }, "NestedOptional": { "Text":"or no dupnav" }, "NestedRequired": { "Text":"or nr" }, "NestedCollection": [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ], "NestedCollection": [ { "Text":"or nc 1 dupnav" }, { "Text":"or nc 2 dupnav" } ], "NestedRequired": { "Text":"or nr dupnav" } }', +'{"NestedOptional": { "Text":"rr no" }, "NestedOptional": { "Text":"rr no dupnav" }, "NestedRequired": { "Text":"rr nr" }, "NestedCollection": [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ], "NestedCollection": [ { "Text":"rr nc 1 dupnav" }, { "Text":"rr nc 2 dupnav" } ], "NestedRequired": { "Text":"rr nr dupnav" } }', +'[ +{"NestedOptional": { "Text":"c 1 no" }, "NestedOptional": { "Text":"c 1 no dupnav" }, "NestedRequired": { "Text":"c 1 nr" }, "NestedCollection": [ { "Text":"c 1 nc 1" }, { "Text":"c 1 nc 2" } ], "NestedCollection": [ { "Text":"c 1 nc 1 dupnav" }, { "Text":"c 1 nc 2 dupnav" } ], "NestedRequired": { "Text":"c 1 nr dupnav" } }, +{"NestedOptional": { "Text":"c 2 no" }, "NestedOptional": { "Text":"c 2 no dupnav" }, "NestedRequired": { "Text":"c 2 nr" }, "NestedCollection": [ { "Text":"c 2 nc 1" }, { "Text":"c 2 nc 2" } ], "NestedCollection": [ { "Text":"c 2 nc 1 dupnav" }, { "Text":"c 2 nc 2 dupnav" } ], "NestedRequired": { "Text":"c 2 nr dupnav" } } +]') +"""); + + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Scenario], [OptionalReference], [RequiredReference], [Collection]) +VALUES( +3, +'duplicated scalars', +'{"NestedOptional": { "Text":"or no", "Text":"or no dupprop" }, "NestedRequired": { "Text":"or nr", "Text":"or nr dupprop" }, "NestedCollection": [ { "Text":"or nc 1", "Text":"or nc 1 dupprop" }, { "Text":"or nc 2", "Text":"or nc 2 dupprop" } ] }', +'{"NestedOptional": { "Text":"rr no", "Text":"rr no dupprop" }, "NestedRequired": { "Text":"rr nr", "Text":"rr nr dupprop" }, "NestedCollection": [ { "Text":"rr nc 1", "Text":"rr nc 1 dupprop" }, { "Text":"rr nc 2", "Text":"rr nc 2 dupprop" } ] }', +'[ +{"NestedOptional": { "Text":"c 1 no", "Text":"c 1 no dupprop" }, "NestedRequired": { "Text":"c 1 nr", "Text":"c 1 nr dupprop" }, "NestedCollection": [ { "Text":"c 1 nc 1", "Text":"c 1 nc 1 dupprop" }, { "Text":"c 1 nc 2", "Text":"c 1 nc 2 dupprop" } ] }, +{"NestedOptional": { "Text":"c 2 no", "Text":"c 2 no dupprop" }, "NestedRequired": { "Text":"c 2 nr", "Text":"c 2 nr dupprop" }, "NestedCollection": [ { "Text":"c 2 nc 1", "Text":"c 2 nc 1 dupprop" }, { "Text":"c 2 nc 2", "Text":"c 2 nc 2 dupprop" } ] } +]') +"""); + + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Scenario], [OptionalReference], [RequiredReference], [Collection]) +VALUES( +4, +'empty navigation property names', +'{"": { "Text":"or no" }, "": { "Text":"or nr" }, "": [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ] }', +'{"": { "Text":"rr no" }, "": { "Text":"rr nr" }, "": [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ] }', +'[ +{"": { "Text":"c 1 no" }, "": { "Text":"c 1 nr" }, "": [ { "Text":"c 1 nc 1" }, { "Text":"c 1 nc 2" } ] }, +{"": { "Text":"c 2 no" }, "": { "Text":"c 2 nr" }, "": [ { "Text":"c 2 nc 1" }, { "Text":"c 2 nc 2" } ] } +]') +"""); + + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Scenario], [OptionalReference], [RequiredReference], [Collection]) +VALUES( +5, +'empty scalar property names', +'{"NestedOptional": { "":"or no" }, "NestedRequired": { "":"or nr" }, "NestedCollection": [ { "":"or nc 1" }, { "":"or nc 2" } ] }', +'{"NestedOptional": { "":"rr no" }, "NestedRequired": { "":"rr nr" }, "NestedCollection": [ { "":"rr nc 1" }, { "":"rr nc 2" } ] }', +'[ +{"NestedOptional": { "":"c 1 no" }, "NestedRequired": { "":"c 1 nr" }, "NestedCollection": [ { "":"c 1 nc 1" }, { "":"c 1 nc 2" } ] }, +{"NestedOptional": { "":"c 2 no" }, "NestedRequired": { "":"c 2 nr" }, "NestedCollection": [ { "":"c 2 nc 1" }, { "":"c 2 nc 2" } ] } +]') +"""); + + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Scenario], [OptionalReference], [RequiredReference], [Collection]) +VALUES( +10, +'null navigation property names', +'{null: { "Text":"or no" }, null: { "Text":"or nr" }, null: [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ] }', +'{null: { "Text":"rr no" }, null: { "Text":"rr nr" }, null: [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ] }', +'[ +{null: { "Text":"c 1 no" }, null: { "Text":"c 1 nr" }, null: [ { "Text":"c 1 nc 1" }, { "Text":"c 1 nc 2" } ] }, +{null: { "Text":"c 2 no" }, null: { "Text":"c 2 nr" }, null: [ { "Text":"c 2 nc 1" }, { "Text":"c 2 nc 2" } ] } +]') +"""); + + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Scenario], [OptionalReference], [RequiredReference], [Collection]) +VALUES( +11, +'null scalar property names', +'{"NestedOptional": { null:"or no", "Text":"or no nonnull" }, "NestedRequired": { null:"or nr", "Text":"or nr nonnull" }, "NestedCollection": [ { null:"or nc 1", "Text":"or nc 1 nonnull" }, { null:"or nc 2", "Text":"or nc 2 nonnull" } ] }', +'{"NestedOptional": { null:"rr no", "Text":"rr no nonnull" }, "NestedRequired": { null:"rr nr", "Text":"rr nr nonnull" }, "NestedCollection": [ { null:"rr nc 1", "Text":"rr nc 1 nonnull" }, { null:"rr nc 2", "Text":"rr nc 2 nonnull" } ] }', +'[ +{"NestedOptional": { null:"c 1 no", "Text":"c 1 no nonnull" }, "NestedRequired": { null:"c 1 nr", "Text":"c 1 nr nonnull" }, "NestedCollection": [ { null:"c 1 nc 1", "Text":"c 1 nc 1 nonnull" }, { null:"c 1 nc 2", "Text":"c 1 nc 2 nonnull" } ] }, +{"NestedOptional": { null:"c 2 no", "Text":"c 2 no nonnull" }, "NestedRequired": { null:"c 2 nr", "Text":"c 2 nr nonnull" }, "NestedCollection": [ { null:"c 2 nc 1", "Text":"c 2 nc 1 nonnull" }, { null:"c 2 nc 2", "Text":"c 2 nc 2 nonnull" } ] } +]') +"""); + } + + #region PrimitiveCollectionInColumn + + [Fact] + public virtual async Task Materialize_json_null_primitive_collection_mapped_to_column_is_null() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingPrimitiveCollectionInColumn, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedPrimitiveCollectionInColumn); + + using var context = contextFactory.CreateDbContext(); + + // The primitive collection is mapped to its own column via CollectionToJsonStringConverter (which does NOT + // handle the JSON 'null' token). Legacy/external data may store the JSON 'null' token (the literal string + // "null", which Utf8JsonReader tokenizes as JsonTokenType.Null) rather than a SQL NULL. The materializer peeks + // the first token and short-circuits to null instead of letting JsonCollectionOfReferencesReaderWriter throw + // "Invalid token type: 'Null'". See issues #34881 and #38454. + var result = await context.Set() + .Where(x => x.Id < 4).OrderBy(x => x.Id).ToListAsync(); + + Assert.Equal(3, result.Count); + Assert.Equal(["a", "b"], result[0].Tags); + Assert.Null(result[1].Tags); // JSON 'null' token + Assert.Null(result[2].Tags); // SQL NULL + } + + [Fact] + public virtual async Task Materialize_empty_json_primitive_collection_mapped_to_column_throws() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingPrimitiveCollectionInColumn, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedPrimitiveCollectionInColumn); + + using var context = contextFactory.CreateDbContext(); + + // An empty/whitespace JSON string in the column isn't valid JSON; the diagnostics here match the converter + // path (JsonValueReaderWriter.FromJsonString), which rejects it with CoreStrings.EmptyJsonString. + var exception = await Assert.ThrowsAsync(() + => context.Set().Where(x => x.Id == 4).ToListAsync()); + + Assert.Equal(CoreStrings.EmptyJsonString, exception.Message); + } + + [Fact] + public virtual async Task Materialize_json_null_required_primitive_collection_mapped_to_column_throws() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingPrimitiveCollectionInColumn, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedPrimitiveCollectionInColumn); + + using var context = contextFactory.CreateDbContext(); + + // The required primitive collection column holds the JSON 'null' token. Since the property is required, the + // materializer throws a clear, property-named error rather than silently materializing null. See #34881. + var exception = await Assert.ThrowsAsync(() + => context.Set().Where(x => x.Id == 5).ToListAsync()); + + Assert.Equal(RelationalStrings.NullValueInRequiredJsonProperty("RequiredTags"), exception.Message); + } + + [Fact] + public virtual async Task Project_json_null_primitive_collection_mapped_to_column_is_null() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingPrimitiveCollectionInColumn, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedPrimitiveCollectionInColumn); + + using var context = contextFactory.CreateDbContext(); + + // Projecting the collection column directly reaches the materializer without an IProperty. The JSON 'null' + // token (and SQL NULL) must still be materialized as null rather than throwing "Invalid token type: 'Null'". + var tags = await context.Set() + .Where(x => x.Id < 4).OrderBy(x => x.Id).Select(x => x.Tags).ToListAsync(); + + Assert.Equal(3, tags.Count); + Assert.Equal(["a", "b"], tags[0]); + Assert.Null(tags[1]); // JSON 'null' token + Assert.Null(tags[2]); // SQL NULL + } + + protected virtual async Task SeedPrimitiveCollectionInColumn(DbContext ctx) + { + // primitive collection column contains a JSON array + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Tags], [RequiredTags]) +VALUES(1, N'["a","b"]', N'[]') +"""); + + // primitive collection column contains a JSON null token (the literal string "null", not a SQL NULL) + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Tags], [RequiredTags]) +VALUES(2, N'null', N'[]') +"""); + + // primitive collection column is SQL NULL + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Tags], [RequiredTags]) +VALUES(3, NULL, N'[]') +"""); + + // primitive collection column contains an empty (invalid) JSON string + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Tags], [RequiredTags]) +VALUES(4, N'', N'[]') +"""); + + // required primitive collection column contains a JSON null token + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Tags], [RequiredTags]) +VALUES(5, N'["a","b"]', N'null') +"""); + } + + protected virtual void OnModelCreatingPrimitiveCollectionInColumn(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.ToTable("Entities"); + b.Property(x => x.Id).ValueGeneratedNever(); + b.PrimitiveCollection(x => x.Tags); + b.PrimitiveCollection(x => x.RequiredTags).IsRequired(); + }); + + protected class ContextPrimitiveCollectionInColumn(DbContextOptions options) : DbContext(options) + { + public class MyEntity + { + public int Id { get; set; } + + // IList matches the legacy mapping reported in #34881. + public IList Tags { get; set; } + + public IList RequiredTags { get; set; } + } + } + + #endregion + + #region JsonPropertyWithConverters + + [Fact] + public virtual async Task Materialize_json_null_property_with_converters() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingJsonPropertyWithConverters, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedJsonPropertyWithConverters); + + using var context = contextFactory.CreateDbContext(); + var result = await context.Set().SingleAsync(x => x.Id == 1); + + Assert.Equal("e1", result.Reference.Name); + + // Both properties have a JSON 'null' token value. The streaming JSON shaper's null guard + // (CreateReadJsonPropertyValueExpression) routes the null based on the converter's ConvertsNulls flag: + // - ConvertsNulls == true -> ConvertFromProvider(default) maps DB null to the "FROM_DB_NULL" sentinel. + // - ConvertsNulls == false -> returns default(string) (null) without invoking the converter. + Assert.Equal("FROM_DB_NULL", result.Reference.ConvertedHandlingNulls); + Assert.Null(result.Reference.ConvertedNotHandlingNulls); + } + + protected virtual async Task SeedJsonPropertyWithConverters(DbContext ctx) + => await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Reference]) +VALUES(1, '{"Name":"e1","ConvertedHandlingNulls":null,"ConvertedNotHandlingNulls":null}') +"""); + + protected virtual void OnModelCreatingJsonPropertyWithConverters(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.ToTable("Entities"); + b.Property(x => x.Id).ValueGeneratedNever(); + b.OwnsOne( + x => x.Reference, b => + { + b.ToJson().HasColumnType(JsonColumnType); + b.Property(x => x.ConvertedHandlingNulls).HasConversion( + new ValueConverter( + v => v, + v => v ?? "FROM_DB_NULL", + convertsNulls: true)); + b.Property(x => x.ConvertedNotHandlingNulls).HasConversion( + new ValueConverter( + v => v, + v => "FROM_CONVERTER:" + v, + convertsNulls: false)); + }); + }); + + protected class ContextJsonPropertyWithConverters(DbContextOptions options) : DbContext(options) + { + public class MyEntity + { + public int Id { get; set; } + public MyJsonEntity Reference { get; set; } + } + + public class MyJsonEntity + { + public string Name { get; set; } + public string ConvertedHandlingNulls { get; set; } + public string ConvertedNotHandlingNulls { get; set; } + } + } + + #endregion + + #region PrimitiveCollectionInJson + + [Fact] + public virtual async Task Materialize_json_null_optional_primitive_collection_in_json_is_null() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingPrimitiveCollectionInJson, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedPrimitiveCollectionInJson); + + using var context = contextFactory.CreateDbContext(); + var result = await context.Set() + .Where(x => x.Id < 3).OrderBy(x => x.Id).ToListAsync(); + + Assert.Equal(2, result.Count); + + // Id == 1: JSON has "NullableStrings": null -> materialized as null. + Assert.Null(result[0].Reference.NullableStrings); + Assert.Equal(["a", "b"], result[0].Reference.RequiredStrings); + + // Id == 2: JSON has "NullableStrings": ["x", "y"]. + Assert.Equal(["x", "y"], result[1].Reference.NullableStrings); + Assert.Equal(["c", "d"], result[1].Reference.RequiredStrings); + } + + [Fact] + public virtual async Task Materialize_json_null_required_primitive_collection_in_json_throws() + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: OnModelCreatingPrimitiveCollectionInJson, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedPrimitiveCollectionInJson); + + using var context = contextFactory.CreateDbContext(); + + // A required primitive collection nested in a JSON document whose value is a 'null' token yields a clear, + // property-named error rather than the cryptic reader/writer "Invalid token type: 'Null'". + var exception = await Assert.ThrowsAsync(() + => context.Set().Where(x => x.Id == 3).ToListAsync()); + + Assert.Equal(RelationalStrings.NullValueInRequiredJsonProperty("RequiredStrings"), exception.Message); + } + + protected virtual async Task SeedPrimitiveCollectionInJson(DbContext ctx) + { + // optional collection is JSON null + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Reference]) +VALUES(1, '{"NullableStrings":null,"RequiredStrings":["a","b"]}') +"""); + + // optional collection has a value + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Reference]) +VALUES(2, '{"NullableStrings":["x","y"],"RequiredStrings":["c","d"]}') +"""); + + // required collection is JSON null (materialization throws) + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Id], [Reference]) +VALUES(3, '{"NullableStrings":["x","y"],"RequiredStrings":null}') +"""); + } + + protected virtual void OnModelCreatingPrimitiveCollectionInJson(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.ToTable("Entities"); + b.Property(x => x.Id).ValueGeneratedNever(); + b.OwnsOne( + x => x.Reference, b => + { + b.ToJson().HasColumnType(JsonColumnType); + b.PrimitiveCollection(x => x.NullableStrings).IsRequired(false); + b.PrimitiveCollection(x => x.RequiredStrings).IsRequired(); + }); + }); + + protected class ContextPrimitiveCollectionInJson(DbContextOptions options) : DbContext(options) + { + public class MyEntity + { + public int Id { get; set; } + public MyJsonEntity Reference { get; set; } + } + + public class MyJsonEntity + { + public List NullableStrings { get; set; } + public List RequiredStrings { get; set; } + } + } + + #endregion + + #region EnumLegacyValues + + protected virtual async Task Read_enum_property_with_legacy_values_core(bool async) + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: BuildModelEnumLegacyValues, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedEnumLegacyValues); + + using var context = contextFactory.CreateDbContext(); + + var query = context.Set().Select(x => new + { + x.Reference.IntEnum, + x.Reference.ByteEnum, + x.Reference.LongEnum, + x.Reference.NullableEnum + }); + + if (async) + { + await query.ToListAsync(); + } + else + { + query.ToList(); + } + } + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual async Task Read_json_entity_with_enum_properties_with_legacy_values(bool async) + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: BuildModelEnumLegacyValues, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedEnumLegacyValues, + shouldLogCategory: c => c == DbLoggerCategory.Query.Name); + + using (var context = contextFactory.CreateDbContext()) + { + var query = context.Set().Select(x => x.Reference).AsNoTracking(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Equal(1, result.Count); + Assert.Equal(ByteEnumLegacyValues.Redmond, result[0].ByteEnum); + Assert.Equal(IntEnumLegacyValues.Foo, result[0].IntEnum); + Assert.Equal(LongEnumLegacyValues.Three, result[0].LongEnum); + Assert.Equal(ULongEnumLegacyValues.Three, result[0].ULongEnum); + Assert.Equal(IntEnumLegacyValues.Bar, result[0].NullableEnum); + } + + var testLogger = new TestLogger(); + Assert.Single( + ListLoggerFactory.Log, + l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(ByteEnumLegacyValues))); + Assert.Single( + ListLoggerFactory.Log, + l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(IntEnumLegacyValues))); + Assert.Single( + ListLoggerFactory.Log, + l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(LongEnumLegacyValues))); + Assert.Single( + ListLoggerFactory.Log, + l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(ULongEnumLegacyValues))); + } + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual async Task Read_json_entity_collection_with_enum_properties_with_legacy_values(bool async) + { + var contextFactory = await InitializeNonSharedTest( + onModelCreating: BuildModelEnumLegacyValues, + onConfiguring: b => b.ConfigureWarnings(ConfigureWarnings), + seed: SeedEnumLegacyValues, + shouldLogCategory: c => c == DbLoggerCategory.Query.Name); + + using (var context = contextFactory.CreateDbContext()) + { + var query = context.Set().Select(x => x.Collection).AsNoTracking(); + + var result = async + ? await query.ToListAsync() + : query.ToList(); + + Assert.Equal(1, result.Count); + Assert.Equal(2, result[0].Count); + Assert.Equal(ByteEnumLegacyValues.Bellevue, result[0][0].ByteEnum); + Assert.Equal(IntEnumLegacyValues.Foo, result[0][0].IntEnum); + Assert.Equal(LongEnumLegacyValues.One, result[0][0].LongEnum); + Assert.Equal(ULongEnumLegacyValues.One, result[0][0].ULongEnum); + Assert.Equal(IntEnumLegacyValues.Bar, result[0][0].NullableEnum); + Assert.Equal(ByteEnumLegacyValues.Seattle, result[0][1].ByteEnum); + Assert.Equal(IntEnumLegacyValues.Baz, result[0][1].IntEnum); + Assert.Equal(LongEnumLegacyValues.Two, result[0][1].LongEnum); + Assert.Equal(ULongEnumLegacyValues.Two, result[0][1].ULongEnum); + Assert.Null(result[0][1].NullableEnum); + } + + var testLogger = new TestLogger(); + Assert.Single( + ListLoggerFactory.Log, + l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(ByteEnumLegacyValues))); + Assert.Single( + ListLoggerFactory.Log, + l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(IntEnumLegacyValues))); + Assert.Single( + ListLoggerFactory.Log, + l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(LongEnumLegacyValues))); + Assert.Single( + ListLoggerFactory.Log, + l => l.Message == CoreResources.LogStringEnumValueInJson(testLogger).GenerateMessage(nameof(ULongEnumLegacyValues))); + } + + private Task SeedEnumLegacyValues(DbContext ctx) + => ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO [Entities] ([Collection], [Reference], [Id], [Name]) +VALUES( +N'[{"ByteEnum":"Bellevue","IntEnum":"Foo","LongEnum":"One","ULongEnum":"One","Name":"e1_c1","NullableEnum":"Bar"},{"ByteEnum":"Seattle","IntEnum":"Baz","LongEnum":"Two","ULongEnum":"Two","Name":"e1_c2","NullableEnum":null}]', +N'{"ByteEnum":"Redmond","IntEnum":"Foo","LongEnum":"Three","ULongEnum":"Three","Name":"e1_r","NullableEnum":"Bar"}', +1, +N'e1') +"""); + + protected virtual void BuildModelEnumLegacyValues(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.ToTable("Entities"); + b.Property(x => x.Id).ValueGeneratedNever(); + b.OwnsOne(x => x.Reference, b => b.ToJson().HasColumnType(JsonColumnType)); + b.OwnsMany(x => x.Collection, b => b.ToJson().HasColumnType(JsonColumnType)); + }); + + private class MyEntityEnumLegacyValues + { + public int Id { get; set; } + public string Name { get; set; } + + public MyJsonEntityEnumLegacyValues Reference { get; set; } + public List Collection { get; set; } + } + + private class MyJsonEntityEnumLegacyValues + { + public string Name { get; set; } + + // ReSharper disable once UnusedAutoPropertyAccessor.Local + public IntEnumLegacyValues IntEnum { get; set; } + + // ReSharper disable once UnusedAutoPropertyAccessor.Local + public ByteEnumLegacyValues ByteEnum { get; set; } + + // ReSharper disable once UnusedAutoPropertyAccessor.Local + public LongEnumLegacyValues LongEnum { get; set; } + + // ReSharper disable once UnusedAutoPropertyAccessor.Local + public ULongEnumLegacyValues ULongEnum { get; set; } + + // ReSharper disable once UnusedAutoPropertyAccessor.Local + public IntEnumLegacyValues? NullableEnum { get; set; } + } + + private enum IntEnumLegacyValues + { + Foo = int.MinValue, + Bar, + Baz = int.MaxValue, + } + + private enum ByteEnumLegacyValues : byte + { + Seattle, + Redmond, + Bellevue = 255, + } + + private enum LongEnumLegacyValues : long + { + One = long.MinValue, + Two = 1, + Three = long.MaxValue, + } + + private enum ULongEnumLegacyValues : ulong + { + One = ulong.MinValue, + Two = 1, + Three = ulong.MaxValue, + } + + #endregion + + public override async Task Entity_splitting_with_owned_json() + { + await base.Entity_splitting_with_owned_json(); + + AssertSql( + """ +SELECT TOP(2) [m].[Id], [m].[PropertyInMainTable], [o].[PropertyInOtherTable], [m].[Json] +FROM [MyEntity] AS [m] +INNER JOIN [OtherTable] AS [o] ON [m].[Id] = [o].[Id] +"""); + } + + public override async Task SelectMany_over_primitive_collection_nested_in_complex_collection_inside_json_column() + { + await base.SelectMany_over_primitive_collection_nested_in_complex_collection_inside_json_column(); + + switch (JsonColumnType) + { + case "json": + AssertSql( + """ +SELECT [p].[value] +FROM [Cars] AS [c] +CROSS APPLY OPENJSON([c].[CarConfiguration], '$.optionPackages') WITH ( + [packageId] nvarchar(max) '$.packageId', + [partNumbers] nvarchar(max) '$.partNumbers' AS JSON +) AS [o] +CROSS APPLY OPENJSON([o].[partNumbers]) WITH ([value] varchar(32) '$') AS [p] +WHERE [c].[Vin] = '1FA6P8TH8J5123456' AND [c].[DealerId] = 'DEALER-001' AND [o].[packageId] = N'PKG-SPORT' +"""); + break; + case "nvarchar(max)": + AssertSql( + """ +SELECT [p].[value] +FROM [Cars] AS [c] +CROSS APPLY OPENJSON([c].[CarConfiguration], '$.optionPackages') WITH ( + [packageId] nvarchar(max) '$.packageId', + [partNumbers] nvarchar(max) '$.partNumbers' AS JSON +) AS [o] +CROSS APPLY OPENJSON([o].[partNumbers]) WITH ([value] varchar(32) '$') AS [p] +WHERE [c].[Vin] = '1FA6P8TH8J5123456' AND [c].[DealerId] = 'DEALER-001' AND [o].[packageId] = N'PKG-SPORT' +"""); + break; + + default: + throw new UnreachableException(); + } + } + + public override async Task Value_converter_equality_null_scalar() + { + await base.Value_converter_equality_null_scalar(); + + AssertSql( + """ +@entity_equality_complexType='{"IntToString":"\u003Cnull\u003E"}' (Size = 34) + +SELECT COUNT(*) +FROM [Entities] AS [e] +WHERE [e].[Json] = @entity_equality_complexType +"""); + } + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual async Task Read_enum_property_with_legacy_values(bool async) + { + var exception = await Assert.ThrowsAsync(() => Read_enum_property_with_legacy_values_core(async)); + + // When using legacy nvarchar(max) to store JSON, we add a CAST() node to convert the text coming out of JSON_VALUE() + // to the appropriate type (int in this case); if the format isn't correct as here (text instead of int), + // we get: Conversion failed when converting the nvarchar value '...' to data type int + Assert.Equal(245, exception.Number); + } + + protected override string JsonColumnType + => "nvarchar(max)"; + + #endregion + } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ManyToManyQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ManyToManyQuerySqlServerTest.cs index 87f5a538191..f793d157ad4 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ManyToManyQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ManyToManyQuerySqlServerTest.cs @@ -1795,6 +1795,97 @@ FROM [JoinOneToTwo] AS [j] """); } + #region Non-shared tests + + public override async Task SelectMany_with_collection_selector_having_subquery() + { + await base.SelectMany_with_collection_selector_having_subquery(); + + AssertSql( + """ +SELECT [u].[Id] AS [UserId], [s].[Id] AS [OrgId] +FROM [Users] AS [u] +CROSS JOIN ( + SELECT [o1].[Id] + FROM ( + SELECT 1 AS empty + ) AS [e] + LEFT JOIN ( + SELECT [o].[Id] + FROM [Organisations] AS [o] + WHERE EXISTS ( + SELECT 1 + FROM [OrganisationUser] AS [o0] + WHERE [o].[Id] = [o0].[OrganisationId]) + ) AS [o1] ON 1 = 1 +) AS [s] +"""); + } + + public override async Task Many_to_many_load_works_when_join_entity_has_custom_key(bool async) + { + await base.Many_to_many_load_works_when_join_entity_has_custom_key(async); + + AssertSql( + """ +@p='1' + +SELECT TOP(1) [m].[Id] +FROM [ManyM_DB] AS [m] +WHERE [m].[Id] = @p +""", + // + """ +@p='1' + +SELECT [s].[Id], [m].[Id], [s].[Id0], [s0].[Id], [s0].[ManyM_Id], [s0].[ManyN_Id], [s0].[Id0] +FROM [ManyM_DB] AS [m] +INNER JOIN ( + SELECT [m1].[Id], [m0].[Id] AS [Id0], [m0].[ManyM_Id] + FROM [ManyMN_DB] AS [m0] + LEFT JOIN [ManyN_DB] AS [m1] ON [m0].[ManyN_Id] = [m1].[Id] +) AS [s] ON [m].[Id] = [s].[ManyM_Id] +LEFT JOIN ( + SELECT [m2].[Id], [m2].[ManyM_Id], [m2].[ManyN_Id], [m3].[Id] AS [Id0] + FROM [ManyMN_DB] AS [m2] + INNER JOIN [ManyM_DB] AS [m3] ON [m2].[ManyM_Id] = [m3].[Id] + WHERE [m3].[Id] = @p +) AS [s0] ON [s].[Id] = [s0].[ManyN_Id] +WHERE [m].[Id] = @p +ORDER BY [m].[Id], [s].[Id0] +""", + // + """ +@p='1' + +SELECT TOP(1) [m].[Id] +FROM [ManyN_DB] AS [m] +WHERE [m].[Id] = @p +""", + // + """ +@p='1' + +SELECT [s].[Id], [m].[Id], [s].[Id0], [s0].[Id], [s0].[ManyM_Id], [s0].[ManyN_Id], [s0].[Id0] +FROM [ManyN_DB] AS [m] +INNER JOIN ( + SELECT [m1].[Id], [m0].[Id] AS [Id0], [m0].[ManyN_Id] + FROM [ManyMN_DB] AS [m0] + INNER JOIN [ManyM_DB] AS [m1] ON [m0].[ManyM_Id] = [m1].[Id] +) AS [s] ON [m].[Id] = [s].[ManyN_Id] +LEFT JOIN ( + SELECT [m2].[Id], [m2].[ManyM_Id], [m2].[ManyN_Id], [m3].[Id] AS [Id0] + FROM [ManyMN_DB] AS [m2] + INNER JOIN [ManyN_DB] AS [m3] ON [m2].[ManyN_Id] = [m3].[Id] + WHERE [m3].[Id] = @p +) AS [s0] ON [s].[Id] = [s0].[ManyM_Id] +WHERE [m].[Id] = @p +ORDER BY [m].[Id], [s].[Id0] +"""); + } + + #endregion Non-shared tests + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/AdHocComplexTypeQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/AdHocComplexTypeQuerySqliteTest.cs deleted file mode 100644 index b7b7c38fdfe..00000000000 --- a/test/EFCore.Sqlite.FunctionalTests/Query/AdHocComplexTypeQuerySqliteTest.cs +++ /dev/null @@ -1,22 +0,0 @@ -// 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 AdHocComplexTypeQuerySqliteTest(NonSharedFixture fixture) : AdHocComplexTypeQueryRelationalTestBase(fixture) -{ - public override async Task Complex_property_on_split_entity() - { - await base.Complex_property_on_split_entity(); - - AssertSql( - """ -SELECT "h"."Id", "h"."CreatedOn", "h0"."IsTestHook", "h0"."Weight", "h"."Number_Parsed", "h"."Number_Raw" -FROM "Hook" AS "h" -INNER JOIN "HookMetadata" AS "h0" ON "h"."Id" = "h0"."HookId" -"""); - } - - protected override ITestStoreFactory NonSharedTestStoreFactory - => SqliteTestStoreFactory.Instance; -} diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/AdHocJsonQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/AdHocJsonQuerySqliteTest.cs deleted file mode 100644 index 62d3b1b6a03..00000000000 --- a/test/EFCore.Sqlite.FunctionalTests/Query/AdHocJsonQuerySqliteTest.cs +++ /dev/null @@ -1,326 +0,0 @@ -// 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; - -#nullable disable - -public class AdHocJsonQuerySqliteTest(NonSharedFixture fixture) : AdHocJsonQueryRelationalTestBase(fixture) -{ - protected override ITestStoreFactory NonSharedTestStoreFactory - => SqliteTestStoreFactory.Instance; - - protected override async Task Seed21006(Context21006 context) - { - await base.Seed21006(context); - - // missing scalar on top level - await context.Database.ExecuteSqlAsync( - $$$""" -INSERT INTO "Entities" ("Collection", "OptionalReference", "RequiredReference", "Id", "Name") -VALUES ( -'[{"Text":"e2 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 nrr"}},{"Text":"e2 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 nrr"}}]', -'{"Text":"e2 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 or nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 or nrr"}}', -'{"Text":"e2 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 rr nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 rr nrr"}}', -2, -'e2') -"""); - - // missing scalar on nested level - await context.Database.ExecuteSqlAsync( - $$$""" -INSERT INTO "Entities" ("Collection", "OptionalReference", "RequiredReference", "Id", "Name") -VALUES ( -'[{"Number":7,"Text":"e3 c1","NestedCollection":[{"Text":"e3 c1 c1"},{"Text":"e3 c1 c2"}],"NestedOptionalReference":{"Text":"e3 c1 nor"},"NestedRequiredReference":{"Text":"e3 c1 nrr"}},{"Number":7,"Text":"e3 c2","NestedCollection":[{"Text":"e3 c2 c1"},{"Text":"e3 c2 c2"}],"NestedOptionalReference":{"Text":"e3 c2 nor"},"NestedRequiredReference":{"Text":"e3 c2 nrr"}}]', -'{"Number":7,"Text":"e3 or","NestedCollection":[{"Text":"e3 or c1"},{"Text":"e3 or c2"}],"NestedOptionalReference":{"Text":"e3 or nor"},"NestedRequiredReference":{"Text":"e3 or nrr"}}', -'{"Number":7,"Text":"e3 rr","NestedCollection":[{"Text":"e3 rr c1"},{"Text":"e3 rr c2"}],"NestedOptionalReference":{"Text":"e3 rr nor"},"NestedRequiredReference":{"Text":"e3 rr nrr"}}', -3, -'e3') -"""); - - // null scalar on top level - await context.Database.ExecuteSqlAsync( - $$$""" -INSERT INTO [Entities] ("Collection", "OptionalReference", "RequiredReference", "Id", "Name") -VALUES ( -'[{"Number":null,"Text":"e4 c1","NestedCollection":[{"Text":"e4 c1 c1"},{"Text":"e4 c1 c2"}],"NestedOptionalReference":{"Text":"e4 c1 nor"},"NestedRequiredReference":{"Text":"e4 c1 nrr"}},{"Number":null,"Text":"e4 c2","NestedCollection":[{"Text":"e4 c2 c1"},{"Text":"e4 c2 c2"}],"NestedOptionalReference":{"Text":"e4 c2 nor"},"NestedRequiredReference":{"Text":"e4 c2 nrr"}}]', -'{"Number":null,"Text":"e4 or","NestedCollection":[{"Text":"e4 or c1"},{"Text":"e4 or c2"}],"NestedOptionalReference":{"Text":"e4 or nor"},"NestedRequiredReference":{"Text":"e4 or nrr"}}', -'{"Number":null,"Text":"e4 rr","NestedCollection":[{"Text":"e4 rr c1"},{"Text":"e4 rr c2"}],"NestedOptionalReference":{"Text":"e4 rr nor"},"NestedRequiredReference":{"Text":"e4 rr nrr"}}', -4, -'e4') -"""); - - // missing required navigation - await context.Database.ExecuteSqlAsync( - $$$""" -INSERT INTO "Entities" ("Collection", "OptionalReference", "RequiredReference", "Id", "Name") -VALUES ( -'[{"Number":7,"Text":"e5 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 nor"}},{"Number":7,"Text":"e5 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 nor"}}]', -'{"Number":7,"Text":"e5 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 or nor"}}', -'{"Number":7,"Text":"e5 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 rr nor"}}', -5, -'e5') -"""); - - // null required navigation - await context.Database.ExecuteSqlAsync( - $$$""" -INSERT INTO "Entities" ("Collection", "OptionalReference", "RequiredReference", "Id", "Name") -VALUES ( -'[{"Number":7,"Text":"e6 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 nor"},"NestedRequiredReference":null},{"Number":7,"Text":"e6 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 nor"},"NestedRequiredReference":null}]', -'{"Number":7,"Text":"e6 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 or nor"},"NestedRequiredReference":null}', -'{"Number":7,"Text":"e6 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 rr nor"},"NestedRequiredReference":null}', -6, -'e6') -"""); - } - - protected override async Task Seed29219(DbContext ctx) - { - await base.Seed29219(ctx); - - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO "Entities" ("Id", "Reference", "Collection") -VALUES(3, '{ "NonNullableScalar" : 30 }', '[{ "NonNullableScalar" : 10001 }]') -"""); - } - - protected override async Task Seed30028(DbContext ctx) - { - // complete - await ctx.Database.ExecuteSqlAsync( - $$$$""" -INSERT INTO "Entities" ("Id", "Json") -VALUES( -1, -'{"RootName":"e1","Collection":[{"BranchName":"e1 c1","Nested":{"LeafName":"e1 c1 l"}},{"BranchName":"e1 c2","Nested":{"LeafName":"e1 c2 l"}}],"OptionalReference":{"BranchName":"e1 or","Nested":{"LeafName":"e1 or l"}},"RequiredReference":{"BranchName":"e1 rr","Nested":{"LeafName":"e1 rr l"}}}') -"""); - - // missing collection - await ctx.Database.ExecuteSqlAsync( - $$$$""" -INSERT INTO "Entities" ("Id", "Json") -VALUES( -2, -'{"RootName":"e2","OptionalReference":{"BranchName":"e2 or","Nested":{"LeafName":"e2 or l"}},"RequiredReference":{"BranchName":"e2 rr","Nested":{"LeafName":"e2 rr l"}}}') -"""); - - // missing optional reference - await ctx.Database.ExecuteSqlAsync( - $$$$""" -INSERT INTO "Entities" ("Id", "Json") -VALUES( -3, -'{"RootName":"e3","Collection":[{"BranchName":"e3 c1","Nested":{"LeafName":"e3 c1 l"}},{"BranchName":"e3 c2","Nested":{"LeafName":"e3 c2 l"}}],"RequiredReference":{"BranchName":"e3 rr","Nested":{"LeafName":"e3 rr l"}}}') -"""); - - // missing required reference - await ctx.Database.ExecuteSqlAsync( - $$$$""" -INSERT INTO "Entities" ("Id", "Json") -VALUES( -4, -'{"RootName":"e4","Collection":[{"BranchName":"e4 c1","Nested":{"LeafName":"e4 c1 l"}},{"BranchName":"e4 c2","Nested":{"LeafName":"e4 c2 l"}}],"OptionalReference":{"BranchName":"e4 or","Nested":{"LeafName":"e4 or l"}}}') -"""); - } - - protected override async Task Seed33046(DbContext ctx) - => await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO "Reviews" ("Rounds", "Id") -VALUES('[{"RoundNumber":11,"SubRounds":[{"SubRoundNumber":111},{"SubRoundNumber":112}]}]', 1) -"""); - - protected override async Task Seed34960(Context34960 ctx) - { - await base.Seed34960(ctx); - - // JSON nulls - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO "Entities" ("Collection", "Reference", "Id") -VALUES( -'null', -'null', -4) -"""); - - // JSON object where collection should be - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO "Junk" ("Collection", "Reference", "Id") -VALUES( -'{ "DoB":"2000-01-01T00:00:00","Text":"junk" }', -NULL, -1) -"""); - - // JSON array where entity should be - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO "Junk" ("Collection", "Reference", "Id") -VALUES( -NULL, -'[{ "DoB":"2000-01-01T00:00:00","Text":"junk" }]', -2) -"""); - } - - protected override Task SeedJunkInJson(DbContext ctx) - => ctx.Database.ExecuteSqlAsync( - $$$""" -INSERT INTO "Entities" ("Collection", "CollectionWithCtor", "Reference", "ReferenceWithCtor", "Id") -VALUES( -'[{"JunkReference":{"Something":"SomeValue" },"Name":"c11","JunkProperty1":50,"Number":11.5,"JunkCollection1":[],"JunkCollection2":[{"Foo":"junk value"}],"NestedCollection":[{"DoB":"2002-04-01T00:00:00","DummyProp":"Dummy value"},{"DoB":"2002-04-02T00:00:00","DummyReference":{"Foo":5}}],"NestedReference":{"DoB":"2002-03-01T00:00:00"}},{"Name":"c12","Number":12.5,"NestedCollection":[{"DoB":"2002-06-01T00:00:00"},{"DoB":"2002-06-02T00:00:00"}],"NestedDummy":59,"NestedReference":{"DoB":"2002-05-01T00:00:00"}}]', -'[{"MyBool":true,"Name":"c11 ctor","JunkReference":{"Something":"SomeValue","JunkCollection":[{"Foo":"junk value"}]},"NestedCollection":[{"DoB":"2002-08-01T00:00:00"},{"DoB":"2002-08-02T00:00:00"}],"NestedReference":{"DoB":"2002-07-01T00:00:00"}},{"MyBool":false,"Name":"c12 ctor","NestedCollection":[{"DoB":"2002-10-01T00:00:00"},{"DoB":"2002-10-02T00:00:00"}],"JunkCollection":[{"Foo":"junk value"}],"NestedReference":{"DoB":"2002-09-01T00:00:00"}}]', -'{"Name":"r1","JunkCollection":[{"Foo":"junk value"}],"JunkReference":{"Something":"SomeValue" },"Number":1.5,"NestedCollection":[{"DoB":"2000-02-01T00:00:00","JunkReference":{"Something":"SomeValue"}},{"DoB":"2000-02-02T00:00:00"}],"NestedReference":{"DoB":"2000-01-01T00:00:00"}}', -'{"MyBool":true,"JunkCollection":[{"Foo":"junk value"}],"Name":"r1 ctor","JunkReference":{"Something":"SomeValue" },"NestedCollection":[{"DoB":"2001-02-01T00:00:00"},{"DoB":"2001-02-02T00:00:00"}],"NestedReference":{"JunkCollection":[{"Foo":"junk value"}],"DoB":"2001-01-01T00:00:00"}}', -1) -"""); - - protected override Task SeedTrickyBuffering(DbContext ctx) - => ctx.Database.ExecuteSqlAsync( - $$$""" -INSERT INTO "Entities" ("Reference", "Id") -VALUES( -'{"Name": "r1", "Number": 7, "JunkReference":{"Something": "SomeValue" }, "JunkCollection": [{"Foo": "junk value"}], "NestedReference": {"DoB": "2000-01-01T00:00:00"}, "NestedCollection": [{"DoB": "2000-02-01T00:00:00", "JunkReference": {"Something": "SomeValue"}}, {"DoB": "2000-02-02T00:00:00"}]}',1) -"""); - - protected override Task SeedShadowProperties(DbContext ctx) - => ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO "Entities" ("Collection", "CollectionWithCtor", "Reference", "ReferenceWithCtor", "Id", "Name") -VALUES( -'[{"Name":"e1_c1","ShadowDouble":5.5},{"ShadowDouble":20.5,"Name":"e1_c2"}]', -'[{"Name":"e1_c1 ctor","ShadowNullableByte":6},{"ShadowNullableByte":null,"Name":"e1_c2 ctor"}]', -'{"Name":"e1_r", "ShadowString":"Foo"}', -'{"ShadowInt":143,"Name":"e1_r ctor"}', -1, -'e1') -"""); - - protected override async Task SeedNotICollection(DbContext ctx) - { - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO "Entities" ("Json", "Id") -VALUES( -'{"Collection":[{"Bar":11,"Foo":"c11"},{"Bar":12,"Foo":"c12"},{"Bar":13,"Foo":"c13"}]}', -1) -"""); - - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO "Entities" ("Json", "Id") -VALUES( -'{"Collection":[{"Bar":21,"Foo":"c21"},{"Bar":22,"Foo":"c22"}]}', -2) -"""); - } - - protected override async Task SeedBadJsonProperties(ContextBadJsonProperties ctx) - { - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO Entities (Id, Scenario, OptionalReference, RequiredReference, Collection) -VALUES( -1, -'baseline', -'{"NestedOptional": { "Text":"or no" }, "NestedRequired": { "Text":"or nr" }, "NestedCollection": [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ] }', -'{"NestedOptional": { "Text":"rr no" }, "NestedRequired": { "Text":"rr nr" }, "NestedCollection": [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ] }', -'[ -{"NestedOptional": { "Text":"c 1 no" }, "NestedRequired": { "Text":"c 1 nr" }, "NestedCollection": [ { "Text":"c 1 nc 1" }, { "Text":"c 1 nc 2" } ] }, -{"NestedOptional": { "Text":"c 2 no" }, "NestedRequired": { "Text":"c 2 nr" }, "NestedCollection": [ { "Text":"c 2 nc 1" }, { "Text":"c 2 nc 2" } ] } -]') -"""); - - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO Entities (Id, Scenario, OptionalReference, RequiredReference, Collection) -VALUES( -2, -'duplicated navigations', -'{"NestedOptional": { "Text":"or no" }, "NestedOptional": { "Text":"or no dupnav" }, "NestedRequired": { "Text":"or nr" }, "NestedCollection": [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ], "NestedCollection": [ { "Text":"or nc 1 dupnav" }, { "Text":"or nc 2 dupnav" } ], "NestedRequired": { "Text":"or nr dupnav" } }', -'{"NestedOptional": { "Text":"rr no" }, "NestedOptional": { "Text":"rr no dupnav" }, "NestedRequired": { "Text":"rr nr" }, "NestedCollection": [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ], "NestedCollection": [ { "Text":"rr nc 1 dupnav" }, { "Text":"rr nc 2 dupnav" } ], "NestedRequired": { "Text":"rr nr dupnav" } }', -'[ -{"NestedOptional": { "Text":"c 1 no" }, "NestedOptional": { "Text":"c 1 no dupnav" }, "NestedRequired": { "Text":"c 1 nr" }, "NestedCollection": [ { "Text":"c 1 nc 1" }, { "Text":"c 1 nc 2" } ], "NestedCollection": [ { "Text":"c 1 nc 1 dupnav" }, { "Text":"c 1 nc 2 dupnav" } ], "NestedRequired": { "Text":"c 1 nr dupnav" } }, -{"NestedOptional": { "Text":"c 2 no" }, "NestedOptional": { "Text":"c 2 no dupnav" }, "NestedRequired": { "Text":"c 2 nr" }, "NestedCollection": [ { "Text":"c 2 nc 1" }, { "Text":"c 2 nc 2" } ], "NestedCollection": [ { "Text":"c 2 nc 1 dupnav" }, { "Text":"c 2 nc 2 dupnav" } ], "NestedRequired": { "Text":"c 2 nr dupnav" } } -]') -"""); - - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO Entities (Id, Scenario, OptionalReference, RequiredReference, Collection) -VALUES( -3, -'duplicated scalars', -'{"NestedOptional": { "Text":"or no", "Text":"or no dupprop" }, "NestedRequired": { "Text":"or nr", "Text":"or nr dupprop" }, "NestedCollection": [ { "Text":"or nc 1", "Text":"or nc 1 dupprop" }, { "Text":"or nc 2", "Text":"or nc 2 dupprop" } ] }', -'{"NestedOptional": { "Text":"rr no", "Text":"rr no dupprop" }, "NestedRequired": { "Text":"rr nr", "Text":"rr nr dupprop" }, "NestedCollection": [ { "Text":"rr nc 1", "Text":"rr nc 1 dupprop" }, { "Text":"rr nc 2", "Text":"rr nc 2 dupprop" } ] }', -'[ -{"NestedOptional": { "Text":"c 1 no", "Text":"c 1 no dupprop" }, "NestedRequired": { "Text":"c 1 nr", "Text":"c 1 nr dupprop" }, "NestedCollection": [ { "Text":"c 1 nc 1", "Text":"c 1 nc 1 dupprop" }, { "Text":"c 1 nc 2", "Text":"c 1 nc 2 dupprop" } ] }, -{"NestedOptional": { "Text":"c 2 no", "Text":"c 2 no dupprop" }, "NestedRequired": { "Text":"c 2 nr", "Text":"c 2 nr dupprop" }, "NestedCollection": [ { "Text":"c 2 nc 1", "Text":"c 2 nc 1 dupprop" }, { "Text":"c 2 nc 2", "Text":"c 2 nc 2 dupprop" } ] } -]') -"""); - - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO Entities (Id, Scenario, OptionalReference, RequiredReference, Collection) -VALUES( -4, -'empty navigation property names', -'{"": { "Text":"or no" }, "": { "Text":"or nr" }, "": [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ] }', -'{"": { "Text":"rr no" }, "": { "Text":"rr nr" }, "": [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ] }', -'[ -{"": { "Text":"c 1 no" }, "": { "Text":"c 1 nr" }, "": [ { "Text":"c 1 nc 1" }, { "Text":"c 1 nc 2" } ] }, -{"": { "Text":"c 2 no" }, "": { "Text":"c 2 nr" }, "": [ { "Text":"c 2 nc 1" }, { "Text":"c 2 nc 2" } ] } -]') -"""); - - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO Entities (Id, Scenario, OptionalReference, RequiredReference, Collection) -VALUES( -5, -'empty scalar property names', -'{"NestedOptional": { "":"or no" }, "NestedRequired": { "":"or nr" }, "NestedCollection": [ { "":"or nc 1" }, { "":"or nc 2" } ] }', -'{"NestedOptional": { "":"rr no" }, "NestedRequired": { "":"rr nr" }, "NestedCollection": [ { "":"rr nc 1" }, { "":"rr nc 2" } ] }', -'[ -{"NestedOptional": { "":"c 1 no" }, "NestedRequired": { "":"c 1 nr" }, "NestedCollection": [ { "":"c 1 nc 1" }, { "":"c 1 nc 2" } ] }, -{"NestedOptional": { "":"c 2 no" }, "NestedRequired": { "":"c 2 nr" }, "NestedCollection": [ { "":"c 2 nc 1" }, { "":"c 2 nc 2" } ] } -]') -"""); - - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO Entities (Id, Scenario, OptionalReference, RequiredReference, Collection) -VALUES( -10, -'null navigation property names', -'{null: { "Text":"or no" }, null: { "Text":"or nr" }, null: [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ] }', -'{null: { "Text":"rr no" }, null: { "Text":"rr nr" }, null: [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ] }', -'[ -{null: { "Text":"c 1 no" }, null: { "Text":"c 1 nr" }, null: [ { "Text":"c 1 nc 1" }, { "Text":"c 1 nc 2" } ] }, -{null: { "Text":"c 2 no" }, null: { "Text":"c 2 nr" }, null: [ { "Text":"c 2 nc 1" }, { "Text":"c 2 nc 2" } ] } -]') -"""); - - await ctx.Database.ExecuteSqlAsync( - $$""" -INSERT INTO Entities (Id, Scenario, OptionalReference, RequiredReference, Collection) -VALUES( -11, -'null scalar property names', -'{"NestedOptional": { null:"or no", "Text":"or no nonnull" }, "NestedRequired": { null:"or nr", "Text":"or nr nonnull" }, "NestedCollection": [ { null:"or nc 1", "Text":"or nc 1 nonnull" }, { null:"or nc 2", "Text":"or nc 2 nonnull" } ] }', -'{"NestedOptional": { null:"rr no", "Text":"rr no nonnull" }, "NestedRequired": { null:"rr nr", "Text":"rr nr nonnull" }, "NestedCollection": [ { null:"rr nc 1", "Text":"rr nc 1 nonnull" }, { null:"rr nc 2", "Text":"rr nc 2 nonnull" } ] }', -'[ -{"NestedOptional": { null:"c 1 no", "Text":"c 1 no nonnull" }, "NestedRequired": { null:"c 1 nr", "Text":"c 1 nr nonnull" }, "NestedCollection": [ { null:"c 1 nc 1", "Text":"c 1 nc 1 nonnull" }, { null:"c 1 nc 2", "Text":"c 1 nc 2 nonnull" } ] }, -{"NestedOptional": { null:"c 2 no", "Text":"c 2 no nonnull" }, "NestedRequired": { null:"c 2 nr", "Text":"c 2 nr nonnull" }, "NestedCollection": [ { null:"c 2 nc 1", "Text":"c 2 nc 1 nonnull" }, { null:"c 2 nc 2", "Text":"c 2 nc 2 nonnull" } ] } -]') -"""); - } - - public override Task SelectMany_over_primitive_collection_nested_in_complex_collection_inside_json_column() - => Assert.ThrowsAsync( - base.SelectMany_over_primitive_collection_nested_in_complex_collection_inside_json_column); -} diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/AdHocManyToManyQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/AdHocManyToManyQuerySqliteTest.cs deleted file mode 100644 index 523f4800be6..00000000000 --- a/test/EFCore.Sqlite.FunctionalTests/Query/AdHocManyToManyQuerySqliteTest.cs +++ /dev/null @@ -1,12 +0,0 @@ -// 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; - -#nullable disable - -public class AdHocManyToManyQuerySqliteTest(NonSharedFixture fixture) : AdHocManyToManyQueryRelationalTestBase(fixture) -{ - protected override ITestStoreFactory NonSharedTestStoreFactory - => SqliteTestStoreFactory.Instance; -} diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/ComplexTypeQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/ComplexTypeQuerySqliteTest.cs index 4093b45e405..7fd751eb058 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/ComplexTypeQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/ComplexTypeQuerySqliteTest.cs @@ -1155,6 +1155,309 @@ LIMIT @p1 """); } + #region Non-shared test resources + + 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' (Nullable = true) +@entity_equality_container_Containee1_Id='2' (Nullable = true) +@entity_equality_container_Containee2_Id='3' (Nullable = true) + +SELECT "e"."Id", "e"."ComplexContainer_Id", "e"."ComplexContainer_Containee1_Id", "e"."ComplexContainer_Containee2_Id" +FROM "EntityType" AS "e" +WHERE "e"."ComplexContainer_Id" = @entity_equality_container_Id AND "e"."ComplexContainer_Containee1_Id" = @entity_equality_container_Containee1_Id AND "e"."ComplexContainer_Containee2_Id" = @entity_equality_container_Containee2_Id +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(); + + AssertSql( + """ +SELECT "e"."Complex_Name", "e"."Complex_Number" +FROM "EntityType" AS "e" +"""); + } + + public override async Task Optional_complex_type_with_discriminator() + { + await base.Optional_complex_type_with_discriminator(); + + AssertSql( + """ +SELECT "e"."Id", "e"."AllOptionalsComplexType_Discriminator", "e"."AllOptionalsComplexType_OptionalProperty" +FROM "EntityType" AS "e" +WHERE "e"."AllOptionalsComplexType_Discriminator" IS NULL +LIMIT 2 +""", + // + """ +@p2='3' +@p0='AllOptionalsComplexType' (Size = 23) +@p1='New thing' (Size = 9) + +UPDATE "EntityType" SET "AllOptionalsComplexType_Discriminator" = @p0, "AllOptionalsComplexType_OptionalProperty" = @p1 +WHERE "Id" = @p2 +RETURNING 1; +""", + // + """ +SELECT "e"."Id", "e"."AllOptionalsComplexType_Discriminator", "e"."AllOptionalsComplexType_OptionalProperty" +FROM "EntityType" AS "e" +"""); + } + + public override async Task Non_optional_complex_type_with_all_nullable_properties() + { + await base.Non_optional_complex_type_with_all_nullable_properties(); + + AssertSql( + """ +SELECT "e"."Id", "e"."NonOptionalComplexType_NullableDateTime", "e"."NonOptionalComplexType_NullableString" +FROM "EntityType" AS "e" +LIMIT 2 +"""); + } + + public override async Task Non_optional_complex_type_with_all_nullable_properties_via_left_join() + { + await base.Non_optional_complex_type_with_all_nullable_properties_via_left_join(); + + AssertSql( + """ +SELECT "p0"."Id", "c"."Id", "c"."ParentId", "c"."ComplexType_NullableDateTime", "c"."ComplexType_NullableString" +FROM ( + SELECT "p"."Id" + FROM "Parent" AS "p" + LIMIT 2 +) AS "p0" +LEFT JOIN "Child" AS "c" ON "p0"."Id" = "c"."ParentId" +ORDER BY "p0"."Id" +"""); + } + + public override async Task Nullable_complex_type_with_discriminator_and_shadow_property() + { + await base.Nullable_complex_type_with_discriminator_and_shadow_property(); + + AssertSql( + """ +SELECT "e"."Id", "e"."CreatedBy", "e"."Prop_Discriminator", "e"."Prop_OptionalValue" +FROM "EntityType" AS "e" +"""); + } + + public override async Task Nullable_complex_type_with_discriminator_null_to_non_null_roundtrip() + { + await base.Nullable_complex_type_with_discriminator_null_to_non_null_roundtrip(); + } + + public override async Task Nullable_complex_type_with_discriminator_non_null_to_null_roundtrip() + { + await base.Nullable_complex_type_with_discriminator_non_null_to_null_roundtrip(); + } + + public override async Task Nullable_complex_type_with_discriminator_update_non_null_entity_roundtrip() + { + await base.Nullable_complex_type_with_discriminator_update_non_null_entity_roundtrip(); + } + + public override async Task Nullable_complex_type_with_discriminator_set_to_different_value() + { + await base.Nullable_complex_type_with_discriminator_set_to_different_value(); + } + + public override async Task Nullable_complex_type_with_discriminator_set_to_null() + { + await base.Nullable_complex_type_with_discriminator_set_to_null(); + } + + public override async Task Nested_nullable_complex_type_with_discriminator_null_to_non_null_roundtrip() + { + await base.Nested_nullable_complex_type_with_discriminator_null_to_non_null_roundtrip(); + } + + public override async Task Update_entity_with_nullable_complex_type_and_discriminator_does_not_throw() + { + await base.Update_entity_with_nullable_complex_type_and_discriminator_does_not_throw(); + } + + public override async Task Can_query_by_complex_type_property_with_index() + { + await base.Can_query_by_complex_type_property_with_index(); + + AssertSql( + """ +SELECT "p"."Id_Id", "p"."Address_City", "p"."Address_PostalCode" +FROM "Person" AS "p" +WHERE "p"."Address_City" = 'Seattle' +LIMIT 2 +"""); + } + + public override async Task Can_update_entity_with_index_on_complex_type_property() + { + await base.Can_update_entity_with_index_on_complex_type_property(); + + AssertSql( + """ +SELECT "p"."Id_Id", "p"."Address_City", "p"."Address_PostalCode" +FROM "Person" AS "p" +LIMIT 2 +""", + // + """ +@p1='1' +@p0='98102' (Nullable = false) (Size = 5) + +UPDATE "Person" SET "Address_PostalCode" = @p0 +WHERE "Id_Id" = @p1 +RETURNING 1; +""", + // + """ +SELECT "p"."Id_Id", "p"."Address_City", "p"."Address_PostalCode" +FROM "Person" AS "p" +LIMIT 2 +"""); + } + + public override async Task Can_delete_entity_with_index_on_complex_type_property() + { + await base.Can_delete_entity_with_index_on_complex_type_property(); + + AssertSql( + """ +SELECT "p"."Id_Id", "p"."Address_City", "p"."Address_PostalCode" +FROM "Person" AS "p" +LIMIT 2 +""", + // + """ +@p0='1' + +DELETE FROM "Person" +WHERE "Id_Id" = @p0 +RETURNING 1; +""", + // + """ +SELECT COUNT(*) +FROM "Person" AS "p" +"""); + } + + public override async Task Can_query_by_alternate_key_on_complex_type_property() + { + await base.Can_query_by_alternate_key_on_complex_type_property(); + + AssertSql( + """ +SELECT "p"."Id_Id", "p"."Address_City", "p"."Address_PostalCode" +FROM "Person" AS "p" +WHERE "p"."Address_City" = 'Redmond' +LIMIT 2 +"""); + } + + public override async Task Can_save_batch_swapping_alternate_key_values_on_complex_type_property() + { + await base.Can_save_batch_swapping_alternate_key_values_on_complex_type_property(); + + AssertSql( + """ +SELECT "p"."Id_Id", "p"."Address_City", "p"."Address_PostalCode" +FROM "Person" AS "p" +ORDER BY "p"."Id_Id" +""", + // + """ +@p1='1' +@p0='98103' (Nullable = false) (Size = 5) + +UPDATE "Person" SET "Address_PostalCode" = @p0 +WHERE "Id_Id" = @p1 +RETURNING 1; +""", + // + """ +@p1='2' +@p0='98054' (Nullable = false) (Size = 5) + +UPDATE "Person" SET "Address_PostalCode" = @p0 +WHERE "Id_Id" = @p1 +RETURNING 1; +""", + // + """ +SELECT "p"."Id_Id", "p"."Address_City", "p"."Address_PostalCode" +FROM "Person" AS "p" +ORDER BY "p"."Id_Id" +"""); + } + + public override async Task Complex_json_collection_inside_left_join_subquery() + { + await base.Complex_json_collection_inside_left_join_subquery(); + + AssertSql( + """ +SELECT "p"."Id", "p"."ChildId", "c0"."Id", "c0"."IsPublic", "c0"."c" +FROM "Parent" AS "p" +LEFT JOIN ( + SELECT "c"."Id", "c"."IsPublic", "c"."CareNeeds" AS "c" + FROM "Child" AS "c" + WHERE "c"."IsPublic" +) AS "c0" ON "p"."ChildId" = "c0"."Id" +"""); + } + + public override async Task Select_TPC_base_with_ComplexType() + { + await base.Select_TPC_base_with_ComplexType(); + + AssertSql( + """ +SELECT "t"."Id", "t"."ChildProperty", NULL AS "ChildProperty1", "t"."PropertyInsideComplexThing", "t"."ChildComplexProperty_PropertyInsideComplexThing", NULL AS "ChildComplexProperty_PropertyInsideComplexThing1", 'TpcChild1' AS "Discriminator" +FROM "TpcChild1" AS "t" +UNION ALL +SELECT "t0"."Id", NULL AS "ChildProperty", "t0"."ChildProperty" AS "ChildProperty1", "t0"."PropertyInsideComplexThing", NULL AS "ChildComplexProperty_PropertyInsideComplexThing", "t0"."ChildComplexProperty_PropertyInsideComplexThing" AS "ChildComplexProperty_PropertyInsideComplexThing1", 'TpcChild2' AS "Discriminator" +FROM "TpcChild2" AS "t0" +"""); + } + + public override async Task Complex_type_on_an_entity_mapped_to_view_and_table() + { + await base.Complex_type_on_an_entity_mapped_to_view_and_table(); + + AssertSql( + """ +SELECT "b"."Id", "b"."ComplexThing_Prop1", "b"."ComplexThing_Prop2" +FROM "BlogsView" AS "b" +LIMIT 2 +"""); + } + + public override async Task Complex_property_on_split_entity() + { + await base.Complex_property_on_split_entity(); + + AssertSql( + """ +SELECT "h"."Id", "h"."CreatedOn", "h0"."IsTestHook", "h0"."Weight", "h"."Number_Parsed", "h"."Number_Raw" +FROM "Hook" AS "h" +INNER JOIN "HookMetadata" AS "h0" ON "h"."Id" = "h0"."HookId" +"""); + } + + #endregion Non-shared test resources + [Fact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/JsonQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/JsonQuerySqliteTest.cs index d639fbc2334..72fc33239a7 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/JsonQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/JsonQuerySqliteTest.cs @@ -466,6 +466,324 @@ public override async Task => base.Entity_including_collection_with_json_and_separate_json_projection_AsNoTrackingWithIdentityResolution(async))) .Message); + #region Non-shared test resources + + protected override async Task Seed21006(Context21006 context) + { + await base.Seed21006(context); + + // missing scalar on top level + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO "Entities" ("Collection", "OptionalReference", "RequiredReference", "Id", "Name") +VALUES ( +'[{"Text":"e2 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c1 nrr"}},{"Text":"e2 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 c2 nrr"}}]', +'{"Text":"e2 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 or nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 or nrr"}}', +'{"Text":"e2 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e2 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e2 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 rr nor"},"NestedRequiredReference":{"DoB":"2000-01-01T00:00:00","Text":"e2 rr nrr"}}', +2, +'e2') +"""); + + // missing scalar on nested level + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO "Entities" ("Collection", "OptionalReference", "RequiredReference", "Id", "Name") +VALUES ( +'[{"Number":7,"Text":"e3 c1","NestedCollection":[{"Text":"e3 c1 c1"},{"Text":"e3 c1 c2"}],"NestedOptionalReference":{"Text":"e3 c1 nor"},"NestedRequiredReference":{"Text":"e3 c1 nrr"}},{"Number":7,"Text":"e3 c2","NestedCollection":[{"Text":"e3 c2 c1"},{"Text":"e3 c2 c2"}],"NestedOptionalReference":{"Text":"e3 c2 nor"},"NestedRequiredReference":{"Text":"e3 c2 nrr"}}]', +'{"Number":7,"Text":"e3 or","NestedCollection":[{"Text":"e3 or c1"},{"Text":"e3 or c2"}],"NestedOptionalReference":{"Text":"e3 or nor"},"NestedRequiredReference":{"Text":"e3 or nrr"}}', +'{"Number":7,"Text":"e3 rr","NestedCollection":[{"Text":"e3 rr c1"},{"Text":"e3 rr c2"}],"NestedOptionalReference":{"Text":"e3 rr nor"},"NestedRequiredReference":{"Text":"e3 rr nrr"}}', +3, +'e3') +"""); + + // null scalar on top level + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO [Entities] ("Collection", "OptionalReference", "RequiredReference", "Id", "Name") +VALUES ( +'[{"Number":null,"Text":"e4 c1","NestedCollection":[{"Text":"e4 c1 c1"},{"Text":"e4 c1 c2"}],"NestedOptionalReference":{"Text":"e4 c1 nor"},"NestedRequiredReference":{"Text":"e4 c1 nrr"}},{"Number":null,"Text":"e4 c2","NestedCollection":[{"Text":"e4 c2 c1"},{"Text":"e4 c2 c2"}],"NestedOptionalReference":{"Text":"e4 c2 nor"},"NestedRequiredReference":{"Text":"e4 c2 nrr"}}]', +'{"Number":null,"Text":"e4 or","NestedCollection":[{"Text":"e4 or c1"},{"Text":"e4 or c2"}],"NestedOptionalReference":{"Text":"e4 or nor"},"NestedRequiredReference":{"Text":"e4 or nrr"}}', +'{"Number":null,"Text":"e4 rr","NestedCollection":[{"Text":"e4 rr c1"},{"Text":"e4 rr c2"}],"NestedOptionalReference":{"Text":"e4 rr nor"},"NestedRequiredReference":{"Text":"e4 rr nrr"}}', +4, +'e4') +"""); + + // missing required navigation + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO "Entities" ("Collection", "OptionalReference", "RequiredReference", "Id", "Name") +VALUES ( +'[{"Number":7,"Text":"e5 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 c1 nor"}},{"Number":7,"Text":"e5 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 c2 nor"}}]', +'{"Number":7,"Text":"e5 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 or nor"}}', +'{"Number":7,"Text":"e5 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e5 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e5 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e5 rr nor"}}', +5, +'e5') +"""); + + // null required navigation + await context.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO "Entities" ("Collection", "OptionalReference", "RequiredReference", "Id", "Name") +VALUES ( +'[{"Number":7,"Text":"e6 c1","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 c1 nor"},"NestedRequiredReference":null},{"Number":7,"Text":"e6 c2","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 c2 nor"},"NestedRequiredReference":null}]', +'{"Number":7,"Text":"e6 or","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 or c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 or c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 or nor"},"NestedRequiredReference":null}', +'{"Number":7,"Text":"e6 rr","NestedCollection":[{"DoB":"2000-01-01T00:00:00","Text":"e6 rr c1"},{"DoB":"2000-01-01T00:00:00","Text":"e6 rr c2"}],"NestedOptionalReference":{"DoB":"2000-01-01T00:00:00","Text":"e6 rr nor"},"NestedRequiredReference":null}', +6, +'e6') +"""); + } + + protected override async Task Seed29219(DbContext ctx) + { + await base.Seed29219(ctx); + + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO "Entities" ("Id", "Reference", "Collection") +VALUES(3, '{ "NonNullableScalar" : 30 }', '[{ "NonNullableScalar" : 10001 }]') +"""); + } + + protected override async Task Seed30028(DbContext ctx) + { + // complete + await ctx.Database.ExecuteSqlAsync( + $$$$""" +INSERT INTO "Entities" ("Id", "Json") +VALUES( +1, +'{"RootName":"e1","Collection":[{"BranchName":"e1 c1","Nested":{"LeafName":"e1 c1 l"}},{"BranchName":"e1 c2","Nested":{"LeafName":"e1 c2 l"}}],"OptionalReference":{"BranchName":"e1 or","Nested":{"LeafName":"e1 or l"}},"RequiredReference":{"BranchName":"e1 rr","Nested":{"LeafName":"e1 rr l"}}}') +"""); + + // missing collection + await ctx.Database.ExecuteSqlAsync( + $$$$""" +INSERT INTO "Entities" ("Id", "Json") +VALUES( +2, +'{"RootName":"e2","OptionalReference":{"BranchName":"e2 or","Nested":{"LeafName":"e2 or l"}},"RequiredReference":{"BranchName":"e2 rr","Nested":{"LeafName":"e2 rr l"}}}') +"""); + + // missing optional reference + await ctx.Database.ExecuteSqlAsync( + $$$$""" +INSERT INTO "Entities" ("Id", "Json") +VALUES( +3, +'{"RootName":"e3","Collection":[{"BranchName":"e3 c1","Nested":{"LeafName":"e3 c1 l"}},{"BranchName":"e3 c2","Nested":{"LeafName":"e3 c2 l"}}],"RequiredReference":{"BranchName":"e3 rr","Nested":{"LeafName":"e3 rr l"}}}') +"""); + + // missing required reference + await ctx.Database.ExecuteSqlAsync( + $$$$""" +INSERT INTO "Entities" ("Id", "Json") +VALUES( +4, +'{"RootName":"e4","Collection":[{"BranchName":"e4 c1","Nested":{"LeafName":"e4 c1 l"}},{"BranchName":"e4 c2","Nested":{"LeafName":"e4 c2 l"}}],"OptionalReference":{"BranchName":"e4 or","Nested":{"LeafName":"e4 or l"}}}') +"""); + } + + protected override async Task Seed33046(DbContext ctx) + => await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO "Reviews" ("Rounds", "Id") +VALUES('[{"RoundNumber":11,"SubRounds":[{"SubRoundNumber":111},{"SubRoundNumber":112}]}]', 1) +"""); + + protected override async Task Seed34960(Context34960 ctx) + { + await base.Seed34960(ctx); + + // JSON nulls + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO "Entities" ("Collection", "Reference", "Id") +VALUES( +'null', +'null', +4) +"""); + + // JSON object where collection should be + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO "Junk" ("Collection", "Reference", "Id") +VALUES( +'{ "DoB":"2000-01-01T00:00:00","Text":"junk" }', +NULL, +1) +"""); + + // JSON array where entity should be + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO "Junk" ("Collection", "Reference", "Id") +VALUES( +NULL, +'[{ "DoB":"2000-01-01T00:00:00","Text":"junk" }]', +2) +"""); + } + + protected override Task SeedJunkInJson(DbContext ctx) + => ctx.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO "Entities" ("Collection", "CollectionWithCtor", "Reference", "ReferenceWithCtor", "Id") +VALUES( +'[{"JunkReference":{"Something":"SomeValue" },"Name":"c11","JunkProperty1":50,"Number":11.5,"JunkCollection1":[],"JunkCollection2":[{"Foo":"junk value"}],"NestedCollection":[{"DoB":"2002-04-01T00:00:00","DummyProp":"Dummy value"},{"DoB":"2002-04-02T00:00:00","DummyReference":{"Foo":5}}],"NestedReference":{"DoB":"2002-03-01T00:00:00"}},{"Name":"c12","Number":12.5,"NestedCollection":[{"DoB":"2002-06-01T00:00:00"},{"DoB":"2002-06-02T00:00:00"}],"NestedDummy":59,"NestedReference":{"DoB":"2002-05-01T00:00:00"}}]', +'[{"MyBool":true,"Name":"c11 ctor","JunkReference":{"Something":"SomeValue","JunkCollection":[{"Foo":"junk value"}]},"NestedCollection":[{"DoB":"2002-08-01T00:00:00"},{"DoB":"2002-08-02T00:00:00"}],"NestedReference":{"DoB":"2002-07-01T00:00:00"}},{"MyBool":false,"Name":"c12 ctor","NestedCollection":[{"DoB":"2002-10-01T00:00:00"},{"DoB":"2002-10-02T00:00:00"}],"JunkCollection":[{"Foo":"junk value"}],"NestedReference":{"DoB":"2002-09-01T00:00:00"}}]', +'{"Name":"r1","JunkCollection":[{"Foo":"junk value"}],"JunkReference":{"Something":"SomeValue" },"Number":1.5,"NestedCollection":[{"DoB":"2000-02-01T00:00:00","JunkReference":{"Something":"SomeValue"}},{"DoB":"2000-02-02T00:00:00"}],"NestedReference":{"DoB":"2000-01-01T00:00:00"}}', +'{"MyBool":true,"JunkCollection":[{"Foo":"junk value"}],"Name":"r1 ctor","JunkReference":{"Something":"SomeValue" },"NestedCollection":[{"DoB":"2001-02-01T00:00:00"},{"DoB":"2001-02-02T00:00:00"}],"NestedReference":{"JunkCollection":[{"Foo":"junk value"}],"DoB":"2001-01-01T00:00:00"}}', +1) +"""); + + protected override Task SeedTrickyBuffering(DbContext ctx) + => ctx.Database.ExecuteSqlAsync( + $$$""" +INSERT INTO "Entities" ("Reference", "Id") +VALUES( +'{"Name": "r1", "Number": 7, "JunkReference":{"Something": "SomeValue" }, "JunkCollection": [{"Foo": "junk value"}], "NestedReference": {"DoB": "2000-01-01T00:00:00"}, "NestedCollection": [{"DoB": "2000-02-01T00:00:00", "JunkReference": {"Something": "SomeValue"}}, {"DoB": "2000-02-02T00:00:00"}]}',1) +"""); + + protected override Task SeedShadowProperties(DbContext ctx) + => ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO "Entities" ("Collection", "CollectionWithCtor", "Reference", "ReferenceWithCtor", "Id", "Name") +VALUES( +'[{"Name":"e1_c1","ShadowDouble":5.5},{"ShadowDouble":20.5,"Name":"e1_c2"}]', +'[{"Name":"e1_c1 ctor","ShadowNullableByte":6},{"ShadowNullableByte":null,"Name":"e1_c2 ctor"}]', +'{"Name":"e1_r", "ShadowString":"Foo"}', +'{"ShadowInt":143,"Name":"e1_r ctor"}', +1, +'e1') +"""); + + protected override async Task SeedNotICollection(DbContext ctx) + { + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO "Entities" ("Json", "Id") +VALUES( +'{"Collection":[{"Bar":11,"Foo":"c11"},{"Bar":12,"Foo":"c12"},{"Bar":13,"Foo":"c13"}]}', +1) +"""); + + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO "Entities" ("Json", "Id") +VALUES( +'{"Collection":[{"Bar":21,"Foo":"c21"},{"Bar":22,"Foo":"c22"}]}', +2) +"""); + } + + protected override async Task SeedBadJsonProperties(ContextBadJsonProperties ctx) + { + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO Entities (Id, Scenario, OptionalReference, RequiredReference, Collection) +VALUES( +1, +'baseline', +'{"NestedOptional": { "Text":"or no" }, "NestedRequired": { "Text":"or nr" }, "NestedCollection": [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ] }', +'{"NestedOptional": { "Text":"rr no" }, "NestedRequired": { "Text":"rr nr" }, "NestedCollection": [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ] }', +'[ +{"NestedOptional": { "Text":"c 1 no" }, "NestedRequired": { "Text":"c 1 nr" }, "NestedCollection": [ { "Text":"c 1 nc 1" }, { "Text":"c 1 nc 2" } ] }, +{"NestedOptional": { "Text":"c 2 no" }, "NestedRequired": { "Text":"c 2 nr" }, "NestedCollection": [ { "Text":"c 2 nc 1" }, { "Text":"c 2 nc 2" } ] } +]') +"""); + + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO Entities (Id, Scenario, OptionalReference, RequiredReference, Collection) +VALUES( +2, +'duplicated navigations', +'{"NestedOptional": { "Text":"or no" }, "NestedOptional": { "Text":"or no dupnav" }, "NestedRequired": { "Text":"or nr" }, "NestedCollection": [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ], "NestedCollection": [ { "Text":"or nc 1 dupnav" }, { "Text":"or nc 2 dupnav" } ], "NestedRequired": { "Text":"or nr dupnav" } }', +'{"NestedOptional": { "Text":"rr no" }, "NestedOptional": { "Text":"rr no dupnav" }, "NestedRequired": { "Text":"rr nr" }, "NestedCollection": [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ], "NestedCollection": [ { "Text":"rr nc 1 dupnav" }, { "Text":"rr nc 2 dupnav" } ], "NestedRequired": { "Text":"rr nr dupnav" } }', +'[ +{"NestedOptional": { "Text":"c 1 no" }, "NestedOptional": { "Text":"c 1 no dupnav" }, "NestedRequired": { "Text":"c 1 nr" }, "NestedCollection": [ { "Text":"c 1 nc 1" }, { "Text":"c 1 nc 2" } ], "NestedCollection": [ { "Text":"c 1 nc 1 dupnav" }, { "Text":"c 1 nc 2 dupnav" } ], "NestedRequired": { "Text":"c 1 nr dupnav" } }, +{"NestedOptional": { "Text":"c 2 no" }, "NestedOptional": { "Text":"c 2 no dupnav" }, "NestedRequired": { "Text":"c 2 nr" }, "NestedCollection": [ { "Text":"c 2 nc 1" }, { "Text":"c 2 nc 2" } ], "NestedCollection": [ { "Text":"c 2 nc 1 dupnav" }, { "Text":"c 2 nc 2 dupnav" } ], "NestedRequired": { "Text":"c 2 nr dupnav" } } +]') +"""); + + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO Entities (Id, Scenario, OptionalReference, RequiredReference, Collection) +VALUES( +3, +'duplicated scalars', +'{"NestedOptional": { "Text":"or no", "Text":"or no dupprop" }, "NestedRequired": { "Text":"or nr", "Text":"or nr dupprop" }, "NestedCollection": [ { "Text":"or nc 1", "Text":"or nc 1 dupprop" }, { "Text":"or nc 2", "Text":"or nc 2 dupprop" } ] }', +'{"NestedOptional": { "Text":"rr no", "Text":"rr no dupprop" }, "NestedRequired": { "Text":"rr nr", "Text":"rr nr dupprop" }, "NestedCollection": [ { "Text":"rr nc 1", "Text":"rr nc 1 dupprop" }, { "Text":"rr nc 2", "Text":"rr nc 2 dupprop" } ] }', +'[ +{"NestedOptional": { "Text":"c 1 no", "Text":"c 1 no dupprop" }, "NestedRequired": { "Text":"c 1 nr", "Text":"c 1 nr dupprop" }, "NestedCollection": [ { "Text":"c 1 nc 1", "Text":"c 1 nc 1 dupprop" }, { "Text":"c 1 nc 2", "Text":"c 1 nc 2 dupprop" } ] }, +{"NestedOptional": { "Text":"c 2 no", "Text":"c 2 no dupprop" }, "NestedRequired": { "Text":"c 2 nr", "Text":"c 2 nr dupprop" }, "NestedCollection": [ { "Text":"c 2 nc 1", "Text":"c 2 nc 1 dupprop" }, { "Text":"c 2 nc 2", "Text":"c 2 nc 2 dupprop" } ] } +]') +"""); + + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO Entities (Id, Scenario, OptionalReference, RequiredReference, Collection) +VALUES( +4, +'empty navigation property names', +'{"": { "Text":"or no" }, "": { "Text":"or nr" }, "": [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ] }', +'{"": { "Text":"rr no" }, "": { "Text":"rr nr" }, "": [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ] }', +'[ +{"": { "Text":"c 1 no" }, "": { "Text":"c 1 nr" }, "": [ { "Text":"c 1 nc 1" }, { "Text":"c 1 nc 2" } ] }, +{"": { "Text":"c 2 no" }, "": { "Text":"c 2 nr" }, "": [ { "Text":"c 2 nc 1" }, { "Text":"c 2 nc 2" } ] } +]') +"""); + + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO Entities (Id, Scenario, OptionalReference, RequiredReference, Collection) +VALUES( +5, +'empty scalar property names', +'{"NestedOptional": { "":"or no" }, "NestedRequired": { "":"or nr" }, "NestedCollection": [ { "":"or nc 1" }, { "":"or nc 2" } ] }', +'{"NestedOptional": { "":"rr no" }, "NestedRequired": { "":"rr nr" }, "NestedCollection": [ { "":"rr nc 1" }, { "":"rr nc 2" } ] }', +'[ +{"NestedOptional": { "":"c 1 no" }, "NestedRequired": { "":"c 1 nr" }, "NestedCollection": [ { "":"c 1 nc 1" }, { "":"c 1 nc 2" } ] }, +{"NestedOptional": { "":"c 2 no" }, "NestedRequired": { "":"c 2 nr" }, "NestedCollection": [ { "":"c 2 nc 1" }, { "":"c 2 nc 2" } ] } +]') +"""); + + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO Entities (Id, Scenario, OptionalReference, RequiredReference, Collection) +VALUES( +10, +'null navigation property names', +'{null: { "Text":"or no" }, null: { "Text":"or nr" }, null: [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ] }', +'{null: { "Text":"rr no" }, null: { "Text":"rr nr" }, null: [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ] }', +'[ +{null: { "Text":"c 1 no" }, null: { "Text":"c 1 nr" }, null: [ { "Text":"c 1 nc 1" }, { "Text":"c 1 nc 2" } ] }, +{null: { "Text":"c 2 no" }, null: { "Text":"c 2 nr" }, null: [ { "Text":"c 2 nc 1" }, { "Text":"c 2 nc 2" } ] } +]') +"""); + + await ctx.Database.ExecuteSqlAsync( + $$""" +INSERT INTO Entities (Id, Scenario, OptionalReference, RequiredReference, Collection) +VALUES( +11, +'null scalar property names', +'{"NestedOptional": { null:"or no", "Text":"or no nonnull" }, "NestedRequired": { null:"or nr", "Text":"or nr nonnull" }, "NestedCollection": [ { null:"or nc 1", "Text":"or nc 1 nonnull" }, { null:"or nc 2", "Text":"or nc 2 nonnull" } ] }', +'{"NestedOptional": { null:"rr no", "Text":"rr no nonnull" }, "NestedRequired": { null:"rr nr", "Text":"rr nr nonnull" }, "NestedCollection": [ { null:"rr nc 1", "Text":"rr nc 1 nonnull" }, { null:"rr nc 2", "Text":"rr nc 2 nonnull" } ] }', +'[ +{"NestedOptional": { null:"c 1 no", "Text":"c 1 no nonnull" }, "NestedRequired": { null:"c 1 nr", "Text":"c 1 nr nonnull" }, "NestedCollection": [ { null:"c 1 nc 1", "Text":"c 1 nc 1 nonnull" }, { null:"c 1 nc 2", "Text":"c 1 nc 2 nonnull" } ] }, +{"NestedOptional": { null:"c 2 no", "Text":"c 2 no nonnull" }, "NestedRequired": { null:"c 2 nr", "Text":"c 2 nr nonnull" }, "NestedCollection": [ { null:"c 2 nc 1", "Text":"c 2 nc 1 nonnull" }, { null:"c 2 nc 2", "Text":"c 2 nc 2 nonnull" } ] } +]') +"""); + } + + public override Task SelectMany_over_primitive_collection_nested_in_complex_collection_inside_json_column() + => Assert.ThrowsAsync( + base.SelectMany_over_primitive_collection_nested_in_complex_collection_inside_json_column); + + #endregion + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); }