From 1dcfdfb3b3cfb1de10692f61094a5a41becb32c2 Mon Sep 17 00:00:00 2001 From: Andrew Peters Date: Mon, 19 Jun 2017 10:35:39 -0700 Subject: [PATCH] Fix #8843 - Automatic eager loading of owned types. - Adds INavigation.IsEager - navs can now be marked as eager by-default (core metadata only). - Adds a core convention that sets IsEager=true for principal-to-dependent navs when FK.IsOwnership=true. - Updates the query compiler to auto-synthesize Include trees for owned navs on result QSREs. - Adds a new query test suite for testing queries involving ownership. --- .../Query/ComplexNavigationsQueryTestBase.cs | 6 ++ .../{ => Query}/OneToOneQueryFixtureBase.cs | 2 +- .../Query/OwnedQueryFixtureBase.cs | 98 +++++++++++++++++++ .../Query/OwnedQueryTestBase.cs | 57 +++++++++++ .../Internal/CoreConventionSetBuilder.cs | 4 +- .../NavigationEagerLoadingConvention.cs | 25 +++++ src/EFCore/Metadata/IMutableNavigation.cs | 5 + src/EFCore/Metadata/INavigation.cs | 5 + src/EFCore/Metadata/Internal/ForeignKey.cs | 19 ++-- .../Internal/InternalEntityTypeBuilder.cs | 3 + src/EFCore/Metadata/Internal/Navigation.cs | 6 ++ src/EFCore/Query/EntityQueryModelVisitor.cs | 83 ++++++++++++++++ src/EFCore/Query/Internal/IncludeCompiler.cs | 3 +- src/EFCore/breakingchanges.netcore.json | 15 +++ .../Query/OwnedQueryInMemoryFixture.cs | 33 +++++++ .../Query/OwnedQueryInMemoryTest.cs | 21 ++++ .../PropertyEntrySqlServerTest.cs | 2 +- ...mplexNavigationsOwnedQuerySqlServerTest.cs | 14 ++- .../Query/OwnedQuerySqlServerFixture.cs | 44 +++++++++ .../Query/OwnedQuerySqlServerTest.cs | 46 +++++++++ .../AutoincrementTest.cs | 1 + .../Query/OwnedQuerySqliteFixture.cs | 44 +++++++++ .../Query/OwnedQuerySqliteTest.cs | 37 +++++++ 23 files changed, 555 insertions(+), 18 deletions(-) rename src/EFCore.Specification.Tests/{ => Query}/OneToOneQueryFixtureBase.cs (98%) create mode 100644 src/EFCore.Specification.Tests/Query/OwnedQueryFixtureBase.cs create mode 100644 src/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs create mode 100644 src/EFCore/Metadata/Conventions/Internal/NavigationEagerLoadingConvention.cs create mode 100644 test/EFCore.InMemory.FunctionalTests/Query/OwnedQueryInMemoryFixture.cs create mode 100644 test/EFCore.InMemory.FunctionalTests/Query/OwnedQueryInMemoryTest.cs create mode 100644 test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerFixture.cs create mode 100644 test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs create mode 100644 test/EFCore.Sqlite.FunctionalTests/Query/OwnedQuerySqliteFixture.cs create mode 100644 test/EFCore.Sqlite.FunctionalTests/Query/OwnedQuerySqliteTest.cs diff --git a/src/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs b/src/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs index 48591ce354b..467bca3712f 100644 --- a/src/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs +++ b/src/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs @@ -346,6 +346,12 @@ public virtual void Simple_owned_level1() AssertQuery(l1s => l1s.Include(l1 => l1.OneToOne_Required_PK), elementSorter: e => e.Id); } + [ConditionalFact] + public virtual void Simple_owned_level1_convention() + { + AssertQuery(l1s => l1s, elementSorter: e => e.Id); + } + [ConditionalFact] public virtual void Simple_owned_level1_level2() { diff --git a/src/EFCore.Specification.Tests/OneToOneQueryFixtureBase.cs b/src/EFCore.Specification.Tests/Query/OneToOneQueryFixtureBase.cs similarity index 98% rename from src/EFCore.Specification.Tests/OneToOneQueryFixtureBase.cs rename to src/EFCore.Specification.Tests/Query/OneToOneQueryFixtureBase.cs index 9ca59d1f242..d7bd42e7fef 100644 --- a/src/EFCore.Specification.Tests/OneToOneQueryFixtureBase.cs +++ b/src/EFCore.Specification.Tests/Query/OneToOneQueryFixtureBase.cs @@ -1,7 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -namespace Microsoft.EntityFrameworkCore +namespace Microsoft.EntityFrameworkCore.Query { public abstract class OneToOneQueryFixtureBase { diff --git a/src/EFCore.Specification.Tests/Query/OwnedQueryFixtureBase.cs b/src/EFCore.Specification.Tests/Query/OwnedQueryFixtureBase.cs new file mode 100644 index 00000000000..f8895a51e97 --- /dev/null +++ b/src/EFCore.Specification.Tests/Query/OwnedQueryFixtureBase.cs @@ -0,0 +1,98 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.EntityFrameworkCore.Query +{ + public abstract class OwnedQueryFixtureBase + { + protected virtual void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().OwnsOne(p => p.PersonAddress).OwnsOne(a => a.Country); + modelBuilder.Entity().OwnsOne(p => p.BranchAddress).OwnsOne(a => a.Country); + modelBuilder.Entity().OwnsOne(p => p.LeafAAddress).OwnsOne(a => a.Country); + modelBuilder.Entity().OwnsOne(p => p.LeafBAddress).OwnsOne(a => a.Country); + } + + protected static void AddTestData(DbContext context) + { + context.Set().AddRange( + new OwnedPerson + { + PersonAddress = new OwnedAddress + { + Country = new OwnedCountry { Name = "USA" } + } + }, + new Branch + { + PersonAddress = new OwnedAddress + { + Country = new OwnedCountry { Name = "USA" } + }, + BranchAddress = new OwnedAddress + { + Country = new OwnedCountry { Name = "Canada" } + } + }, + new LeafA + { + PersonAddress = new OwnedAddress + { + Country = new OwnedCountry { Name = "USA" } + }, + BranchAddress = new OwnedAddress + { + Country = new OwnedCountry { Name = "Canada" } + }, + LeafAAddress = new OwnedAddress + { + Country = new OwnedCountry { Name = "Mexico" } + } + }, + new LeafB + { + PersonAddress = new OwnedAddress + { + Country = new OwnedCountry { Name = "USA" } + }, + LeafBAddress = new OwnedAddress + { + Country = new OwnedCountry { Name = "Panama" } + } + }); + + context.SaveChanges(); + } + } + + public class OwnedAddress + { + public OwnedCountry Country { get; set; } + } + + public class OwnedCountry + { + public string Name { get; set; } + } + + public class OwnedPerson + { + public int Id { get; set; } + public OwnedAddress PersonAddress { get; set; } + } + + public class Branch : OwnedPerson + { + public OwnedAddress BranchAddress { get; set; } + } + + public class LeafA : Branch + { + public OwnedAddress LeafAAddress { get; set; } + } + + public class LeafB : OwnedPerson + { + public OwnedAddress LeafBAddress { get; set; } + } +} diff --git a/src/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs b/src/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs new file mode 100644 index 00000000000..196e4250d5f --- /dev/null +++ b/src/EFCore.Specification.Tests/Query/OwnedQueryTestBase.cs @@ -0,0 +1,57 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using Xunit; +// ReSharper disable InconsistentNaming + +namespace Microsoft.EntityFrameworkCore.Query +{ + public abstract class OwnedQueryTestBase + { + [Fact] + public virtual void Query_for_base_type_loads_all_owned_navs() + { + using (var context = CreateContext()) + { + var people = context.Set().ToList(); + + Assert.Equal(4, people.Count); + Assert.True(people.All(p => p.PersonAddress != null)); + Assert.True(people.OfType().All(b => b.BranchAddress != null)); + Assert.True(people.OfType().All(a => a.LeafAAddress != null)); + Assert.True(people.OfType().All(b => b.LeafBAddress != null)); + } + } + + [Fact] + public virtual void Query_for_branch_type_loads_all_owned_navs() + { + using (var context = CreateContext()) + { + var people = context.Set().ToList(); + + Assert.Equal(2, people.Count); + Assert.True(people.All(p => p.PersonAddress != null)); + Assert.True(people.All(b => b.BranchAddress != null)); + Assert.True(people.OfType().All(a => a.LeafAAddress != null)); + } + } + + [Fact] + public virtual void Query_for_leaf_type_loads_all_owned_navs() + { + using (var context = CreateContext()) + { + var people = context.Set().ToList(); + + Assert.Equal(1, people.Count); + Assert.True(people.All(p => p.PersonAddress != null)); + Assert.True(people.All(b => b.BranchAddress != null)); + Assert.True(people.All(a => a.LeafAAddress != null)); + } + } + + protected abstract DbContext CreateContext(); + } +} diff --git a/src/EFCore/Metadata/Conventions/Internal/CoreConventionSetBuilder.cs b/src/EFCore/Metadata/Conventions/Internal/CoreConventionSetBuilder.cs index 20fe2eb4657..ab2c9473c33 100644 --- a/src/EFCore/Metadata/Conventions/Internal/CoreConventionSetBuilder.cs +++ b/src/EFCore/Metadata/Conventions/Internal/CoreConventionSetBuilder.cs @@ -110,6 +110,8 @@ public virtual ConventionSet CreateConventionSet() conventionSet.ForeignKeyUniquenessChangedConventions.Add(foreignKeyPropertyDiscoveryConvention); conventionSet.ForeignKeyUniquenessChangedConventions.Add(foreignKeyIndexConvention); + conventionSet.ForeignKeyOwnershipChangedConventions.Add(new NavigationEagerLoadingConvention()); + conventionSet.ModelBuiltConventions.Add(new ModelCleanupConvention()); conventionSet.ModelBuiltConventions.Add(keyAttributeConvention); conventionSet.ModelBuiltConventions.Add(new IgnoredMembersValidationConvention()); @@ -144,7 +146,7 @@ public virtual ConventionSet CreateConventionSet() conventionSet.PropertyFieldChangedConventions.Add(maxLengthAttributeConvention); conventionSet.PropertyFieldChangedConventions.Add(stringLengthAttributeConvention); conventionSet.PropertyFieldChangedConventions.Add(timestampAttributeConvention); - + return conventionSet; } } diff --git a/src/EFCore/Metadata/Conventions/Internal/NavigationEagerLoadingConvention.cs b/src/EFCore/Metadata/Conventions/Internal/NavigationEagerLoadingConvention.cs new file mode 100644 index 00000000000..e9d9e8c814a --- /dev/null +++ b/src/EFCore/Metadata/Conventions/Internal/NavigationEagerLoadingConvention.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.EntityFrameworkCore.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions.Internal +{ + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public class NavigationEagerLoadingConvention : IForeignKeyOwnershipChangedConvention + { + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual InternalRelationshipBuilder Apply(InternalRelationshipBuilder relationshipBuilder) + { + relationshipBuilder.Metadata.PrincipalToDependent.IsEager = relationshipBuilder.Metadata.IsOwnership; + + return relationshipBuilder; + } + } +} diff --git a/src/EFCore/Metadata/IMutableNavigation.cs b/src/EFCore/Metadata/IMutableNavigation.cs index f891faa42cd..d4d8a95863a 100644 --- a/src/EFCore/Metadata/IMutableNavigation.cs +++ b/src/EFCore/Metadata/IMutableNavigation.cs @@ -23,5 +23,10 @@ public interface IMutableNavigation : INavigation, IMutablePropertyBase /// Gets the foreign key that defines the relationship this navigation property will navigate. /// new IMutableForeignKey ForeignKey { get; } + + /// + /// Determines whether this navigation should be eager loaded by default. + /// + new bool IsEager { get; set; } } } diff --git a/src/EFCore/Metadata/INavigation.cs b/src/EFCore/Metadata/INavigation.cs index 239af697a8b..2540b7a4cb4 100644 --- a/src/EFCore/Metadata/INavigation.cs +++ b/src/EFCore/Metadata/INavigation.cs @@ -17,5 +17,10 @@ public interface INavigation : IPropertyBase /// Gets the foreign key that defines the relationship this navigation property will navigate. /// IForeignKey ForeignKey { get; } + + /// + /// Determines whether this navigation should be eager loaded by default. + /// + bool IsEager { get; } } } diff --git a/src/EFCore/Metadata/Internal/ForeignKey.cs b/src/EFCore/Metadata/Internal/ForeignKey.cs index 888b5cdea0e..15adb455209 100644 --- a/src/EFCore/Metadata/Internal/ForeignKey.cs +++ b/src/EFCore/Metadata/Internal/ForeignKey.cs @@ -364,12 +364,9 @@ public virtual ForeignKey SetIsUnique(bool unique, ConfigurationSource configura _isUnique = unique; UpdateIsUniqueConfigurationSource(configurationSource); - if (isChanging) - { - return DeclaringEntityType.Model.ConventionDispatcher.OnForeignKeyUniquenessChanged(Builder)?.Metadata; - } - - return this; + return isChanging + ? DeclaringEntityType.Model.ConventionDispatcher.OnForeignKeyUniquenessChanged(Builder)?.Metadata + : this; } private static bool DefaultIsUnique => false; @@ -394,7 +391,7 @@ public virtual void UpdateIsUniqueConfigurationSource(ConfigurationSource config public virtual bool IsRequired { get { return !Properties.Any(p => p.IsNullable); } - set { SetIsRequired(value, ConfigurationSource.Explicit); } + set => SetIsRequired(value, ConfigurationSource.Explicit); } /// @@ -454,8 +451,8 @@ public virtual void UpdateIsRequiredConfigurationSource(ConfigurationSource conf /// public virtual DeleteBehavior DeleteBehavior { - get { return _deleteBehavior ?? DefaultDeleteBehavior; } - set { SetDeleteBehavior(value, ConfigurationSource.Explicit); } + get => _deleteBehavior ?? DefaultDeleteBehavior; + set => SetDeleteBehavior(value, ConfigurationSource.Explicit); } /// @@ -489,8 +486,8 @@ public virtual void UpdateDeleteBehaviorConfigurationSource(ConfigurationSource /// public virtual bool IsOwnership { - get { return _isOwnership ?? DefaultIsOwnership; } - set { SetIsOwnership(value, ConfigurationSource.Explicit); } + get => _isOwnership ?? DefaultIsOwnership; + set => SetIsOwnership(value, ConfigurationSource.Explicit); } /// diff --git a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs index c0ba03e4194..fae16ac671f 100644 --- a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs @@ -1729,12 +1729,15 @@ private InternalRelationshipBuilder Owns( var existingNavigation = Metadata .FindNavigationsInHierarchy(navigation.Name) .SingleOrDefault(n => n.GetTargetType().Name == targetEntityType.Name && n.GetTargetType().HasDefiningNavigation()); + var builder = existingNavigation?.ForeignKey.Builder; + if (builder != null) { builder = builder.RelatedEntityTypes(Metadata, existingNavigation.GetTargetType(), configurationSource); builder = builder?.IsOwnership(true, configurationSource); builder = builder?.Navigations(inverse, navigation, configurationSource); + return builder == null ? null : batch.Run(builder); } diff --git a/src/EFCore/Metadata/Internal/Navigation.cs b/src/EFCore/Metadata/Internal/Navigation.cs index 7b6739e99e0..de6b33d3b44 100644 --- a/src/EFCore/Metadata/Internal/Navigation.cs +++ b/src/EFCore/Metadata/Internal/Navigation.cs @@ -47,6 +47,12 @@ public Navigation( /// directly from your code. This API may change or be removed in future releases. /// public virtual ForeignKey ForeignKey { get; } + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual bool IsEager { get; set; } /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used diff --git a/src/EFCore/Query/EntityQueryModelVisitor.cs b/src/EFCore/Query/EntityQueryModelVisitor.cs index d6e9e80c190..7b3f285f60d 100644 --- a/src/EFCore/Query/EntityQueryModelVisitor.cs +++ b/src/EFCore/Query/EntityQueryModelVisitor.cs @@ -264,6 +264,9 @@ protected virtual void OptimizeQueryModel( { Check.NotNull(queryModel, nameof(queryModel)); + new EagerLoadingExpressionVisitor(_queryCompilationContext, _querySourceTracingExpressionVisitorFactory) + .VisitQueryModel(queryModel); + // First pass of optimizations _queryOptimizer.Optimize(QueryCompilationContext, queryModel); @@ -306,6 +309,86 @@ protected virtual void OptimizeQueryModel( QueryCompilationContext.Logger.QueryModelOptimized(queryModel); } + private class EagerLoadingExpressionVisitor : QueryModelVisitorBase + { + private readonly QueryCompilationContext _queryCompilationContext; + private readonly QuerySourceTracingExpressionVisitor _querySourceTracingExpressionVisitor; + + public EagerLoadingExpressionVisitor( + QueryCompilationContext queryCompilationContext, + IQuerySourceTracingExpressionVisitorFactory querySourceTracingExpressionVisitorFactory) + { + _queryCompilationContext = queryCompilationContext; + + _querySourceTracingExpressionVisitor = querySourceTracingExpressionVisitorFactory.Create(); + } + + public override void VisitMainFromClause(MainFromClause fromClause, QueryModel queryModel) + { + ApplyIncludesForOwnedNavigations(new QuerySourceReferenceExpression(fromClause), queryModel); + + base.VisitMainFromClause(fromClause, queryModel); + } + + protected override void VisitBodyClauses(ObservableCollection bodyClauses, QueryModel queryModel) + { + foreach (var querySource in bodyClauses.OfType()) + { + ApplyIncludesForOwnedNavigations(new QuerySourceReferenceExpression(querySource), queryModel); + } + + base.VisitBodyClauses(bodyClauses, queryModel); + } + + private void ApplyIncludesForOwnedNavigations(QuerySourceReferenceExpression querySourceReferenceExpression, QueryModel queryModel) + { + if (_querySourceTracingExpressionVisitor + .FindResultQuerySourceReferenceExpression( + queryModel.SelectClause.Selector, + querySourceReferenceExpression.ReferencedQuerySource) != null) + { + var entityType = _queryCompilationContext.Model.FindEntityType(querySourceReferenceExpression.Type); + + if (entityType != null) + { + var stack = new Stack(); + + WalkNavigations(querySourceReferenceExpression, entityType, stack); + } + } + } + + private void WalkNavigations(Expression querySourceReferenceExpression, IEntityType entityType, Stack stack) + { + var outboundNavigations + = entityType.GetNavigations() + .Concat(entityType.GetDerivedTypes().SelectMany(et => et.GetDeclaredNavigations())) + .Where(n => n.IsEager) + .ToList(); + + if (outboundNavigations.Count == 0 + && stack.Count > 0) + { + _queryCompilationContext.AddAnnotations( + new[] + { + new IncludeResultOperator(stack.Reverse().ToArray(), querySourceReferenceExpression) + }); + } + else + { + foreach (var navigation in outboundNavigations) + { + stack.Push(navigation); + + WalkNavigations(querySourceReferenceExpression, navigation.GetTargetType(), stack); + + stack.Pop(); + } + } + } + } + private class NondeterministicResultCheckingVisitor : QueryModelVisitorBase { private readonly IDiagnosticsLogger _logger; diff --git a/src/EFCore/Query/Internal/IncludeCompiler.cs b/src/EFCore/Query/Internal/IncludeCompiler.cs index e4ecd3f033c..9e6b4c34c57 100644 --- a/src/EFCore/Query/Internal/IncludeCompiler.cs +++ b/src/EFCore/Query/Internal/IncludeCompiler.cs @@ -74,8 +74,7 @@ public virtual void CompileIncludes( return; } - foreach (var includeLoadTree - in CreateIncludeLoadTrees(queryModel.SelectClause.Selector)) + foreach (var includeLoadTree in CreateIncludeLoadTrees(queryModel.SelectClause.Selector)) { includeLoadTree.Compile( _queryCompilationContext, diff --git a/src/EFCore/breakingchanges.netcore.json b/src/EFCore/breakingchanges.netcore.json index d1bd89077c8..d1d06a2aabf 100644 --- a/src/EFCore/breakingchanges.netcore.json +++ b/src/EFCore/breakingchanges.netcore.json @@ -798,5 +798,20 @@ "TypeId": "public abstract class Microsoft.EntityFrameworkCore.Infrastructure.ModelSource : Microsoft.EntityFrameworkCore.Infrastructure.IModelSource", "MemberId": "protected virtual System.Void FindSets(Microsoft.EntityFrameworkCore.ModelBuilder modelBuilder, Microsoft.EntityFrameworkCore.DbContext context)", "Kind": "Removal" + }, + { + "TypeId": "public interface Microsoft.EntityFrameworkCore.Metadata.IMutableNavigation : Microsoft.EntityFrameworkCore.Metadata.INavigation, Microsoft.EntityFrameworkCore.Metadata.IMutablePropertyBase", + "MemberId": "System.Boolean get_IsEager()", + "Kind": "Addition" + }, + { + "TypeId": "public interface Microsoft.EntityFrameworkCore.Metadata.IMutableNavigation : Microsoft.EntityFrameworkCore.Metadata.INavigation, Microsoft.EntityFrameworkCore.Metadata.IMutablePropertyBase", + "MemberId": "System.Void set_IsEager(System.Boolean value)", + "Kind": "Addition" + }, + { + "TypeId": "public interface Microsoft.EntityFrameworkCore.Metadata.INavigation : Microsoft.EntityFrameworkCore.Metadata.IPropertyBase", + "MemberId": "System.Boolean get_IsEager()", + "Kind": "Addition" } ] diff --git a/test/EFCore.InMemory.FunctionalTests/Query/OwnedQueryInMemoryFixture.cs b/test/EFCore.InMemory.FunctionalTests/Query/OwnedQueryInMemoryFixture.cs new file mode 100644 index 00000000000..ee8b219773d --- /dev/null +++ b/test/EFCore.InMemory.FunctionalTests/Query/OwnedQueryInMemoryFixture.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Microsoft.EntityFrameworkCore.Query +{ + public class OwnedQueryInMemoryFixture : OwnedQueryFixtureBase + { + private readonly DbContextOptions _options; + + public OwnedQueryInMemoryFixture() + { + var serviceProvider = new ServiceCollection() + .AddEntityFrameworkInMemoryDatabase() + .AddSingleton(TestModelSource.GetFactory(OnModelCreating)) + .AddSingleton(new TestLoggerFactory()) + .BuildServiceProvider(); + + _options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(nameof(OwnedQueryInMemoryFixture)) + .UseInternalServiceProvider(serviceProvider).Options; + + using (var context = new DbContext(_options)) + { + AddTestData(context); + } + } + + public DbContext CreateContext() => new DbContext(_options); + } +} diff --git a/test/EFCore.InMemory.FunctionalTests/Query/OwnedQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/OwnedQueryInMemoryTest.cs new file mode 100644 index 00000000000..85605f65f49 --- /dev/null +++ b/test/EFCore.InMemory.FunctionalTests/Query/OwnedQueryInMemoryTest.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.EntityFrameworkCore.Query +{ + public class OwnedQueryInMemoryTest : OwnedQueryTestBase, IClassFixture + { + private readonly OwnedQueryInMemoryFixture _fixture; + + public OwnedQueryInMemoryTest(OwnedQueryInMemoryFixture fixture, ITestOutputHelper testOutputHelper) + { + _fixture = fixture; + //TestLoggerFactory.TestOutputHelper = testOutputHelper; + } + + protected override DbContext CreateContext() => _fixture.CreateContext(); + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/PropertyEntrySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/PropertyEntrySqlServerTest.cs index b239846a998..eec643928d3 100644 --- a/test/EFCore.SqlServer.FunctionalTests/PropertyEntrySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/PropertyEntrySqlServerTest.cs @@ -20,7 +20,7 @@ public override void Property_entry_original_value_is_set() base.Property_entry_original_value_is_set(); Assert.Contains( - @"SELECT TOP(1) [e].[Id], [e].[EngineSupplierId], [e].[Name] + @"SELECT TOP(1) [e].[Id], [e].[EngineSupplierId], [e].[Name], [e].[Id], [e].[StorageLocation_Latitude], [e].[StorageLocation_Longitude] FROM [Engines] AS [e]", Sql); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsOwnedQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsOwnedQuerySqlServerTest.cs index e4d04fd6225..d105ebf72c9 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsOwnedQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsOwnedQuerySqlServerTest.cs @@ -27,9 +27,19 @@ public override void Simple_owned_level1() base.Simple_owned_level1(); AssertSql( - @"SELECT [l1].[Id], [l1].[Date], [l1].[Name], [l1].[Id], [l1].[OneToOne_Required_PK_Date], [l1].[OneToOne_Required_PK_Level1_Optional_Id], [l1].[OneToOne_Required_PK_Level1_Required_Id], [l1].[OneToOne_Required_PK_Name], [l1].[OneToOne_Required_PK_OneToOne_Optional_PK_InverseId] + @"SELECT [l1].[Id], [l1].[Date], [l1].[Name], [l1].[Id], [l1].[OneToOne_Required_PK_Date], [l1].[OneToOne_Required_PK_Level1_Optional_Id], [l1].[OneToOne_Required_PK_Level1_Required_Id], [l1].[OneToOne_Required_PK_Name], [l1].[OneToOne_Required_PK_OneToOne_Optional_PK_InverseId], [l1].[Id], [l1].[OneToOne_Required_PK_OneToOne_Required_PK_Level2_Optional_Id], [l1].[OneToOne_Required_PK_OneToOne_Required_PK_Level2_Required_Id], [l1].[OneToOne_Required_PK_OneToOne_Required_PK_Name], [l1].[OneToOne_Required_PK_OneToOne_Required_PK_OneToOne_Optional_PK_InverseId], [l1].[Id], [l1].[OneToOne_Required_PK_OneToOne_Required_PK_OneToOne_Required_PK_Level3_Optional_Id], [l1].[OneToOne_Required_PK_OneToOne_Required_PK_OneToOne_Required_PK_Level3_Required_Id], [l1].[OneToOne_Required_PK_OneToOne_Required_PK_OneToOne_Required_PK_Name], [l1].[OneToOne_Required_PK_OneToOne_Required_PK_OneToOne_Required_PK_OneToOne_Optional_PK_InverseId] FROM [Level1] AS [l1]"); } + + [Fact] + public override void Simple_owned_level1_convention() + { + base.Simple_owned_level1_convention(); + + AssertSql( + @"SELECT [l].[Id], [l].[Date], [l].[Name], [l].[Id], [l].[OneToOne_Required_PK_Date], [l].[OneToOne_Required_PK_Level1_Optional_Id], [l].[OneToOne_Required_PK_Level1_Required_Id], [l].[OneToOne_Required_PK_Name], [l].[OneToOne_Required_PK_OneToOne_Optional_PK_InverseId], [l].[Id], [l].[OneToOne_Required_PK_OneToOne_Required_PK_Level2_Optional_Id], [l].[OneToOne_Required_PK_OneToOne_Required_PK_Level2_Required_Id], [l].[OneToOne_Required_PK_OneToOne_Required_PK_Name], [l].[OneToOne_Required_PK_OneToOne_Required_PK_OneToOne_Optional_PK_InverseId], [l].[Id], [l].[OneToOne_Required_PK_OneToOne_Required_PK_OneToOne_Required_PK_Level3_Optional_Id], [l].[OneToOne_Required_PK_OneToOne_Required_PK_OneToOne_Required_PK_Level3_Required_Id], [l].[OneToOne_Required_PK_OneToOne_Required_PK_OneToOne_Required_PK_Name], [l].[OneToOne_Required_PK_OneToOne_Required_PK_OneToOne_Required_PK_OneToOne_Optional_PK_InverseId] +FROM [Level1] AS [l]"); + } [Fact] public override void Simple_owned_level1_level2() @@ -37,7 +47,7 @@ public override void Simple_owned_level1_level2() base.Simple_owned_level1_level2(); AssertSql( - @"SELECT [l1].[Id], [l1].[Date], [l1].[Name], [l1].[Id], [l1].[OneToOne_Required_PK_Date], [l1].[OneToOne_Required_PK_Level1_Optional_Id], [l1].[OneToOne_Required_PK_Level1_Required_Id], [l1].[OneToOne_Required_PK_Name], [l1].[OneToOne_Required_PK_OneToOne_Optional_PK_InverseId], [l1].[Id], [l1].[OneToOne_Required_PK_OneToOne_Required_PK_Level2_Optional_Id], [l1].[OneToOne_Required_PK_OneToOne_Required_PK_Level2_Required_Id], [l1].[OneToOne_Required_PK_OneToOne_Required_PK_Name], [l1].[OneToOne_Required_PK_OneToOne_Required_PK_OneToOne_Optional_PK_InverseId] + @"SELECT [l1].[Id], [l1].[Date], [l1].[Name], [l1].[Id], [l1].[OneToOne_Required_PK_Date], [l1].[OneToOne_Required_PK_Level1_Optional_Id], [l1].[OneToOne_Required_PK_Level1_Required_Id], [l1].[OneToOne_Required_PK_Name], [l1].[OneToOne_Required_PK_OneToOne_Optional_PK_InverseId], [l1].[Id], [l1].[OneToOne_Required_PK_OneToOne_Required_PK_Level2_Optional_Id], [l1].[OneToOne_Required_PK_OneToOne_Required_PK_Level2_Required_Id], [l1].[OneToOne_Required_PK_OneToOne_Required_PK_Name], [l1].[OneToOne_Required_PK_OneToOne_Required_PK_OneToOne_Optional_PK_InverseId], [l1].[Id], [l1].[OneToOne_Required_PK_OneToOne_Required_PK_OneToOne_Required_PK_Level3_Optional_Id], [l1].[OneToOne_Required_PK_OneToOne_Required_PK_OneToOne_Required_PK_Level3_Required_Id], [l1].[OneToOne_Required_PK_OneToOne_Required_PK_OneToOne_Required_PK_Name], [l1].[OneToOne_Required_PK_OneToOne_Required_PK_OneToOne_Required_PK_OneToOne_Optional_PK_InverseId] FROM [Level1] AS [l1]"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerFixture.cs b/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerFixture.cs new file mode 100644 index 00000000000..2e3af469c3f --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerFixture.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.EntityFrameworkCore.Utilities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Microsoft.EntityFrameworkCore.Query +{ + public class OwnedQuerySqlServerFixture : OwnedQueryFixtureBase, IDisposable + { + private readonly DbContextOptions _options; + private readonly SqlServerTestStore _testStore; + + public TestSqlLoggerFactory TestSqlLoggerFactory { get; } = new TestSqlLoggerFactory(); + + public OwnedQuerySqlServerFixture() + { + _testStore = SqlServerTestStore.Create("OwnedQueryTest"); + + _options = new DbContextOptionsBuilder() + .UseSqlServer(_testStore.ConnectionString, b => b.ApplyConfiguration()) + .UseInternalServiceProvider( + new ServiceCollection() + .AddEntityFrameworkSqlServer() + .AddSingleton(TestModelSource.GetFactory(OnModelCreating)) + .AddSingleton(TestSqlLoggerFactory) + .BuildServiceProvider()) + .Options; + + using (var context = new DbContext(_options)) + { + context.Database.EnsureCreated(); + + AddTestData(context); + } + } + + public DbContext CreateContext() => new DbContext(_options); + + public void Dispose() => _testStore.Dispose(); + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs new file mode 100644 index 00000000000..019279d5bc9 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Query/OwnedQuerySqlServerTest.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit; + +namespace Microsoft.EntityFrameworkCore.Query +{ + public class OwnedQuerySqlServerTest : OwnedQueryTestBase, IClassFixture + { + private readonly OwnedQuerySqlServerFixture _fixture; + + public OwnedQuerySqlServerTest(OwnedQuerySqlServerFixture fixture) + { + _fixture = fixture; + } + + [Fact(Skip = "#8907")] + public override void Query_for_base_type_loads_all_owned_navs() + { + base.Query_for_base_type_loads_all_owned_navs(); + + AssertSql(""); + } + + [Fact(Skip = "#8907")] + public override void Query_for_branch_type_loads_all_owned_navs() + { + base.Query_for_branch_type_loads_all_owned_navs(); + + AssertSql(""); + } + + [Fact(Skip = "#8907")] + public override void Query_for_leaf_type_loads_all_owned_navs() + { + base.Query_for_leaf_type_loads_all_owned_navs(); + + AssertSql(""); + } + + protected override DbContext CreateContext() => _fixture.CreateContext(); + + private void AssertSql(params string[] expected) + => _fixture.TestSqlLoggerFactory.AssertBaseline(expected); + } +} diff --git a/test/EFCore.Sqlite.FunctionalTests/AutoincrementTest.cs b/test/EFCore.Sqlite.FunctionalTests/AutoincrementTest.cs index 5eb046eb8d9..e596f643016 100644 --- a/test/EFCore.Sqlite.FunctionalTests/AutoincrementTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/AutoincrementTest.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using Microsoft.EntityFrameworkCore.Query; using Microsoft.Extensions.DependencyInjection; using Xunit; diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/OwnedQuerySqliteFixture.cs b/test/EFCore.Sqlite.FunctionalTests/Query/OwnedQuerySqliteFixture.cs new file mode 100644 index 00000000000..0de110abaea --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/Query/OwnedQuerySqliteFixture.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Microsoft.EntityFrameworkCore.Query +{ + public class OwnedQuerySqliteFixture : OwnedQueryFixtureBase, IDisposable + { + private readonly DbContextOptions _options; + private readonly SqliteTestStore _testStore; + + public TestSqlLoggerFactory TestSqlLoggerFactory { get; } = new TestSqlLoggerFactory(); + + public OwnedQuerySqliteFixture() + { + var serviceProvider = new ServiceCollection() + .AddEntityFrameworkSqlite() + .AddSingleton(TestModelSource.GetFactory(OnModelCreating)) + .AddSingleton(TestSqlLoggerFactory) + .BuildServiceProvider(); + + _testStore = SqliteTestStore.CreateScratch(); + + _options = new DbContextOptionsBuilder() + .UseSqlite(_testStore.ConnectionString) + .UseInternalServiceProvider(serviceProvider) + .Options; + + using (var context = new DbContext(_options)) + { + context.Database.EnsureClean(); + + AddTestData(context); + } + } + + public DbContext CreateContext() => new DbContext(_options); + + public void Dispose() => _testStore.Dispose(); + } +} diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/OwnedQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/OwnedQuerySqliteTest.cs new file mode 100644 index 00000000000..a8398d1790d --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/Query/OwnedQuerySqliteTest.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Xunit; + +namespace Microsoft.EntityFrameworkCore.Query +{ + public class OwnedQuerySqliteTest : OwnedQueryTestBase, IClassFixture + { + private readonly OwnedQuerySqliteFixture _fixture; + + public OwnedQuerySqliteTest(OwnedQuerySqliteFixture fixture) + { + _fixture = fixture; + } + + [Fact(Skip = "#8907")] + public override void Query_for_base_type_loads_all_owned_navs() + { + base.Query_for_base_type_loads_all_owned_navs(); + } + + [Fact(Skip = "#8907")] + public override void Query_for_branch_type_loads_all_owned_navs() + { + base.Query_for_branch_type_loads_all_owned_navs(); + } + + [Fact(Skip = "#8907")] + public override void Query_for_leaf_type_loads_all_owned_navs() + { + base.Query_for_leaf_type_loads_all_owned_navs(); + } + + protected override DbContext CreateContext() => _fixture.CreateContext(); + } +}