diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocJsonQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocJsonQueryCosmosTest.cs deleted file mode 100644 index 8aac913ff9f..00000000000 --- a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocJsonQueryCosmosTest.cs +++ /dev/null @@ -1,1644 +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.Storage.Internal; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Microsoft.EntityFrameworkCore.Query; - -public class AdHocJsonQueryCosmosTest(NonSharedFixture fixture) : AdHocJsonQueryTestBase(fixture) -{ - #region 21006 - - public override async Task Project_root_with_missing_scalars(bool async) - { - if (async) - { - await base.Project_root_with_missing_scalars(async); - - AssertSql( - """ -SELECT VALUE c -FROM root c -WHERE (c["Id"] < 4) -"""); - } - } - - [ConditionalTheory(Skip = "issue #35702")] - public override async Task Project_top_level_json_entity_with_missing_scalars(bool async) - { - if (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) - { - if (async) - { - await AssertTranslationFailed(() => base.Project_nested_json_entity_with_missing_scalars(async)); - - AssertSql(); - } - } - - [ConditionalTheory(Skip = "issue #34067")] - public override async Task Project_top_level_entity_with_null_value_required_scalars(bool async) - { - if (async) - { - await base.Project_top_level_entity_with_null_value_required_scalars(async); - - AssertSql( - """ -SELECT c["Id"], c -FROM root c -WHERE (c["Id"] = 4) -"""); - } - } - - public override async Task Project_root_entity_with_missing_required_navigation(bool async) - { - if (async) - { - await base.Project_root_entity_with_missing_required_navigation(async); - - AssertSql( - """ -ReadItem(?, ?) -"""); - } - } - - public override async Task Project_missing_required_navigation(bool async) - { - if (async) - { - await base.Project_missing_required_navigation(async); - - AssertSql( - """ -SELECT VALUE c -FROM root c -WHERE (c["Id"] = 5) -"""); - } - } - - public override async Task Project_root_entity_with_null_required_navigation(bool async) - { - if (async) - { - await base.Project_root_entity_with_null_required_navigation(async); - - AssertSql( - """ -ReadItem(?, ?) -"""); - } - } - - public override async Task Project_null_required_navigation(bool async) - { - if (async) - { - await base.Project_null_required_navigation(async); - - AssertSql( - """ -SELECT VALUE c -FROM root c -WHERE (c["Id"] = 6) -"""); - } - } - - public override async Task Project_missing_required_scalar(bool async) - { - if (async) - { - await base.Project_missing_required_scalar(async); - - AssertSql( - """ -SELECT c["Id"], c["RequiredReference"]["Number"] -FROM root c -WHERE (c["Id"] = 2) -"""); - } - } - - public override async Task Project_null_required_scalar(bool async) - { - if (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 - - [ConditionalTheory(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( - $"Deserialized JSON type '{typeof(JObject).FullName}' is not compatible with expected type '{typeof(JArray).FullName}'. Path 'Collection'.", - 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( - $"Deserialized JSON type '{typeof(JArray).FullName}' is not compatible with expected type '{typeof(JObject).FullName}'. Path 'Reference'.", - 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 - - 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 - - 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; - - 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); - - var nullNavs = - """ -{ - "Id": 10, - "$type": "Entity", - "id": "10", - "Scenario": "null navigation property names", - "OptionalReference": {null: { "Text":"or no" }, null: { "Text":"or nr" }, null: [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ] }, - "RequiredReference": {null: { "Text":"rr no" }, null: { "Text":"rr nr" }, null: [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ] }, - "Collection": - [ - {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 AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - entitiesContainer, - nullNavs, - CancellationToken.None); - - var nullScalars = - """ -{ - "Id": 11, - "$type": "Entity", - "id": "11", - "Scenario": "null scalar property names", - "OptionalReference": {"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" } ] }, - "RequiredReference": {"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" } ] }, - "Collection": - [ - {"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" } ] } - ] -} -"""; - - await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( - entitiesContainer, - nullScalars, - CancellationToken.None); - } - - #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/JsonQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/JsonQueryCosmosTest.cs index 7b4b1da164e..57cd5f12424 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/JsonQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/JsonQueryCosmosTest.cs @@ -5,6 +5,11 @@ using Microsoft.EntityFrameworkCore.Cosmos.Internal; using Microsoft.EntityFrameworkCore.TestModels.JsonQuery; using Microsoft.EntityFrameworkCore.TestUtilities.Xunit; +using Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +#pragma warning disable EF8001 // Owned JSON entities are obsolete namespace Microsoft.EntityFrameworkCore.Query; @@ -2114,6 +2119,1638 @@ public override async Task Project_json_entity_in_tracking_query_fails_even_when Assert.Equal(CoreStrings.OwnedEntitiesCannotBeTrackedWithoutTheirOwner, message); } + + #region 21006 + + public override async Task Project_root_with_missing_scalars(bool async) + { + if (async) + { + await base.Project_root_with_missing_scalars(async); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["Id"] < 4) +"""); + } + } + + [ConditionalTheory(Skip = "issue #35702")] + public override async Task Project_top_level_json_entity_with_missing_scalars(bool async) + { + if (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) + { + if (async) + { + await AssertTranslationFailed(() => base.Project_nested_json_entity_with_missing_scalars(async)); + + AssertSql(); + } + } + + [ConditionalTheory(Skip = "issue #34067")] + public override async Task Project_top_level_entity_with_null_value_required_scalars(bool async) + { + if (async) + { + await base.Project_top_level_entity_with_null_value_required_scalars(async); + + AssertSql( + """ +SELECT c["Id"], c +FROM root c +WHERE (c["Id"] = 4) +"""); + } + } + + public override async Task Project_root_entity_with_missing_required_navigation(bool async) + { + if (async) + { + await base.Project_root_entity_with_missing_required_navigation(async); + + AssertSql( + """ +ReadItem(?, ?) +"""); + } + } + + public override async Task Project_missing_required_navigation(bool async) + { + if (async) + { + await base.Project_missing_required_navigation(async); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["Id"] = 5) +"""); + } + } + + public override async Task Project_root_entity_with_null_required_navigation(bool async) + { + if (async) + { + await base.Project_root_entity_with_null_required_navigation(async); + + AssertSql( + """ +ReadItem(?, ?) +"""); + } + } + + public override async Task Project_null_required_navigation(bool async) + { + if (async) + { + await base.Project_null_required_navigation(async); + + AssertSql( + """ +SELECT VALUE c +FROM root c +WHERE (c["Id"] = 6) +"""); + } + } + + public override async Task Project_missing_required_scalar(bool async) + { + if (async) + { + await base.Project_missing_required_scalar(async); + + AssertSql( + """ +SELECT c["Id"], c["RequiredReference"]["Number"] +FROM root c +WHERE (c["Id"] = 2) +"""); + } + } + + public override async Task Project_null_required_scalar(bool async) + { + if (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 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( + $"Deserialized JSON type '{typeof(JObject).FullName}' is not compatible with expected type '{typeof(JArray).FullName}'. Path 'Collection'.", + 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( + $"Deserialized JSON type '{typeof(JArray).FullName}' is not compatible with expected type '{typeof(JObject).FullName}'. Path 'Reference'.", + 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 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 ArrayOfPrimitives + + protected override void OnModelCreatingArrayOfPrimitives(ModelBuilder modelBuilder) + => base.OnModelCreatingArrayOfPrimitives(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 30028 + + [ConditionalTheory(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 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 LazyLoadingProxies + + protected override void OnModelCreatingLazyLoadingProxies(ModelBuilder modelBuilder) + => base.OnModelCreatingLazyLoadingProxies(modelBuilder); + + #endregion + + #region ShadowProperties + + 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 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 BadJsonProperties + + // missing collection comes back as empty on Cosmos + public override Task Bad_json_properties_empty_navigations(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); + + var nullNavs = + """ +{ + "Id": 10, + "$type": "Entity", + "id": "10", + "Scenario": "null navigation property names", + "OptionalReference": {null: { "Text":"or no" }, null: { "Text":"or nr" }, null: [ { "Text":"or nc 1" }, { "Text":"or nc 2" } ] }, + "RequiredReference": {null: { "Text":"rr no" }, null: { "Text":"rr nr" }, null: [ { "Text":"rr nc 1" }, { "Text":"rr nc 2" } ] }, + "Collection": + [ + {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 AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + nullNavs, + CancellationToken.None); + + var nullScalars = + """ +{ + "Id": 11, + "$type": "Entity", + "id": "11", + "Scenario": "null scalar property names", + "OptionalReference": {"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" } ] }, + "RequiredReference": {"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" } ] }, + "Collection": + [ + {"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" } ] } + ] +} +"""; + + await AdHocCosmosTestHelpers.CreateCustomEntityHelperAsync( + entitiesContainer, + nullScalars, + CancellationToken.None); + } + + #endregion + + + private TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + => TestSqlLoggerFactory.AssertBaseline(expected); + + new static async Task AssertTranslationFailed(Func query) + => Assert.Contains( + CoreStrings.TranslationFailed("")[..^1], + (await Assert.ThrowsAsync(query)).Message); + + protected override DbContextOptionsBuilder AddNonSharedOptions(DbContextOptionsBuilder builder) + => builder.ConfigureWarnings(w => w.Ignore(CosmosEventId.NoPartitionKeyDefined)); + + protected override ITestStoreFactory NonSharedTestStoreFactory + => CosmosTestStoreFactory.Instance; + } 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 74c255a1248..00000000000 --- a/test/EFCore.Relational.Specification.Tests/Query/AdHocComplexTypeQueryRelationalTestBase.cs +++ /dev/null @@ -1,178 +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 - - [ConditionalFact] - 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 - - [ConditionalFact] - 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 - - [ConditionalFact] - 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 - - 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 e62be31c9f2..00000000000 --- a/test/EFCore.Relational.Specification.Tests/Query/AdHocJsonQueryRelationalTestBase.cs +++ /dev/null @@ -1,701 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#pragma warning disable EF8001 // Owned JSON entities are obsolete - -using System.Text.Json; - -namespace Microsoft.EntityFrameworkCore.Query; - -#nullable disable - -public abstract class AdHocJsonQueryRelationalTestBase(NonSharedFixture fixture) : AdHocJsonQueryTestBase(fixture) -{ - #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 - - [ConditionalFact] - 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); - } - - [ConditionalFact] - 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); - } - - [ConditionalFact] - 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 - - [ConditionalFact] // #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 - - [ConditionalFact] - 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 - - 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..2636fda6060 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/ComplexTypeQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/ComplexTypeQueryRelationalTestBase.cs @@ -71,6 +71,175 @@ public override async Task Union_two_different_struct_complex_type(bool async) AssertSql(); } + #region Non-shared tests + + #region 37205 + + [ConditionalFact] + 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 + + [ConditionalFact] + 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 + + [ConditionalFact] + 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 + + #endregion Non-shared tests + 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 a7644f81825..506865e3763 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/JsonQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/JsonQueryRelationalTestBase.cs @@ -3,6 +3,10 @@ using Microsoft.EntityFrameworkCore.TestModels.JsonQuery; +using System.Text.Json; + +#pragma warning disable EF8001 // Owned JSON entities are obsolete + namespace Microsoft.EntityFrameworkCore.Query; public abstract class JsonQueryRelationalTestBase(TFixture fixture) : JsonQueryTestBase(fixture) @@ -107,6 +111,257 @@ public virtual Task FromSql_on_entity_with_json_inheritance_project_reference_on assertOrder: true); [ConditionalTheory, MemberData(nameof(IsAsyncData))] + + #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 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 34293 + + [ConditionalFact] + 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); + } + + [ConditionalFact] + 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); + } + + [ConditionalFact] + 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; } = null!; + + public class Entity + { + public int Id { get; set; } + public JsonRoot Json { get; set; } = null!; + } + + public class JsonRoot + { + public DateTime Date { get; set; } + + public JsonBranch Required { get; set; } = null!; + } + + 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 + public virtual async Task Json_projection_using_queryable_methods_on_top_of_JSON_collection_AsNoTrackingWithIdentityResolution( bool async) { @@ -565,4 +820,444 @@ public virtual async Task Json_projection_second_element_projected_before_owner_ nameof(QueryTrackingBehavior.NoTrackingWithIdentityResolution)), message); } + + #region 32310 + + protected override void OnModelCreating32310(ModelBuilder modelBuilder) + { + base.OnModelCreating32310(modelBuilder); + + modelBuilder.Entity().OwnsOne(e => e.Visits).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 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 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 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 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 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 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 Entity splitting + + [ConditionalFact] // #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; } = null!; + } + + public class JsonEntity + { + public string? Foo { get; set; } + } + } + + #endregion + + #region HasJsonPropertyName + + [ConditionalFact] + 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; } = null!; + + public class Entity + { + public int Id { get; set; } + public JsonComplexType Json { get; set; } = null!; + } + + public class JsonComplexType + { + public string? String { get; set; } + + public JsonNestedType Nested { get; set; } = null!; + public List NestedCollection { get; set; } = null!; + } + + public class JsonNestedType + { + public int Int { get; set; } + } + } + + #endregion HasJsonPropertyName + + #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 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 + + protected TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + protected virtual string? JsonColumnType + => null; + } + diff --git a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs deleted file mode 100644 index 5f7faf6978d..00000000000 --- a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs +++ /dev/null @@ -1,287 +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 - - [ConditionalFact] - 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 - { - 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().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 - - [ConditionalFact] - 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.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 - - [ConditionalFact] - public virtual async Task Optional_complex_type_with_discriminator() - { - var contextFactory = await InitializeNonSharedTest( - seed: context => - { - context.AddRange( - new ContextShadowDiscriminator.EntityType - { - AllOptionalsComplexType = new ContextShadowDiscriminator.AllOptionalsComplexType { OptionalProperty = "Non-null" } - }, - new ContextShadowDiscriminator.EntityType - { - AllOptionalsComplexType = new ContextShadowDiscriminator.AllOptionalsComplexType { OptionalProperty = null } - }, - new ContextShadowDiscriminator.EntityType - { - 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(); - } - - private class ContextShadowDiscriminator(DbContextOptions options) : DbContext(options) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - => modelBuilder.Entity() - .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 - - [ConditionalFact] - public virtual async Task Non_optional_complex_type_with_all_nullable_properties() - { - var contextFactory = await InitializeNonSharedTest( - seed: context => - { - context.Add( - new Context37162.EntityType - { - 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().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 Issue37337 - - [ConditionalFact] - public virtual async Task Nullable_complex_type_with_discriminator_and_shadow_property() - { - var contextFactory = await InitializeNonSharedTest( - seed: context => - { - context.Add( - new Context37337.EntityType - { - Prop = new Context37337.OptionalComplexProperty - { - OptionalValue = true - } - }); - 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); - } - - private class Context37337(DbContextOptions options) : DbContext(options) - { - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - var entity = modelBuilder.Entity(); - entity.Property(p => p.Id); - 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("CreatedBy").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 - - 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 8371d13018c..00000000000 --- a/test/EFCore.Specification.Tests/Query/AdHocJsonQueryTestBase.cs +++ /dev/null @@ -1,1845 +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 - - [ConditionalTheory, 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)); - } - - [ConditionalTheory, 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)); - } - - [ConditionalTheory, 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)); - } - - [ConditionalTheory, 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); - } - - [ConditionalTheory, 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)); - } - - [ConditionalTheory, 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); - } - - [ConditionalTheory, 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)); - } - - [ConditionalTheory, 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); - } - - [ConditionalTheory, 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); - } - - [ConditionalTheory, 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 - - [ConditionalFact] - 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); - } - - [ConditionalFact] - 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 - - [ConditionalFact] - 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); - } - - [ConditionalTheory, 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 - - [ConditionalFact] - 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 - - [ConditionalFact] - 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 - - [ConditionalFact] - 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 - - [ConditionalFact] - public virtual async Task Project_entity_with_json_null_values() - { - var contextFactory = await InitializeNonSharedTest(seed: Seed34960, onModelCreating: OnModelCreating34960); - - using var context = contextFactory.CreateDbContext(); - var query = await context.Entities.ToListAsync(); - } - - [ConditionalFact] - public virtual async Task Try_project_collection_but_JSON_is_entity() - { - var contextFactory = await InitializeNonSharedTest(seed: Seed34960, onModelCreating: OnModelCreating34960); - using var context = contextFactory.CreateDbContext(); - - await context.Junk.AsNoTracking().Where(x => x.Id == 1).Select(x => x.Collection).FirstOrDefaultAsync(); - } - - [ConditionalFact] - public virtual async Task Try_project_reference_but_JSON_is_collection() - { - var contextFactory = await InitializeNonSharedTest(seed: Seed34960, onModelCreating: OnModelCreating34960); - 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 - - [ConditionalFact] - 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); - } - - [ConditionalFact(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); - } - - [ConditionalFact] - 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(); - } - - [ConditionalFact] - 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]); - } - - [ConditionalFact] - 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]); - } - - [ConditionalFact, 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 - - [ConditionalFact] - 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); - } - - [ConditionalFact] - 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 - - [ConditionalFact] - 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 - - [ConditionalFact] - 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); - } - - [ConditionalFact] - 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); - } - - [ConditionalFact] - 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 - - [ConditionalFact] - 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); - } - - 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 }; - var c12 = new ContextLazyLoadingProxies.MyJsonEntity { Name = "c12", Number = 12 }; - var c13 = new ContextLazyLoadingProxies.MyJsonEntity { Name = "c13", Number = 13 }; - - var r2 = new ContextLazyLoadingProxies.MyJsonEntityWithCtor("r2", 2); - var c21 = new ContextLazyLoadingProxies.MyJsonEntity { Name = "c21", Number = 21 }; - var c22 = new ContextLazyLoadingProxies.MyJsonEntity { Name = "c22", Number = 22 }; - - 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; } - } - } - - #endregion - - #region NotICollection - - [ConditionalFact] - 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 - - [ConditionalTheory, 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); - } - - [ConditionalTheory, 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); - } - - [ConditionalTheory, 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); - } - - [ConditionalTheory, 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); - } - - [ConditionalTheory, 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"); - } - - [ConditionalTheory, 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 c6a4dd2038f..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 - - [ConditionalFact] - 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 - - [ConditionalTheory, 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 = new List { 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 23062c6dc78..f1b3a599c94 100644 --- a/test/EFCore.Specification.Tests/Query/ComplexTypeQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/ComplexTypeQueryTestBase.cs @@ -806,4 +806,283 @@ 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 + + [ConditionalFact] + 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 + { + 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().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 + + [ConditionalFact] + 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.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 34749 + + #region ShadowDiscriminator + + [ConditionalFact] + public virtual async Task Optional_complex_type_with_discriminator() + { + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + context.AddRange( + new ContextShadowDiscriminator.EntityType + { + AllOptionalsComplexType = new ContextShadowDiscriminator.AllOptionalsComplexType { OptionalProperty = "Non-null" } + }, + new ContextShadowDiscriminator.EntityType + { + AllOptionalsComplexType = new ContextShadowDiscriminator.AllOptionalsComplexType { OptionalProperty = null } + }, + new ContextShadowDiscriminator.EntityType + { + 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(); + } + + private class ContextShadowDiscriminator(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity() + .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 + + [ConditionalFact] + public virtual async Task Non_optional_complex_type_with_all_nullable_properties() + { + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + context.Add( + new Context37162.EntityType + { + 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().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 37337 + + [ConditionalFact] + public virtual async Task Nullable_complex_type_with_discriminator_and_shadow_property() + { + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + context.Add( + new Context37337.EntityType + { + Prop = new Context37337.OptionalComplexProperty + { + OptionalValue = true + } + }); + 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); + } + + private class Context37337(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + var entity = modelBuilder.Entity(); + entity.Property(p => p.Id); + 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("CreatedBy").IsRequired(false); + } + + public class EntityType + { + public Guid Id { get; set; } + public OptionalComplexProperty? Prop { get; set; } + } + + public class OptionalComplexProperty + { + public bool? OptionalValue { get; set; } + } + } + + #endregion 37337 + + #endregion Non-shared test resources } diff --git a/test/EFCore.Specification.Tests/Query/JsonQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/JsonQueryTestBase.cs index 5a5aa932f5b..d77eb872995 100644 --- a/test/EFCore.Specification.Tests/Query/JsonQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/JsonQueryTestBase.cs @@ -347,6 +347,635 @@ public virtual Task Project_entity_with_single_owned(bool async) async, ss => ss.Set()); + #region 32939 + + [ConditionalFact] + 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 21006 + + [ConditionalTheory, 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)); + } + + [ConditionalTheory, 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)); + } + + [ConditionalTheory, 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)); + } + + [ConditionalTheory, 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); + } + + [ConditionalTheory, 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)); + } + + [ConditionalTheory, 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); + } + + [ConditionalTheory, 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)); + } + + [ConditionalTheory, 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); + } + + [ConditionalTheory, 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); + } + + [ConditionalTheory, 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 34960 + + [ConditionalFact] + public virtual async Task Project_entity_with_json_null_values() + { + var contextFactory = await InitializeNonSharedTest(seed: Seed34960, onModelCreating: OnModelCreating34960); + + using var context = contextFactory.CreateDbContext(); + var query = await context.Entities.ToListAsync(); + } + + [ConditionalFact] + public virtual async Task Try_project_collection_but_JSON_is_entity() + { + var contextFactory = await InitializeNonSharedTest(seed: Seed34960, onModelCreating: OnModelCreating34960); + using var context = contextFactory.CreateDbContext(); + + await context.Junk.AsNoTracking().Where(x => x.Id == 1).Select(x => x.Collection).FirstOrDefaultAsync(); + } + + [ConditionalFact] + public virtual async Task Try_project_reference_but_JSON_is_collection() + { + var contextFactory = await InitializeNonSharedTest(seed: Seed34960, onModelCreating: OnModelCreating34960); + 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 + + [ConditionalTheory, MemberData(nameof(IsAsyncData))] public virtual Task LeftJoin_json_entities(bool async) => AssertQuery( @@ -773,6 +1402,60 @@ public virtual Task Json_collection_ElementAt_and_pushdown(bool async) ss => ss.Set() .Select(x => new { x.Id, CollectionElement = x.OwnedCollectionRoot.Select(xx => xx.Number).ElementAt(0) })); + #region 32310 + + [ConditionalFact] + 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 + + [ConditionalTheory, MemberData(nameof(IsAsyncData))] public virtual Task Json_collection_Any_with_predicate(bool async) => AssertQuery( @@ -1193,6 +1876,305 @@ public virtual async Task Json_collection_Select_entity_with_initializer_Element ss => ss.Set() .Select(x => x.OwnedCollectionRoot.Select(xx => new JsonEntityBasic { Id = x.Id }).ElementAt(0))); + #region 33046 + + [ConditionalFact] + 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 ArrayOfPrimitives + + [ConditionalFact] + 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); + } + + [ConditionalFact(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); + } + + [ConditionalFact] + 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(); + } + + [ConditionalFact] + 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]); + } + + [ConditionalFact] + 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]); + } + + [ConditionalFact, 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 NotICollection + + [ConditionalFact] + 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 + + [ConditionalTheory, MemberData(nameof(IsAsyncData))] public virtual Task Json_projection_deduplication_with_collection_indexer_in_original(bool async) => AssertQuery( @@ -1263,6 +2245,131 @@ public virtual Task Json_projection_deduplication_with_collection_in_original_an }); } + #region 30028 + + [ConditionalFact] + 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); + } + + [ConditionalTheory, 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 + + [ConditionalTheory, MemberData(nameof(IsAsyncData))] public virtual Task Json_collection_index_in_projection_using_constant_when_owner_is_present(bool async) => AssertQuery( @@ -1533,6 +2640,96 @@ public virtual Task Json_scalar_optional_null_semantics(bool async) .Where(x => x.OwnedReferenceRoot.Name != x.OwnedReferenceRoot.OwnedReferenceBranch.OwnedReferenceLeaf.SomethingSomething) .Select(x => x.Name)); + #region 29219 + + [ConditionalFact] + 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); + } + + [ConditionalFact] + 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 + + [ConditionalTheory, MemberData(nameof(IsAsyncData))] public virtual Task Group_by_on_json_scalar(bool async) => AssertQuery( @@ -1740,6 +2937,104 @@ public virtual Task Json_with_projection_of_mix_of_json_collections_json_referen AssertCollection(e.EntityCollection, a.EntityCollection); }); + #region LazyLoadingProxies + + [ConditionalFact] + 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); + } + + 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 }; + var c12 = new ContextLazyLoadingProxies.MyJsonEntity { Name = "c12", Number = 12 }; + var c13 = new ContextLazyLoadingProxies.MyJsonEntity { Name = "c13", Number = 13 }; + + var r2 = new ContextLazyLoadingProxies.MyJsonEntityWithCtor("r2", 2); + var c21 = new ContextLazyLoadingProxies.MyJsonEntity { Name = "c21", Number = 21 }; + var c22 = new ContextLazyLoadingProxies.MyJsonEntity { Name = "c22", Number = 22 }; + + 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; } + } + } + + #endregion + + [ConditionalTheory, MemberData(nameof(IsAsyncData))] public virtual Task Json_all_types_entity_projection(bool async) => AssertQuery( @@ -1788,6 +3083,143 @@ public virtual Task Json_all_types_projection_individual_properties(bool async) x.Reference.TestNullableEnumWithConverterThatHandlesNulls, })); + #region ShadowProperties + + [ConditionalFact] + 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); + } + + [ConditionalFact] + 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); + } + + [ConditionalFact] + 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 + + [ConditionalTheory, MemberData(nameof(IsAsyncData))] public virtual Task Json_boolean_predicate(bool async) => AssertQuery( @@ -2331,6 +3763,408 @@ public virtual Task Json_projection_second_element_projected_before_owner_nested AssertEqual(e.Owner, a.Owner); }); + #region JunkInJson + + [ConditionalFact] + 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); + } + + [ConditionalFact] + 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 + + [ConditionalFact] + 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 BadJsonProperties + + [ConditionalTheory, 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); + } + + [ConditionalTheory, 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); + } + + [ConditionalTheory, 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); + } + + [ConditionalTheory, 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); + } + + [ConditionalTheory, 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"); + } + + [ConditionalTheory, 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 + + [ConditionalTheory, MemberData(nameof(IsAsyncData))] public virtual Task Project_json_reference_in_tracking_query_fails(bool async) // verify exception on the provider level, relational and core throw different exceptions @@ -2356,4 +4190,13 @@ public virtual Task Project_json_entity_in_tracking_query_fails_even_when_owner_ x.OwnedReferenceRoot, x.OwnedCollectionRoot })); + + 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 7580293d42e..8183d20ee3c 100644 --- a/test/EFCore.Specification.Tests/Query/ManyToManyQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/ManyToManyQueryTestBase.cs @@ -987,4 +987,175 @@ protected ManyToManyContext CreateContext() protected virtual void ClearLog() { } + + #region Non-shared test resources + + #region 7973 + + [ConditionalFact] + 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 + + [ConditionalTheory, 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 = new List { 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 de51ef8ec48..00000000000 --- a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocComplexTypeQuerySqlServerTest.cs +++ /dev/null @@ -1,122 +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 - - [ConditionalFact] - 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 2e1cb8eb454..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; - -[SqlServerCondition(SqlServerCondition.SupportsJsonType)] -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 123a5a2228e..00000000000 --- a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocJsonQuerySqlServerTestBase.cs +++ /dev/null @@ -1,685 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#pragma warning disable EF8001 // Owned JSON entities are obsolete - -using Microsoft.Data.SqlClient; -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 EnumLegacyValues - - [ConditionalTheory, 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(); - } - } - - [ConditionalTheory, 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))); - } - - [ConditionalTheory, 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] -"""); - } -} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocManyToManyQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/AdHocManyToManyQuerySqlServerTest.cs deleted file mode 100644 index 521ab49a063..00000000000 --- a/test/EFCore.SqlServer.FunctionalTests/Query/AdHocManyToManyQuerySqlServerTest.cs +++ /dev/null @@ -1,101 +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], [s].[Id], [s0].[Id] -""", - // - """ -@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], [s].[Id], [s0].[Id] -"""); - } -} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexTypeQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexTypeQuerySqlServerTest.cs index c6032822599..4569af9b158 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; @@ -1261,6 +1262,172 @@ ORDER BY [c1].[Id] DESC """); } + #region Non-shared tests + + #region 33449 + + 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 +"""); + } + + #endregion 33449 + + #region 34749 + + 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] +"""); + } + + #endregion 34749 + + #region ShadowDiscriminator + + public override async Task Optional_complex_type_with_discriminator() + { + await base.Optional_complex_type_with_discriminator(); + + AssertSql(); + } + + #endregion ShadowDiscriminator + + #region 37162 + + public override async Task Non_optional_complex_type_with_all_nullable_properties() + { + await base.Non_optional_complex_type_with_all_nullable_properties(); + + AssertSql(); + } + + #endregion 37162 + + #region 37337 + + public override async Task Nullable_complex_type_with_discriminator_and_shadow_property() + { + await base.Nullable_complex_type_with_discriminator_and_shadow_property(); + + AssertSql(); + } + + #endregion 37337 + + #region 37205 + + public override async Task Complex_json_collection_inside_left_join_subquery() + { + await base.Complex_json_collection_inside_left_join_subquery(); + + AssertSql(); + } + + #endregion 37205 + + #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 36837 + + [ConditionalFact] + 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 + + #endregion Non-shared tests + [ConditionalFact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPCManyToManyQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPCManyToManyQuerySqlServerTest.cs index 338965af09a..03fb0e0b974 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPCManyToManyQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPCManyToManyQuerySqlServerTest.cs @@ -2811,6 +2811,114 @@ 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( + """ +SET NOCOUNT ON; +INSERT INTO [ManyM_DB] +OUTPUT INSERTED.[Id] +DEFAULT VALUES; +INSERT INTO [ManyN_DB] +OUTPUT INSERTED.[Id] +DEFAULT VALUES; +""", + // + """ +@p0='1' +@p1='1' (Nullable = true) + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +INSERT INTO [ManyMN_DB] ([ManyM_Id], [ManyN_Id]) +OUTPUT INSERTED.[Id] +VALUES (@p0, @p1); +""", + // + """ +@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], [s].[Id], [s0].[Id] +""", + // + """ +@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], [s].[Id], [s0].[Id] +"""); + } + 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 05ba2fb710b..dd3f58a534f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPTManyToManyQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/Inheritance/TPTManyToManyQuerySqlServerTest.cs @@ -2758,6 +2758,114 @@ 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( + """ +SET NOCOUNT ON; +INSERT INTO [ManyM_DB] +OUTPUT INSERTED.[Id] +DEFAULT VALUES; +INSERT INTO [ManyN_DB] +OUTPUT INSERTED.[Id] +DEFAULT VALUES; +""", + // + """ +@p0='1' +@p1='1' (Nullable = true) + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +INSERT INTO [ManyMN_DB] ([ManyM_Id], [ManyN_Id]) +OUTPUT INSERTED.[Id] +VALUES (@p0, @p1); +""", + // + """ +@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], [s].[Id], [s0].[Id] +""", + // + """ +@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], [s].[Id], [s0].[Id] +"""); + } + 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 5a46b59f420..07ecf1ba18a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryJsonTypeSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQueryJsonTypeSqlServerTest.cs @@ -5,6 +5,11 @@ using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore.TestModels.JsonQuery; +using Microsoft.EntityFrameworkCore.Diagnostics.Internal; +using Microsoft.EntityFrameworkCore.SqlServer.Diagnostics.Internal; + +#pragma warning disable EF8001 // Owned JSON entities are obsolete + namespace Microsoft.EntityFrameworkCore.Query; [SqlServerCondition(SqlServerCondition.SupportsJsonType)] @@ -3141,6 +3146,727 @@ FROM [JsonEntitiesBasic] AS [j] """); } + + #region Non-shared test overrides and seeds + + 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" } ] } +]') +"""); + } + + #endregion + + #region EnumLegacyValues + + [ConditionalTheory, MemberData(nameof(IsAsyncData))] + public virtual async Task Read_enum_property_with_legacy_values(bool async) + { + await Assert.ThrowsAsync(() => Read_enum_property_with_legacy_values_core(async)); + } + + private 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(); + } + } + + [ConditionalTheory, 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))); + } + + [ConditionalTheory, 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] +"""); + } + + #region BadJsonProperties (json type) + + public override Task Bad_json_properties_duplicated_navigations(bool async) + => Task.CompletedTask; + + public override Task Bad_json_properties_duplicated_scalars(bool async) + => Task.CompletedTask; + + public override Task Bad_json_properties_empty_navigations(bool async) + => Task.CompletedTask; + + public override Task Bad_json_properties_empty_scalars(bool async) + => Task.CompletedTask; + + public override Task Bad_json_properties_null_navigations(bool async) + => Task.CompletedTask; + + public override Task Bad_json_properties_null_scalars(bool async) + => Task.CompletedTask; + + public override async Task Project_entity_with_json_null_values() + { + var exception = await Assert.ThrowsAsync( + () => base.Project_entity_with_json_null_values()); + } + + public override async Task Try_project_collection_but_JSON_is_entity() + { + var exception = await Assert.ThrowsAnyAsync( + () => base.Try_project_collection_but_JSON_is_entity()); + } + + public override async Task Try_project_reference_but_JSON_is_collection() + { + var exception = await Assert.ThrowsAnyAsync( + () => base.Try_project_reference_but_JSON_is_collection()); + } + + #endregion + + protected override string NonSharedStoreName + => "AdHocJsonQueryJsonTypeTest"; + + protected override void ClearLog() + => Fixture.TestSqlLoggerFactory.Clear(); + + protected override void ConfigureWarnings(WarningsConfigurationBuilder builder) + { + base.ConfigureWarnings(builder); + + builder.Log(CoreEventId.StringEnumValueInJson); + } + + protected override string JsonColumnType + => "json"; + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs index 6b4b4bb2c87..567399df7b8 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/JsonQuerySqlServerTest.cs @@ -3,6 +3,10 @@ using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore.TestModels.JsonQuery; +using Microsoft.EntityFrameworkCore.Diagnostics.Internal; +using Microsoft.EntityFrameworkCore.SqlServer.Diagnostics.Internal; + +#pragma warning disable EF8001 // Owned JSON entities are obsolete namespace Microsoft.EntityFrameworkCore.Query; @@ -3099,6 +3103,687 @@ FROM [JsonEntitiesBasic] AS [j] """); } + + #region Non-shared test overrides and seeds + + 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" } ] } +]') +"""); + } + + #endregion + + #region EnumLegacyValues + + [ConditionalTheory, 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)); + + // Arithmetic overflow error converting expression to data type int. + Assert.Equal(245, exception.Number); + } + + private 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(); + } + } + + [ConditionalTheory, 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))); + } + + [ConditionalTheory, 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] +"""); + } + + protected override void ClearLog() + => Fixture.TestSqlLoggerFactory.Clear(); + + protected override void ConfigureWarnings(WarningsConfigurationBuilder builder) + { + base.ConfigureWarnings(builder); + + builder.Log(CoreEventId.StringEnumValueInJson); + } + + protected override string JsonColumnType + => "nvarchar(max)"; + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ManyToManyQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ManyToManyQuerySqlServerTest.cs index 2542abbca90..5592c6e40fe 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ManyToManyQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ManyToManyQuerySqlServerTest.cs @@ -1800,6 +1800,118 @@ 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( + """ +SET NOCOUNT ON; +INSERT INTO [ManyM_DB] +OUTPUT INSERTED.[Id] +DEFAULT VALUES; +INSERT INTO [ManyN_DB] +OUTPUT INSERTED.[Id] +DEFAULT VALUES; +""", + // + """ +@p0='1' +@p1='1' (Nullable = true) + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +INSERT INTO [ManyMN_DB] ([ManyM_Id], [ManyN_Id]) +OUTPUT INSERTED.[Id] +VALUES (@p0, @p1); +""", + // + """ +@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], [s].[Id], [s0].[Id] +""", + // + """ +@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], [s].[Id], [s0].[Id] +"""); + } + + #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 08a4d239cd5..00000000000 --- a/test/EFCore.Sqlite.FunctionalTests/Query/AdHocComplexTypeQuerySqliteTest.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 AdHocComplexTypeQuerySqliteTest(NonSharedFixture fixture) : AdHocComplexTypeQueryRelationalTestBase(fixture) -{ - 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 0cff495dc96..00000000000 --- a/test/EFCore.Sqlite.FunctionalTests/Query/AdHocJsonQuerySqliteTest.cs +++ /dev/null @@ -1,322 +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" } ] } -]') -"""); - } -} 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 b9ed270549c..2d226477ae1 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/ComplexTypeQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/ComplexTypeQuerySqliteTest.cs @@ -1155,6 +1155,66 @@ LIMIT @p1 """); } + #region Non-shared tests + + 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(); + } + + 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(); + } + + public override async Task Optional_complex_type_with_discriminator() + { + await base.Optional_complex_type_with_discriminator(); + + AssertSql(); + } + + public override async Task Non_optional_complex_type_with_all_nullable_properties() + { + await base.Non_optional_complex_type_with_all_nullable_properties(); + + AssertSql(); + } + + public override async Task Nullable_complex_type_with_discriminator_and_shadow_property() + { + await base.Nullable_complex_type_with_discriminator_and_shadow_property(); + + AssertSql(); + } + + public override async Task Complex_json_collection_inside_left_join_subquery() + { + await base.Complex_json_collection_inside_left_join_subquery(); + + AssertSql(); + } + + public override async Task Select_TPC_base_with_ComplexType() + { + await base.Select_TPC_base_with_ComplexType(); + + AssertSql(); + } + + 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(); + } + + #endregion Non-shared tests + [ConditionalFact] 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 b415fe4fdf0..f405102f15f 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/JsonQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/JsonQuerySqliteTest.cs @@ -6,6 +6,8 @@ using Microsoft.EntityFrameworkCore.TestModels.JsonQuery; using Xunit.Sdk; +#pragma warning disable EF8001 // Owned JSON entities are obsolete + namespace Microsoft.EntityFrameworkCore.Query; #nullable disable @@ -457,6 +459,317 @@ public override Task Json_nested_collection_anonymous_projection_in_projection_N public override Task Json_projection_using_queryable_methods_on_top_of_JSON_collection_AsNoTrackingWithIdentityResolution(bool async) => Task.CompletedTask; + + 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" } ] } +]') +"""); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); }