From 9d7d45edece210e1194568fb7ed48b6a161b169a Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Tue, 28 Apr 2026 21:22:06 -0700 Subject: [PATCH] Support keys and indexes that traverse complex-type properties Fixes #31246 Fixes #28605 --- .agents/skills/run-apichief/SKILL.md | 3 +- .../Internal/CosmosModelValidator.cs | 10 +- .../Storage/Internal/CosmosClientWrapper.cs | 4 +- .../Properties/DesignStrings.Designer.cs | 106 ++++---- .../Properties/DesignStrings.resx | 44 +-- .../Internal/CSharpDbContextGenerator.cs | 6 +- .../Internal/CSharpEntityTypeGenerator.cs | 8 +- .../CSharpRuntimeModelCodeGenerator.cs | 70 ++++- ...nalCSharpRuntimeAnnotationCodeGenerator.cs | 22 +- .../EFCore.Relational.baseline.json | 17 +- .../RelationalComplexPropertyExtensions.cs | 60 +++++ .../Extensions/RelationalIndexExtensions.cs | 12 +- .../RelationalPropertyExtensions.cs | 12 + .../RelationalModelValidator.cs | 134 +++++++-- src/EFCore.Relational/Metadata/IColumnBase.cs | 2 +- .../Metadata/Internal/JsonColumn.cs | 3 +- .../Metadata/Internal/JsonColumnBase.cs | 3 +- .../Internal/RelationalIndexExtensions.cs | 63 ++++- .../Metadata/Internal/RelationalModel.cs | 98 +++++-- .../Properties/RelationalStrings.Designer.cs | 82 ++++-- .../Properties/RelationalStrings.resx | 20 +- ...anslatingExpressionVisitor.CreateSelect.cs | 25 +- ...yableMethodTranslatingExpressionVisitor.cs | 4 +- .../SqlServerIndexBuilderExtensions.cs | 7 +- .../Internal/SqlServerModelValidator.cs | 13 +- .../Conventions/SqlServerIndexConvention.cs | 8 +- .../Internal/SqlServerAnnotationProvider.cs | 4 +- .../Internal/SqlServerIndexExtensions.cs | 13 +- src/EFCore/Design/ICSharpHelper.cs | 28 ++ src/EFCore/EFCore.baseline.json | 95 +++++-- .../Internal/ExpressionExtensions.cs | 50 ++++ src/EFCore/Infrastructure/ModelValidator.cs | 92 +++++++ .../Metadata/Builders/EntityTypeBuilder`.cs | 8 +- .../Conventions/ForeignKeyIndexConvention.cs | 6 +- .../ProviderConventionSetBuilder.cs | 1 + .../KeyComplexPropertyConvention.cs | 137 ++++++++++ .../Conventions/RuntimeModelConvention.cs | 43 ++- src/EFCore/Metadata/IConventionEntityType.cs | 14 +- src/EFCore/Metadata/IConventionIndex.cs | 2 +- src/EFCore/Metadata/IEntityType.cs | 4 +- src/EFCore/Metadata/IIndex.cs | 2 +- src/EFCore/Metadata/IMutableEntityType.cs | 14 +- src/EFCore/Metadata/IMutableIndex.cs | 2 +- src/EFCore/Metadata/IReadOnlyEntityType.cs | 4 +- src/EFCore/Metadata/IReadOnlyIndex.cs | 2 +- .../Metadata/Internal/ComplexProperty.cs | 2 +- .../Internal/ComplexPropertyNameComparer.cs | 92 +++++++ src/EFCore/Metadata/Internal/ComplexType.cs | 9 +- src/EFCore/Metadata/Internal/EntityType.cs | 128 ++++++--- src/EFCore/Metadata/Internal/Index.cs | 37 ++- .../Internal/InternalEntityTypeBuilder.cs | 90 ++++++- .../Internal/InternalTypeBaseBuilder.cs | 151 ++++++++++- src/EFCore/Metadata/Internal/Key.cs | 2 +- src/EFCore/Metadata/Internal/Navigation.cs | 2 +- src/EFCore/Metadata/Internal/Property.cs | 28 +- src/EFCore/Metadata/Internal/PropertyBase.cs | 34 +++ .../Metadata/Internal/PropertyListComparer.cs | 16 +- .../Metadata/Internal/PropertyNameComparer.cs | 14 +- .../Metadata/Internal/ServiceProperty.cs | 2 +- .../Metadata/Internal/SkipNavigation.cs | 2 +- src/EFCore/Metadata/Internal/TypeBase.cs | 23 +- src/EFCore/Metadata/RuntimeEntityType.cs | 12 +- src/EFCore/Metadata/RuntimeIndex.cs | 28 +- src/EFCore/Metadata/RuntimeKey.cs | 2 +- src/EFCore/Metadata/RuntimeProperty.cs | 2 - src/EFCore/Metadata/RuntimePropertyBase.cs | 9 + src/EFCore/Properties/CoreStrings.Designer.cs | 48 ++++ src/EFCore/Properties/CoreStrings.resx | 18 ++ .../CosmosModelBuilderGenericTest.cs | 24 ++ .../Query/AdHocComplexTypeQueryCosmosTest.cs | 30 +++ .../DependentBaseUnsafeAccessors.cs | 15 ++ .../PrincipalDerivedEntityType.cs | 124 ++++++++- .../PrincipalDerivedUnsafeAccessors.cs | 3 + .../Scaffolding/CompiledModelCosmosTest.cs | 4 + ...rpMigrationsGeneratorTest.ModelSnapshot.cs | 14 +- .../DependentBaseUnsafeAccessors.cs | 15 ++ .../ComplexTypes/PrincipalBaseEntityType.cs | 5 + .../PrincipalDerivedEntityType.cs | 124 ++++++++- .../PrincipalDerivedUnsafeAccessors.cs | 3 + .../CompiledModelRelationalTestBase.cs | 19 ++ .../RelationalModelValidatorTest.cs | 252 +++++++++++++++++ .../Metadata/RelationalModelTest.cs | 85 +++++- .../ModelBuilderTest.ComplexType.cs | 199 ++++++++++++++ .../ModelBuilderTest.Inheritance.cs | 8 +- .../ModelBuilderTest.NonGeneric.cs | 8 +- .../Query/AdHocComplexTypeQueryTestBase.cs | 160 +++++++++++ .../Scaffolding/CompiledModelTestBase.cs | 40 ++- .../ComplexTypes/DbContextModelBuilder.cs | 254 +++++++++++------- .../DependentBaseUnsafeAccessors.cs | 15 ++ .../ComplexTypes/PrincipalBaseEntityType.cs | 5 + .../PrincipalDerivedEntityType.cs | 136 +++++++++- .../PrincipalDerivedUnsafeAccessors.cs | 3 + .../Scaffolding/CompiledModelSqlServerTest.cs | 5 + .../SqlServerModelValidatorTest.cs | 72 +++++ .../Diagnostics/CoreEventIdTest.cs | 3 + .../Infrastructure/ModelValidatorTest.cs | 176 ++++++++++++ .../Conventions/ConventionDispatcherTest.cs | 2 +- .../Metadata/Internal/EntityTypeTest.cs | 4 +- .../Internal/InternalEntityTypeBuilderTest.cs | 42 ++- .../Internal/PropertyListComparerTest.cs | 162 +++++++++++ 100 files changed, 3603 insertions(+), 516 deletions(-) create mode 100644 src/EFCore/Metadata/Conventions/KeyComplexPropertyConvention.cs create mode 100644 src/EFCore/Metadata/Internal/ComplexPropertyNameComparer.cs create mode 100644 test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/ComplexTypes/DependentBaseUnsafeAccessors.cs create mode 100644 test/EFCore.InMemory.FunctionalTests/Scaffolding/Baselines/ComplexTypes/DependentBaseUnsafeAccessors.cs create mode 100644 test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/DependentBaseUnsafeAccessors.cs create mode 100644 test/EFCore.Tests/Metadata/Internal/PropertyListComparerTest.cs diff --git a/.agents/skills/run-apichief/SKILL.md b/.agents/skills/run-apichief/SKILL.md index d78eef1d2e6..e78bbca1c44 100644 --- a/.agents/skills/run-apichief/SKILL.md +++ b/.agents/skills/run-apichief/SKILL.md @@ -26,9 +26,10 @@ Default to `emit baseline` if the user only asks to "run ApiChief". ### 1. Identify the target project(s) -- Match user intent against project folders under `src/` such as `EFCore`, `EFCore.Cosmos`, `EFCore.Relational`, or `Microsoft.Data.Sqlite.Core`. +- Match user intent against project folders under `src/`. - Ask for clarification only if the target is ambiguous. - The checked-in baseline path convention in this repo is `src//.baseline.json`. +- If asked just to update the baselines, run EFCore.ApiBaseline.Tests tests and skip the rest of the steps below. ### 2. Prepare the environment diff --git a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelValidator.cs b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelValidator.cs index 53d434431ad..90865ded89e 100644 --- a/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelValidator.cs +++ b/src/EFCore.Cosmos/Infrastructure/Internal/CosmosModelValidator.cs @@ -514,8 +514,10 @@ protected virtual void ValidateVectorIndex( string.Join(",", index.Properties.Select(e => e.Name)))); } - if (index.Properties[0].GetVectorDistanceFunction() == null - || index.Properties[0].GetVectorDimensions() == null) + var firstIndexProperty = index.Properties[0] as IProperty; + if (firstIndexProperty == null + || firstIndexProperty.GetVectorDistanceFunction() == null + || firstIndexProperty.GetVectorDimensions() == null) { throw new InvalidOperationException( CosmosStrings.VectorIndexOnNonVector( @@ -542,7 +544,9 @@ protected virtual void ValidateFullTextIndex( string.Join(",", index.Properties.Select(e => e.Name)))); } - if (index.Properties[0].GetIsFullTextSearchEnabled() != true) + var firstFullTextProperty = index.Properties[0] as IProperty; + if (firstFullTextProperty == null + || firstFullTextProperty.GetIsFullTextSearchEnabled() != true) { throw new InvalidOperationException( CosmosStrings.FullTextIndexOnNonFullTextProperty( diff --git a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs index 31706f1bf2b..4b68a8ef6cd 100644 --- a/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs +++ b/src/EFCore.Cosmos/Storage/Internal/CosmosClientWrapper.cs @@ -199,7 +199,7 @@ private static async Task CreateContainerIfNotExistsOnceAsync( } vectorIndexes.Add( - new VectorIndexPath { Path = GetJsonPropertyPathFromRoot(index.Properties[0]), Type = vectorIndexType.Value }); + new VectorIndexPath { Path = GetJsonPropertyPathFromRoot((IReadOnlyProperty)index.Properties[0]), Type = vectorIndexType.Value }); } if (index.IsFullTextIndex() == true) @@ -213,7 +213,7 @@ private static async Task CreateContainerIfNotExistsOnceAsync( } fullTextIndexPaths.Add( - new FullTextIndexPath { Path = GetJsonPropertyPathFromRoot(index.Properties[0]) }); + new FullTextIndexPath { Path = GetJsonPropertyPathFromRoot((IReadOnlyProperty)index.Properties[0]) }); } } diff --git a/src/EFCore.Design/Properties/DesignStrings.Designer.cs b/src/EFCore.Design/Properties/DesignStrings.Designer.cs index 140351da3f2..32ecdee53a5 100644 --- a/src/EFCore.Design/Properties/DesignStrings.Designer.cs +++ b/src/EFCore.Design/Properties/DesignStrings.Designer.cs @@ -43,12 +43,6 @@ public static string BadMigrationName(object? name, object? characters) GetString("BadMigrationName", nameof(name), nameof(characters)), name, characters); - /// - /// A migration name must be specified. - /// - public static string MigrationNameRequired - => GetString("MigrationNameRequired"); - /// /// Sequence '{sequenceName}' cannot be scaffolded because it uses type '{typeName}' which is unsupported. /// @@ -206,6 +200,14 @@ public static string CouldNotGetInterceptableLocation(object? node) GetString("CouldNotGetInterceptableLocation", nameof(node)), node); + /// + /// Creating and applying migration '{migrationName}'. + /// + public static string CreatingAndApplyingMigration(object? migrationName) + => string.Format( + GetString("CreatingAndApplyingMigration", nameof(migrationName)), + migrationName); + /// /// Successfully dropped database '{name}'. /// @@ -242,6 +244,14 @@ public static string DuplicateMigrationName(object? migrationName) GetString("DuplicateMigrationName", nameof(migrationName)), migrationName); + /// + /// The dynamic migration '{migrationId}' was not found. Only migrations applied in the current session using CreateAndApplyMigration can be reverted. + /// + public static string DynamicMigrationNotFound(object? migrationId) + => string.Format( + GetString("DynamicMigrationNotFound", nameof(migrationId)), + migrationId); + /// /// Dynamic LINQ queries are not supported when precompiling queries. /// @@ -444,12 +454,35 @@ public static string MalformedCreateHostBuilder public static string ManuallyDeleted => GetString("ManuallyDeleted"); + /// + /// Failed to compile migration '{migrationId}'. Errors: + /// {errors} + /// + public static string MigrationCompilationFailed(object? migrationId, object? errors) + => string.Format( + GetString("MigrationCompilationFailed", nameof(migrationId), nameof(errors)), + migrationId, errors); + + /// + /// Migration '{migrationId}' was successfully created and applied. + /// + public static string MigrationCreatedAndApplied(object? migrationId) + => string.Format( + GetString("MigrationCreatedAndApplied", nameof(migrationId)), + migrationId); + /// /// The target migration. If '0', all migrations will be reverted. Defaults to the last migration. /// public static string MigrationDescription => GetString("MigrationDescription"); + /// + /// A migration name must be specified. + /// + public static string MigrationNameRequired + => GetString("MigrationNameRequired"); + /// /// Your target project '{assembly}' doesn't match your migrations assembly '{migrationsAssembly}'. Either change your target project or change your migrations assembly. /// Change your migrations assembly by using DbContextOptionsBuilder. E.g. options.UseSqlServer(connection, b => b.MigrationsAssembly("{assembly}")). By default, the migrations assembly is the assembly containing the DbContext. @@ -460,6 +493,14 @@ public static string MigrationsAssemblyMismatch(object? assembly, object? migrat GetString("MigrationsAssemblyMismatch", nameof(assembly), nameof(migrationsAssembly)), assembly, migrationsAssembly); + /// + /// Could not find migration type with ID '{migrationId}' in the compiled assembly. + /// + public static string MigrationTypeNotFound(object? migrationId) + => string.Format( + GetString("MigrationTypeNotFound", nameof(migrationId)), + migrationId); + /// /// MSBuild Workspace diagnostics:{diagnostics} /// @@ -564,6 +605,12 @@ public static string NoCreateHostBuilder public static string NoDesignTimeServices => GetString("NoDesignTimeServices"); + /// + /// No dynamic migrations have been applied in the current session. Only migrations applied using CreateAndApplyMigration can be reverted with RevertMigration. + /// + public static string NoDynamicMigrationsToRevert + => GetString("NoDynamicMigrationsToRevert"); + /// /// The project language '{language}' isn't supported by the built-in {service} service. You can try looking for an additional NuGet package which supports this language; moving your DbContext type to a C# class library referenced by this project; or manually implementing and registering the design-time service for the programming language. /// @@ -610,20 +657,6 @@ public static string NonRelationalProvider(object? provider) public static string NoPendingModelChanges => GetString("NoPendingModelChanges"); - /// - /// No dynamic migrations have been applied in the current session. Only migrations applied using CreateAndApplyMigration can be reverted with RevertMigration. - /// - public static string NoDynamicMigrationsToRevert - => GetString("NoDynamicMigrationsToRevert"); - - /// - /// The dynamic migration '{migrationId}' was not found. Only migrations applied in the current session using CreateAndApplyMigration can be reverted. - /// - public static string DynamicMigrationNotFound(object? migrationId) - => string.Format( - GetString("DynamicMigrationNotFound", nameof(migrationId)), - migrationId); - /// /// No referenced design-time services were found. /// @@ -946,39 +979,6 @@ public static string WritingSnapshot(object? file) GetString("WritingSnapshot", nameof(file)), file); - /// - /// Failed to compile migration '{migrationId}'. Errors: - /// {errors} - /// - public static string MigrationCompilationFailed(object? migrationId, object? errors) - => string.Format( - GetString("MigrationCompilationFailed", nameof(migrationId), nameof(errors)), - migrationId, errors); - - /// - /// Could not find migration type with ID '{migrationId}' in the compiled assembly. - /// - public static string MigrationTypeNotFound(object? migrationId) - => string.Format( - GetString("MigrationTypeNotFound", nameof(migrationId)), - migrationId); - - /// - /// Creating and applying migration '{migrationName}'. - /// - public static string CreatingAndApplyingMigration(object? migrationName) - => string.Format( - GetString("CreatingAndApplyingMigration", nameof(migrationName)), - migrationName); - - /// - /// Migration '{migrationId}' was successfully created and applied. - /// - public static string MigrationCreatedAndApplied(object? migrationId) - => string.Format( - GetString("MigrationCreatedAndApplied", nameof(migrationId)), - migrationId); - private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name)!; diff --git a/src/EFCore.Design/Properties/DesignStrings.resx b/src/EFCore.Design/Properties/DesignStrings.resx index f167b4fb0da..5442f8ae07a 100644 --- a/src/EFCore.Design/Properties/DesignStrings.resx +++ b/src/EFCore.Design/Properties/DesignStrings.resx @@ -126,9 +126,6 @@ The migration name '{name}' is not valid. Migration names cannot contain any of the following characters: '{characters}'. - - A migration name must be specified. - Sequence '{sequenceName}' cannot be scaffolded because it uses type '{typeName}' which is unsupported. @@ -193,6 +190,9 @@ Consider changing your target project to the DbContext project by using the Pack Couldn't get interceptable location for: '{node}'. + + Creating and applying migration '{migrationName}'. + Successfully dropped database '{name}'. @@ -208,6 +208,9 @@ Consider changing your target project to the DbContext project by using the Pack The name '{migrationName}' is used by an existing migration. + + The dynamic migration '{migrationId}' was not found. Only migrations applied in the current session using CreateAndApplyMigration can be reverted. + Dynamic LINQ queries are not supported when precompiling queries. @@ -289,14 +292,27 @@ Consider changing your target project to the DbContext project by using the Pack The model snapshot and the backing model of the last migration are different. Continuing under the assumption that the last migration was deleted manually. + + Failed to compile migration '{migrationId}'. Errors: +{errors} + + + Migration '{migrationId}' was successfully created and applied. + The target migration. If '0', all migrations will be reverted. Defaults to the last migration. + + A migration name must be specified. + Your target project '{assembly}' doesn't match your migrations assembly '{migrationsAssembly}'. Either change your target project or change your migrations assembly. Change your migrations assembly by using DbContextOptionsBuilder. E.g. options.UseSqlServer(connection, b => b.MigrationsAssembly("{assembly}")). By default, the migrations assembly is the assembly containing the DbContext. Change your target project to the migrations project by using the Package Manager Console's Default project drop-down list, or by executing "dotnet ef" from the directory containing the migrations project. + + Could not find migration type with ID '{migrationId}' in the compiled assembly. + MSBuild Workspace diagnostics:{diagnostics} @@ -342,6 +358,9 @@ Change your target project to the migrations project by using the Package Manage No design-time services were found. + + No dynamic migrations have been applied in the current session. Only migrations applied using CreateAndApplyMigration can be reverted with RevertMigration. + The project language '{language}' isn't supported by the built-in {service} service. You can try looking for an additional NuGet package which supports this language; moving your DbContext type to a C# class library referenced by this project; or manually implementing and registering the design-time service for the programming language. @@ -360,12 +379,6 @@ Change your target project to the migrations project by using the Package Manage No changes have been made to the model since the last migration. - - No dynamic migrations have been applied in the current session. Only migrations applied using CreateAndApplyMigration can be reverted with RevertMigration. - - - The dynamic migration '{migrationId}' was not found. Only migrations applied in the current session using CreateAndApplyMigration can be reverted. - No referenced design-time services were found. @@ -499,17 +512,4 @@ Change your target project to the migrations project by using the Package Manage Writing model snapshot to '{file}'. - - Failed to compile migration '{migrationId}'. Errors: -{errors} - - - Could not find migration type with ID '{migrationId}' in the compiled assembly. - - - Creating and applying migration '{migrationName}'. - - - Migration '{migrationId}' was successfully created and applied. - \ No newline at end of file diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs index 2e19dcae2d2..841a6024a3d 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpDbContextGenerator.cs @@ -1,7 +1,7 @@ // ------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version: 17.0.0.0 +// Runtime Version: 18.0.0.0 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -21,7 +21,7 @@ namespace Microsoft.EntityFrameworkCore.Scaffolding.Internal /// /// Class to produce the template output /// - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "17.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "18.0.0.0")] public partial class CSharpDbContextGenerator : CSharpDbContextGeneratorBase { /// @@ -603,7 +603,7 @@ public virtual void Initialize() /// /// Base class for this transformation /// - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "17.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "18.0.0.0")] public class CSharpDbContextGeneratorBase { #region Fields diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs index 9e43546ebf1..f34bc4a92d7 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs @@ -1,7 +1,7 @@ // ------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version: 17.0.0.0 +// Runtime Version: 18.0.0.0 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -21,7 +21,7 @@ namespace Microsoft.EntityFrameworkCore.Scaffolding.Internal /// /// Class to produce the template output /// - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "17.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "18.0.0.0")] public partial class CSharpEntityTypeGenerator : CSharpEntityTypeGeneratorBase { /// @@ -400,7 +400,7 @@ public virtual void Initialize() /// /// Base class for this transformation /// - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "17.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "18.0.0.0")] public class CSharpEntityTypeGeneratorBase { #region Fields @@ -415,7 +415,7 @@ public class CSharpEntityTypeGeneratorBase /// /// The string builder that generation-time code is using to assemble generated output /// - protected System.Text.StringBuilder GenerationEnvironment + public System.Text.StringBuilder GenerationEnvironment { get { diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs index 70e7775f09e..9c28d25dded 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs @@ -1998,7 +1998,7 @@ private void GeneratePropertyBaseParameters( private void FindProperties( string entityTypeVariable, - IEnumerable properties, + IEnumerable properties, IndentedStringBuilder mainBuilder, bool nullable, IDictionary? scopeVariables = null) @@ -2021,11 +2021,11 @@ private void FindProperties( { mainBuilder.Append(propertyVariable); } - else + else if (property.DeclaringType is IEntityType) { mainBuilder .Append(entityTypeVariable) - .Append(".FindProperty(") + .Append(property is IComplexProperty ? ".FindComplexProperty(" : ".FindProperty(") .Append(_code.Literal(property.Name)) .Append(')'); @@ -2035,6 +2035,70 @@ private void FindProperties( .Append('!'); } } + else + { + // Property is declared on a complex type. Walk the chain from the entity type down to the + // complex type, then look up the leaf property/complex property. At each level, check + // whether the complex property/type already has a variable in scope and start from there. + var chain = new List(); + var typeBase = (IReadOnlyTypeBase)property.DeclaringType; + string? startVariable = null; + var startVariableIsComplexProperty = false; + while (typeBase is IReadOnlyComplexType complexType) + { + if (scopeVariables != null + && scopeVariables.TryGetValue(complexType, out startVariable)) + { + break; + } + + if (scopeVariables != null + && scopeVariables.TryGetValue(complexType.ComplexProperty, out startVariable)) + { + startVariableIsComplexProperty = true; + break; + } + + chain.Insert(0, complexType.ComplexProperty.Name); + typeBase = complexType.ComplexProperty.DeclaringType; + } + + mainBuilder.Append(startVariable ?? entityTypeVariable); + if (startVariableIsComplexProperty) + { + if (nullable) + { + mainBuilder.Append('!'); + } + + mainBuilder.Append(".ComplexType"); + } + + foreach (var complexPropertyName in chain) + { + mainBuilder + .Append(".FindComplexProperty(") + .Append(_code.Literal(complexPropertyName)) + .Append(')'); + + if (nullable) + { + mainBuilder.Append('!'); + } + + mainBuilder.Append(".ComplexType"); + } + + mainBuilder + .Append(property is IComplexProperty ? ".FindComplexProperty(" : ".FindProperty(") + .Append(_code.Literal(property.Name)) + .Append(')'); + + if (nullable) + { + mainBuilder.Append('!'); + } + } } mainBuilder.Append(" }"); diff --git a/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs b/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs index 9ad6851b62f..f7531ffa9a7 100644 --- a/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs +++ b/src/EFCore.Relational/Design/Internal/RelationalCSharpRuntimeAnnotationCodeGenerator.cs @@ -1200,7 +1200,7 @@ private void Create( mainBuilder .AppendLine($"var {keyVariable} = RelationalModel.GetKey(this,").IncrementIndent() .AppendLine($"{code.Literal(mappedKey.DeclaringEntityType.Name)},") - .AppendLine($"{code.Literal(mappedKey.Properties.Select(p => p.Name).ToArray())});") + .AppendLine($"{code.Literal(mappedKey.Properties.Select(GetPropertyPathFromContainingEntity).ToArray())});") .DecrementIndent(); } @@ -1269,7 +1269,7 @@ private void Create( .AppendLine($"{code.Literal(mappedIndex.DeclaringEntityType.Name)},") .AppendLine( $"{(mappedIndex.Name == null - ? code.Literal(mappedIndex.Properties.Select(p => p.Name).ToArray()) + ? code.Literal(mappedIndex.Properties.Select(GetPropertyPathFromContainingEntity).ToArray()) : code.Literal(mappedIndex.Name))});") .DecrementIndent(); } @@ -2676,6 +2676,24 @@ private static string Capitalize(string @string) } } + private static string GetPropertyPathFromContainingEntity(IPropertyBase property) + { + if (property.DeclaringType is not IComplexType) + { + return property.Name; + } + + var segments = new List { property.Name }; + var typeBase = property.DeclaringType; + while (typeBase is IComplexType complexType) + { + segments.Insert(0, complexType.ComplexProperty.Name); + typeBase = complexType.ComplexProperty.DeclaringType; + } + + return string.Join(".", segments); + } + private static void AppendLiteral(StoreObjectIdentifier storeObject, IndentedStringBuilder builder, ICSharpHelper code) { builder.Append("StoreObjectIdentifier."); diff --git a/src/EFCore.Relational/EFCore.Relational.baseline.json b/src/EFCore.Relational/EFCore.Relational.baseline.json index 9e5f52bc2f0..c869192e5a3 100644 --- a/src/EFCore.Relational/EFCore.Relational.baseline.json +++ b/src/EFCore.Relational/EFCore.Relational.baseline.json @@ -10673,6 +10673,9 @@ { "Member": "static Microsoft.EntityFrameworkCore.Metadata.ConfigurationSource? GetJsonPropertyNameConfigurationSource(this Microsoft.EntityFrameworkCore.Metadata.IConventionComplexProperty complexProperty);" }, + { + "Member": "static System.Collections.Generic.IEnumerable GetMappedStoreObjects(this Microsoft.EntityFrameworkCore.Metadata.IReadOnlyComplexProperty property, Microsoft.EntityFrameworkCore.Metadata.StoreObjectType storeObjectType);" + }, { "Member": "static void SetJsonPropertyName(this Microsoft.EntityFrameworkCore.Metadata.IMutableComplexProperty complexProperty, string? name);" }, @@ -14112,7 +14115,7 @@ "Member": "override void ValidateIndex(Microsoft.EntityFrameworkCore.Metadata.IIndex index, Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger logger);" }, { - "Member": "virtual void ValidateIndexMappedToTable(Microsoft.EntityFrameworkCore.Metadata.IIndex index, Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger logger);" + "Member": "override void ValidateIndexOnComplexProperty(Microsoft.EntityFrameworkCore.Metadata.IIndex index, System.Collections.Generic.IReadOnlyList complexProperties, Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger logger);" }, { "Member": "virtual void ValidateIndexPropertyMapping(Microsoft.EntityFrameworkCore.Metadata.IIndex index, Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger logger);" @@ -16459,6 +16462,12 @@ { "Member": "static string IncorrectDefaultValueType(object? value, object? valueType, object? property, object? propertyType, object? entityType);" }, + { + "Member": "static string IndexOnNonJsonComplexProperty(object? indexProperties, object? entityType, object? property);" + }, + { + "Member": "static string IndexPropertiesMixedJsonAndNonJsonMapping(object? indexProperties, object? entityType);" + }, { "Member": "static string InsertDataOperationNoModel(object? table);" }, @@ -16570,6 +16579,9 @@ { "Member": "static string KeylessMappingStrategy(object? mappingStrategy, object? entityType);" }, + { + "Member": "static string KeyPropertyInJsonComplexType(object? keyProperties, object? entityType, object? property);" + }, { "Member": "static string LastUsedWithoutOrderBy(object? method);" }, @@ -16843,6 +16855,9 @@ { "Member": "static string UnhandledExpressionInVisitor(object? expression, object? expressionType, object? visitor);" }, + { + "Member": "static string UniqueIndexOnComplexProperty(object? indexProperties, object? entityType, object? property);" + }, { "Member": "static string UnknownOperation(object? sqlGeneratorType, object? operationType);" }, diff --git a/src/EFCore.Relational/Extensions/RelationalComplexPropertyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalComplexPropertyExtensions.cs index 4c17d16ab1b..b94e0861cad 100644 --- a/src/EFCore.Relational/Extensions/RelationalComplexPropertyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalComplexPropertyExtensions.cs @@ -53,4 +53,64 @@ public static void SetJsonPropertyName(this IMutableComplexProperty complexPrope /// The for the JSON property name for a given complex property. public static ConfigurationSource? GetJsonPropertyNameConfigurationSource(this IConventionComplexProperty complexProperty) => complexProperty.ComplexType.GetJsonPropertyNameConfigurationSource(); + + /// + /// + /// Returns the table-like store objects to which this complex property is mapped if it's mapped to a JSON column. + /// + /// + /// This method is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// + /// + /// The complex property. + /// The type of the store object. + /// The table-like store objects to which this property is mapped. + public static IEnumerable GetMappedStoreObjects( + this IReadOnlyComplexProperty property, + StoreObjectType storeObjectType) + { + var declaringType = property.DeclaringType; + var declaringStoreObject = StoreObjectIdentifier.Create(declaringType, storeObjectType); + + // TODO: Support different JSON column names for different store objects. Issue #28584 + if (declaringStoreObject != null + && property.ComplexType.GetContainerColumnName() != null) + { + yield return declaringStoreObject.Value; + } + + if (storeObjectType is StoreObjectType.Function or StoreObjectType.SqlQuery) + { + yield break; + } + + // TODO: Support entity splitting with JSON columns. Issue #36172 + // foreach (var fragment in declaringType.GetMappingFragments(storeObjectType)) + // { + // if (property.ComplexType.GetContainerColumnName(fragment.StoreObject) != null) + // { + // yield return fragment.StoreObject; + // } + // } + + if (declaringType.GetMappingStrategy() == RelationalAnnotationNames.TphMappingStrategy) + { + yield break; + } + + if (declaringType is IReadOnlyEntityType entityType) + { + foreach (var derivedType in entityType.GetDerivedTypes()) + { + // TODO: Support different JSON column names in derived types. Issue #38214 + var derivedStoreObject = StoreObjectIdentifier.Create(derivedType, storeObjectType); + if (derivedStoreObject != null + && property.ComplexType.GetContainerColumnName() != null) + { + yield return derivedStoreObject.Value; + } + } + } + } } diff --git a/src/EFCore.Relational/Extensions/RelationalIndexExtensions.cs b/src/EFCore.Relational/Extensions/RelationalIndexExtensions.cs index 7b0c50f9326..0c3bda36e45 100644 --- a/src/EFCore.Relational/Extensions/RelationalIndexExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalIndexExtensions.cs @@ -63,11 +63,17 @@ public static class RelationalIndexExtensions return null; } + var columnNames = index.GetColumnNames(); + if (columnNames == null) + { + return null; + } + var baseName = new StringBuilder() .Append("IX_") .Append(tableName) .Append('_') - .AppendJoin(index.Properties.Select(p => p.GetColumnName()), "_") + .AppendJoin(columnNames, "_") .ToString(); return Uniquifier.Truncate(baseName, index.DeclaringEntityType.Model.GetMaxIdentifierLength()); @@ -86,7 +92,7 @@ public static class RelationalIndexExtensions return null; } - var columnNames = index.Properties.GetColumnNames(storeObject); + var columnNames = index.GetColumnNames(storeObject); if (columnNames == null) { return null; @@ -103,7 +109,7 @@ public static class RelationalIndexExtensions .FindRowInternalForeignKeys(storeObject) .SelectMany(fk => fk.PrincipalEntityType.GetIndexes())) { - var otherColumnNames = otherIndex.Properties.GetColumnNames(storeObject); + var otherColumnNames = otherIndex.GetColumnNames(storeObject); if ((otherColumnNames != null) && otherColumnNames.SequenceEqual(columnNames)) { diff --git a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs index 58e00ff9355..65860f71242 100644 --- a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs @@ -86,6 +86,18 @@ static bool ShouldBeMapped(IReadOnlyProperty property, in StoreObjectIdentifier } } } + else + { + var containingEntityType = property.DeclaringType.ContainingEntityType; + foreach (var containingType in containingEntityType.GetDerivedTypesInclusive()) + { + if (StoreObjectIdentifier.Create(containingType, storeObject.StoreObjectType) == storeObject) + { + tableFound = true; + break; + } + } + } if (!tableFound) { diff --git a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs index 10168e4dd08..1753184f4b8 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalModelValidator.cs @@ -202,6 +202,19 @@ protected override void ValidateKey( { base.ValidateKey(key, logger); + foreach (var property in key.Properties) + { + if (property.DeclaringType is IComplexType complexType + && complexType.IsMappedToJson()) + { + throw new InvalidOperationException( + RelationalStrings.KeyPropertyInJsonComplexType( + key.Properties.Format(), + key.DeclaringEntityType.DisplayName(), + property.Name)); + } + } + ValidateDefaultValuesOnKey(key, logger); ValidateValueGeneration(key, logger); } @@ -1856,7 +1869,6 @@ protected virtual void ValidateSharedIndexesCompatibility( var indexName = index.GetDatabaseName(table); if (indexName == null) { - ValidateIndexPropertyMapping(index, logger); continue; } @@ -2670,26 +2682,43 @@ protected override void ValidateIndex( { base.ValidateIndex(index, logger); - ValidateIndexMappedToTable(index, logger); + ValidateIndexPropertyMapping(index, logger); } - /// - /// Validates that the properties of the index are all mapped to columns on at least one table. - /// - /// The index to validate. - /// The logger to use. - protected virtual void ValidateIndexMappedToTable( + /// + protected override void ValidateIndexOnComplexProperty( IIndex index, + IReadOnlyList complexProperties, IDiagnosticsLogger logger) { - if (index.DeclaringEntityType.GetTableName() != null - || ((IConventionIndex)index).GetConfigurationSource() == ConfigurationSource.Convention) + var complexCollectionProperty = complexProperties.FirstOrDefault(cp => cp.IsCollection); + if (complexCollectionProperty != null) { - return; + throw new InvalidOperationException( + CoreStrings.IndexOnComplexCollection( + index.Properties.Format(), + index.DeclaringEntityType.DisplayName(), + complexCollectionProperty.Name)); } - // The case where the index declaring type is mapped to a table is handled in ValidateSharedIndexesCompatibility - ValidateIndexPropertyMapping(index, logger); + var nonJsonComplexProperty = complexProperties.FirstOrDefault(cp => !cp.ComplexType.IsMappedToJson()); + if (nonJsonComplexProperty != null) + { + throw new InvalidOperationException( + RelationalStrings.IndexOnNonJsonComplexProperty( + index.Properties.Format(), + index.DeclaringEntityType.DisplayName(), + nonJsonComplexProperty.Name)); + } + + if (index.IsUnique) + { + throw new InvalidOperationException( + RelationalStrings.UniqueIndexOnComplexProperty( + index.Properties.Format(), + index.DeclaringEntityType.DisplayName(), + complexProperties[0].Name)); + } } /// @@ -2706,16 +2735,72 @@ protected virtual void ValidateIndexPropertyMapping( return; } - IReadOnlyProperty? propertyNotMappedToAnyTable = null; + var entityType = index.DeclaringEntityType; + IReadOnlyPropertyBase? propertyNotMappedToAnyTable = null; (string, List)? firstPropertyTables = null; (string, List)? lastPropertyTables = null; HashSet? overlappingTables = null; - foreach (var property in index.Properties) + bool? anyJsonContained = null; + var allJsonContained = true; + foreach (var propertyBase in index.Properties) { - var tablesMappedToProperty = property.GetMappedStoreObjects(StoreObjectType.Table).ToList(); + var isJsonContained = false; + string propertyName = null!; + List tablesMappedToProperty = null!; + if (propertyBase is IReadOnlyProperty property) + { + if (property.DeclaringType is IReadOnlyComplexType complexType + && complexType.IsMappedToJson()) + { + isJsonContained = true; + } + else + { + propertyName = property.Name; + tablesMappedToProperty = property.GetMappedStoreObjects(StoreObjectType.Table).ToList(); + } + } + else if (propertyBase is IReadOnlyComplexProperty complexProperty) + { + Check.DebugAssert(!complexProperty.IsCollection, "Collections of complex properties must not appear in indexes at this point."); + Check.DebugAssert(complexProperty.ComplexType.IsMappedToJson(), "Complex properties in indexes must be mapped to JSON at this point."); + + if (complexProperty.DeclaringType is IReadOnlyComplexType declaringComplexType + && declaringComplexType.IsMappedToJson()) + { + isJsonContained = true; + } + else + { + propertyName = complexProperty.Name; + tablesMappedToProperty = complexProperty.GetMappedStoreObjects(StoreObjectType.Table).ToList(); + } + } + else + { + throw new UnreachableException($"Properties of type {propertyBase.GetType().Name} are not supported in indexes."); + } + + if (anyJsonContained != null && anyJsonContained != isJsonContained) + { + throw new InvalidOperationException( + RelationalStrings.IndexPropertiesMixedJsonAndNonJsonMapping( + index.Properties.Format(), + entityType.DisplayName())); + } + + anyJsonContained = isJsonContained; + if (anyJsonContained == true) + { + continue; + } + + allJsonContained = false; + if (tablesMappedToProperty.Count == 0) { - propertyNotMappedToAnyTable = property; + propertyNotMappedToAnyTable = propertyBase; + overlappingTables = null; if (firstPropertyTables != null) @@ -2729,11 +2814,11 @@ protected virtual void ValidateIndexPropertyMapping( if (firstPropertyTables == null) { - firstPropertyTables = (property.Name, tablesMappedToProperty); + firstPropertyTables = (propertyName, tablesMappedToProperty); } else { - lastPropertyTables = (property.Name, tablesMappedToProperty); + lastPropertyTables = (propertyName, tablesMappedToProperty); } if (propertyNotMappedToAnyTable != null) @@ -2761,13 +2846,16 @@ protected virtual void ValidateIndexPropertyMapping( { if (firstPropertyTables == null) { - logger.AllIndexPropertiesNotMappedToAnyTable( - index.DeclaringEntityType, index); + if (!allJsonContained) + { + logger.AllIndexPropertiesNotMappedToAnyTable( + entityType, index); + } } else { logger.IndexPropertiesBothMappedAndNotMappedToTable( - index.DeclaringEntityType, index, propertyNotMappedToAnyTable!.Name); + entityType, index, propertyNotMappedToAnyTable!.Name); } } else if (overlappingTables.Count == 0) @@ -2776,7 +2864,7 @@ protected virtual void ValidateIndexPropertyMapping( Check.DebugAssert(lastPropertyTables != null); logger.IndexPropertiesMappedToNonOverlappingTables( - index.DeclaringEntityType, + entityType, index, firstPropertyTables.Value.Item1, firstPropertyTables.Value.Item2.Select(t => (t.Name, t.Schema)).ToList(), diff --git a/src/EFCore.Relational/Metadata/IColumnBase.cs b/src/EFCore.Relational/Metadata/IColumnBase.cs index 3942ef997fa..fdb75924d84 100644 --- a/src/EFCore.Relational/Metadata/IColumnBase.cs +++ b/src/EFCore.Relational/Metadata/IColumnBase.cs @@ -70,7 +70,7 @@ ValueComparer ProviderValueComparer for (var i = 0; i < PropertyMappings.Count; i++) { var mapping = PropertyMappings[i]; - if (mapping.Property.DeclaringType.IsAssignableFrom(entityType)) + if (mapping.Property.DeclaringType.ContainingEntityType.IsAssignableFrom(entityType)) { return mapping; } diff --git a/src/EFCore.Relational/Metadata/Internal/JsonColumn.cs b/src/EFCore.Relational/Metadata/Internal/JsonColumn.cs index 5aeccba175b..035fb745b3a 100644 --- a/src/EFCore.Relational/Metadata/Internal/JsonColumn.cs +++ b/src/EFCore.Relational/Metadata/Internal/JsonColumn.cs @@ -34,7 +34,8 @@ public JsonColumn( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override RelationalTypeMapping GetDefaultStoreTypeMapping() - => (RelationalTypeMapping)Table.Model.Model.GetModelDependencies().TypeMappingSource.FindMapping(typeof(JsonTypePlaceholder))!; + => ((IRelationalTypeMappingSource)Table.Model.Model.GetModelDependencies().TypeMappingSource) + .FindMapping(typeof(JsonTypePlaceholder), StoreType)!; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Relational/Metadata/Internal/JsonColumnBase.cs b/src/EFCore.Relational/Metadata/Internal/JsonColumnBase.cs index 9cf65b177c6..7090b77180f 100644 --- a/src/EFCore.Relational/Metadata/Internal/JsonColumnBase.cs +++ b/src/EFCore.Relational/Metadata/Internal/JsonColumnBase.cs @@ -34,5 +34,6 @@ public JsonColumnBase( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// protected override RelationalTypeMapping GetDefaultStoreTypeMapping() - => (RelationalTypeMapping)Table.Model.Model.GetModelDependencies().TypeMappingSource.FindMapping(typeof(JsonTypePlaceholder))!; + => ((IRelationalTypeMappingSource)Table.Model.Model.GetModelDependencies().TypeMappingSource) + .FindMapping(typeof(JsonTypePlaceholder), StoreType)!; } diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalIndexExtensions.cs b/src/EFCore.Relational/Metadata/Internal/RelationalIndexExtensions.cs index 705f8dd2028..5e775f1d3a7 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalIndexExtensions.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalIndexExtensions.cs @@ -23,8 +23,8 @@ public static bool AreCompatible( in StoreObjectIdentifier storeObject, bool shouldThrow) { - var columnNames = index.Properties.GetColumnNames(storeObject); - var duplicateColumnNames = duplicateIndex.Properties.GetColumnNames(storeObject); + var columnNames = GetColumnNames(index, storeObject); + var duplicateColumnNames = GetColumnNames(duplicateIndex, storeObject); if (columnNames == null || duplicateColumnNames == null) { @@ -56,8 +56,8 @@ public static bool AreCompatible( duplicateIndex.DeclaringEntityType.DisplayName(), index.DeclaringEntityType.GetSchemaQualifiedTableName(), index.GetDatabaseName(storeObject), - index.Properties.FormatColumns(storeObject), - duplicateIndex.Properties.FormatColumns(storeObject))); + FormatColumnNames(columnNames), + FormatColumnNames(duplicateColumnNames))); } return false; @@ -122,4 +122,59 @@ public static bool AreCompatible( return true; } + private static string FormatColumnNames(IEnumerable columnNames) + => "{" + string.Join(", ", columnNames.Select(n => "'" + n + "'")) + "}"; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static IReadOnlyList? GetColumnNames(this IReadOnlyIndex index) + => GetColumnNames(index, storeObject: null); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static IReadOnlyList? GetColumnNames(this IReadOnlyIndex index, in StoreObjectIdentifier storeObject) + => GetColumnNames(index, (StoreObjectIdentifier?)storeObject); + + private static IReadOnlyList? GetColumnNames(IReadOnlyIndex index, StoreObjectIdentifier? storeObject) + { + var names = new List(index.Properties.Count); + foreach (var property in index.Properties) + { + switch (property) + { + case IReadOnlyProperty scalar: + var columnName = storeObject is { } so ? scalar.GetColumnName(so) : scalar.GetColumnName(); + if (columnName == null) + { + return null; + } + + names.Add(columnName); + break; + + case IReadOnlyComplexProperty { IsCollection: false } complexProperty: + var containerColumnName = complexProperty.ComplexType.GetContainerColumnName(); + if (string.IsNullOrEmpty(containerColumnName)) + { + return null; + } + + names.Add(containerColumnName); + break; + + default: + return null; + } + } + + return names; + } } diff --git a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs index 6c606e7d9fd..0dcc1b6b58c 100644 --- a/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs +++ b/src/EFCore.Relational/Metadata/Internal/RelationalModel.cs @@ -1776,6 +1776,51 @@ private static IEnumerable GetTableColumnMappings(IProperty prop .FirstOrDefault(cm => cm.TableMapping.Table == table) ?.Column; + private static Column[]? GetIndexColumns(Table table, IIndex index) + { + var columns = new List(index.Properties.Count); + foreach (var propertyBase in index.Properties) + { + if (!TryAppendIndexColumns(table, propertyBase, columns)) + { + return null; + } + } + + return columns.ToArray(); + } + + private static bool TryAppendIndexColumns(Table table, IPropertyBase propertyBase, List columns) + { + switch (propertyBase) + { + case IProperty property: + Check.DebugAssert(property.DeclaringType is not IComplexType complexType || !complexType.IsMappedToJson(), + "Properties mapped to JSON should not be indexed directly; the index should be on the JSON container column instead."); + + if (FindColumn(table, property) is not Column column) + { + return false; + } + + columns.Add(column); + return true; + + case IComplexProperty { IsCollection: false } complexProperty: + var containerColumnName = complexProperty.ComplexType.GetContainerColumnName(); + if (string.IsNullOrEmpty(containerColumnName) + || table.FindColumn(containerColumnName) is not Column jsonColumn) + { + return false; + } + + columns.Add(jsonColumn); + return true; + + default: + return false; + } + } private static void PopulateTableConfiguration(Table table, bool designTime) { var storeObject = StoreObjectIdentifier.Table(table.Name, table.Schema); @@ -1836,20 +1881,7 @@ private static void PopulateTableConfiguration(Table table, bool designTime) if (!table.Indexes.TryGetValue(name, out var tableIndex)) { - var columns = new Column[index.Properties.Count]; - for (var i = 0; i < columns.Length; i++) - { - if (FindColumn(table, index.Properties[i]) is Column indexColumn) - { - columns[i] = indexColumn; - } - else - { - columns = null; - break; - } - } - + var columns = GetIndexColumns(table, index); if (columns == null) { continue; @@ -2398,7 +2430,7 @@ public static IKey GetKey( { var declaringEntityType = model.FindEntityType(declaringEntityTypeName)!; - return declaringEntityType.FindKey(properties.Select(p => declaringEntityType.FindProperty(p)!).ToArray())!; + return declaringEntityType.FindKey(properties.Select(p => FindPropertyByPath(declaringEntityType, p)!).ToArray())!; } /// @@ -2430,9 +2462,43 @@ public static IIndex GetIndex( { var declaringEntityType = model.FindEntityType(declaringEntityTypeName)!; - return declaringEntityType.FindIndex(properties.Select(p => declaringEntityType.FindProperty(p)!).ToArray())!; + return declaringEntityType.FindIndex(properties.Select(p => FindPropertyBaseByPath(declaringEntityType, p)!).ToArray())!; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static IReadOnlyPropertyBase? FindPropertyBaseByPath(IReadOnlyEntityType declaringEntityType, string propertyPath) + { + var segments = propertyPath.Split('.'); + IReadOnlyTypeBase currentType = declaringEntityType; + for (var i = 0; i < segments.Length - 1; i++) + { + var nestedComplexProperty = currentType.FindComplexProperty(segments[i]); + if (nestedComplexProperty == null) + { + return null; + } + + currentType = nestedComplexProperty.ComplexType; + } + + var lastSegment = segments[^1]; + return (IReadOnlyPropertyBase?)currentType.FindProperty(lastSegment) ?? currentType.FindComplexProperty(lastSegment); + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static IReadOnlyProperty? FindPropertyByPath(IReadOnlyEntityType declaringEntityType, string propertyPath) + => FindPropertyBaseByPath(declaringEntityType, propertyPath) as IReadOnlyProperty; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 052278bb940..f617f593ecd 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -1045,6 +1045,30 @@ public static string IncorrectDefaultValueType(object? value, object? valueType, GetString("IncorrectDefaultValueType", nameof(value), nameof(valueType), nameof(property), nameof(propertyType), nameof(entityType)), value, valueType, property, propertyType, entityType); + /// + /// The index {indexProperties} on the entity type '{entityType}' cannot be configured because it is defined on the complex property '{property}', which is not mapped to a JSON column. Indexes on complex properties are only supported when the complex property is mapped to JSON. + /// + public static string IndexOnNonJsonComplexProperty(object? indexProperties, object? entityType, object? property) + => string.Format( + GetString("IndexOnNonJsonComplexProperty", nameof(indexProperties), nameof(entityType), nameof(property)), + indexProperties, entityType, property); + + /// + /// The index {indexProperties} on the entity type '{entityType}' cannot be configured as unique because it contains the complex property '{property}'. Unique indexes are not supported on complex properties. + /// + public static string UniqueIndexOnComplexProperty(object? indexProperties, object? entityType, object? property) + => string.Format( + GetString("UniqueIndexOnComplexProperty", nameof(indexProperties), nameof(entityType), nameof(property)), + indexProperties, entityType, property); + + /// + /// The index {indexProperties} on the entity type '{entityType}' cannot be configured because some of its properties are contained within a complex property mapped to a JSON column while others are not. All properties of an index must either all be mapped to JSON or all be mapped to regular columns. + /// + public static string IndexPropertiesMixedJsonAndNonJsonMapping(object? indexProperties, object? entityType) + => string.Format( + GetString("IndexPropertiesMixedJsonAndNonJsonMapping", nameof(indexProperties), nameof(entityType)), + indexProperties, entityType); + /// /// The data insertion operation on '{table}' is not associated with a model. Either add a model to the migration, or specify the column types in all data operations. /// @@ -1395,6 +1419,14 @@ public static string KeylessMappingStrategy(object? mappingStrategy, object? ent GetString("KeylessMappingStrategy", nameof(mappingStrategy), nameof(entityType)), mappingStrategy, entityType); + /// + /// The key {keyProperties} on the entity type '{entityType}' cannot be configured because the property '{property}' is contained in a complex type mapped to a JSON column. Keys cannot reference properties that are stored inside a JSON document. + /// + public static string KeyPropertyInJsonComplexType(object? keyProperties, object? entityType, object? property) + => string.Format( + GetString("KeyPropertyInJsonComplexType", nameof(keyProperties), nameof(entityType), nameof(property)), + keyProperties, entityType, property); + /// /// Queries performing '{method}' operation must have a deterministic sort order. Rewrite the query to apply an 'OrderBy' operation on the sequence before calling '{method}'. /// @@ -3838,31 +3870,6 @@ public static EventDefinition LogNoModelSnapshotFound(IDiagnosticsLogger return (EventDefinition)definition; } - /// - /// Pending model changes were detected for context '{contextType}', but the model snapshot was created with EF Core version '{efVersion}'. These changes may be caused by improvements in snapshot generation in newer versions of EF Core. Consider adding an empty migration to regenerate the snapshot. - /// - public static EventDefinition LogOldMigrationVersion(IDiagnosticsLogger logger) - { - var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogOldMigrationVersion; - if (definition == null) - { - definition = NonCapturingLazyInitializer.EnsureInitialized( - ref ((RelationalLoggingDefinitions)logger.Definitions).LogOldMigrationVersion, - logger, - static logger => new EventDefinition( - logger.Options, - RelationalEventId.OldMigrationVersionWarning, - LogLevel.Warning, - "RelationalEventId.OldMigrationVersionWarning", - level => LoggerMessage.Define( - level, - RelationalEventId.OldMigrationVersionWarning, - _resourceManager.GetString("LogOldMigrationVersion")!))); - } - - return (EventDefinition)definition; - } - /// /// The model for context '{contextType}' changes each time it is built. This is usually caused by dynamic values used in a 'HasData' call (e.g. `new DateTime()`, `Guid.NewGuid()`). Add a new migration and examine its contents to locate the cause, and replace the dynamic call with a static, hardcoded value. See https://aka.ms/efcore-docs-pending-changes. /// @@ -3913,6 +3920,31 @@ public static EventDefinition LogNonTransactionalMigrationOperat return (EventDefinition)definition; } + /// + /// Pending model changes were detected for context '{contextType}', but the model snapshot was created with EF Core version '{efVersion}'. These changes may be caused by improvements in snapshot generation in newer versions of EF Core. Consider adding an empty migration to regenerate the snapshot. + /// + public static EventDefinition LogOldMigrationVersion(IDiagnosticsLogger logger) + { + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogOldMigrationVersion; + if (definition == null) + { + definition = NonCapturingLazyInitializer.EnsureInitialized( + ref ((RelationalLoggingDefinitions)logger.Definitions).LogOldMigrationVersion, + logger, + static logger => new EventDefinition( + logger.Options, + RelationalEventId.OldMigrationVersionWarning, + LogLevel.Warning, + "RelationalEventId.OldMigrationVersionWarning", + level => LoggerMessage.Define( + level, + RelationalEventId.OldMigrationVersionWarning, + _resourceManager.GetString("LogOldMigrationVersion")!))); + } + + return (EventDefinition)definition; + } + /// /// Opened connection to database '{database}' on server '{server}'. /// diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 3dc08fa39a6..6b2743ded12 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -511,6 +511,15 @@ Default value '{value}' of type '{valueType}' cannot be set on property '{property}' of type '{propertyType}' in entity type '{entityType}'. + + The index {indexProperties} on the entity type '{entityType}' cannot be configured because it is defined on the complex property '{property}', which is not mapped to a JSON column. Indexes on complex properties are only supported when the complex property is mapped to JSON. + + + The index {indexProperties} on the entity type '{entityType}' cannot be configured as unique because it contains the complex property '{property}'. Unique indexes are not supported on complex properties. + + + The index {indexProperties} on the entity type '{entityType}' cannot be configured because some of its properties are contained within a complex property mapped to a JSON column while others are not. All properties of an index must either all be mapped to JSON or all be mapped to regular columns. + The data insertion operation on '{table}' is not associated with a model. Either add a model to the migration, or specify the column types in all data operations. @@ -649,6 +658,9 @@ The mapping strategy '{mappingStrategy}' used for '{entityType}' is not supported for keyless entity types. See https://go.microsoft.com/fwlink/?linkid=2130430 for more information. + + The key {keyProperties} on the entity type '{entityType}' cannot be configured because the property '{property}' is contained in a complex type mapped to a JSON column. Keys cannot reference properties that are stored inside a JSON document. + Queries performing '{method}' operation must have a deterministic sort order. Rewrite the query to apply an 'OrderBy' operation on the sequence before calling '{method}'. @@ -876,10 +888,6 @@ Model snapshot was not found in assembly '{migrationsAssembly}'. Skipping pending model changes check. Information RelationalEventId.ModelSnapshotNotFound string - - Pending model changes were detected for context '{contextType}', but the model snapshot was created with EF Core version '{efVersion}'. These changes may be caused by improvements in snapshot generation in newer versions of EF Core. Consider adding an empty migration to regenerate the snapshot. - Warning RelationalEventId.OldMigrationVersionWarning string string - The model for context '{contextType}' changes each time it is built. This is usually caused by dynamic values used in a 'HasData' call (e.g. `new DateTime()`, `Guid.NewGuid()`). Add a new migration and examine its contents to locate the cause, and replace the dynamic call with a static, hardcoded value. See https://aka.ms/efcore-docs-pending-changes. Error RelationalEventId.PendingModelChangesWarning string @@ -888,6 +896,10 @@ The migration operation '{operation}' from migration '{migration}' cannot be executed in a transaction. If the app is terminated or an unrecoverable error occurs while this operation is being executed then the migration will be left in a partially applied state and would need to be reverted manually before it can be applied again. Create a separate migration that contains just this operation. Warning RelationalEventId.NonTransactionalMigrationOperationWarning string string + + Pending model changes were detected for context '{contextType}', but the model snapshot was created with EF Core version '{efVersion}'. These changes may be caused by improvements in snapshot generation in newer versions of EF Core. Consider adding an empty migration to regenerate the snapshot. + Warning RelationalEventId.OldMigrationVersionWarning string string + Opened connection to database '{database}' on server '{server}'. Debug RelationalEventId.ConnectionOpened string string diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.CreateSelect.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.CreateSelect.cs index e203fc5fc72..fadfeac8ca1 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.CreateSelect.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.CreateSelect.cs @@ -463,7 +463,30 @@ private SelectExpression GenerateSingleTableSelect(IEntityType entityType, ITabl { foreach (var property in primaryKey.Properties) { - identifier.Add((propertyMap[property], property.GetKeyValueComparer())); + if (!propertyMap.TryGetValue(property, out var columnExpression)) + { + // The key property is declared on a complex type; navigate the complex-property chain + // from the entity to its declaring complex type and bind the property there. + var chain = new Stack(); + for (var current = property.DeclaringType as IComplexType; + current != null; + current = current.ComplexProperty.DeclaringType as IComplexType) + { + chain.Push(current.ComplexProperty); + } + + var shaper = (RelationalStructuralTypeShaperExpression)complexPropertyMap[chain.Pop()]; + var complexProjection = (StructuralTypeProjectionExpression)shaper.ValueBufferExpression; + while (chain.Count > 0) + { + shaper = (RelationalStructuralTypeShaperExpression)complexProjection.BindComplexProperty(chain.Pop()); + complexProjection = (StructuralTypeProjectionExpression)shaper.ValueBufferExpression; + } + + columnExpression = complexProjection.BindProperty(property); + } + + identifier.Add((columnExpression, property.GetKeyValueComparer())); } } diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index bc3f88391ee..a6120445123 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -2506,7 +2506,9 @@ private static bool IsToOneJoin(ShapedQueryExpression inner, LambdaExpression in // Check if the inner key properties are covered by a unique index (e.g. unique FK in a 1:1 relationship). foreach (var index in entityType.GetIndexes()) { - if (index.IsUnique && index.Properties.SequenceEqual(keyProperties)) + if (index.IsUnique + && index.Properties.Count == keyProperties.Count + && index.Properties.OfType().SequenceEqual(keyProperties)) { return true; } diff --git a/src/EFCore.SqlServer/Extensions/SqlServerIndexBuilderExtensions.cs b/src/EFCore.SqlServer/Extensions/SqlServerIndexBuilderExtensions.cs index 0e105be2504..9cd20abcb90 100644 --- a/src/EFCore.SqlServer/Extensions/SqlServerIndexBuilderExtensions.cs +++ b/src/EFCore.SqlServer/Extensions/SqlServerIndexBuilderExtensions.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 Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.SqlServer.Metadata.Internal; // ReSharper disable once CheckNamespace @@ -167,7 +168,11 @@ public static IndexBuilder IncludeProperties( IncludeProperties( indexBuilder, - includeExpression.GetMemberAccessList().Select(EntityFrameworkMemberInfoExtensions.GetSimpleMemberName).ToArray()); +#pragma warning disable EF1001 // Internal EF Core API usage. + includeExpression.GetMemberAccessChainList() + .Select(chain => string.Join(".", chain.Select(EntityFrameworkMemberInfoExtensions.GetSimpleMemberName))) + .ToArray()); +#pragma warning restore EF1001 // Internal EF Core API usage. return indexBuilder; } diff --git a/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs b/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs index fbc3ed070a6..3fed75ed434 100644 --- a/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs +++ b/src/EFCore.SqlServer/Infrastructure/Internal/SqlServerModelValidator.cs @@ -245,8 +245,10 @@ protected virtual void ValidateIndexIncludeProperties(IIndex index) var includeProperties = index.GetIncludeProperties(); if (includeProperties?.Count > 0) { +#pragma warning disable EF1001 // Internal EF Core API usage. var notFound = includeProperties - .FirstOrDefault(i => index.DeclaringEntityType.FindProperty(i) == null); + .FirstOrDefault(i => RelationalModel.FindPropertyByPath(index.DeclaringEntityType, i) == null); +#pragma warning restore EF1001 // Internal EF Core API usage. if (notFound != null) { @@ -319,16 +321,17 @@ protected virtual void ValidateFullTextIndex( } // Validate that FTS columns are text or varbinary types - foreach (var property in index.Properties) + foreach (var propertyBase in index.Properties) { - var typeMapping = (RelationalTypeMapping?)property.FindTypeMapping(); + var property = propertyBase as IProperty; + var typeMapping = (RelationalTypeMapping?)property?.FindTypeMapping(); if (typeMapping?.StoreTypeNameBase is not ("varchar" or "nvarchar" or "varbinary" or "binary")) { throw new InvalidOperationException( SqlServerStrings.FullTextIndexOnInvalidColumn( index.DisplayName(), entityType.DisplayName(), - property.Name)); + propertyBase.Name)); } } @@ -384,7 +387,7 @@ protected virtual void ValidateVectorIndex(IIndex index) index.DeclaringEntityType.DisplayName())); } - var typeMapping = property.FindTypeMapping(); + var typeMapping = (property as IProperty)?.FindTypeMapping(); if (typeMapping is not SqlServerVectorTypeMapping) { diff --git a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerIndexConvention.cs b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerIndexConvention.cs index b83cc49b1d7..0b60779247b 100644 --- a/src/EFCore.SqlServer/Metadata/Conventions/SqlServerIndexConvention.cs +++ b/src/EFCore.SqlServer/Metadata/Conventions/SqlServerIndexConvention.cs @@ -206,8 +206,14 @@ private string CreateIndexFilter(List nullableColumns) var nullableColumns = new List(); var table = StoreObjectIdentifier.Table(tableName, index.DeclaringEntityType.GetSchema()); - foreach (var property in index.Properties) + foreach (var propertyBase in index.Properties) { + if (propertyBase is not IReadOnlyProperty property) + { + // Unique indexes on complex properties are not supported + return null; + } + var columnName = property.GetColumnName(table); if (columnName == null) { diff --git a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs index 2e8784e2afd..2697eb9ef2b 100644 --- a/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs +++ b/src/EFCore.SqlServer/Metadata/Internal/SqlServerAnnotationProvider.cs @@ -233,10 +233,12 @@ public override IEnumerable For(ITableIndex index, bool designTime) if (modelIndex.GetIncludeProperties(table) is { } includeProperties) { +#pragma warning disable EF1001 // Internal EF Core API usage. var includeColumns = includeProperties - .Select(p => modelIndex.DeclaringEntityType.FindProperty(p)! + .Select(p => RelationalModel.FindPropertyByPath(modelIndex.DeclaringEntityType, p)! .GetColumnName(StoreObjectIdentifier.Table(table.Name, table.Schema))) .ToArray(); +#pragma warning restore EF1001 // Internal EF Core API usage. yield return new Annotation( SqlServerAnnotationNames.Include, diff --git a/src/EFCore.SqlServer/Metadata/Internal/SqlServerIndexExtensions.cs b/src/EFCore.SqlServer/Metadata/Internal/SqlServerIndexExtensions.cs index 06d055c1323..eac1c4d0efa 100644 --- a/src/EFCore.SqlServer/Metadata/Internal/SqlServerIndexExtensions.cs +++ b/src/EFCore.SqlServer/Metadata/Internal/SqlServerIndexExtensions.cs @@ -137,10 +137,13 @@ public static bool AreCompatibleForSqlServer( return true; static bool SameColumnNames(IReadOnlyIndex index, IReadOnlyIndex duplicateIndex, StoreObjectIdentifier storeObject) - => index.GetIncludeProperties()!.Select(p => index.DeclaringEntityType.FindProperty(p)!.GetColumnName(storeObject)) +#pragma warning disable EF1001 // Internal EF Core API usage. + => index.GetIncludeProperties()! + .Select(p => RelationalModel.FindPropertyByPath(index.DeclaringEntityType, p)!.GetColumnName(storeObject)) .SequenceEqual( duplicateIndex.GetIncludeProperties()!.Select(p - => duplicateIndex.DeclaringEntityType.FindProperty(p)!.GetColumnName(storeObject))); + => RelationalModel.FindPropertyByPath(duplicateIndex.DeclaringEntityType, p)!.GetColumnName(storeObject))); +#pragma warning restore EF1001 // Internal EF Core API usage. } private static string FormatInclude(IReadOnlyIndex index, StoreObjectIdentifier storeObject) @@ -149,7 +152,9 @@ private static string FormatInclude(IReadOnlyIndex index, StoreObjectIdentifier : "{'" + string.Join( "', '", - index.GetIncludeProperties()!.Select(p => index.DeclaringEntityType.FindProperty(p) - ?.GetColumnName(storeObject))) +#pragma warning disable EF1001 // Internal EF Core API usage. + index.GetIncludeProperties()!.Select(p + => RelationalModel.FindPropertyByPath(index.DeclaringEntityType, p)?.GetColumnName(storeObject))) +#pragma warning restore EF1001 // Internal EF Core API usage. + "'}"; } diff --git a/src/EFCore/Design/ICSharpHelper.cs b/src/EFCore/Design/ICSharpHelper.cs index 6581ece0618..9f37459a130 100644 --- a/src/EFCore/Design/ICSharpHelper.cs +++ b/src/EFCore/Design/ICSharpHelper.cs @@ -91,6 +91,34 @@ public interface ICSharpHelper string Lambda(IEnumerable properties, string? lambdaIdentifier = null) => Lambda(properties.Select(p => p.Name).ToList(), lambdaIdentifier); + /// + /// Generates a property accessor lambda. Properties declared on a complex type are emitted using the dotted path + /// from the entity type down to the property. + /// + /// The properties. + /// The identifier to use for parameter in the lambda. + /// The lambda. + string Lambda(IEnumerable properties, string? lambdaIdentifier = null) + => Lambda(properties.Select(GetDottedPathFromContainingEntity).ToList(), lambdaIdentifier); + + private static string GetDottedPathFromContainingEntity(IPropertyBase property) + { + if (property.DeclaringType is not IReadOnlyComplexType) + { + return property.Name; + } + + var segments = new List { property.Name }; + var typeBase = (IReadOnlyTypeBase)property.DeclaringType; + while (typeBase is IReadOnlyComplexType complexType) + { + segments.Insert(0, complexType.ComplexProperty.Name); + typeBase = complexType.ComplexProperty.DeclaringType; + } + + return string.Join(".", segments); + } + /// /// Generates a multidimensional array literal. /// diff --git a/src/EFCore/EFCore.baseline.json b/src/EFCore/EFCore.baseline.json index bffa286d455..523bc1d7eaa 100644 --- a/src/EFCore/EFCore.baseline.json +++ b/src/EFCore/EFCore.baseline.json @@ -4130,9 +4130,21 @@ { "Member": "static string IncorrectNumberOfArguments(object? method, object? argumentCount, object? parameterCount);" }, + { + "Member": "static string IndexOnComplexCollection(object? indexProperties, object? entityType, object? property);" + }, + { + "Member": "static string IndexOnComplexProperty(object? indexProperties, object? entityType, object? property);" + }, { "Member": "static string IndexPropertiesWrongEntity(object? indexProperties, object? entityType);" }, + { + "Member": "static string IndexPropertyMustBePropertyOrComplexProperty(object? property, object? entityType);" + }, + { + "Member": "static string IndexValueFactoryWithComplexProperty(object? indexProperties, object? entityType, object? property);" + }, { "Member": "static string IndexWrongType(object? index, object? entityType, object? otherEntityType);" }, @@ -4247,6 +4259,12 @@ { "Member": "static string KeylessTypeWithKey(object? keyProperties, object? entityType);" }, + { + "Member": "static string KeyOnComplexCollection(object? keyProperties, object? entityType, object? property);" + }, + { + "Member": "static string KeyOnNullableComplexProperty(object? keyProperties, object? entityType, object? property);" + }, { "Member": "static string KeyPropertiesWrongEntity(object? keyProperties, object? entityType);" }, @@ -9195,7 +9213,7 @@ "Member": "ForeignKeyIndexConvention(Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure.ProviderConventionSetBuilderDependencies dependencies);" }, { - "Member": "virtual bool AreIndexedBy(System.Collections.Generic.IReadOnlyList properties, bool unique, System.Collections.Generic.IReadOnlyList coveringIndexProperties, bool coveringIndexUnique);" + "Member": "virtual bool AreIndexedBy(System.Collections.Generic.IReadOnlyList properties, bool unique, System.Collections.Generic.IReadOnlyList coveringIndexProperties, bool coveringIndexUnique);" }, { "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.IConventionIndex? CreateIndex(System.Collections.Generic.IReadOnlyList properties, bool unique, Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionEntityTypeBuilder entityTypeBuilder);" @@ -10150,16 +10168,16 @@ "Member": "Microsoft.EntityFrameworkCore.Metadata.IConventionForeignKey? AddForeignKey(System.Collections.Generic.IReadOnlyList properties, Microsoft.EntityFrameworkCore.Metadata.IConventionKey principalKey, Microsoft.EntityFrameworkCore.Metadata.IConventionEntityType principalEntityType, bool setComponentConfigurationSource = true, bool fromDataAnnotation = false);" }, { - "Member": "Microsoft.EntityFrameworkCore.Metadata.IConventionIndex? AddIndex(Microsoft.EntityFrameworkCore.Metadata.IConventionProperty property, bool fromDataAnnotation = false);" + "Member": "Microsoft.EntityFrameworkCore.Metadata.IConventionIndex? AddIndex(Microsoft.EntityFrameworkCore.Metadata.IConventionPropertyBase property, bool fromDataAnnotation = false);" }, { - "Member": "Microsoft.EntityFrameworkCore.Metadata.IConventionIndex? AddIndex(System.Collections.Generic.IReadOnlyList properties, bool fromDataAnnotation = false);" + "Member": "Microsoft.EntityFrameworkCore.Metadata.IConventionIndex? AddIndex(System.Collections.Generic.IReadOnlyList properties, bool fromDataAnnotation = false);" }, { - "Member": "Microsoft.EntityFrameworkCore.Metadata.IConventionIndex? AddIndex(Microsoft.EntityFrameworkCore.Metadata.IConventionProperty property, string name, bool fromDataAnnotation = false);" + "Member": "Microsoft.EntityFrameworkCore.Metadata.IConventionIndex? AddIndex(Microsoft.EntityFrameworkCore.Metadata.IConventionPropertyBase property, string name, bool fromDataAnnotation = false);" }, { - "Member": "Microsoft.EntityFrameworkCore.Metadata.IConventionIndex? AddIndex(System.Collections.Generic.IReadOnlyList properties, string name, bool fromDataAnnotation = false);" + "Member": "Microsoft.EntityFrameworkCore.Metadata.IConventionIndex? AddIndex(System.Collections.Generic.IReadOnlyList properties, string name, bool fromDataAnnotation = false);" }, { "Member": "Microsoft.EntityFrameworkCore.Metadata.IConventionKey? AddKey(Microsoft.EntityFrameworkCore.Metadata.IConventionProperty property, bool fromDataAnnotation = false);" @@ -10204,10 +10222,10 @@ "Member": "System.Collections.Generic.IEnumerable FindForeignKeys(System.Collections.Generic.IReadOnlyList properties);" }, { - "Member": "Microsoft.EntityFrameworkCore.Metadata.IConventionIndex? FindIndex(Microsoft.EntityFrameworkCore.Metadata.IReadOnlyProperty property);" + "Member": "Microsoft.EntityFrameworkCore.Metadata.IConventionIndex? FindIndex(Microsoft.EntityFrameworkCore.Metadata.IReadOnlyPropertyBase property);" }, { - "Member": "Microsoft.EntityFrameworkCore.Metadata.IConventionIndex? FindIndex(System.Collections.Generic.IReadOnlyList properties);" + "Member": "Microsoft.EntityFrameworkCore.Metadata.IConventionIndex? FindIndex(System.Collections.Generic.IReadOnlyList properties);" }, { "Member": "Microsoft.EntityFrameworkCore.Metadata.IConventionIndex? FindIndex(string name);" @@ -10357,7 +10375,7 @@ "Member": "Microsoft.EntityFrameworkCore.Metadata.IConventionForeignKey? RemoveForeignKey(Microsoft.EntityFrameworkCore.Metadata.IReadOnlyForeignKey foreignKey);" }, { - "Member": "Microsoft.EntityFrameworkCore.Metadata.IConventionIndex? RemoveIndex(System.Collections.Generic.IReadOnlyList properties);" + "Member": "Microsoft.EntityFrameworkCore.Metadata.IConventionIndex? RemoveIndex(System.Collections.Generic.IReadOnlyList properties);" }, { "Member": "Microsoft.EntityFrameworkCore.Metadata.IConventionIndex? RemoveIndex(Microsoft.EntityFrameworkCore.Metadata.IReadOnlyIndex index);" @@ -10891,7 +10909,7 @@ "Member": "Microsoft.EntityFrameworkCore.Metadata.IConventionEntityType DeclaringEntityType { get; }" }, { - "Member": "System.Collections.Generic.IReadOnlyList Properties { get; }" + "Member": "System.Collections.Generic.IReadOnlyList Properties { get; }" } ] }, @@ -12156,6 +12174,9 @@ { "Member": "string Lambda(System.Collections.Generic.IEnumerable properties, string? lambdaIdentifier = null);" }, + { + "Member": "string Lambda(System.Collections.Generic.IEnumerable properties, string? lambdaIdentifier = null);" + }, { "Member": "string Literal(object?[, ] values);" }, @@ -12719,13 +12740,13 @@ "Member": "System.Collections.Generic.IEnumerable FindForeignKeys(System.Collections.Generic.IReadOnlyList properties);" }, { - "Member": "Microsoft.EntityFrameworkCore.Metadata.IIndex? FindIndex(System.Collections.Generic.IReadOnlyList properties);" + "Member": "Microsoft.EntityFrameworkCore.Metadata.IIndex? FindIndex(System.Collections.Generic.IReadOnlyList properties);" }, { "Member": "Microsoft.EntityFrameworkCore.Metadata.IIndex? FindIndex(string name);" }, { - "Member": "Microsoft.EntityFrameworkCore.Metadata.IIndex? FindIndex(Microsoft.EntityFrameworkCore.Metadata.IReadOnlyProperty property);" + "Member": "Microsoft.EntityFrameworkCore.Metadata.IIndex? FindIndex(Microsoft.EntityFrameworkCore.Metadata.IReadOnlyPropertyBase property);" }, { "Member": "Microsoft.EntityFrameworkCore.Metadata.IKey? FindKey(Microsoft.EntityFrameworkCore.Metadata.IReadOnlyProperty property);" @@ -13163,7 +13184,7 @@ "Member": "Microsoft.EntityFrameworkCore.Metadata.IEntityType DeclaringEntityType { get; }" }, { - "Member": "System.Collections.Generic.IReadOnlyList Properties { get; }" + "Member": "System.Collections.Generic.IReadOnlyList Properties { get; }" } ] }, @@ -13658,16 +13679,16 @@ "Member": "Microsoft.EntityFrameworkCore.Metadata.IMutableForeignKey AddForeignKey(System.Collections.Generic.IReadOnlyList properties, Microsoft.EntityFrameworkCore.Metadata.IMutableKey principalKey, Microsoft.EntityFrameworkCore.Metadata.IMutableEntityType principalEntityType);" }, { - "Member": "Microsoft.EntityFrameworkCore.Metadata.IMutableIndex AddIndex(Microsoft.EntityFrameworkCore.Metadata.IMutableProperty property);" + "Member": "Microsoft.EntityFrameworkCore.Metadata.IMutableIndex AddIndex(Microsoft.EntityFrameworkCore.Metadata.IMutablePropertyBase property);" }, { - "Member": "Microsoft.EntityFrameworkCore.Metadata.IMutableIndex AddIndex(System.Collections.Generic.IReadOnlyList properties);" + "Member": "Microsoft.EntityFrameworkCore.Metadata.IMutableIndex AddIndex(System.Collections.Generic.IReadOnlyList properties);" }, { - "Member": "Microsoft.EntityFrameworkCore.Metadata.IMutableIndex AddIndex(Microsoft.EntityFrameworkCore.Metadata.IMutableProperty property, string name);" + "Member": "Microsoft.EntityFrameworkCore.Metadata.IMutableIndex AddIndex(Microsoft.EntityFrameworkCore.Metadata.IMutablePropertyBase property, string name);" }, { - "Member": "Microsoft.EntityFrameworkCore.Metadata.IMutableIndex AddIndex(System.Collections.Generic.IReadOnlyList properties, string name);" + "Member": "Microsoft.EntityFrameworkCore.Metadata.IMutableIndex AddIndex(System.Collections.Generic.IReadOnlyList properties, string name);" }, { "Member": "Microsoft.EntityFrameworkCore.Metadata.IMutableKey AddKey(Microsoft.EntityFrameworkCore.Metadata.IMutableProperty property);" @@ -13715,10 +13736,10 @@ "Member": "System.Collections.Generic.IEnumerable FindForeignKeys(System.Collections.Generic.IReadOnlyList properties);" }, { - "Member": "Microsoft.EntityFrameworkCore.Metadata.IMutableIndex? FindIndex(Microsoft.EntityFrameworkCore.Metadata.IReadOnlyProperty property);" + "Member": "Microsoft.EntityFrameworkCore.Metadata.IMutableIndex? FindIndex(Microsoft.EntityFrameworkCore.Metadata.IReadOnlyPropertyBase property);" }, { - "Member": "Microsoft.EntityFrameworkCore.Metadata.IMutableIndex? FindIndex(System.Collections.Generic.IReadOnlyList properties);" + "Member": "Microsoft.EntityFrameworkCore.Metadata.IMutableIndex? FindIndex(System.Collections.Generic.IReadOnlyList properties);" }, { "Member": "Microsoft.EntityFrameworkCore.Metadata.IMutableIndex? FindIndex(string name);" @@ -13847,7 +13868,7 @@ "Member": "Microsoft.EntityFrameworkCore.Metadata.IMutableForeignKey? RemoveForeignKey(Microsoft.EntityFrameworkCore.Metadata.IReadOnlyForeignKey foreignKey);" }, { - "Member": "Microsoft.EntityFrameworkCore.Metadata.IMutableIndex? RemoveIndex(System.Collections.Generic.IReadOnlyList properties);" + "Member": "Microsoft.EntityFrameworkCore.Metadata.IMutableIndex? RemoveIndex(System.Collections.Generic.IReadOnlyList properties);" }, { "Member": "Microsoft.EntityFrameworkCore.Metadata.IMutableIndex? RemoveIndex(Microsoft.EntityFrameworkCore.Metadata.IReadOnlyIndex index);" @@ -13975,7 +13996,7 @@ "Member": "bool IsUnique { get; set; }" }, { - "Member": "System.Collections.Generic.IReadOnlyList Properties { get; }" + "Member": "System.Collections.Generic.IReadOnlyList Properties { get; }" } ] }, @@ -15388,13 +15409,13 @@ "Member": "System.Collections.Generic.IEnumerable FindForeignKeys(System.Collections.Generic.IReadOnlyList properties);" }, { - "Member": "Microsoft.EntityFrameworkCore.Metadata.IReadOnlyIndex? FindIndex(System.Collections.Generic.IReadOnlyList properties);" + "Member": "Microsoft.EntityFrameworkCore.Metadata.IReadOnlyIndex? FindIndex(System.Collections.Generic.IReadOnlyList properties);" }, { "Member": "Microsoft.EntityFrameworkCore.Metadata.IReadOnlyIndex? FindIndex(string name);" }, { - "Member": "Microsoft.EntityFrameworkCore.Metadata.IReadOnlyIndex? FindIndex(Microsoft.EntityFrameworkCore.Metadata.IReadOnlyProperty property);" + "Member": "Microsoft.EntityFrameworkCore.Metadata.IReadOnlyIndex? FindIndex(Microsoft.EntityFrameworkCore.Metadata.IReadOnlyPropertyBase property);" }, { "Member": "Microsoft.EntityFrameworkCore.Metadata.IReadOnlyKey? FindKey(System.Collections.Generic.IReadOnlyList properties);" @@ -15629,7 +15650,7 @@ "Member": "string? Name { get; }" }, { - "Member": "System.Collections.Generic.IReadOnlyList Properties { get; }" + "Member": "System.Collections.Generic.IReadOnlyList Properties { get; }" } ] }, @@ -17376,6 +17397,25 @@ } ] }, + { + "Type": "class Microsoft.EntityFrameworkCore.Metadata.Conventions.KeyComplexPropertyConvention : Microsoft.EntityFrameworkCore.Metadata.Conventions.IKeyAddedConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IKeyRemovedConvention", + "Methods": [ + { + "Member": "KeyComplexPropertyConvention(Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure.ProviderConventionSetBuilderDependencies dependencies);" + }, + { + "Member": "virtual void ProcessKeyAdded(Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionKeyBuilder keyBuilder, Microsoft.EntityFrameworkCore.Metadata.Conventions.IConventionContext context);" + }, + { + "Member": "virtual void ProcessKeyRemoved(Microsoft.EntityFrameworkCore.Metadata.Builders.IConventionEntityTypeBuilder entityTypeBuilder, Microsoft.EntityFrameworkCore.Metadata.IConventionKey key, Microsoft.EntityFrameworkCore.Metadata.Conventions.IConventionContext context);" + } + ], + "Properties": [ + { + "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure.ProviderConventionSetBuilderDependencies Dependencies { get; }" + } + ] + }, { "Type": "class Microsoft.EntityFrameworkCore.Metadata.Conventions.KeyDiscoveryConvention : Microsoft.EntityFrameworkCore.Metadata.Conventions.IEntityTypeAddedConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IPropertyAddedConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IKeyRemovedConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IEntityTypeBaseTypeChangedConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IEntityTypeMemberIgnoredConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IForeignKeyAddedConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IForeignKeyRemovedConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IForeignKeyPropertiesChangedConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IForeignKeyUniquenessChangedConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.IForeignKeyOwnershipChangedConvention, Microsoft.EntityFrameworkCore.Metadata.Conventions.ISkipNavigationForeignKeyChangedConvention", "Methods": [ @@ -18751,6 +18791,9 @@ { "Member": "virtual void ValidateIndex(Microsoft.EntityFrameworkCore.Metadata.IIndex index, Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger logger);" }, + { + "Member": "virtual void ValidateIndexOnComplexProperty(Microsoft.EntityFrameworkCore.Metadata.IIndex index, System.Collections.Generic.IReadOnlyList complexProperties, Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger logger);" + }, { "Member": "virtual void ValidateInheritanceMapping(Microsoft.EntityFrameworkCore.Metadata.IEntityType entityType, Microsoft.EntityFrameworkCore.Diagnostics.IDiagnosticsLogger logger);" }, @@ -22338,7 +22381,7 @@ "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.RuntimeForeignKey AddForeignKey(System.Collections.Generic.IReadOnlyList properties, Microsoft.EntityFrameworkCore.Metadata.RuntimeKey principalKey, Microsoft.EntityFrameworkCore.Metadata.RuntimeEntityType principalEntityType, Microsoft.EntityFrameworkCore.DeleteBehavior deleteBehavior = Microsoft.EntityFrameworkCore.DeleteBehavior.ClientSetNull, bool unique = false, bool required = false, bool requiredDependent = false, bool ownership = false);" }, { - "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.RuntimeIndex AddIndex(System.Collections.Generic.IReadOnlyList properties, string? name = null, bool unique = false);" + "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.RuntimeIndex AddIndex(System.Collections.Generic.IReadOnlyList properties, string? name = null, bool unique = false);" }, { "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.RuntimeKey AddKey(System.Collections.Generic.IReadOnlyList properties);" @@ -22368,7 +22411,7 @@ "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.RuntimeForeignKey? FindForeignKey(System.Collections.Generic.IReadOnlyList properties, Microsoft.EntityFrameworkCore.Metadata.IReadOnlyKey principalKey, Microsoft.EntityFrameworkCore.Metadata.IReadOnlyEntityType principalEntityType);" }, { - "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.RuntimeIndex? FindIndex(System.Collections.Generic.IReadOnlyList properties);" + "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.RuntimeIndex? FindIndex(System.Collections.Generic.IReadOnlyList properties);" }, { "Member": "virtual Microsoft.EntityFrameworkCore.Metadata.RuntimeIndex? FindIndex(string name);" @@ -22483,7 +22526,7 @@ "Member": "virtual string? Name { get; }" }, { - "Member": "virtual System.Collections.Generic.IReadOnlyList Properties { get; }" + "Member": "virtual System.Collections.Generic.IReadOnlyList Properties { get; }" } ] }, diff --git a/src/EFCore/Extensions/Internal/ExpressionExtensions.cs b/src/EFCore/Extensions/Internal/ExpressionExtensions.cs index fb3344d3f33..668508aaf04 100644 --- a/src/EFCore/Extensions/Internal/ExpressionExtensions.cs +++ b/src/EFCore/Extensions/Internal/ExpressionExtensions.cs @@ -70,6 +70,56 @@ public static Expression MakeHasSentinel( return equalsExpression; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static IReadOnlyList>? MatchMemberAccessChainList( + this LambdaExpression lambdaExpression) + { + Check.DebugAssert(lambdaExpression.Body != null, "lambdaExpression.Body is null"); + Check.DebugAssert( + lambdaExpression.Parameters.Count == 1, + "lambdaExpression.Parameters.Count is " + lambdaExpression.Parameters.Count + ". Should be 1."); + + var parameterExpression = lambdaExpression.Parameters[0]; + + if (RemoveConvert(lambdaExpression.Body) is NewExpression newExpression) + { + var chains = new List>(newExpression.Arguments.Count); + foreach (var argument in newExpression.Arguments) + { + var chain = MatchMemberAccess(parameterExpression, argument); + if (chain == null) + { + return null; + } + + chains.Add(chain); + } + + return chains; + } + + var memberPath = MatchMemberAccess(parameterExpression, lambdaExpression.Body); + + return memberPath != null ? new[] { memberPath } : null; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public static IReadOnlyList> GetMemberAccessChainList( + this LambdaExpression expression) + => expression.MatchMemberAccessChainList() + ?? throw new ArgumentException( + CoreStrings.InvalidMembersExpression(expression)); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore/Infrastructure/ModelValidator.cs b/src/EFCore/Infrastructure/ModelValidator.cs index 1329ff21646..dd97b5b14a8 100644 --- a/src/EFCore/Infrastructure/ModelValidator.cs +++ b/src/EFCore/Infrastructure/ModelValidator.cs @@ -260,8 +260,56 @@ protected virtual void ValidateIndex( IIndex index, IDiagnosticsLogger logger) { + List? complexProperties = null; + foreach (var property in index.Properties) + { + if (property is IComplexProperty complexProperty) + { + (complexProperties ??= []).Add(complexProperty); + } + else if (property is not IProperty) + { + throw new InvalidOperationException( + CoreStrings.IndexPropertyMustBePropertyOrComplexProperty( + property.Name, + index.DeclaringEntityType.DisplayName())); + } + + if (property.DeclaringType is not IComplexType) + { + continue; + } + + ValidateComplexPropertyChainForKeyOrIndex( + property, + static (props, type, propName) => CoreStrings.IndexOnComplexCollection(props, type, propName), + nullableErrorFactory: null, + index.Properties.Format(), + index.DeclaringEntityType.DisplayName()); + } + + if (complexProperties != null) + { + ValidateIndexOnComplexProperty(index, complexProperties, logger); + } } + /// + /// Validates an index that contains a complex property. + /// + /// The index to validate. + /// The complex properties contained in the index. + /// The logger to use. + protected virtual void ValidateIndexOnComplexProperty( + IIndex index, + IReadOnlyList complexProperties, + IDiagnosticsLogger logger) + => throw new InvalidOperationException( + CoreStrings.IndexOnComplexProperty( + index.Properties.Format(), + index.DeclaringEntityType.DisplayName(), + complexProperties[0].Name)); + /// /// Validates a single key. /// @@ -273,6 +321,50 @@ protected virtual void ValidateKey( { ValidateShadowKey(key, logger); ValidateMutableKey(key, logger); + + foreach (var property in key.Properties) + { + if (property.DeclaringType is not IComplexType) + { + continue; + } + + ValidateComplexPropertyChainForKeyOrIndex( + property, + static (props, type, propName) => CoreStrings.KeyOnComplexCollection(props, type, propName), + static (props, type, propName) => CoreStrings.KeyOnNullableComplexProperty(props, type, propName), + key.Properties.Format(), + key.DeclaringEntityType.DisplayName()); + } + } + + private static void ValidateComplexPropertyChainForKeyOrIndex( + IPropertyBase property, + Func collectionErrorFactory, + Func? nullableErrorFactory, + string propertyListFormatted, + string entityTypeName) + { + var typeBase = property.DeclaringType; + while (typeBase is IComplexType complexType) + { + var complexProperty = complexType.ComplexProperty; + + if (complexProperty.IsCollection) + { + throw new InvalidOperationException( + collectionErrorFactory(propertyListFormatted, entityTypeName, complexProperty.Name)); + } + + if (nullableErrorFactory != null + && complexProperty.IsNullable) + { + throw new InvalidOperationException( + nullableErrorFactory(propertyListFormatted, entityTypeName, complexProperty.Name)); + } + + typeBase = complexProperty.DeclaringType; + } } /// diff --git a/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs b/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs index d6e27881858..bc2dd6419b8 100644 --- a/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs +++ b/src/EFCore/Metadata/Builders/EntityTypeBuilder`.cs @@ -82,7 +82,7 @@ public virtual EntityTypeBuilder HasBaseType() public virtual KeyBuilder HasKey(Expression> keyExpression) => new KeyBuilder( Builder.PrimaryKey( - Check.NotNull(keyExpression).GetMemberAccessList(), + Check.NotNull(keyExpression).GetMemberAccessChainList(), ConfigurationSource.Explicit)!.Metadata); /// @@ -114,7 +114,7 @@ public virtual KeyBuilder HasKey(Expression> keyExpressio public virtual KeyBuilder HasAlternateKey(Expression> keyExpression) => new( Builder.HasKey( - Check.NotNull(keyExpression).GetMemberAccessList(), + Check.NotNull(keyExpression).GetMemberAccessChainList(), ConfigurationSource.Explicit)!.Metadata); /// @@ -873,7 +873,7 @@ public virtual EntityTypeBuilder HasQueryFilter(string filterKey, Expre public virtual IndexBuilder HasIndex(Expression> indexExpression) => new( Builder.HasIndex( - Check.NotNull(indexExpression).GetMemberAccessList(), + Check.NotNull(indexExpression).GetMemberAccessChainList(), ConfigurationSource.Explicit)!.Metadata); /// @@ -898,7 +898,7 @@ public virtual IndexBuilder HasIndex( string name) => new( Builder.HasIndex( - Check.NotNull(indexExpression).GetMemberAccessList(), + Check.NotNull(indexExpression).GetMemberAccessChainList(), name, ConfigurationSource.Explicit)!.Metadata); diff --git a/src/EFCore/Metadata/Conventions/ForeignKeyIndexConvention.cs b/src/EFCore/Metadata/Conventions/ForeignKeyIndexConvention.cs index 4e3c896a4fc..102778e150c 100644 --- a/src/EFCore/Metadata/Conventions/ForeignKeyIndexConvention.cs +++ b/src/EFCore/Metadata/Conventions/ForeignKeyIndexConvention.cs @@ -367,11 +367,11 @@ public virtual void ProcessIndexUniquenessChanged( /// Whether the existing index is unique. /// if the existing index covers the given properties. protected virtual bool AreIndexedBy( - IReadOnlyList properties, + IReadOnlyList properties, bool unique, - IReadOnlyList coveringIndexProperties, + IReadOnlyList coveringIndexProperties, bool coveringIndexUnique) - => (!unique && coveringIndexProperties.Select(p => p.Name).StartsWith(properties.Select(p => p.Name))) + => (!unique && coveringIndexProperties.StartsWith(properties)) || (unique && coveringIndexUnique && coveringIndexProperties.SequenceEqual(properties)); private static void RemoveIndex(IConventionIndex index) diff --git a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs index 298eb968efb..f92e934b0b4 100644 --- a/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs +++ b/src/EFCore/Metadata/Conventions/Infrastructure/ProviderConventionSetBuilder.cs @@ -91,6 +91,7 @@ public virtual ConventionSet CreateConventionSet() conventionSet.Add(new ConstructorBindingConvention(Dependencies)); conventionSet.Add(new KeyAttributeConvention(Dependencies)); conventionSet.Add(new IndexAttributeConvention(Dependencies)); + conventionSet.Add(new KeyComplexPropertyConvention(Dependencies)); conventionSet.Add(new ForeignKeyIndexConvention(Dependencies)); conventionSet.Add(new ForeignKeyPropertyDiscoveryConvention(Dependencies)); conventionSet.Add(new NonNullableReferencePropertyConvention(Dependencies)); diff --git a/src/EFCore/Metadata/Conventions/KeyComplexPropertyConvention.cs b/src/EFCore/Metadata/Conventions/KeyComplexPropertyConvention.cs new file mode 100644 index 00000000000..fc5b048a267 --- /dev/null +++ b/src/EFCore/Metadata/Conventions/KeyComplexPropertyConvention.cs @@ -0,0 +1,137 @@ +// 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.Metadata.Internal; + +namespace Microsoft.EntityFrameworkCore.Metadata.Conventions; + +/// +/// A convention that marks every complex property in the path to a property used by a key as +/// required, since the value of such a property must be available in order to compute the key value. +/// +/// +/// +/// When the last referencing key is removed, the implicit non-nullable configuration applied +/// here is reverted as long as no other key keeps the chain in use and no explicit configuration +/// was applied by the user. +/// +/// +/// See Model building conventions for more information and examples. +/// +/// +public class KeyComplexPropertyConvention : + IKeyAddedConvention, + IKeyRemovedConvention +{ + /// + /// Creates a new instance of . + /// + /// Parameter object containing dependencies for this convention. + public KeyComplexPropertyConvention(ProviderConventionSetBuilderDependencies dependencies) + => Dependencies = dependencies; + + /// + /// Dependencies for this service. + /// + protected virtual ProviderConventionSetBuilderDependencies Dependencies { get; } + + /// + public virtual void ProcessKeyAdded( + IConventionKeyBuilder keyBuilder, + IConventionContext context) + => MarkChainAsRequired(keyBuilder.Metadata.Properties); + + /// + public virtual void ProcessKeyRemoved( + IConventionEntityTypeBuilder entityTypeBuilder, + IConventionKey key, + IConventionContext context) + => TryRevertChain(key.Properties); + + private static void MarkChainAsRequired(IReadOnlyList properties) + { + foreach (var property in properties) + { + if (property.DeclaringType is not IConventionComplexType) + { + continue; + } + + // Walk the complex-property chain from the entity down to the property's declaring complex type + // and configure each link as required by convention. + // TODO: Use layering #15898 - the implicit "required because referenced by a key" state + // should be tracked separately so it can be cleanly reverted without round-tripping through + // the convention configuration source. + foreach (var complexProperty in EnumerateChain(property)) + { + complexProperty.Builder.IsRequired(true); + } + } + } + + private static void TryRevertChain(IReadOnlyList properties) + { + foreach (var property in properties) + { + if (property.DeclaringType is not IConventionComplexType) + { + continue; + } + + foreach (var complexProperty in EnumerateChain(property)) + { + if (complexProperty.GetIsNullableConfigurationSource() != ConfigurationSource.Convention) + { + continue; + } + + if (IsStillReferenced(complexProperty)) + { + continue; + } + + // TODO: Use layering #15898 - we currently can't distinguish a Convention-set IsRequired + // that was applied because of a key from one applied for any other reason, so we + // only revert when no key in the entity references any property declared on the + // complex type (or any of its nested complex types). + complexProperty.Builder.IsRequired(null); + } + } + } + + private static IEnumerable EnumerateChain(IConventionPropertyBase property) + { + var typeBase = property.DeclaringType; + while (typeBase is IConventionComplexType complexType) + { + yield return (IConventionComplexProperty)complexType.ComplexProperty; + typeBase = complexType.ComplexProperty.DeclaringType; + } + } + + private static bool IsStillReferenced(IConventionComplexProperty complexProperty) + { + var entityType = complexProperty.DeclaringType.ContainingEntityType; + var coveredTypes = new HashSet(); + CollectCoveredTypes(complexProperty.ComplexType, coveredTypes); + + foreach (var key in entityType.GetKeys()) + { + if (key.Properties.Any(p => coveredTypes.Contains(p.DeclaringType))) + { + return true; + } + } + + return false; + } + + private static void CollectCoveredTypes(IReadOnlyComplexType complexType, HashSet coveredTypes) + { + coveredTypes.Add(complexType); + foreach (var nested in complexType.GetComplexProperties()) + { + CollectCoveredTypes(nested.ComplexType, coveredTypes); + } + } +} diff --git a/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs b/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs index 30fc69b6a23..b249d96a023 100644 --- a/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs +++ b/src/EFCore/Metadata/Conventions/RuntimeModelConvention.cs @@ -610,7 +610,46 @@ protected virtual void ProcessComplexTypeAnnotations( } private static RuntimeKey Create(IKey key, RuntimeEntityType runtimeEntityType) - => runtimeEntityType.AddKey(runtimeEntityType.FindProperties(key.Properties.Select(p => p.Name))!); + => runtimeEntityType.AddKey( + key.Properties.Select(p => FindRuntimeProperty(runtimeEntityType, p)).ToArray()); + + private static RuntimeProperty FindRuntimeProperty(RuntimeEntityType runtimeEntityType, IProperty property) + => (RuntimeProperty)FindRuntimePropertyBase(runtimeEntityType, property); + + private static RuntimePropertyBase FindRuntimePropertyBase(RuntimeEntityType runtimeEntityType, IPropertyBase property) + { + if (property.DeclaringType is IEntityType) + { + if (property is IComplexProperty) + { + return (RuntimePropertyBase)runtimeEntityType.FindComplexProperty(property.Name)!; + } + + return runtimeEntityType.FindProperty(property.Name)!; + } + + // Build the chain of complex property names from entity down to the property's declaring type. + var chain = new List(); + var typeBase = (IReadOnlyTypeBase)property.DeclaringType; + while (typeBase is IReadOnlyComplexType complexType) + { + chain.Insert(0, complexType.ComplexProperty.Name); + typeBase = complexType.ComplexProperty.DeclaringType; + } + + ITypeBase currentType = runtimeEntityType; + foreach (var complexPropertyName in chain) + { + currentType = currentType.FindComplexProperty(complexPropertyName)!.ComplexType; + } + + if (property is IComplexProperty) + { + return (RuntimePropertyBase)currentType.FindComplexProperty(property.Name)!; + } + + return (RuntimePropertyBase)currentType.FindProperty(property.Name)!; + } /// /// Updates the key annotations that will be set on the read-only object. @@ -639,7 +678,7 @@ protected virtual void ProcessKeyAnnotations( private static RuntimeIndex Create(IIndex index, RuntimeEntityType runtimeEntityType) => runtimeEntityType.AddIndex( - runtimeEntityType.FindProperties(index.Properties.Select(p => p.Name))!, + index.Properties.Select(p => FindRuntimePropertyBase(runtimeEntityType, p)).ToArray(), index.Name, index.IsUnique); diff --git a/src/EFCore/Metadata/IConventionEntityType.cs b/src/EFCore/Metadata/IConventionEntityType.cs index 16a7512ed65..6e0f27accfb 100644 --- a/src/EFCore/Metadata/IConventionEntityType.cs +++ b/src/EFCore/Metadata/IConventionEntityType.cs @@ -621,7 +621,7 @@ public interface IConventionEntityType : IReadOnlyEntityType, IConventionTypeBas /// The property to be indexed. /// Indicates whether the configuration was specified using a data annotation. /// The newly created index. - IConventionIndex? AddIndex(IConventionProperty property, bool fromDataAnnotation = false) + IConventionIndex? AddIndex(IConventionPropertyBase property, bool fromDataAnnotation = false) => AddIndex([property], fromDataAnnotation); /// @@ -630,7 +630,7 @@ public interface IConventionEntityType : IReadOnlyEntityType, IConventionTypeBas /// The properties that are to be indexed. /// Indicates whether the configuration was specified using a data annotation. /// The newly created index. - IConventionIndex? AddIndex(IReadOnlyList properties, bool fromDataAnnotation = false); + IConventionIndex? AddIndex(IReadOnlyList properties, bool fromDataAnnotation = false); /// /// Adds a named index to this entity type. @@ -640,7 +640,7 @@ public interface IConventionEntityType : IReadOnlyEntityType, IConventionTypeBas /// Indicates whether the configuration was specified using a data annotation. /// The newly created index. IConventionIndex? AddIndex( - IConventionProperty property, + IConventionPropertyBase property, string name, bool fromDataAnnotation = false) => AddIndex([property], name, fromDataAnnotation); @@ -653,7 +653,7 @@ public interface IConventionEntityType : IReadOnlyEntityType, IConventionTypeBas /// Indicates whether the configuration was specified using a data annotation. /// The newly created index. IConventionIndex? AddIndex( - IReadOnlyList properties, + IReadOnlyList properties, string name, bool fromDataAnnotation = false); @@ -665,7 +665,7 @@ public interface IConventionEntityType : IReadOnlyEntityType, IConventionTypeBas /// /// The property to find the index on. /// The index, or if none is found. - new IConventionIndex? FindIndex(IReadOnlyProperty property) + new IConventionIndex? FindIndex(IReadOnlyPropertyBase property) => FindIndex([property]); /// @@ -676,7 +676,7 @@ public interface IConventionEntityType : IReadOnlyEntityType, IConventionTypeBas /// /// The properties to find the index on. /// The index, or if none is found. - new IConventionIndex? FindIndex(IReadOnlyList properties); + new IConventionIndex? FindIndex(IReadOnlyList properties); /// /// Gets the index with the given name. Returns if no such index exists. @@ -715,7 +715,7 @@ public interface IConventionEntityType : IReadOnlyEntityType, IConventionTypeBas /// /// The properties that make up the index. /// The index that was removed. - IConventionIndex? RemoveIndex(IReadOnlyList properties); + IConventionIndex? RemoveIndex(IReadOnlyList properties); /// /// Removes an index from this entity type. diff --git a/src/EFCore/Metadata/IConventionIndex.cs b/src/EFCore/Metadata/IConventionIndex.cs index ccad73d3b7e..525a160fc30 100644 --- a/src/EFCore/Metadata/IConventionIndex.cs +++ b/src/EFCore/Metadata/IConventionIndex.cs @@ -26,7 +26,7 @@ public interface IConventionIndex : IReadOnlyIndex, IConventionAnnotatable /// /// Gets the properties that this index is defined on. /// - new IReadOnlyList Properties { get; } + new IReadOnlyList Properties { get; } /// /// Gets the entity type the index is defined on. This may be different from the type that diff --git a/src/EFCore/Metadata/IEntityType.cs b/src/EFCore/Metadata/IEntityType.cs index c4a0487a481..592e84b4f24 100644 --- a/src/EFCore/Metadata/IEntityType.cs +++ b/src/EFCore/Metadata/IEntityType.cs @@ -380,7 +380,7 @@ IEnumerable GetNavigationsInHierarchy() /// /// The properties to find the index on. /// The index, or if none is found. - new IIndex? FindIndex(IReadOnlyList properties); + new IIndex? FindIndex(IReadOnlyList properties); /// /// Gets the index with the given name. Returns if no such index exists. @@ -394,7 +394,7 @@ IEnumerable GetNavigationsInHierarchy() /// /// The property to find the index on. /// The index, or if none is found. - new IIndex? FindIndex(IReadOnlyProperty property) + new IIndex? FindIndex(IReadOnlyPropertyBase property) => FindIndex([property]); /// diff --git a/src/EFCore/Metadata/IIndex.cs b/src/EFCore/Metadata/IIndex.cs index 50bb417d988..4067a7f7d4e 100644 --- a/src/EFCore/Metadata/IIndex.cs +++ b/src/EFCore/Metadata/IIndex.cs @@ -14,7 +14,7 @@ public interface IIndex : IReadOnlyIndex, IAnnotatable /// /// Gets the properties that this index is defined on. /// - new IReadOnlyList Properties { get; } + new IReadOnlyList Properties { get; } /// /// Gets the entity type the index is defined on. This may be different from the type that diff --git a/src/EFCore/Metadata/IMutableEntityType.cs b/src/EFCore/Metadata/IMutableEntityType.cs index ca7449a17db..f78a28861a3 100644 --- a/src/EFCore/Metadata/IMutableEntityType.cs +++ b/src/EFCore/Metadata/IMutableEntityType.cs @@ -575,7 +575,7 @@ IMutableSkipNavigation AddSkipNavigation( /// /// The property to be indexed. /// The newly created index. - IMutableIndex AddIndex(IMutableProperty property) + IMutableIndex AddIndex(IMutablePropertyBase property) => AddIndex([property]); /// @@ -583,7 +583,7 @@ IMutableIndex AddIndex(IMutableProperty property) /// /// The properties that are to be indexed. /// The newly created index. - IMutableIndex AddIndex(IReadOnlyList properties); + IMutableIndex AddIndex(IReadOnlyList properties); /// /// Adds a named index to this entity type. @@ -591,7 +591,7 @@ IMutableIndex AddIndex(IMutableProperty property) /// The property to be indexed. /// The name of the index. /// The newly created index. - IMutableIndex AddIndex(IMutableProperty property, string name) + IMutableIndex AddIndex(IMutablePropertyBase property, string name) => AddIndex([property], name); /// @@ -600,14 +600,14 @@ IMutableIndex AddIndex(IMutableProperty property, string name) /// The properties that are to be indexed. /// The name of the index. /// The newly created index. - IMutableIndex AddIndex(IReadOnlyList properties, string name); + IMutableIndex AddIndex(IReadOnlyList properties, string name); /// /// Gets the index defined on the given property. Returns if no index is defined. /// /// The property to find the index on. /// The index, or if none is found. - new IMutableIndex? FindIndex(IReadOnlyProperty property) + new IMutableIndex? FindIndex(IReadOnlyPropertyBase property) => FindIndex([property]); /// @@ -618,7 +618,7 @@ IMutableIndex AddIndex(IMutableProperty property, string name) /// /// The properties to find the index on. /// The index, or if none is found. - new IMutableIndex? FindIndex(IReadOnlyList properties); + new IMutableIndex? FindIndex(IReadOnlyList properties); /// /// Gets the index with the given name. Returns if no such index exists. @@ -657,7 +657,7 @@ IMutableIndex AddIndex(IMutableProperty property, string name) /// /// The properties that make up the index. /// The removed index, or if the index was not found. - IMutableIndex? RemoveIndex(IReadOnlyList properties); + IMutableIndex? RemoveIndex(IReadOnlyList properties); /// /// Removes an index from this entity type. diff --git a/src/EFCore/Metadata/IMutableIndex.cs b/src/EFCore/Metadata/IMutableIndex.cs index bd77f85679f..42b3bbcda92 100644 --- a/src/EFCore/Metadata/IMutableIndex.cs +++ b/src/EFCore/Metadata/IMutableIndex.cs @@ -31,7 +31,7 @@ public interface IMutableIndex : IReadOnlyIndex, IMutableAnnotatable /// /// Gets the properties that this index is defined on. /// - new IReadOnlyList Properties { get; } + new IReadOnlyList Properties { get; } /// /// Gets the entity type the index is defined on. This may be different from the type that diff --git a/src/EFCore/Metadata/IReadOnlyEntityType.cs b/src/EFCore/Metadata/IReadOnlyEntityType.cs index d376eeb9e01..e64c551b777 100644 --- a/src/EFCore/Metadata/IReadOnlyEntityType.cs +++ b/src/EFCore/Metadata/IReadOnlyEntityType.cs @@ -523,7 +523,7 @@ bool IsInOwnershipPath(IReadOnlyEntityType targetType) /// /// The properties to find the index on. /// The index, or if none is found. - IReadOnlyIndex? FindIndex(IReadOnlyList properties); + IReadOnlyIndex? FindIndex(IReadOnlyList properties); /// /// Gets the index with the given name. Returns if no such index exists. @@ -540,7 +540,7 @@ bool IsInOwnershipPath(IReadOnlyEntityType targetType) /// /// The property to find the index on. /// The index, or if none is found. - IReadOnlyIndex? FindIndex(IReadOnlyProperty property) + IReadOnlyIndex? FindIndex(IReadOnlyPropertyBase property) => FindIndex([property]); /// diff --git a/src/EFCore/Metadata/IReadOnlyIndex.cs b/src/EFCore/Metadata/IReadOnlyIndex.cs index e353e3234f0..00dd1721849 100644 --- a/src/EFCore/Metadata/IReadOnlyIndex.cs +++ b/src/EFCore/Metadata/IReadOnlyIndex.cs @@ -16,7 +16,7 @@ public interface IReadOnlyIndex : IReadOnlyAnnotatable /// /// Gets the properties that this index is defined on. /// - IReadOnlyList Properties { get; } + IReadOnlyList Properties { get; } /// /// Gets the name of this index. diff --git a/src/EFCore/Metadata/Internal/ComplexProperty.cs b/src/EFCore/Metadata/Internal/ComplexProperty.cs index 7587b3ccc65..c5e324da293 100644 --- a/src/EFCore/Metadata/Internal/ComplexProperty.cs +++ b/src/EFCore/Metadata/Internal/ComplexProperty.cs @@ -82,7 +82,7 @@ public virtual InternalComplexPropertyBuilder Builder /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual bool IsInModel + public override bool IsInModel => _builder is not null && DeclaringType.IsInModel; diff --git a/src/EFCore/Metadata/Internal/ComplexPropertyNameComparer.cs b/src/EFCore/Metadata/Internal/ComplexPropertyNameComparer.cs new file mode 100644 index 00000000000..35fa899725f --- /dev/null +++ b/src/EFCore/Metadata/Internal/ComplexPropertyNameComparer.cs @@ -0,0 +1,92 @@ +// 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.Metadata.Internal; + +/// +/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to +/// the same compatibility standards as public APIs. It may be changed or removed without notice in +/// any release. You should only use it directly in your code with extreme caution and knowing that +/// doing so can result in application failures when updating to a new Entity Framework Core release. +/// +public sealed class ComplexPropertyNameComparer : IComparer, IEqualityComparer +{ + private readonly IReadOnlyTypeBase _typeBase; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public ComplexPropertyNameComparer(IReadOnlyTypeBase typeBase) + => _typeBase = typeBase; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public int Compare(string? x, string? y) + { + if (StringComparer.Ordinal.Equals(x, y)) + { + return 0; + } + + var primaryKeyProperties = _typeBase.ContainingEntityType?.FindPrimaryKey()?.Properties; + if (primaryKeyProperties is { Count: > 0 }) + { + var xContainsKey = x != null && ContainsKeyProperty(x, primaryKeyProperties); + var yContainsKey = y != null && ContainsKeyProperty(y, primaryKeyProperties); + + if (xContainsKey != yContainsKey) + { + return xContainsKey ? -1 : 1; + } + } + + return StringComparer.Ordinal.Compare(x, y); + } + + private bool ContainsKeyProperty( + string complexPropertyName, + IReadOnlyList primaryKeyProperties) + { + for (var i = 0; i < primaryKeyProperties.Count; i++) + { + var keyProperty = primaryKeyProperties[i]; + for (var current = keyProperty.DeclaringType as IReadOnlyComplexType; + current != null; + current = current.ComplexProperty.DeclaringType as IReadOnlyComplexType) + { + if (current.ComplexProperty.DeclaringType == _typeBase + && current.ComplexProperty.Name == complexPropertyName) + { + return true; + } + } + } + + return false; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public bool Equals(string? x, string? y) + => StringComparer.Ordinal.Equals(x, y); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public int GetHashCode(string obj) + => StringComparer.Ordinal.GetHashCode(obj); +} diff --git a/src/EFCore/Metadata/Internal/ComplexType.cs b/src/EFCore/Metadata/Internal/ComplexType.cs index 91c29431ce0..3b11450d51b 100644 --- a/src/EFCore/Metadata/Internal/ComplexType.cs +++ b/src/EFCore/Metadata/Internal/ComplexType.cs @@ -89,13 +89,8 @@ protected override InternalTypeBaseBuilder BaseBuilder /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual EntityType ContainingEntityType - => ComplexProperty.DeclaringType switch - { - EntityType entityType => entityType, - ComplexType declaringComplexType => declaringComplexType.ContainingEntityType, - _ => throw new NotImplementedException() - }; + public override EntityType ContainingEntityType + => ComplexProperty.DeclaringType.ContainingEntityType; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/Metadata/Internal/EntityType.cs b/src/EFCore/Metadata/Internal/EntityType.cs index a5b15b86883..5ea8f1cc458 100644 --- a/src/EFCore/Metadata/Internal/EntityType.cs +++ b/src/EFCore/Metadata/Internal/EntityType.cs @@ -28,7 +28,7 @@ private readonly SortedDictionary _skipNavigations private readonly SortedDictionary _serviceProperties = new(StringComparer.Ordinal); - private readonly SortedDictionary, Index> _unnamedIndexes + private readonly SortedDictionary, Index> _unnamedIndexes = new(PropertyListComparer.Instance); private readonly SortedDictionary _namedIndexes @@ -479,6 +479,18 @@ private bool InheritsFrom(EntityType entityType) public new virtual EntityType GetRootType() => (EntityType)((IReadOnlyTypeBase)this).GetRootType(); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public override EntityType ContainingEntityType + { + [DebuggerStepThrough] + get => this; + } + /// /// Runs the conventions when an annotation was set or removed. /// @@ -599,34 +611,30 @@ public override IEnumerable FindMembersInHierarchy(string name) if (oldPrimaryKey != null) { + DetachKeyProperties(oldPrimaryKey.Properties); + foreach (var property in oldPrimaryKey.Properties) { - Properties.Remove(property.Name); property.PrimaryKey = null; } _primaryKey = null; - foreach (var property in oldPrimaryKey.Properties) - { - Properties.Add(property.Name, property); - } + ReattachKeyPropertiesInOrder(oldPrimaryKey.Properties); } if (properties?.Count > 0 && newKey != null) { + DetachKeyProperties(newKey.Properties); + foreach (var property in newKey.Properties) { - Properties.Remove(property.Name); property.PrimaryKey = newKey; } _primaryKey = newKey; - foreach (var property in newKey.Properties) - { - Properties.Add(property.Name, property); - } + ReattachKeyPropertiesInOrder(newKey.Properties); UpdatePrimaryKeyConfigurationSource(configurationSource); } @@ -638,6 +646,55 @@ public override IEnumerable FindMembersInHierarchy(string name) return (Key?)Model.ConventionDispatcher.OnPrimaryKeyChanged(Builder, newKey, oldPrimaryKey); } + private void DetachKeyProperties(IReadOnlyList properties) + { + foreach (var property in properties) + { + property.DeclaringType.Properties.Remove(property.Name); + } + + var visitedComplexProps = new HashSet(); + foreach (var property in properties) + { + for (var current = property.DeclaringType as ComplexType; + current != null; + current = current.ComplexProperty.DeclaringType as ComplexType) + { + if (!visitedComplexProps.Add(current.ComplexProperty)) + { + break; + } + + current.ComplexProperty.DeclaringType.ComplexProperties.Remove(current.ComplexProperty.Name); + } + } + } + + private void ReattachKeyPropertiesInOrder(IReadOnlyList properties) + { + foreach (var property in properties) + { + property.DeclaringType.Properties.Add(property.Name, property); + } + + var visitedComplexProps = new HashSet(); + foreach (var property in properties) + { + for (var current = property.DeclaringType as ComplexType; + current != null; + current = current.ComplexProperty.DeclaringType as ComplexType) + { + if (!visitedComplexProps.Add(current.ComplexProperty)) + { + break; + } + + current.ComplexProperty.DeclaringType.ComplexProperties.Add( + current.ComplexProperty.Name, current.ComplexProperty); + } + } + } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -741,7 +798,9 @@ private void UpdatePrimaryKeyConfigurationSource(ConfigurationSource configurati } } - if (FindProperty(property.Name) != property + if ((property.DeclaringType is EntityType + ? FindProperty(property.Name) != property + : property.DeclaringType.ContainingEntityType != this) || !property.IsInModel) { throw new InvalidOperationException(CoreStrings.KeyPropertiesWrongEntity(properties.Format(), DisplayName())); @@ -1914,7 +1973,7 @@ public virtual IEnumerable GetDerivedReferencingSkipNavigations( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual Index? AddIndex( - IReadOnlyList properties, + IReadOnlyList properties, ConfigurationSource configurationSource) { Check.NotEmpty(properties); @@ -1943,7 +2002,7 @@ public virtual IEnumerable GetDerivedReferencingSkipNavigations( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual Index? AddIndex( - IReadOnlyList properties, + IReadOnlyList properties, string name, ConfigurationSource configurationSource) { @@ -1971,7 +2030,7 @@ public virtual IEnumerable GetDerivedReferencingSkipNavigations( return (Index?)Model.ConventionDispatcher.OnIndexAdded(index.Builder)?.Metadata; } - private static void UpdatePropertyIndexes(IReadOnlyList properties, Index index) + private static void UpdatePropertyIndexes(IReadOnlyList properties, Index index) { foreach (var property in properties) { @@ -2001,7 +2060,7 @@ private static void UpdatePropertyIndexes(IReadOnlyList properties, In /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual Index? FindIndex(IReadOnlyList properties) + public virtual Index? FindIndex(IReadOnlyList properties) { Check.HasNoNulls(properties); Check.NotEmpty(properties); @@ -2050,7 +2109,7 @@ public virtual IEnumerable GetDerivedIndexes() /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual Index? FindDeclaredIndex(IReadOnlyList properties) + public virtual Index? FindDeclaredIndex(IReadOnlyList properties) => _unnamedIndexes.GetValueOrDefault(Check.NotEmpty(properties)); /// @@ -2068,7 +2127,7 @@ public virtual IEnumerable GetDerivedIndexes() /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual IEnumerable FindDerivedIndexes(IReadOnlyList properties) + public virtual IEnumerable FindDerivedIndexes(IReadOnlyList properties) => DirectlyDerivedTypes.Count == 0 ? [] : (IEnumerable)GetDerivedTypes() @@ -2086,14 +2145,13 @@ public virtual IEnumerable FindDerivedIndexes(string name) : (IEnumerable)GetDerivedTypes() .Select(et => et.FindDeclaredIndex(Check.NotEmpty(name))) .Where(i => i != null); - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual IEnumerable FindIndexesInHierarchy(IReadOnlyList properties) + public virtual IEnumerable FindIndexesInHierarchy(IReadOnlyList properties) => DirectlyDerivedTypes.Count == 0 ? ToEnumerable(FindIndex(properties)) : ToEnumerable(FindIndex(properties)).Concat(FindDerivedIndexes(properties)); @@ -2115,7 +2173,7 @@ public virtual IEnumerable FindIndexesInHierarchy(string name) /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual Index? RemoveIndex(IReadOnlyList properties) + public virtual Index? RemoveIndex(IReadOnlyList properties) { Check.NotEmpty(properties); @@ -3858,8 +3916,8 @@ IEnumerable IEntityType.GetProperties() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerStepThrough] - IMutableIndex IMutableEntityType.AddIndex(IReadOnlyList properties) - => AddIndex(properties as IReadOnlyList ?? properties.Cast().ToList(), ConfigurationSource.Explicit)!; + IMutableIndex IMutableEntityType.AddIndex(IReadOnlyList properties) + => AddIndex(properties as IReadOnlyList ?? properties.Cast().ToList(), ConfigurationSource.Explicit)!; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -3868,8 +3926,8 @@ IMutableIndex IMutableEntityType.AddIndex(IReadOnlyList proper /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerStepThrough] - IMutableIndex IMutableEntityType.AddIndex(IReadOnlyList properties, string name) - => AddIndex(properties as IReadOnlyList ?? properties.Cast().ToList(), name, ConfigurationSource.Explicit)!; + IMutableIndex IMutableEntityType.AddIndex(IReadOnlyList properties, string name) + => AddIndex(properties as IReadOnlyList ?? properties.Cast().ToList(), name, ConfigurationSource.Explicit)!; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -3878,9 +3936,9 @@ IMutableIndex IMutableEntityType.AddIndex(IReadOnlyList proper /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerStepThrough] - IConventionIndex? IConventionEntityType.AddIndex(IReadOnlyList properties, bool fromDataAnnotation) + IConventionIndex? IConventionEntityType.AddIndex(IReadOnlyList properties, bool fromDataAnnotation) => AddIndex( - properties as IReadOnlyList ?? properties.Cast().ToList(), + properties as IReadOnlyList ?? properties.Cast().ToList(), fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); /// @@ -3891,11 +3949,11 @@ IMutableIndex IMutableEntityType.AddIndex(IReadOnlyList proper /// [DebuggerStepThrough] IConventionIndex? IConventionEntityType.AddIndex( - IReadOnlyList properties, + IReadOnlyList properties, string name, bool fromDataAnnotation) => AddIndex( - properties as IReadOnlyList ?? properties.Cast().ToList(), + properties as IReadOnlyList ?? properties.Cast().ToList(), name, fromDataAnnotation ? ConfigurationSource.DataAnnotation : ConfigurationSource.Convention); @@ -3906,7 +3964,7 @@ IMutableIndex IMutableEntityType.AddIndex(IReadOnlyList proper /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerStepThrough] - IReadOnlyIndex? IReadOnlyEntityType.FindIndex(IReadOnlyList properties) + IReadOnlyIndex? IReadOnlyEntityType.FindIndex(IReadOnlyList properties) => FindIndex(properties); /// @@ -3916,7 +3974,7 @@ IMutableIndex IMutableEntityType.AddIndex(IReadOnlyList proper /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerStepThrough] - IMutableIndex? IMutableEntityType.FindIndex(IReadOnlyList properties) + IMutableIndex? IMutableEntityType.FindIndex(IReadOnlyList properties) => FindIndex(properties); /// @@ -3926,7 +3984,7 @@ IMutableIndex IMutableEntityType.AddIndex(IReadOnlyList proper /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerStepThrough] - IConventionIndex? IConventionEntityType.FindIndex(IReadOnlyList properties) + IConventionIndex? IConventionEntityType.FindIndex(IReadOnlyList properties) => FindIndex(properties); /// @@ -3936,7 +3994,7 @@ IMutableIndex IMutableEntityType.AddIndex(IReadOnlyList proper /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerStepThrough] - IIndex? IEntityType.FindIndex(IReadOnlyList properties) + IIndex? IEntityType.FindIndex(IReadOnlyList properties) => FindIndex(properties); /// @@ -4066,7 +4124,7 @@ IEnumerable IEntityType.GetIndexes() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerStepThrough] - IConventionIndex? IConventionEntityType.RemoveIndex(IReadOnlyList properties) + IConventionIndex? IConventionEntityType.RemoveIndex(IReadOnlyList properties) => RemoveIndex(properties); /// @@ -4076,7 +4134,7 @@ IEnumerable IEntityType.GetIndexes() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [DebuggerStepThrough] - IMutableIndex? IMutableEntityType.RemoveIndex(IReadOnlyList properties) + IMutableIndex? IMutableEntityType.RemoveIndex(IReadOnlyList properties) => RemoveIndex(properties); /// diff --git a/src/EFCore/Metadata/Internal/Index.cs b/src/EFCore/Metadata/Internal/Index.cs index 5e6dde29540..9202f980563 100644 --- a/src/EFCore/Metadata/Internal/Index.cs +++ b/src/EFCore/Metadata/Internal/Index.cs @@ -33,7 +33,7 @@ public class Index : ConventionAnnotatable, IMutableIndex, IConventionIndex, IIn /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public Index( - IReadOnlyList properties, + IReadOnlyList properties, EntityType declaringEntityType, ConfigurationSource configurationSource) { @@ -48,7 +48,10 @@ public Index( } } - if (declaringEntityType.FindProperty(property.Name) != property + if ((property.DeclaringType is EntityType + ? declaringEntityType.FindProperty(property.Name) != property + && declaringEntityType.FindComplexProperty(property.Name) != property + : property.DeclaringType.ContainingEntityType != declaringEntityType) || !property.IsInModel) { throw new InvalidOperationException(CoreStrings.IndexPropertiesWrongEntity(properties.Format(), declaringEntityType.DisplayName())); @@ -69,7 +72,7 @@ public Index( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public Index( - IReadOnlyList properties, + IReadOnlyList properties, string name, EntityType declaringEntityType, ConfigurationSource configurationSource) @@ -82,7 +85,7 @@ public Index( /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual IReadOnlyList Properties { [DebuggerStepThrough] get; } + public virtual IReadOnlyList Properties { [DebuggerStepThrough] get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -317,7 +320,23 @@ public virtual IDependentKeyValueFactory GetNullableValueFactory() ref _nullableValueFactory, this, static index => { index.EnsureReadOnly(); - return new CompositeValueFactory(index.Properties); + + var properties = new List(index.Properties.Count); + foreach (var property in index.Properties) + { + if (property is IComplexProperty complexProperty) + { + throw new InvalidOperationException( + CoreStrings.IndexValueFactoryWithComplexProperty( + index.Properties.Format(), + index.DeclaringEntityType.DisplayName(), + complexProperty.Name)); + } + + properties.Add((IProperty)property); + } + + return new CompositeValueFactory(properties); }); /// @@ -356,7 +375,7 @@ public virtual string DisplayName() /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - IReadOnlyList IReadOnlyIndex.Properties + IReadOnlyList IReadOnlyIndex.Properties { [DebuggerStepThrough] get => Properties; @@ -380,7 +399,7 @@ IReadOnlyEntityType IReadOnlyIndex.DeclaringEntityType /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - IReadOnlyList IMutableIndex.Properties + IReadOnlyList IMutableIndex.Properties { [DebuggerStepThrough] get => Properties; @@ -428,7 +447,7 @@ IConventionAnnotatableBuilder IConventionAnnotatable.Builder /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - IReadOnlyList IConventionIndex.Properties + IReadOnlyList IConventionIndex.Properties { [DebuggerStepThrough] get => Properties; @@ -440,7 +459,7 @@ IReadOnlyList IConventionIndex.Properties /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - IReadOnlyList IIndex.Properties + IReadOnlyList IIndex.Properties { [DebuggerStepThrough] get => Properties; diff --git a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs index b7f507e130d..a167820f377 100644 --- a/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalEntityTypeBuilder.cs @@ -56,6 +56,17 @@ public InternalEntityTypeBuilder(EntityType metadata, InternalModelBuilder model ConfigurationSource configurationSource) => PrimaryKey(GetOrCreateProperties(clrMembers, configurationSource), configurationSource); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalKeyBuilder? PrimaryKey( + IReadOnlyList>? memberChains, + ConfigurationSource configurationSource) + => PrimaryKey(GetOrCreateProperties(memberChains, configurationSource), configurationSource); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -229,6 +240,15 @@ public virtual bool CanSetPrimaryKey( public virtual InternalKeyBuilder? HasKey(IReadOnlyList clrMembers, ConfigurationSource configurationSource) => HasKeyInternal(GetOrCreateProperties(clrMembers, configurationSource), configurationSource); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalKeyBuilder? HasKey(IReadOnlyList> memberChains, ConfigurationSource configurationSource) + => HasKeyInternal(GetOrCreateProperties(memberChains, configurationSource), configurationSource); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -1621,9 +1641,10 @@ public virtual bool CanSetQueryFilter(QueryFilter queryFilter) var shouldBeDetached = false; foreach (var property in index.Properties) { - if (removedInheritedProperties.Contains(property)) + if (property is Property primitive + && removedInheritedProperties.Contains(primitive)) { - removedInheritedPropertiesToDuplicate.Add(property); + removedInheritedPropertiesToDuplicate.Add(primitive); shouldBeDetached = true; } } @@ -2082,7 +2103,7 @@ private void RemoveKeyIfUnused(Key key, ConfigurationSource configurationSource /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual InternalIndexBuilder? HasIndex(IReadOnlyList propertyNames, ConfigurationSource configurationSource) - => HasIndex(GetOrCreateProperties(propertyNames, configurationSource), configurationSource); + => HasIndex(ToPropertyBaseList(GetOrCreateProperties(propertyNames, configurationSource)), configurationSource); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2094,7 +2115,7 @@ private void RemoveKeyIfUnused(Key key, ConfigurationSource configurationSource IReadOnlyList propertyNames, string name, ConfigurationSource configurationSource) - => HasIndex(GetOrCreateProperties(propertyNames, configurationSource), name, configurationSource); + => HasIndex(ToPropertyBaseList(GetOrCreateProperties(propertyNames, configurationSource)), name, configurationSource); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2105,7 +2126,18 @@ private void RemoveKeyIfUnused(Key key, ConfigurationSource configurationSource public virtual InternalIndexBuilder? HasIndex( IReadOnlyList clrMembers, ConfigurationSource configurationSource) - => HasIndex(GetOrCreateProperties(clrMembers, configurationSource), configurationSource); + => HasIndex(ToPropertyBaseList(GetOrCreateProperties(clrMembers, configurationSource)), configurationSource); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalIndexBuilder? HasIndex( + IReadOnlyList> memberChains, + ConfigurationSource configurationSource) + => HasIndex(GetOrCreatePropertyBases(memberChains, configurationSource), configurationSource); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2117,7 +2149,7 @@ private void RemoveKeyIfUnused(Key key, ConfigurationSource configurationSource IReadOnlyList clrMembers, string name, ConfigurationSource configurationSource) - => HasIndex(GetOrCreateProperties(clrMembers, configurationSource), name, configurationSource); + => HasIndex(ToPropertyBaseList(GetOrCreateProperties(clrMembers, configurationSource)), name, configurationSource); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2126,7 +2158,19 @@ private void RemoveKeyIfUnused(Key key, ConfigurationSource configurationSource /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual InternalIndexBuilder? HasIndex( - IReadOnlyList? properties, + IReadOnlyList> memberChains, + string name, + ConfigurationSource configurationSource) + => HasIndex(GetOrCreatePropertyBases(memberChains, configurationSource), name, configurationSource); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalIndexBuilder? HasIndex( + IReadOnlyList? properties, ConfigurationSource configurationSource) { if (properties == null) @@ -2165,7 +2209,7 @@ private void RemoveKeyIfUnused(Key key, ConfigurationSource configurationSource /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual InternalIndexBuilder? HasIndex( - IReadOnlyList? properties, + IReadOnlyList? properties, string name, ConfigurationSource configurationSource) { @@ -2212,7 +2256,7 @@ private void RemoveKeyIfUnused(Key key, ConfigurationSource configurationSource private InternalIndexBuilder? HasIndex( Index? index, - IReadOnlyList properties, + IReadOnlyList properties, string? name, ConfigurationSource configurationSource) { @@ -2230,6 +2274,32 @@ private void RemoveKeyIfUnused(Key key, ConfigurationSource configurationSource return index?.Builder; } + private static IReadOnlyList? ToPropertyBaseList(IReadOnlyList? properties) + => properties == null ? null : (IReadOnlyList)properties.Cast().ToList(); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalIndexBuilder? HasIndex( + IReadOnlyList? properties, + ConfigurationSource configurationSource) + => HasIndex(ToPropertyBaseList(properties), configurationSource); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual InternalIndexBuilder? HasIndex( + IReadOnlyList? properties, + string name, + ConfigurationSource configurationSource) + => HasIndex(ToPropertyBaseList(properties), name, configurationSource); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -2276,7 +2346,7 @@ public virtual bool CanHaveIndex( : Metadata.RemoveIndex(index.Name); Check.DebugAssert(removedIndex == index, "removedIndex != index"); - RemoveUnusedImplicitProperties(index.Properties); + RemoveUnusedImplicitProperties(index.Properties.OfType().ToList()); return this; } diff --git a/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs b/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs index a25af2b88c6..dffa4522d5b 100644 --- a/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs +++ b/src/EFCore/Metadata/Internal/InternalTypeBaseBuilder.cs @@ -547,7 +547,11 @@ public virtual (bool, IReadOnlyList?) TryCreateUniqueProperties( for (var i = 0; i < propertyNames.Count; i++) { var propertyName = propertyNames[i]; - var property = Metadata.FindProperty(propertyName); + + var resolved = ResolveComplexChainByName(propertyName); + var typeBuilder = resolved.Builder; + var leafName = resolved.FinalName; + var property = typeBuilder.Metadata.FindProperty(leafName); if (property == null) { var type = referencedProperties == null @@ -561,11 +565,11 @@ public virtual (bool, IReadOnlyList?) TryCreateUniqueProperties( return null; } - var propertyBuilder = Property( + var propertyBuilder = typeBuilder.Property( required ? type : type?.MakeNullable(), - propertyName, + leafName, typeConfigurationSource: null, configurationSource.Value); @@ -631,6 +635,76 @@ public virtual (bool, IReadOnlyList?) TryCreateUniqueProperties( return list; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IReadOnlyList? GetOrCreateProperties( + IReadOnlyList>? memberChains, + ConfigurationSource? configurationSource) + { + if (memberChains == null) + { + return null; + } + + var list = new List(memberChains.Count); + foreach (var memberChain in memberChains) + { + var (ownerBuilder, finalMember) = ResolveComplexChain(memberChain); + var propertyBuilder = ownerBuilder.Property(finalMember, configurationSource); + if (propertyBuilder == null) + { + return null; + } + + list.Add(propertyBuilder.Metadata); + } + + return list; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IReadOnlyList? GetOrCreatePropertyBases( + IReadOnlyList>? memberChains, + ConfigurationSource? configurationSource) + { + if (memberChains == null) + { + return null; + } + + var list = new List(memberChains.Count); + foreach (var memberChain in memberChains) + { + var (ownerBuilder, finalMember) = ResolveComplexChain(memberChain); + var existing = ownerBuilder.Metadata.FindMembersInHierarchy(finalMember.GetSimpleMemberName()) + .FirstOrDefault(); + if (existing is ComplexProperty complexProperty) + { + list.Add(complexProperty); + continue; + } + + var propertyBuilder = ownerBuilder.Property(finalMember, configurationSource); + if (propertyBuilder == null) + { + return null; + } + + list.Add(propertyBuilder.Metadata); + } + + return list; + } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -640,6 +714,36 @@ public virtual (bool, IReadOnlyList?) TryCreateUniqueProperties( public virtual IReadOnlyList? GetActualProperties( IReadOnlyList? properties, ConfigurationSource? configurationSource) + { + var actual = GetActualProperties((IReadOnlyList?)properties, configurationSource); + if (actual == null) + { + return null; + } + + if (actual is IReadOnlyList typed) + { + return typed; + } + + var result = new Property[actual.Count]; + for (var i = 0; i < result.Length; i++) + { + result[i] = (Property)actual[i]; + } + + return result; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IReadOnlyList? GetActualProperties( + IReadOnlyList? properties, + ConfigurationSource? configurationSource) { if (properties == null) { @@ -653,8 +757,7 @@ public virtual (bool, IReadOnlyList?) TryCreateUniqueProperties( for (var i = 0;; i++) { - var property = properties[i]; - if (!property.IsInModel || !property.DeclaringType.IsAssignableFrom(Metadata)) + if (!IsActual(properties[i])) { break; } @@ -665,14 +768,38 @@ public virtual (bool, IReadOnlyList?) TryCreateUniqueProperties( } } - var actualProperties = new Property[properties.Count]; + var actualProperties = new PropertyBase[properties.Count]; for (var i = 0; i < actualProperties.Length; i++) { - var property = properties[i]; + var member = properties[i]; + if (member is not Property property) + { + if (!IsActual(member)) + { + var resolved = Metadata.FindMembersInHierarchy(member.Name) + .FirstOrDefault(m => m.GetType() == member.GetType()); + if (resolved == null) + { + return null; + } + + member = resolved; + } + + actualProperties[i] = member; + continue; + } + var typeConfigurationSource = property.GetTypeConfigurationSource(); - var builder = Property( + var typeBuilder = property.IsInModel + && property.DeclaringType is ComplexType ownerComplex + && ownerComplex.ContainingEntityType.IsAssignableFrom(Metadata) + ? ownerComplex.Builder + : this; + + var builder = typeBuilder.Property( typeConfigurationSource.Overrides(ConfigurationSource.DataAnnotation) - || (property.IsInModel && Metadata.IsAssignableFrom(property.DeclaringType)) + || (property.IsInModel && typeBuilder.Metadata.IsAssignableFrom(property.DeclaringType)) ? property.ClrType : null, property.Name, @@ -691,6 +818,12 @@ public virtual (bool, IReadOnlyList?) TryCreateUniqueProperties( return actualProperties; } + private bool IsActual(PropertyBase property) + => ((IConventionPropertyBase)property).IsInModel + && (property.DeclaringType.IsAssignableFrom(Metadata) + || (property.DeclaringType is ComplexType complexType + && complexType.ContainingEntityType.IsAssignableFrom(Metadata))); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore/Metadata/Internal/Key.cs b/src/EFCore/Metadata/Internal/Key.cs index 83626e3f0f3..23d7097e20a 100644 --- a/src/EFCore/Metadata/Internal/Key.cs +++ b/src/EFCore/Metadata/Internal/Key.cs @@ -52,7 +52,7 @@ public Key(IReadOnlyList properties, ConfigurationSource configuration public virtual EntityType DeclaringEntityType { [DebuggerStepThrough] - get => (EntityType)Properties[0].DeclaringType; + get => Properties[0].DeclaringType.ContainingEntityType; } /// diff --git a/src/EFCore/Metadata/Internal/Navigation.cs b/src/EFCore/Metadata/Internal/Navigation.cs index 8166df3a019..d90d4b718c9 100644 --- a/src/EFCore/Metadata/Internal/Navigation.cs +++ b/src/EFCore/Metadata/Internal/Navigation.cs @@ -84,7 +84,7 @@ public virtual InternalNavigationBuilder Builder /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual bool IsInModel + public override bool IsInModel => _builder is not null && ForeignKey.IsInModel; diff --git a/src/EFCore/Metadata/Internal/Property.cs b/src/EFCore/Metadata/Internal/Property.cs index a77954aa53b..5f6c0ae2dd8 100644 --- a/src/EFCore/Metadata/Internal/Property.cs +++ b/src/EFCore/Metadata/Internal/Property.cs @@ -75,7 +75,7 @@ public virtual InternalPropertyBuilder Builder /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual bool IsInModel + public override bool IsInModel => _builder is not null && DeclaringType.IsInModel; @@ -1539,32 +1539,6 @@ public virtual bool IsForeignKey() public virtual IEnumerable GetContainingForeignKeys() => ForeignKeys?.OrderBy(fk => fk, ForeignKeyComparer.Instance) ?? Enumerable.Empty(); - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual List? Indexes { get; set; } - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual bool IsIndex() - => Indexes != null; - - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual IEnumerable GetContainingIndexes() - => Indexes?.OrderBy(i => i.Properties, PropertyListComparer.Instance) ?? Enumerable.Empty(); - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore/Metadata/Internal/PropertyBase.cs b/src/EFCore/Metadata/Internal/PropertyBase.cs index b277ecab38a..3fc3837eff0 100644 --- a/src/EFCore/Metadata/Internal/PropertyBase.cs +++ b/src/EFCore/Metadata/Internal/PropertyBase.cs @@ -67,6 +67,14 @@ protected PropertyBase( public override bool IsReadOnly => DeclaringType.Model.IsReadOnly; + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public abstract bool IsInModel { get; } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -105,6 +113,32 @@ public virtual FieldInfo? FieldInfo /// public abstract bool IsCollection { get; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual List? Indexes { get; set; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool IsIndex() + => Indexes != null; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IEnumerable GetContainingIndexes() + => Indexes?.OrderBy(i => i.Properties, PropertyListComparer.Instance) ?? Enumerable.Empty(); + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore/Metadata/Internal/PropertyListComparer.cs b/src/EFCore/Metadata/Internal/PropertyListComparer.cs index f8a91b27d8a..3fbe0867d75 100644 --- a/src/EFCore/Metadata/Internal/PropertyListComparer.cs +++ b/src/EFCore/Metadata/Internal/PropertyListComparer.cs @@ -10,8 +10,8 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal; /// doing so can result in application failures when updating to a new Entity Framework Core release. /// // Sealed for perf -public sealed class PropertyListComparer : IComparer>, - IEqualityComparer> +public sealed class PropertyListComparer : IComparer>, + IEqualityComparer> { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -31,7 +31,7 @@ private PropertyListComparer() /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public int Compare(IReadOnlyList? x, IReadOnlyList? y) + public int Compare(IReadOnlyList? x, IReadOnlyList? y) { if (ReferenceEquals(x, y)) { @@ -59,6 +59,11 @@ public int Compare(IReadOnlyList? x, IReadOnlyList? x, IReadOnlyList - public bool Equals(IReadOnlyList? x, IReadOnlyList? y) + public bool Equals(IReadOnlyList? x, IReadOnlyList? y) => Compare(x, y) == 0; /// @@ -80,12 +85,13 @@ public bool Equals(IReadOnlyList? x, IReadOnlyList - public int GetHashCode(IReadOnlyList obj) + public int GetHashCode(IReadOnlyList obj) { var hash = new HashCode(); for (var i = 0; i < obj.Count; i++) { hash.Add(obj[i].Name); + hash.Add(obj[i].DeclaringType.Name); } return hash.ToHashCode(); diff --git a/src/EFCore/Metadata/Internal/PropertyNameComparer.cs b/src/EFCore/Metadata/Internal/PropertyNameComparer.cs index f22dacee4ea..6f906845597 100644 --- a/src/EFCore/Metadata/Internal/PropertyNameComparer.cs +++ b/src/EFCore/Metadata/Internal/PropertyNameComparer.cs @@ -11,7 +11,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal; /// public sealed class PropertyNameComparer : IComparer, IEqualityComparer { - private readonly IReadOnlyEntityType? _entityType; + private readonly IReadOnlyTypeBase _typeBase; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -20,7 +20,7 @@ public sealed class PropertyNameComparer : IComparer, IEqualityComparer< /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public PropertyNameComparer(IReadOnlyTypeBase typeBase) - => _entityType = typeBase as IReadOnlyEntityType; + => _typeBase = typeBase; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -33,12 +33,18 @@ public int Compare(string? x, string? y) var xIndex = -1; var yIndex = -1; - var properties = _entityType?.FindPrimaryKey()?.Properties; + var properties = _typeBase.ContainingEntityType?.FindPrimaryKey()?.Properties; if (properties != null) { for (var i = 0; i < properties.Count; i++) { - var name = properties[i].Name; + var keyProperty = properties[i]; + if (keyProperty.DeclaringType != _typeBase) + { + continue; + } + + var name = keyProperty.Name; if (name == x) { diff --git a/src/EFCore/Metadata/Internal/ServiceProperty.cs b/src/EFCore/Metadata/Internal/ServiceProperty.cs index b25e3fcc1a3..72c5e447c86 100644 --- a/src/EFCore/Metadata/Internal/ServiceProperty.cs +++ b/src/EFCore/Metadata/Internal/ServiceProperty.cs @@ -86,7 +86,7 @@ public virtual InternalServicePropertyBuilder Builder /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual bool IsInModel + public override bool IsInModel => _builder is not null && DeclaringEntityType.IsInModel; diff --git a/src/EFCore/Metadata/Internal/SkipNavigation.cs b/src/EFCore/Metadata/Internal/SkipNavigation.cs index bb3aac745b2..c5fb4cf508d 100644 --- a/src/EFCore/Metadata/Internal/SkipNavigation.cs +++ b/src/EFCore/Metadata/Internal/SkipNavigation.cs @@ -92,7 +92,7 @@ public virtual InternalSkipNavigationBuilder Builder /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual bool IsInModel + public override bool IsInModel => _builder is not null && DeclaringEntityType.IsInModel; diff --git a/src/EFCore/Metadata/Internal/TypeBase.cs b/src/EFCore/Metadata/Internal/TypeBase.cs index 3b3fe513e32..33fd5296817 100644 --- a/src/EFCore/Metadata/Internal/TypeBase.cs +++ b/src/EFCore/Metadata/Internal/TypeBase.cs @@ -16,7 +16,7 @@ namespace Microsoft.EntityFrameworkCore.Metadata.Internal; public abstract class TypeBase : ConventionAnnotatable, IMutableTypeBase, IConventionTypeBase, IRuntimeTypeBase { private readonly SortedDictionary _properties; - private readonly SortedDictionary _complexProperties = new(StringComparer.Ordinal); + private readonly SortedDictionary _complexProperties; private readonly Dictionary _ignoredMembers = new(StringComparer.Ordinal); private TypeBase? _baseType; @@ -59,6 +59,7 @@ protected TypeBase( HasSharedClrType = false; IsPropertyBag = type.IsPropertyBagType(); _properties = new SortedDictionary(new PropertyNameComparer(this)); + _complexProperties = new SortedDictionary(new ComplexPropertyNameComparer(this)); } /// @@ -80,6 +81,7 @@ protected TypeBase( HasSharedClrType = true; IsPropertyBag = type.IsPropertyBagType(); _properties = new SortedDictionary(new PropertyNameComparer(this)); + _complexProperties = new SortedDictionary(new ComplexPropertyNameComparer(this)); } /// @@ -288,6 +290,14 @@ public virtual bool IsAssignableFrom(TypeBase derivedType) public virtual TypeBase GetRootType() => (TypeBase)((IReadOnlyTypeBase)this).GetRootType(); + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public abstract EntityType ContainingEntityType { [DebuggerStepThrough] get; } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -901,9 +911,18 @@ public virtual IEnumerable GetFlattenedValueGeneratingProperties() /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - protected virtual SortedDictionary Properties + protected internal virtual SortedDictionary Properties => _properties; + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + protected internal virtual SortedDictionary ComplexProperties + => _complexProperties; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore/Metadata/RuntimeEntityType.cs b/src/EFCore/Metadata/RuntimeEntityType.cs index 3cca9c22f8c..9bdf6e7e41c 100644 --- a/src/EFCore/Metadata/RuntimeEntityType.cs +++ b/src/EFCore/Metadata/RuntimeEntityType.cs @@ -21,7 +21,7 @@ public class RuntimeEntityType : RuntimeTypeBase, IRuntimeEntityType private readonly Utilities.OrderedDictionary _navigations; private Utilities.OrderedDictionary? _skipNavigations; private Utilities.OrderedDictionary? _serviceProperties; - private readonly Utilities.OrderedDictionary, RuntimeIndex> _unnamedIndexes; + private readonly Utilities.OrderedDictionary, RuntimeIndex> _unnamedIndexes; private Utilities.OrderedDictionary? _namedIndexes; private readonly Utilities.OrderedDictionary, RuntimeKey> _keys; private Utilities.OrderedDictionary? _triggers; @@ -88,7 +88,7 @@ public RuntimeEntityType( } _unnamedIndexes = - new Utilities.OrderedDictionary, RuntimeIndex>( + new Utilities.OrderedDictionary, RuntimeIndex>( unnamedIndexCount, PropertyListComparer.Instance); if (namedIndexCount > 0) { @@ -532,7 +532,7 @@ public virtual IEnumerable FindSkipNavigationsInHierarchy /// A value indicating whether the values assigned to the indexed properties are unique. /// The newly created index. public virtual RuntimeIndex AddIndex( - IReadOnlyList properties, + IReadOnlyList properties, string? name = null, bool unique = false) { @@ -569,7 +569,7 @@ public virtual RuntimeIndex AddIndex( /// /// The properties to find the index on. /// The index, or if none is found. - public virtual RuntimeIndex? FindIndex(IReadOnlyList properties) + public virtual RuntimeIndex? FindIndex(IReadOnlyList properties) => _unnamedIndexes.TryGetValue(properties, out var index) ? index : BaseType?.FindIndex(properties); @@ -1200,12 +1200,12 @@ IEnumerable IEntityType.GetSkipNavigations() /// [DebuggerStepThrough] - IReadOnlyIndex? IReadOnlyEntityType.FindIndex(IReadOnlyList properties) + IReadOnlyIndex? IReadOnlyEntityType.FindIndex(IReadOnlyList properties) => FindIndex(properties); /// [DebuggerStepThrough] - IIndex? IEntityType.FindIndex(IReadOnlyList properties) + IIndex? IEntityType.FindIndex(IReadOnlyList properties) => FindIndex(properties); /// diff --git a/src/EFCore/Metadata/RuntimeIndex.cs b/src/EFCore/Metadata/RuntimeIndex.cs index f0631916ad5..5fc710b6c92 100644 --- a/src/EFCore/Metadata/RuntimeIndex.cs +++ b/src/EFCore/Metadata/RuntimeIndex.cs @@ -27,7 +27,7 @@ public class RuntimeIndex : RuntimeAnnotatableBase, IIndex /// [EntityFrameworkInternal] public RuntimeIndex( - IReadOnlyList properties, + IReadOnlyList properties, RuntimeEntityType declaringEntityType, string? name, bool unique) @@ -41,7 +41,7 @@ public RuntimeIndex( /// /// Gets the properties that this index is defined on. /// - public virtual IReadOnlyList Properties { get; } + public virtual IReadOnlyList Properties { get; } /// /// Gets the name of this index. @@ -84,7 +84,7 @@ public virtual DebugView DebugView () => ((IIndex)this).ToDebugString(MetadataDebugStringOptions.LongDefault)); /// - IReadOnlyList IReadOnlyIndex.Properties + IReadOnlyList IReadOnlyIndex.Properties { [DebuggerStepThrough] get => Properties; @@ -98,7 +98,7 @@ IReadOnlyEntityType IReadOnlyIndex.DeclaringEntityType } /// - IReadOnlyList IIndex.Properties + IReadOnlyList IIndex.Properties { [DebuggerStepThrough] get => Properties; @@ -122,5 +122,23 @@ bool IReadOnlyIndex.IsUnique [DebuggerStepThrough] IDependentKeyValueFactory IIndex.GetNullableValueFactory() => (IDependentKeyValueFactory)NonCapturingLazyInitializer.EnsureInitialized( - ref _nullableValueFactory, this, static index => new CompositeValueFactory(index.Properties)); + ref _nullableValueFactory, this, static index => + { + var properties = new List(index.Properties.Count); + foreach (var property in index.Properties) + { + if (property is IComplexProperty complexProperty) + { + throw new InvalidOperationException( + CoreStrings.IndexValueFactoryWithComplexProperty( + index.Properties.Format(), + ((IReadOnlyEntityType)index.DeclaringEntityType).DisplayName(), + complexProperty.Name)); + } + + properties.Add((IProperty)property); + } + + return new CompositeValueFactory(properties); + }); } diff --git a/src/EFCore/Metadata/RuntimeKey.cs b/src/EFCore/Metadata/RuntimeKey.cs index 90e3c14d2e0..4929418a905 100644 --- a/src/EFCore/Metadata/RuntimeKey.cs +++ b/src/EFCore/Metadata/RuntimeKey.cs @@ -42,7 +42,7 @@ public RuntimeKey(IReadOnlyList properties) public virtual RuntimeEntityType DeclaringEntityType { [DebuggerStepThrough] - get => (RuntimeEntityType)Properties[0].DeclaringType; + get => (RuntimeEntityType)((IReadOnlyTypeBase)Properties[0].DeclaringType).ContainingEntityType; } /// diff --git a/src/EFCore/Metadata/RuntimeProperty.cs b/src/EFCore/Metadata/RuntimeProperty.cs index 94f69f49c93..c508c376d4a 100644 --- a/src/EFCore/Metadata/RuntimeProperty.cs +++ b/src/EFCore/Metadata/RuntimeProperty.cs @@ -225,8 +225,6 @@ private IEnumerable GetContainingForeignKeys() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] - public virtual List? Indexes { get; set; } - private IEnumerable GetContainingIndexes() => Indexes ?? Enumerable.Empty(); diff --git a/src/EFCore/Metadata/RuntimePropertyBase.cs b/src/EFCore/Metadata/RuntimePropertyBase.cs index 3cfc93221cd..f84446ea90a 100644 --- a/src/EFCore/Metadata/RuntimePropertyBase.cs +++ b/src/EFCore/Metadata/RuntimePropertyBase.cs @@ -89,6 +89,15 @@ PropertyAccessMode IReadOnlyPropertyBase.GetPropertyAccessMode() /// public abstract bool IsCollection { get; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + [EntityFrameworkInternal] + public virtual List? Indexes { get; set; } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 7f65927b402..1ffe01d3bc6 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -1711,6 +1711,30 @@ public static string IncorrectNumberOfArguments(object? method, object? argument GetString("IncorrectNumberOfArguments", nameof(method), nameof(argumentCount), nameof(parameterCount)), method, argumentCount, parameterCount); + /// + /// The index {indexProperties} on the entity type '{entityType}' cannot be configured because it traverses the complex collection '{property}'. Indexes cannot reference properties whose path goes through a complex collection. + /// + public static string IndexOnComplexCollection(object? indexProperties, object? entityType, object? property) + => string.Format( + GetString("IndexOnComplexCollection", nameof(indexProperties), nameof(entityType), nameof(property)), + indexProperties, entityType, property); + + /// + /// The index {indexProperties} on the entity type '{entityType}' cannot be configured because it is defined on the complex property '{property}'. Indexes are not supported on complex properties. + /// + public static string IndexOnComplexProperty(object? indexProperties, object? entityType, object? property) + => string.Format( + GetString("IndexOnComplexProperty", nameof(indexProperties), nameof(entityType), nameof(property)), + indexProperties, entityType, property); + + /// + /// A value factory cannot be created for the index {indexProperties} on the entity type '{entityType}' because it contains the complex property '{property}'. Index value factories are not supported for indexes that contain complex properties. + /// + public static string IndexValueFactoryWithComplexProperty(object? indexProperties, object? entityType, object? property) + => string.Format( + GetString("IndexValueFactoryWithComplexProperty", nameof(indexProperties), nameof(entityType), nameof(property)), + indexProperties, entityType, property); + /// /// The specified index properties {indexProperties} are not declared on the entity type '{entityType}'. Ensure that index properties are declared on the target entity type. /// @@ -1719,6 +1743,14 @@ public static string IndexPropertiesWrongEntity(object? indexProperties, object? GetString("IndexPropertiesWrongEntity", nameof(indexProperties), nameof(entityType)), indexProperties, entityType); + /// + /// The index property '{property}' on the entity type '{entityType}' cannot be configured because it is not a scalar property or a complex property. Only scalar properties and complex properties can be referenced by an index. + /// + public static string IndexPropertyMustBePropertyOrComplexProperty(object? property, object? entityType) + => string.Format( + GetString("IndexPropertyMustBePropertyOrComplexProperty", nameof(property), nameof(entityType)), + property, entityType); + /// /// The index {index} cannot be removed from the entity type '{entityType}' because it is defined on the entity type '{otherEntityType}'. /// @@ -2041,6 +2073,22 @@ public static string KeylessTypeWithKey(object? keyProperties, object? entityTyp GetString("KeylessTypeWithKey", nameof(keyProperties), nameof(entityType)), keyProperties, entityType); + /// + /// The key {keyProperties} on the entity type '{entityType}' cannot be configured because it traverses the complex collection '{property}'. Keys cannot reference properties whose path goes through a complex collection. + /// + public static string KeyOnComplexCollection(object? keyProperties, object? entityType, object? property) + => string.Format( + GetString("KeyOnComplexCollection", nameof(keyProperties), nameof(entityType), nameof(property)), + keyProperties, entityType, property); + + /// + /// The key {keyProperties} on the entity type '{entityType}' cannot be configured because the complex property '{property}' it traverses is configured as optional. Mark the complex property as required in order to use one of its properties as part of a key. + /// + public static string KeyOnNullableComplexProperty(object? keyProperties, object? entityType, object? property) + => string.Format( + GetString("KeyOnNullableComplexProperty", nameof(keyProperties), nameof(entityType), nameof(property)), + keyProperties, entityType, property); + /// /// The specified key properties {keyProperties} are not declared on the entity type '{entityType}'. Ensure key properties are declared on the target entity type. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index ba84a1aebd1..c2665c3bc1f 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -763,9 +763,21 @@ '{method}' was invoked with {argumentCount} arguments, but has {parameterCount} parameters. + + The index {indexProperties} on the entity type '{entityType}' cannot be configured because it traverses the complex collection '{property}'. Indexes cannot reference properties whose path goes through a complex collection. + + + The index {indexProperties} on the entity type '{entityType}' cannot be configured because it is defined on the complex property '{property}'. Indexes are not supported on complex properties. + + + A value factory cannot be created for the index {indexProperties} on the entity type '{entityType}' because it contains the complex property '{property}'. Index value factories are not supported for indexes that contain complex properties. + The specified index properties {indexProperties} are not declared on the entity type '{entityType}'. Ensure that index properties are declared on the target entity type. + + The index property '{property}' on the entity type '{entityType}' cannot be configured because it is not a scalar property or a complex property. Only scalar properties and complex properties can be referenced by an index. + The index {index} cannot be removed from the entity type '{entityType}' because it is defined on the entity type '{otherEntityType}'. @@ -889,6 +901,12 @@ The key {keyProperties} cannot be added to keyless type '{entityType}'. + + The key {keyProperties} on the entity type '{entityType}' cannot be configured because it traverses the complex collection '{property}'. Keys cannot reference properties whose path goes through a complex collection. + + + The key {keyProperties} on the entity type '{entityType}' cannot be configured because the complex property '{property}' it traverses is configured as optional. Mark the complex property as required in order to use one of its properties as part of a key. + The specified key properties {keyProperties} are not declared on the entity type '{entityType}'. Ensure key properties are declared on the target entity type. diff --git a/test/EFCore.Cosmos.FunctionalTests/ModelBuilding/CosmosModelBuilderGenericTest.cs b/test/EFCore.Cosmos.FunctionalTests/ModelBuilding/CosmosModelBuilderGenericTest.cs index 6560246328a..c5a07fbfc59 100644 --- a/test/EFCore.Cosmos.FunctionalTests/ModelBuilding/CosmosModelBuilderGenericTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/ModelBuilding/CosmosModelBuilderGenericTest.cs @@ -759,6 +759,30 @@ public override void Properties_can_have_provider_type_set() public override void Complex_properties_can_be_configured_by_type() => base.Complex_properties_can_be_configured_by_type(); + public override void Can_define_index_on_complex_property_via_lambda() + => Assert.Equal( + CosmosStrings.IndexesExist(nameof(ComplexProperties), "Up"), + Assert.Throws( + base.Can_define_index_on_complex_property_via_lambda).Message); + + public override void Can_define_composite_index_mixing_entity_and_complex_property_via_lambda() + => Assert.Equal( + CosmosStrings.IndexesExist(nameof(ComplexProperties), "Id,Up"), + Assert.Throws( + base.Can_define_composite_index_mixing_entity_and_complex_property_via_lambda).Message); + + public override void Can_define_index_on_complex_property_via_string_dotted_path() + => Assert.Equal( + CosmosStrings.IndexesExist(nameof(ComplexProperties), "Up"), + Assert.Throws( + base.Can_define_index_on_complex_property_via_string_dotted_path).Message); + + public override void Index_on_complex_property_marks_chain_as_required() + => Assert.Equal( + CosmosStrings.IndexesExist(nameof(ComplexProperties), "Up"), + Assert.Throws( + base.Index_on_complex_property_marks_chain_as_required).Message); + public override void Can_set_complex_property_annotation() { var modelBuilder = CreateModelBuilder(); diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs index 502e8227722..7ee83188406 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/AdHocComplexTypeQueryCosmosTest.cs @@ -221,6 +221,36 @@ protected override DbContextOptionsBuilder AddNonSharedOptions(DbContextOptionsB => base.AddNonSharedOptions(builder) .ConfigureWarnings(w => w.Ignore(CosmosEventId.NoPartitionKeyDefined)); + public override async Task Can_query_by_complex_type_property_with_index() + => Assert.Equal( + CosmosStrings.IndexesExist("Person", "PostalCode"), + (await Assert.ThrowsAsync( + base.Can_query_by_complex_type_property_with_index)).Message); + + public override async Task Can_update_entity_with_index_on_complex_type_property() + => Assert.Equal( + CosmosStrings.IndexesExist("Person", "PostalCode"), + (await Assert.ThrowsAsync( + base.Can_update_entity_with_index_on_complex_type_property)).Message); + + public override async Task Can_delete_entity_with_index_on_complex_type_property() + => Assert.Equal( + CosmosStrings.IndexesExist("Person", "PostalCode"), + (await Assert.ThrowsAsync( + base.Can_delete_entity_with_index_on_complex_type_property)).Message); + + public override async Task Can_query_by_alternate_key_on_complex_type_property() + => Assert.Equal( + CosmosStrings.IndexesExist("Person", "PostalCode"), + (await Assert.ThrowsAsync( + base.Can_query_by_alternate_key_on_complex_type_property)).Message); + + public override async Task Can_save_batch_swapping_alternate_key_values_on_complex_type_property() + => Assert.Equal( + CosmosStrings.IndexesExist("Person", "PostalCode"), + (await Assert.ThrowsAsync( + base.Can_save_batch_swapping_alternate_key_values_on_complex_type_property)).Message); + [ConditionalFact] public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); diff --git a/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/ComplexTypes/DependentBaseUnsafeAccessors.cs b/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/ComplexTypes/DependentBaseUnsafeAccessors.cs new file mode 100644 index 00000000000..ae6e1779611 --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/ComplexTypes/DependentBaseUnsafeAccessors.cs @@ -0,0 +1,15 @@ +// +using System.Runtime.CompilerServices; +using Microsoft.EntityFrameworkCore.Scaffolding; + +#pragma warning disable 219, 612, 618 +#nullable disable + +namespace TestNamespace +{ + public static class DependentBaseUnsafeAccessors + { + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "k__BackingField")] + public static extern ref TKey Id(CompiledModelTestBase.DependentBase @this); + } +} diff --git a/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedEntityType.cs b/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedEntityType.cs index a96688aec7c..11c7598aa8d 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedEntityType.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedEntityType.cs @@ -34,12 +34,119 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas discriminatorProperty: "$type", discriminatorValue: "PrincipalDerived", propertyCount: 0, - complexPropertyCount: 1); + complexPropertyCount: 2); + DependentComplexProperty.Create(runtimeEntityType); ManyOwnedComplexProperty.Create(runtimeEntityType); return runtimeEntityType; } + public static class DependentComplexProperty + { + public static RuntimeComplexProperty Create(RuntimeEntityType declaringType) + { + var complexProperty = declaringType.AddComplexProperty("Dependent", + typeof(CompiledModelTestBase.DependentBase), + "Microsoft.EntityFrameworkCore.Scaffolding.CompiledModelTestBase+PrincipalDerived>.Dependent#DependentBase", + typeof(CompiledModelTestBase.DependentBase), + propertyInfo: typeof(CompiledModelTestBase.PrincipalDerived>).GetProperty("Dependent", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(CompiledModelTestBase.PrincipalDerived>).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true, + propertyCount: 1); + + var complexType = complexProperty.ComplexType; + complexProperty.SetGetter( + CompiledModelTestBase.DependentBase (CompiledModelTestBase.PrincipalDerived> instance) => PrincipalDerivedUnsafeAccessors>.Dependent(instance), + bool (CompiledModelTestBase.PrincipalDerived> instance) => PrincipalDerivedUnsafeAccessors>.Dependent(instance) == null); + complexProperty.SetSetter( + CompiledModelTestBase.PrincipalDerived> (CompiledModelTestBase.PrincipalDerived> instance, CompiledModelTestBase.DependentBase value) => + { + PrincipalDerivedUnsafeAccessors>.Dependent(instance) = value; + return instance; + }); + complexProperty.SetMaterializationSetter( + CompiledModelTestBase.PrincipalDerived> (CompiledModelTestBase.PrincipalDerived> instance, CompiledModelTestBase.DependentBase value) => + { + PrincipalDerivedUnsafeAccessors>.Dependent(instance) = value; + return instance; + }); + complexProperty.SetAccessors( + CompiledModelTestBase.DependentBase (IInternalEntry entry) => PrincipalDerivedUnsafeAccessors>.Dependent(((CompiledModelTestBase.PrincipalDerived>)(entry.Entity))), + CompiledModelTestBase.DependentBase (IInternalEntry entry) => PrincipalDerivedUnsafeAccessors>.Dependent(((CompiledModelTestBase.PrincipalDerived>)(entry.Entity))), + null, + CompiledModelTestBase.DependentBase (IInternalEntry entry) => entry.GetCurrentValue>(complexProperty)); + complexProperty.SetPropertyIndexes( + index: 2, + originalValueIndex: 36, + shadowIndex: -1, + relationshipIndex: -1, + storeGenerationIndex: -1); + var id = complexType.AddProperty( + "Id", + typeof(byte?), + propertyInfo: typeof(CompiledModelTestBase.DependentBase).GetProperty("Id", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(CompiledModelTestBase.DependentBase).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + id.SetGetter( + byte? (CompiledModelTestBase.PrincipalDerived> entity, IReadOnlyList indices) => (PrincipalDerivedUnsafeAccessors>.Dependent(entity) == null ? default(byte? ) : DependentBaseUnsafeAccessors.Id(PrincipalDerivedUnsafeAccessors>.Dependent(entity))), + bool (CompiledModelTestBase.PrincipalDerived> entity, IReadOnlyList indices) => !((PrincipalDerivedUnsafeAccessors>.Dependent(entity) == null ? default(byte? ) : DependentBaseUnsafeAccessors.Id(PrincipalDerivedUnsafeAccessors>.Dependent(entity))).HasValue), + byte? (CompiledModelTestBase.DependentBase instance) => DependentBaseUnsafeAccessors.Id(instance), + bool (CompiledModelTestBase.DependentBase instance) => !(DependentBaseUnsafeAccessors.Id(instance).HasValue)); + id.SetSetter( + (CompiledModelTestBase.PrincipalDerived> entity, IReadOnlyList indices, byte? value) => + { + var level1 = PrincipalDerivedUnsafeAccessors>.Dependent(entity); + DependentBaseUnsafeAccessors.Id(level1) = value; + }, + CompiledModelTestBase.DependentBase (CompiledModelTestBase.DependentBase instance, byte? value) => + { + DependentBaseUnsafeAccessors.Id(instance) = value; + return instance; + }); + id.SetMaterializationSetter( + (CompiledModelTestBase.PrincipalDerived> entity, IReadOnlyList indices, byte? value) => + { + var level1 = PrincipalDerivedUnsafeAccessors>.Dependent(entity); + DependentBaseUnsafeAccessors.Id(level1) = value; + }, + CompiledModelTestBase.DependentBase (CompiledModelTestBase.DependentBase instance, byte? value) => + { + DependentBaseUnsafeAccessors.Id(instance) = value; + return instance; + }); + id.SetAccessors( + byte? (IInternalEntry entry) => (PrincipalDerivedUnsafeAccessors>.Dependent(((CompiledModelTestBase.PrincipalDerived>)(entry.Entity))) == null ? default(byte? ) : DependentBaseUnsafeAccessors.Id(PrincipalDerivedUnsafeAccessors>.Dependent(((CompiledModelTestBase.PrincipalDerived>)(entry.Entity))))), + byte? (IInternalEntry entry) => (PrincipalDerivedUnsafeAccessors>.Dependent(((CompiledModelTestBase.PrincipalDerived>)(entry.Entity))) == null ? default(byte? ) : DependentBaseUnsafeAccessors.Id(PrincipalDerivedUnsafeAccessors>.Dependent(((CompiledModelTestBase.PrincipalDerived>)(entry.Entity))))), + byte? (IInternalEntry entry) => entry.ReadOriginalValue(id, 37), + byte? (IInternalEntry entry) => entry.GetCurrentValue(id)); + id.SetPropertyIndexes( + index: 35, + originalValueIndex: 37, + shadowIndex: -1, + relationshipIndex: -1, + storeGenerationIndex: -1); + id.TypeMapping = CosmosTypeMapping.Default.Clone( + comparer: new ValueComparer( + bool (byte v1, byte v2) => v1 == v2, + int (byte v) => ((int)v), + byte (byte v) => v), + keyComparer: new ValueComparer( + bool (byte v1, byte v2) => v1 == v2, + int (byte v) => ((int)v), + byte (byte v) => v), + providerValueComparer: new ValueComparer( + bool (byte v1, byte v2) => v1 == v2, + int (byte v) => ((int)v), + byte (byte v) => v), + clrType: typeof(byte), + jsonValueReaderWriter: JsonByteReaderWriter.Instance); + id.SetComparer(new NullableValueComparer(id.TypeMapping.Comparer)); + id.SetKeyComparer(new NullableValueComparer(id.TypeMapping.KeyComparer)); + + return complexProperty; + } + } + public static class ManyOwnedComplexProperty { public static RuntimeComplexProperty Create(RuntimeEntityType declaringType) @@ -83,7 +190,7 @@ public static RuntimeComplexProperty Create(RuntimeEntityType declaringType) IList (IInternalEntry entry) => entry.GetCurrentValue>(complexProperty)); complexProperty.SetPropertyIndexes( index: 0, - originalValueIndex: 36, + originalValueIndex: 38, shadowIndex: -1, relationshipIndex: -1, storeGenerationIndex: -1); @@ -1878,6 +1985,9 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) var valueTypeEnumerable1 = principalBase.FindProperty("ValueTypeEnumerable"); var valueTypeIList1 = principalBase.FindProperty("ValueTypeIList"); var valueTypeList1 = principalBase.FindProperty("ValueTypeList"); + var dependent = runtimeEntityType.FindComplexProperty("Dependent"); + var dependentBasebyte = dependent.ComplexType; + var id1 = dependentBasebyte.FindProperty("Id"); var manyOwned = runtimeEntityType.FindComplexProperty("ManyOwned"); var ownedCollection = manyOwned.ComplexType; var details0 = ownedCollection.FindProperty("Details"); @@ -1895,7 +2005,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) var enum21 = principalBase0.FindProperty("Enum2"); var flagsEnum11 = principalBase0.FindProperty("FlagsEnum1"); var flagsEnum21 = principalBase0.FindProperty("FlagsEnum2"); - var id1 = principalBase0.FindProperty("Id"); + var id2 = principalBase0.FindProperty("Id"); var refTypeEnumerable3 = principalBase0.FindProperty("RefTypeEnumerable"); var refTypeIList3 = principalBase0.FindProperty("RefTypeIList"); var valueTypeArray3 = principalBase0.FindProperty("ValueTypeArray"); @@ -1909,7 +2019,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) var structuralType1 = ((CompiledModelTestBase.PrincipalDerived>)(source.Entity)); var liftedArg0 = ((ISnapshot)(new Snapshot, IList, DateTime[], IEnumerable, IList, List, string, JObject, string, int, IEnumerable, IList, DateTime[], IEnumerable, IList, List, object, Guid, CompiledModelTestBase.AnEnum, CompiledModelTestBase.AnEnum?, CompiledModelTestBase.AFlagsEnum, CompiledModelTestBase.AFlagsEnum, long?>((source.GetCurrentValue(id) == null ? null : ((ValueComparer)(((IProperty)id).GetValueComparer())).Snapshot(source.GetCurrentValue(id))), (source.GetCurrentValue(type) == null ? null : ((ValueComparer)(((IProperty)type).GetValueComparer())).Snapshot(source.GetCurrentValue(type))), ((ValueComparer)(((IProperty)enum1).GetValueComparer())).Snapshot(source.GetCurrentValue(enum1)), (source.GetCurrentValue(enum2) == null ? null : ((ValueComparer)(((IProperty)enum2).GetValueComparer())).Snapshot(source.GetCurrentValue(enum2))), ((ValueComparer)(((IProperty)flagsEnum1).GetValueComparer())).Snapshot(source.GetCurrentValue(flagsEnum1)), ((ValueComparer)(((IProperty)flagsEnum2).GetValueComparer())).Snapshot(source.GetCurrentValue(flagsEnum2)), (source.GetCurrentValue(principalBaseId) == null ? null : ((ValueComparer)(((IProperty)principalBaseId).GetValueComparer())).Snapshot(source.GetCurrentValue(principalBaseId))), (((object)(source.GetCurrentValue>(refTypeEnumerable))) == null ? null : ((IEnumerable)(((ValueComparer)(((IProperty)refTypeEnumerable).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeEnumerable))))))), (((object)(source.GetCurrentValue>(refTypeIList))) == null ? null : ((IList)(((ValueComparer)(((IProperty)refTypeIList).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeIList))))))), (((IEnumerable)(source.GetCurrentValue(valueTypeArray))) == null ? null : ((DateTime[])(((ValueComparer>)(((IProperty)valueTypeArray).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue(valueTypeArray))))))), (source.GetCurrentValue>(valueTypeEnumerable) == null ? null : ((ValueComparer>)(((IProperty)valueTypeEnumerable).GetValueComparer())).Snapshot(source.GetCurrentValue>(valueTypeEnumerable))), (((IEnumerable)(source.GetCurrentValue>(valueTypeIList))) == null ? null : ((IList)(((ValueComparer>)(((IProperty)valueTypeIList).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue>(valueTypeIList))))))), (((IEnumerable)(source.GetCurrentValue>(valueTypeList))) == null ? null : ((List)(((ValueComparer>)(((IProperty)valueTypeList).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue>(valueTypeList))))))), (source.GetCurrentValue(__id) == null ? null : ((ValueComparer)(((IProperty)__id).GetValueComparer())).Snapshot(source.GetCurrentValue(__id))), (source.GetCurrentValue(__jObject) == null ? null : ((ValueComparer)(((IProperty)__jObject).GetValueComparer())).Snapshot(source.GetCurrentValue(__jObject))), (source.GetCurrentValue(details) == null ? null : ((ValueComparer)(((IProperty)details).GetValueComparer())).Snapshot(source.GetCurrentValue(details))), ((ValueComparer)(((IProperty)number).GetValueComparer())).Snapshot(source.GetCurrentValue(number)), (((object)(source.GetCurrentValue>(refTypeEnumerable0))) == null ? null : ((IEnumerable)(((ValueComparer)(((IProperty)refTypeEnumerable0).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeEnumerable0))))))), (((object)(source.GetCurrentValue>(refTypeIList0))) == null ? null : ((IList)(((ValueComparer)(((IProperty)refTypeIList0).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeIList0))))))), (((IEnumerable)(source.GetCurrentValue(valueTypeArray0))) == null ? null : ((DateTime[])(((ValueComparer>)(((IProperty)valueTypeArray0).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue(valueTypeArray0))))))), (source.GetCurrentValue>(valueTypeEnumerable0) == null ? null : ((ValueComparer>)(((IProperty)valueTypeEnumerable0).GetValueComparer())).Snapshot(source.GetCurrentValue>(valueTypeEnumerable0))), (((IEnumerable)(source.GetCurrentValue>(valueTypeIList0))) == null ? null : ((IList)(((ValueComparer>)(((IProperty)valueTypeIList0).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue>(valueTypeIList0))))))), (((IEnumerable)(source.GetCurrentValue>(valueTypeList0))) == null ? null : ((List)(((ValueComparer>)(((IProperty)valueTypeList0).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue>(valueTypeList0))))))), ((object)(source.GetCurrentValue(principal))), ((ValueComparer)(((IProperty)alternateId).GetValueComparer())).Snapshot(source.GetCurrentValue(alternateId)), ((ValueComparer)(((IProperty)enum10).GetValueComparer())).Snapshot(source.GetCurrentValue(enum10)), (source.GetCurrentValue(enum20) == null ? null : ((ValueComparer)(((IProperty)enum20).GetValueComparer())).Snapshot(source.GetCurrentValue(enum20))), ((ValueComparer)(((IProperty)flagsEnum10).GetValueComparer())).Snapshot(source.GetCurrentValue(flagsEnum10)), ((ValueComparer)(((IProperty)flagsEnum20).GetValueComparer())).Snapshot(source.GetCurrentValue(flagsEnum20)), (source.GetCurrentValue(id0) == null ? null : ((ValueComparer)(((IProperty)id0).GetValueComparer())).Snapshot(source.GetCurrentValue(id0)))))); var structuralType2 = ((CompiledModelTestBase.PrincipalDerived>)(source.Entity)); - return ((ISnapshot)(new MultiSnapshot(new ISnapshot[] { liftedArg0, ((ISnapshot)(new Snapshot, IList, DateTime[], IEnumerable, IList, List, object>((((object)(source.GetCurrentValue>(refTypeEnumerable1))) == null ? null : ((IEnumerable)(((ValueComparer)(((IProperty)refTypeEnumerable1).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeEnumerable1))))))), (((object)(source.GetCurrentValue>(refTypeIList1))) == null ? null : ((IList)(((ValueComparer)(((IProperty)refTypeIList1).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeIList1))))))), (((IEnumerable)(source.GetCurrentValue(valueTypeArray1))) == null ? null : ((DateTime[])(((ValueComparer>)(((IProperty)valueTypeArray1).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue(valueTypeArray1))))))), (source.GetCurrentValue>(valueTypeEnumerable1) == null ? null : ((ValueComparer>)(((IProperty)valueTypeEnumerable1).GetValueComparer())).Snapshot(source.GetCurrentValue>(valueTypeEnumerable1))), (((IEnumerable)(source.GetCurrentValue>(valueTypeIList1))) == null ? null : ((IList)(((ValueComparer>)(((IProperty)valueTypeIList1).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue>(valueTypeIList1))))))), (((IEnumerable)(source.GetCurrentValue>(valueTypeList1))) == null ? null : ((List)(((ValueComparer>)(((IProperty)valueTypeList1).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue>(valueTypeList1))))))), SnapshotFactoryFactory.SnapshotComplexCollection(((IList)(source.GetCurrentValue>(manyOwned))), manyOwned)))) }))); + return ((ISnapshot)(new MultiSnapshot(new ISnapshot[] { liftedArg0, ((ISnapshot)(new Snapshot, IList, DateTime[], IEnumerable, IList, List, object, byte?, object>((((object)(source.GetCurrentValue>(refTypeEnumerable1))) == null ? null : ((IEnumerable)(((ValueComparer)(((IProperty)refTypeEnumerable1).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeEnumerable1))))))), (((object)(source.GetCurrentValue>(refTypeIList1))) == null ? null : ((IList)(((ValueComparer)(((IProperty)refTypeIList1).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeIList1))))))), (((IEnumerable)(source.GetCurrentValue(valueTypeArray1))) == null ? null : ((DateTime[])(((ValueComparer>)(((IProperty)valueTypeArray1).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue(valueTypeArray1))))))), (source.GetCurrentValue>(valueTypeEnumerable1) == null ? null : ((ValueComparer>)(((IProperty)valueTypeEnumerable1).GetValueComparer())).Snapshot(source.GetCurrentValue>(valueTypeEnumerable1))), (((IEnumerable)(source.GetCurrentValue>(valueTypeIList1))) == null ? null : ((IList)(((ValueComparer>)(((IProperty)valueTypeIList1).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue>(valueTypeIList1))))))), (((IEnumerable)(source.GetCurrentValue>(valueTypeList1))) == null ? null : ((List)(((ValueComparer>)(((IProperty)valueTypeList1).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue>(valueTypeList1))))))), ((object)(source.GetCurrentValue>(dependent))), (source.GetCurrentValue(id1) == null ? null : ((ValueComparer)(((IProperty)id1).GetValueComparer())).Snapshot(source.GetCurrentValue(id1))), SnapshotFactoryFactory.SnapshotComplexCollection(((IList)(source.GetCurrentValue>(manyOwned))), manyOwned)))) }))); }); runtimeEntityType.SetStoreGeneratedValuesFactory( ISnapshot () => ((ISnapshot)(new Snapshot((default(long? ) == null ? null : ((ValueComparer)(((IProperty)principalBaseId).GetValueComparer())).Snapshot(default(long? ))))))); @@ -1926,11 +2036,11 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) return ((ISnapshot)(new Snapshot((source.GetCurrentValue(id) == null ? null : ((ValueComparer)(((IProperty)id).GetKeyValueComparer())).Snapshot(source.GetCurrentValue(id))), (source.GetCurrentValue(principalBaseId) == null ? null : ((ValueComparer)(((IProperty)principalBaseId).GetKeyValueComparer())).Snapshot(source.GetCurrentValue(principalBaseId))), SnapshotFactoryFactory.SnapshotCollection(source.GetCurrentValue>(deriveds))))); }); runtimeEntityType.SetCounts(new PropertyCounts( - propertyCount: 35, + propertyCount: 36, navigationCount: 1, - complexPropertyCount: 2, + complexPropertyCount: 3, complexCollectionCount: 1, - originalValueCount: 37, + originalValueCount: 39, shadowCount: 4, relationshipCount: 3, storeGeneratedCount: 1)); diff --git a/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedUnsafeAccessors.cs b/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedUnsafeAccessors.cs index 8d3af16dcd9..a499bc54a83 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedUnsafeAccessors.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedUnsafeAccessors.cs @@ -11,6 +11,9 @@ namespace TestNamespace public static class PrincipalDerivedUnsafeAccessors where TDependent : class { + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "k__BackingField")] + public static extern ref TDependent Dependent(CompiledModelTestBase.PrincipalDerived @this); + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "ManyOwned")] public static extern ref IList ManyOwned(CompiledModelTestBase.PrincipalDerived @this); } diff --git a/test/EFCore.Cosmos.FunctionalTests/Scaffolding/CompiledModelCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Scaffolding/CompiledModelCosmosTest.cs index 8c63b118deb..a703d8bdb20 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Scaffolding/CompiledModelCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Scaffolding/CompiledModelCosmosTest.cs @@ -622,6 +622,7 @@ protected override void BuildComplexTypesModel(ModelBuilder modelBuilder) eb.ComplexProperty( c => c.Owned, ob => { + ob.IsRequired(); ob.Ignore(e => e.RefTypeArray); ob.Ignore(e => e.RefTypeList); ob.ComplexProperty( @@ -662,6 +663,9 @@ protected override int ExpectedComplexTypeProperties protected override bool SupportsNonAutoLoadedProperties => false; + protected override bool SupportsIndexes + => false; + protected override TestHelpers TestHelpers => CosmosTestHelpers.Instance; diff --git a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs index 01a4d3dc81e..9db974a264a 100644 --- a/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs +++ b/test/EFCore.Design.Tests/Migrations/Design/CSharpMigrationsGeneratorTest.ModelSnapshot.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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; @@ -7263,12 +7263,12 @@ public virtual void IndexAttribute_causes_column_to_have_key_or_index_column_len p0 => { Assert.Equal("FirstName", p0.Name); - Assert.Equal("nvarchar(450)", p0.GetColumnType()); + Assert.Equal("nvarchar(450)", ((IReadOnlyProperty)p0).GetColumnType()); }, p1 => { Assert.Equal("LastName", p1.Name); - Assert.Equal("nvarchar(450)", p1.GetColumnType()); + Assert.Equal("nvarchar(450)", ((IReadOnlyProperty)p1).GetColumnType()); } )); @@ -7309,12 +7309,12 @@ public virtual void IndexAttribute_name_is_stored_in_snapshot() p0 => { Assert.Equal("FirstName", p0.Name); - Assert.Equal("nvarchar(450)", p0.GetColumnType()); + Assert.Equal("nvarchar(450)", ((IReadOnlyProperty)p0).GetColumnType()); }, p1 => { Assert.Equal("LastName", p1.Name); - Assert.Equal("nvarchar(450)", p1.GetColumnType()); + Assert.Equal("nvarchar(450)", ((IReadOnlyProperty)p1).GetColumnType()); } ); }); @@ -7358,12 +7358,12 @@ public virtual void IndexAttribute_IsUnique_is_stored_in_snapshot() p0 => { Assert.Equal("FirstName", p0.Name); - Assert.Equal("nvarchar(450)", p0.GetColumnType()); + Assert.Equal("nvarchar(450)", ((IReadOnlyProperty)p0).GetColumnType()); }, p1 => { Assert.Equal("LastName", p1.Name); - Assert.Equal("nvarchar(450)", p1.GetColumnType()); + Assert.Equal("nvarchar(450)", ((IReadOnlyProperty)p1).GetColumnType()); } ); }); diff --git a/test/EFCore.InMemory.FunctionalTests/Scaffolding/Baselines/ComplexTypes/DependentBaseUnsafeAccessors.cs b/test/EFCore.InMemory.FunctionalTests/Scaffolding/Baselines/ComplexTypes/DependentBaseUnsafeAccessors.cs new file mode 100644 index 00000000000..ae6e1779611 --- /dev/null +++ b/test/EFCore.InMemory.FunctionalTests/Scaffolding/Baselines/ComplexTypes/DependentBaseUnsafeAccessors.cs @@ -0,0 +1,15 @@ +// +using System.Runtime.CompilerServices; +using Microsoft.EntityFrameworkCore.Scaffolding; + +#pragma warning disable 219, 612, 618 +#nullable disable + +namespace TestNamespace +{ + public static class DependentBaseUnsafeAccessors + { + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "k__BackingField")] + public static extern ref TKey Id(CompiledModelTestBase.DependentBase @this); + } +} diff --git a/test/EFCore.InMemory.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalBaseEntityType.cs b/test/EFCore.InMemory.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalBaseEntityType.cs index 66c2ff075b1..138ffca16bc 100644 --- a/test/EFCore.InMemory.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalBaseEntityType.cs +++ b/test/EFCore.InMemory.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalBaseEntityType.cs @@ -36,6 +36,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas complexPropertyCount: 1, navigationCount: 1, foreignKeyCount: 1, + namedIndexCount: 1, keyCount: 1); var id = runtimeEntityType.AddProperty( @@ -919,6 +920,10 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas new[] { id }); runtimeEntityType.SetPrimaryKey(key); + var iX_PrincipalBase_Id_Owned_Number = runtimeEntityType.AddIndex( + new[] { id, runtimeEntityType.FindComplexProperty("Owned").ComplexType.FindProperty("Number") }, + name: "IX_PrincipalBase_Id_Owned_Number"); + return runtimeEntityType; } diff --git a/test/EFCore.InMemory.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedEntityType.cs b/test/EFCore.InMemory.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedEntityType.cs index e0120935e5c..5939095f165 100644 --- a/test/EFCore.InMemory.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedEntityType.cs +++ b/test/EFCore.InMemory.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedEntityType.cs @@ -33,12 +33,119 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas discriminatorProperty: "Discriminator", discriminatorValue: "PrincipalDerived>", propertyCount: 0, - complexPropertyCount: 1); + complexPropertyCount: 2); + DependentComplexProperty.Create(runtimeEntityType); ManyOwnedComplexProperty.Create(runtimeEntityType); return runtimeEntityType; } + public static class DependentComplexProperty + { + public static RuntimeComplexProperty Create(RuntimeEntityType declaringType) + { + var complexProperty = declaringType.AddComplexProperty("Dependent", + typeof(CompiledModelTestBase.DependentBase), + "Microsoft.EntityFrameworkCore.Scaffolding.CompiledModelTestBase+PrincipalDerived>.Dependent#DependentBase", + typeof(CompiledModelTestBase.DependentBase), + propertyInfo: typeof(CompiledModelTestBase.PrincipalDerived>).GetProperty("Dependent", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(CompiledModelTestBase.PrincipalDerived>).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true, + propertyCount: 1); + + var complexType = complexProperty.ComplexType; + complexProperty.SetGetter( + CompiledModelTestBase.DependentBase (CompiledModelTestBase.PrincipalDerived> instance) => PrincipalDerivedUnsafeAccessors>.Dependent(instance), + bool (CompiledModelTestBase.PrincipalDerived> instance) => PrincipalDerivedUnsafeAccessors>.Dependent(instance) == null); + complexProperty.SetSetter( + CompiledModelTestBase.PrincipalDerived> (CompiledModelTestBase.PrincipalDerived> instance, CompiledModelTestBase.DependentBase value) => + { + PrincipalDerivedUnsafeAccessors>.Dependent(instance) = value; + return instance; + }); + complexProperty.SetMaterializationSetter( + CompiledModelTestBase.PrincipalDerived> (CompiledModelTestBase.PrincipalDerived> instance, CompiledModelTestBase.DependentBase value) => + { + PrincipalDerivedUnsafeAccessors>.Dependent(instance) = value; + return instance; + }); + complexProperty.SetAccessors( + CompiledModelTestBase.DependentBase (IInternalEntry entry) => PrincipalDerivedUnsafeAccessors>.Dependent(((CompiledModelTestBase.PrincipalDerived>)(entry.Entity))), + CompiledModelTestBase.DependentBase (IInternalEntry entry) => PrincipalDerivedUnsafeAccessors>.Dependent(((CompiledModelTestBase.PrincipalDerived>)(entry.Entity))), + null, + CompiledModelTestBase.DependentBase (IInternalEntry entry) => entry.GetCurrentValue>(complexProperty)); + complexProperty.SetPropertyIndexes( + index: 2, + originalValueIndex: 40, + shadowIndex: -1, + relationshipIndex: -1, + storeGenerationIndex: -1); + var id = complexType.AddProperty( + "Id", + typeof(byte?), + propertyInfo: typeof(CompiledModelTestBase.DependentBase).GetProperty("Id", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(CompiledModelTestBase.DependentBase).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + id.SetGetter( + byte? (CompiledModelTestBase.PrincipalDerived> entity, IReadOnlyList indices) => (PrincipalDerivedUnsafeAccessors>.Dependent(entity) == null ? default(byte? ) : DependentBaseUnsafeAccessors.Id(PrincipalDerivedUnsafeAccessors>.Dependent(entity))), + bool (CompiledModelTestBase.PrincipalDerived> entity, IReadOnlyList indices) => !((PrincipalDerivedUnsafeAccessors>.Dependent(entity) == null ? default(byte? ) : DependentBaseUnsafeAccessors.Id(PrincipalDerivedUnsafeAccessors>.Dependent(entity))).HasValue), + byte? (CompiledModelTestBase.DependentBase instance) => DependentBaseUnsafeAccessors.Id(instance), + bool (CompiledModelTestBase.DependentBase instance) => !(DependentBaseUnsafeAccessors.Id(instance).HasValue)); + id.SetSetter( + (CompiledModelTestBase.PrincipalDerived> entity, IReadOnlyList indices, byte? value) => + { + var level1 = PrincipalDerivedUnsafeAccessors>.Dependent(entity); + DependentBaseUnsafeAccessors.Id(level1) = value; + }, + CompiledModelTestBase.DependentBase (CompiledModelTestBase.DependentBase instance, byte? value) => + { + DependentBaseUnsafeAccessors.Id(instance) = value; + return instance; + }); + id.SetMaterializationSetter( + (CompiledModelTestBase.PrincipalDerived> entity, IReadOnlyList indices, byte? value) => + { + var level1 = PrincipalDerivedUnsafeAccessors>.Dependent(entity); + DependentBaseUnsafeAccessors.Id(level1) = value; + }, + CompiledModelTestBase.DependentBase (CompiledModelTestBase.DependentBase instance, byte? value) => + { + DependentBaseUnsafeAccessors.Id(instance) = value; + return instance; + }); + id.SetAccessors( + byte? (IInternalEntry entry) => (PrincipalDerivedUnsafeAccessors>.Dependent(((CompiledModelTestBase.PrincipalDerived>)(entry.Entity))) == null ? default(byte? ) : DependentBaseUnsafeAccessors.Id(PrincipalDerivedUnsafeAccessors>.Dependent(((CompiledModelTestBase.PrincipalDerived>)(entry.Entity))))), + byte? (IInternalEntry entry) => (PrincipalDerivedUnsafeAccessors>.Dependent(((CompiledModelTestBase.PrincipalDerived>)(entry.Entity))) == null ? default(byte? ) : DependentBaseUnsafeAccessors.Id(PrincipalDerivedUnsafeAccessors>.Dependent(((CompiledModelTestBase.PrincipalDerived>)(entry.Entity))))), + byte? (IInternalEntry entry) => entry.ReadOriginalValue(id, 41), + byte? (IInternalEntry entry) => entry.GetCurrentValue(id)); + id.SetPropertyIndexes( + index: 39, + originalValueIndex: 41, + shadowIndex: -1, + relationshipIndex: -1, + storeGenerationIndex: -1); + id.TypeMapping = InMemoryTypeMapping.Default.Clone( + comparer: new ValueComparer( + bool (byte v1, byte v2) => v1 == v2, + int (byte v) => ((int)v), + byte (byte v) => v), + keyComparer: new ValueComparer( + bool (byte v1, byte v2) => v1 == v2, + int (byte v) => ((int)v), + byte (byte v) => v), + providerValueComparer: new ValueComparer( + bool (byte v1, byte v2) => v1 == v2, + int (byte v) => ((int)v), + byte (byte v) => v), + clrType: typeof(byte), + jsonValueReaderWriter: JsonByteReaderWriter.Instance); + id.SetComparer(new NullableValueComparer(id.TypeMapping.Comparer)); + id.SetKeyComparer(new NullableValueComparer(id.TypeMapping.KeyComparer)); + + return complexProperty; + } + } + public static class ManyOwnedComplexProperty { public static RuntimeComplexProperty Create(RuntimeEntityType declaringType) @@ -82,7 +189,7 @@ public static RuntimeComplexProperty Create(RuntimeEntityType declaringType) IList (IInternalEntry entry) => entry.GetCurrentValue>(complexProperty)); complexProperty.SetPropertyIndexes( index: 0, - originalValueIndex: 40, + originalValueIndex: 42, shadowIndex: -1, relationshipIndex: -1, storeGenerationIndex: -1); @@ -2279,6 +2386,9 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) var valueTypeEnumerable1 = principalBase.FindProperty("ValueTypeEnumerable"); var valueTypeIList1 = principalBase.FindProperty("ValueTypeIList"); var valueTypeList1 = principalBase.FindProperty("ValueTypeList"); + var dependent = runtimeEntityType.FindComplexProperty("Dependent"); + var dependentBasebyte = dependent.ComplexType; + var id1 = dependentBasebyte.FindProperty("Id"); var manyOwned = runtimeEntityType.FindComplexProperty("ManyOwned"); var ownedCollection = manyOwned.ComplexType; var details0 = ownedCollection.FindProperty("Details"); @@ -2298,7 +2408,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) var enum21 = principalBase0.FindProperty("Enum2"); var flagsEnum11 = principalBase0.FindProperty("FlagsEnum1"); var flagsEnum21 = principalBase0.FindProperty("FlagsEnum2"); - var id1 = principalBase0.FindProperty("Id"); + var id2 = principalBase0.FindProperty("Id"); var refTypeArray3 = principalBase0.FindProperty("RefTypeArray"); var refTypeEnumerable3 = principalBase0.FindProperty("RefTypeEnumerable"); var refTypeIList3 = principalBase0.FindProperty("RefTypeIList"); @@ -2314,7 +2424,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) var structuralType1 = ((CompiledModelTestBase.PrincipalDerived>)(source.Entity)); var liftedArg0 = ((ISnapshot)(new Snapshot, IList, List, DateTime[], IEnumerable, IList, List, string, int, IPAddress[], IEnumerable, IList, List, DateTime[], IEnumerable, IList, List, object, Guid, CompiledModelTestBase.AnEnum, CompiledModelTestBase.AnEnum?, CompiledModelTestBase.AFlagsEnum>((source.GetCurrentValue(id) == null ? null : ((ValueComparer)(((IProperty)id).GetValueComparer())).Snapshot(source.GetCurrentValue(id))), (source.GetCurrentValue(discriminator) == null ? null : ((ValueComparer)(((IProperty)discriminator).GetValueComparer())).Snapshot(source.GetCurrentValue(discriminator))), ((ValueComparer)(((IProperty)enum1).GetValueComparer())).Snapshot(source.GetCurrentValue(enum1)), (source.GetCurrentValue(enum2) == null ? null : ((ValueComparer)(((IProperty)enum2).GetValueComparer())).Snapshot(source.GetCurrentValue(enum2))), ((ValueComparer)(((IProperty)flagsEnum1).GetValueComparer())).Snapshot(source.GetCurrentValue(flagsEnum1)), ((ValueComparer)(((IProperty)flagsEnum2).GetValueComparer())).Snapshot(source.GetCurrentValue(flagsEnum2)), (source.GetCurrentValue(principalBaseId) == null ? null : ((ValueComparer)(((IProperty)principalBaseId).GetValueComparer())).Snapshot(source.GetCurrentValue(principalBaseId))), (((object)(source.GetCurrentValue(refTypeArray))) == null ? null : ((IPAddress[])(((ValueComparer)(((IProperty)refTypeArray).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue(refTypeArray))))))), (((object)(source.GetCurrentValue>(refTypeEnumerable))) == null ? null : ((IEnumerable)(((ValueComparer)(((IProperty)refTypeEnumerable).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeEnumerable))))))), (((object)(source.GetCurrentValue>(refTypeIList))) == null ? null : ((IList)(((ValueComparer)(((IProperty)refTypeIList).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeIList))))))), (((object)(source.GetCurrentValue>(refTypeList))) == null ? null : ((List)(((ValueComparer)(((IProperty)refTypeList).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeList))))))), (((IEnumerable)(source.GetCurrentValue(valueTypeArray))) == null ? null : ((DateTime[])(((ValueComparer>)(((IProperty)valueTypeArray).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue(valueTypeArray))))))), (source.GetCurrentValue>(valueTypeEnumerable) == null ? null : ((ValueComparer>)(((IProperty)valueTypeEnumerable).GetValueComparer())).Snapshot(source.GetCurrentValue>(valueTypeEnumerable))), (((IEnumerable)(source.GetCurrentValue>(valueTypeIList))) == null ? null : ((IList)(((ValueComparer>)(((IProperty)valueTypeIList).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue>(valueTypeIList))))))), (((IEnumerable)(source.GetCurrentValue>(valueTypeList))) == null ? null : ((List)(((ValueComparer>)(((IProperty)valueTypeList).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue>(valueTypeList))))))), (source.GetCurrentValue(details) == null ? null : ((ValueComparer)(((IProperty)details).GetValueComparer())).Snapshot(source.GetCurrentValue(details))), ((ValueComparer)(((IProperty)number).GetValueComparer())).Snapshot(source.GetCurrentValue(number)), (((object)(source.GetCurrentValue(refTypeArray0))) == null ? null : ((IPAddress[])(((ValueComparer)(((IProperty)refTypeArray0).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue(refTypeArray0))))))), (((object)(source.GetCurrentValue>(refTypeEnumerable0))) == null ? null : ((IEnumerable)(((ValueComparer)(((IProperty)refTypeEnumerable0).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeEnumerable0))))))), (((object)(source.GetCurrentValue>(refTypeIList0))) == null ? null : ((IList)(((ValueComparer)(((IProperty)refTypeIList0).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeIList0))))))), (((object)(source.GetCurrentValue>(refTypeList0))) == null ? null : ((List)(((ValueComparer)(((IProperty)refTypeList0).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeList0))))))), (((IEnumerable)(source.GetCurrentValue(valueTypeArray0))) == null ? null : ((DateTime[])(((ValueComparer>)(((IProperty)valueTypeArray0).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue(valueTypeArray0))))))), (source.GetCurrentValue>(valueTypeEnumerable0) == null ? null : ((ValueComparer>)(((IProperty)valueTypeEnumerable0).GetValueComparer())).Snapshot(source.GetCurrentValue>(valueTypeEnumerable0))), (((IEnumerable)(source.GetCurrentValue>(valueTypeIList0))) == null ? null : ((IList)(((ValueComparer>)(((IProperty)valueTypeIList0).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue>(valueTypeIList0))))))), (((IEnumerable)(source.GetCurrentValue>(valueTypeList0))) == null ? null : ((List)(((ValueComparer>)(((IProperty)valueTypeList0).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue>(valueTypeList0))))))), ((object)(source.GetCurrentValue(principal))), ((ValueComparer)(((IProperty)alternateId).GetValueComparer())).Snapshot(source.GetCurrentValue(alternateId)), ((ValueComparer)(((IProperty)enum10).GetValueComparer())).Snapshot(source.GetCurrentValue(enum10)), (source.GetCurrentValue(enum20) == null ? null : ((ValueComparer)(((IProperty)enum20).GetValueComparer())).Snapshot(source.GetCurrentValue(enum20))), ((ValueComparer)(((IProperty)flagsEnum10).GetValueComparer())).Snapshot(source.GetCurrentValue(flagsEnum10))))); var structuralType2 = ((CompiledModelTestBase.PrincipalDerived>)(source.Entity)); - return ((ISnapshot)(new MultiSnapshot(new ISnapshot[] { liftedArg0, ((ISnapshot)(new Snapshot, IList, List, DateTime[], IEnumerable, IList, List, object>(((ValueComparer)(((IProperty)flagsEnum20).GetValueComparer())).Snapshot(source.GetCurrentValue(flagsEnum20)), (source.GetCurrentValue(id0) == null ? null : ((ValueComparer)(((IProperty)id0).GetValueComparer())).Snapshot(source.GetCurrentValue(id0))), (((object)(source.GetCurrentValue(refTypeArray1))) == null ? null : ((IPAddress[])(((ValueComparer)(((IProperty)refTypeArray1).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue(refTypeArray1))))))), (((object)(source.GetCurrentValue>(refTypeEnumerable1))) == null ? null : ((IEnumerable)(((ValueComparer)(((IProperty)refTypeEnumerable1).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeEnumerable1))))))), (((object)(source.GetCurrentValue>(refTypeIList1))) == null ? null : ((IList)(((ValueComparer)(((IProperty)refTypeIList1).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeIList1))))))), (((object)(source.GetCurrentValue>(refTypeList1))) == null ? null : ((List)(((ValueComparer)(((IProperty)refTypeList1).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeList1))))))), (((IEnumerable)(source.GetCurrentValue(valueTypeArray1))) == null ? null : ((DateTime[])(((ValueComparer>)(((IProperty)valueTypeArray1).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue(valueTypeArray1))))))), (source.GetCurrentValue>(valueTypeEnumerable1) == null ? null : ((ValueComparer>)(((IProperty)valueTypeEnumerable1).GetValueComparer())).Snapshot(source.GetCurrentValue>(valueTypeEnumerable1))), (((IEnumerable)(source.GetCurrentValue>(valueTypeIList1))) == null ? null : ((IList)(((ValueComparer>)(((IProperty)valueTypeIList1).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue>(valueTypeIList1))))))), (((IEnumerable)(source.GetCurrentValue>(valueTypeList1))) == null ? null : ((List)(((ValueComparer>)(((IProperty)valueTypeList1).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue>(valueTypeList1))))))), SnapshotFactoryFactory.SnapshotComplexCollection(((IList)(source.GetCurrentValue>(manyOwned))), manyOwned)))) }))); + return ((ISnapshot)(new MultiSnapshot(new ISnapshot[] { liftedArg0, ((ISnapshot)(new Snapshot, IList, List, DateTime[], IEnumerable, IList, List, object, byte?, object>(((ValueComparer)(((IProperty)flagsEnum20).GetValueComparer())).Snapshot(source.GetCurrentValue(flagsEnum20)), (source.GetCurrentValue(id0) == null ? null : ((ValueComparer)(((IProperty)id0).GetValueComparer())).Snapshot(source.GetCurrentValue(id0))), (((object)(source.GetCurrentValue(refTypeArray1))) == null ? null : ((IPAddress[])(((ValueComparer)(((IProperty)refTypeArray1).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue(refTypeArray1))))))), (((object)(source.GetCurrentValue>(refTypeEnumerable1))) == null ? null : ((IEnumerable)(((ValueComparer)(((IProperty)refTypeEnumerable1).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeEnumerable1))))))), (((object)(source.GetCurrentValue>(refTypeIList1))) == null ? null : ((IList)(((ValueComparer)(((IProperty)refTypeIList1).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeIList1))))))), (((object)(source.GetCurrentValue>(refTypeList1))) == null ? null : ((List)(((ValueComparer)(((IProperty)refTypeList1).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeList1))))))), (((IEnumerable)(source.GetCurrentValue(valueTypeArray1))) == null ? null : ((DateTime[])(((ValueComparer>)(((IProperty)valueTypeArray1).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue(valueTypeArray1))))))), (source.GetCurrentValue>(valueTypeEnumerable1) == null ? null : ((ValueComparer>)(((IProperty)valueTypeEnumerable1).GetValueComparer())).Snapshot(source.GetCurrentValue>(valueTypeEnumerable1))), (((IEnumerable)(source.GetCurrentValue>(valueTypeIList1))) == null ? null : ((IList)(((ValueComparer>)(((IProperty)valueTypeIList1).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue>(valueTypeIList1))))))), (((IEnumerable)(source.GetCurrentValue>(valueTypeList1))) == null ? null : ((List)(((ValueComparer>)(((IProperty)valueTypeList1).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue>(valueTypeList1))))))), ((object)(source.GetCurrentValue>(dependent))), (source.GetCurrentValue(id1) == null ? null : ((ValueComparer)(((IProperty)id1).GetValueComparer())).Snapshot(source.GetCurrentValue(id1))), SnapshotFactoryFactory.SnapshotComplexCollection(((IList)(source.GetCurrentValue>(manyOwned))), manyOwned)))) }))); }); runtimeEntityType.SetStoreGeneratedValuesFactory( ISnapshot () => ((ISnapshot)(new Snapshot((default(long? ) == null ? null : ((ValueComparer)(((IProperty)id).GetValueComparer())).Snapshot(default(long? ))), (default(long? ) == null ? null : ((ValueComparer)(((IProperty)principalBaseId).GetValueComparer())).Snapshot(default(long? ))))))); @@ -2331,11 +2441,11 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) return ((ISnapshot)(new Snapshot((source.GetCurrentValue(id) == null ? null : ((ValueComparer)(((IProperty)id).GetKeyValueComparer())).Snapshot(source.GetCurrentValue(id))), (source.GetCurrentValue(principalBaseId) == null ? null : ((ValueComparer)(((IProperty)principalBaseId).GetKeyValueComparer())).Snapshot(source.GetCurrentValue(principalBaseId))), SnapshotFactoryFactory.SnapshotCollection(source.GetCurrentValue>(deriveds))))); }); runtimeEntityType.SetCounts(new PropertyCounts( - propertyCount: 39, + propertyCount: 40, navigationCount: 1, - complexPropertyCount: 2, + complexPropertyCount: 3, complexCollectionCount: 1, - originalValueCount: 41, + originalValueCount: 43, shadowCount: 2, relationshipCount: 3, storeGeneratedCount: 2)); diff --git a/test/EFCore.InMemory.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedUnsafeAccessors.cs b/test/EFCore.InMemory.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedUnsafeAccessors.cs index 8d3af16dcd9..a499bc54a83 100644 --- a/test/EFCore.InMemory.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedUnsafeAccessors.cs +++ b/test/EFCore.InMemory.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedUnsafeAccessors.cs @@ -11,6 +11,9 @@ namespace TestNamespace public static class PrincipalDerivedUnsafeAccessors where TDependent : class { + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "k__BackingField")] + public static extern ref TDependent Dependent(CompiledModelTestBase.PrincipalDerived @this); + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "ManyOwned")] public static extern ref IList ManyOwned(CompiledModelTestBase.PrincipalDerived @this); } diff --git a/test/EFCore.Relational.Specification.Tests/Scaffolding/CompiledModelRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Scaffolding/CompiledModelRelationalTestBase.cs index 4c8eb161906..a20cd02abec 100644 --- a/test/EFCore.Relational.Specification.Tests/Scaffolding/CompiledModelRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Scaffolding/CompiledModelRelationalTestBase.cs @@ -524,6 +524,8 @@ protected override void BuildComplexTypesModel(ModelBuilder modelBuilder) { eb.ComplexCollection, OwnedType>( "ManyOwned", "OwnedCollection", eb => eb.ToJson()); + eb.ComplexProperty(p => p.Dependent, cb => cb.ToJson()); + eb.HasIndex(e => e.Dependent, "IX_PrincipalDerived_Dependent"); }); } @@ -664,6 +666,23 @@ protected override void AssertComplexTypes(IModel model) } } } + + var dependentComplexProperty = principalDerived.FindComplexProperty(nameof(PrincipalDerived>.Dependent))!; + Assert.False(dependentComplexProperty.IsCollection); + Assert.True(dependentComplexProperty.ComplexType.IsMappedToJson()); + Assert.Equal( + nameof(PrincipalDerived>.Dependent), + dependentComplexProperty.ComplexType.GetContainerColumnName()); + + var dependentIndex = principalDerived.GetIndexes().Single(i => i.Name == "IX_PrincipalDerived_Dependent"); + Assert.Single(dependentIndex.Properties); + Assert.Same(dependentComplexProperty, dependentIndex.Properties[0]); + + var principalBaseTable2 = principalBase.GetTableMappings().Single().Table; + var dependentJsonColumn = principalBaseTable2.FindColumn(nameof(PrincipalDerived>.Dependent))!; + Assert.NotNull(dependentJsonColumn); + var dependentTableIndex = principalBaseTable2.Indexes.Single(i => i.Name == "IX_PrincipalDerived_Dependent"); + Assert.Same(dependentJsonColumn, dependentTableIndex.Columns.Single()); } [ConditionalFact] diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs index fb4a5afcc45..054d894d758 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs @@ -126,6 +126,258 @@ public override void Detects_discriminator_property_on_complex_collection() modelBuilder); } + public override void Detects_index_on_complex_collection_property() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.ComplexCollection(e => e.Items).ToJson(); + }); + + var entityType = (EntityType)modelBuilder.Model.FindEntityType(typeof(EntityWithComplexCollection))!; + var collectionProperty = entityType.FindComplexProperty(nameof(EntityWithComplexCollection.Items))!; + entityType.AddIndex([collectionProperty], ConfigurationSource.Explicit); + + VerifyError( + CoreStrings.IndexOnComplexCollection( + "{'Items'}", nameof(EntityWithComplexCollection), nameof(EntityWithComplexCollection.Items)), + modelBuilder); + } + + public override void Detects_index_traversing_complex_collection() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.ComplexCollection(e => e.Items).ToJson(); + }); + + var entityType = (EntityType)modelBuilder.Model.FindEntityType(typeof(EntityWithComplexCollection))!; + var collectionProperty = entityType.FindComplexProperty(nameof(EntityWithComplexCollection.Items))!; + var leaf = collectionProperty.ComplexType.FindProperty(nameof(ComplexCollectionItem.Value))!; + entityType.AddIndex([leaf], ConfigurationSource.Explicit); + + VerifyError( + CoreStrings.IndexOnComplexCollection( + "{'Value'}", nameof(EntityWithComplexCollection), nameof(EntityWithComplexCollection.Items)), + modelBuilder); + } + + public override void Detects_key_traversing_complex_collection() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.ComplexCollection(e => e.Items).ToJson(); + }); + + var entityType = (EntityType)modelBuilder.Model.FindEntityType(typeof(EntityWithComplexCollection))!; + var collectionProperty = entityType.FindComplexProperty(nameof(EntityWithComplexCollection.Items))!; + var leaf = collectionProperty.ComplexType.FindProperty(nameof(ComplexCollectionItem.Value))!; + entityType.AddKey([leaf], ConfigurationSource.Explicit); + + VerifyError( + CoreStrings.KeyOnComplexCollection( + "{'Value'}", nameof(EntityWithComplexCollection), nameof(EntityWithComplexCollection.Items)), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_index_mixing_json_and_non_json_complex_properties() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.Ignore(e => e.OtherSamples); + b.Ignore(e => e.AnotherReferencedEntity); + b.Ignore(e => e.Number); + b.ComplexProperty(e => e.ReferencedEntity).ToJson(); + b.HasIndex(e => new { e.Name, e.ReferencedEntity.SampleEntityId }); + }); + + VerifyError( + RelationalStrings.IndexPropertiesMixedJsonAndNonJsonMapping( + "{'Name', 'SampleEntityId'}", nameof(SampleEntity)), + modelBuilder); + } + + [ConditionalFact] + public virtual void Passes_on_index_mixing_json_complex_property_and_scalar() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.Ignore(e => e.OtherSamples); + b.Ignore(e => e.AnotherReferencedEntity); + b.Ignore(e => e.Number); + b.ComplexProperty(e => e.ReferencedEntity).ToJson(); + b.HasIndex(e => new { e.Name, e.ReferencedEntity }); + }); + + Validate(modelBuilder); + } + + [ConditionalFact] + public virtual void Index_entirely_within_json_complex_property_is_allowed() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.Ignore(e => e.OtherSamples); + b.Ignore(e => e.AnotherReferencedEntity); + b.Ignore(e => e.Number); + b.ComplexProperty(e => e.ReferencedEntity).ToJson(); + b.HasIndex(e => new { e.ReferencedEntity.Id, e.ReferencedEntity.SampleEntityId }); + }); + + Validate(modelBuilder); + } + + [ConditionalFact] + public virtual void Passes_on_index_on_complex_property_mapped_to_json() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.Ignore(e => e.OtherSamples); + b.Ignore(e => e.AnotherReferencedEntity); + b.Ignore(e => e.Name); + b.Ignore(e => e.Number); + b.ComplexProperty(e => e.ReferencedEntity).ToJson(); + b.HasIndex(e => e.ReferencedEntity); + }); + + var model = Validate(modelBuilder); + var index = model.FindEntityType(typeof(SampleEntity))!.GetIndexes().Single(); + } + + [ConditionalFact] + public virtual void GetNullableValueFactory_throws_for_index_containing_complex_property() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.Ignore(e => e.OtherSamples); + b.Ignore(e => e.AnotherReferencedEntity); + b.Ignore(e => e.Name); + b.Ignore(e => e.Number); + b.ComplexProperty(e => e.ReferencedEntity).ToJson(); + b.HasIndex(e => e.ReferencedEntity); + }); + + var model = Validate(modelBuilder); + var index = model.FindEntityType(typeof(SampleEntity))!.GetIndexes().Single(); + + Assert.Equal( + CoreStrings.IndexValueFactoryWithComplexProperty( + "{'ReferencedEntity'}", + nameof(SampleEntity), + nameof(SampleEntity.ReferencedEntity)), + Assert.Throws( + () => index.GetNullableValueFactory>()).Message); + } + + [ConditionalFact] + public virtual void Detects_unique_index_on_complex_property_mapped_to_json() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.Ignore(e => e.OtherSamples); + b.Ignore(e => e.AnotherReferencedEntity); + b.Ignore(e => e.Name); + b.Ignore(e => e.Number); + b.ComplexProperty(e => e.ReferencedEntity).ToJson(); + b.HasIndex(e => e.ReferencedEntity).IsUnique(); + }); + + VerifyError( + RelationalStrings.UniqueIndexOnComplexProperty( + "{'ReferencedEntity'}", + nameof(SampleEntity), + nameof(SampleEntity.ReferencedEntity)), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_alternate_key_on_property_in_json_mapped_complex_type() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.Ignore(e => e.OtherSamples); + b.Ignore(e => e.AnotherReferencedEntity); + b.Ignore(e => e.Name); + b.Ignore(e => e.Number); + b.ComplexProperty(e => e.ReferencedEntity).ToJson(); + b.HasAlternateKey(e => e.ReferencedEntity.SampleEntityId); + }); + + VerifyError( + RelationalStrings.KeyPropertyInJsonComplexType( + "{'SampleEntityId'}", + nameof(SampleEntity), + nameof(ReferencedEntity.SampleEntityId)), + modelBuilder); + } + + public override void Detects_composite_index_with_scalar_and_complex_properties() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.Ignore(e => e.OtherSamples); + b.Ignore(e => e.AnotherReferencedEntity); + b.Ignore(e => e.Number); + b.ComplexProperty(e => e.ReferencedEntity); + }); + + var entityType = (EntityType)modelBuilder.Model.FindEntityType(typeof(SampleEntity))!; + var nameProperty = (Metadata.Internal.Property)entityType.FindProperty(nameof(SampleEntity.Name))!; + var complexProperty = entityType.FindComplexProperty(nameof(SampleEntity.ReferencedEntity))!; + entityType.AddIndex([nameProperty, complexProperty], ConfigurationSource.Explicit); + + VerifyError( + RelationalStrings.IndexOnNonJsonComplexProperty( + "{'Name', 'ReferencedEntity'}", + nameof(SampleEntity), + nameof(SampleEntity.ReferencedEntity)), + modelBuilder); + } + + public override void Detects_index_on_complex_property() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.Ignore(e => e.OtherSamples); + b.Ignore(e => e.AnotherReferencedEntity); + b.Ignore(e => e.Name); + b.Ignore(e => e.Number); + b.ComplexProperty(e => e.ReferencedEntity); + b.HasIndex(e => e.ReferencedEntity); + }); + + VerifyError( + RelationalStrings.IndexOnNonJsonComplexProperty( + "{'ReferencedEntity'}", + nameof(SampleEntity), + nameof(SampleEntity.ReferencedEntity)), + modelBuilder); + } + [ConditionalFact] public virtual void Ignores_bool_with_default_value_false() { diff --git a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs index 0cf21afecd7..633f3783c60 100644 --- a/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/RelationalModelTest.cs @@ -4,7 +4,6 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Data; using Microsoft.EntityFrameworkCore.Metadata.Internal; -using Microsoft.EntityFrameworkCore.Migrations; using NameSpace1; // ReSharper disable InconsistentNaming @@ -3639,6 +3638,90 @@ public void Can_use_relational_model_with_functions_and_json_owned_types() Assert.NotNull(storeFunction.FindColumn("addresses")); } + [ConditionalFact] + public void Alternate_key_on_complex_property_is_mapped_to_unique_constraint() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Ignore(); + modelBuilder.Ignore(); + modelBuilder.Ignore(); + modelBuilder.Ignore
(); + modelBuilder.Entity(b => + { + b.HasAlternateKey("ComplexProperty.Value"); + }); + + var model = Finalize(modelBuilder); + var orderType = model.Model.FindEntityType(typeof(Order))!; + var table = orderType.GetTableMappings().Single().Table; + var expectedColumnName = nameof(Order.ComplexProperty) + "_" + nameof(ComplexData.Value); + + var alternateKey = orderType.GetKeys().Single(k => !k.IsPrimaryKey()); + var uniqueConstraints = (SortedSet)alternateKey + .FindRuntimeAnnotationValue(RelationalAnnotationNames.UniqueConstraintMappings)!; + var uniqueConstraint = Assert.Single(uniqueConstraints); + var column = Assert.Single(uniqueConstraint.Columns); + Assert.Equal(expectedColumnName, column.Name); + Assert.Same(table, column.Table); + } + + [ConditionalFact] + public void Index_on_complex_property_is_mapped_to_table_index() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Ignore(); + modelBuilder.Ignore(); + modelBuilder.Ignore(); + modelBuilder.Ignore
(); + modelBuilder.Entity(b => + { + b.HasIndex("ComplexProperty.Number").IsUnique(); + }); + + var model = Finalize(modelBuilder); + var orderType = model.Model.FindEntityType(typeof(Order))!; + var table = orderType.GetTableMappings().Single().Table; + var expectedColumnName = nameof(Order.ComplexProperty) + "_" + nameof(ComplexData.Number); + + var index = orderType.GetIndexes().Single(); + var tableIndexes = (SortedSet)index + .FindRuntimeAnnotationValue(RelationalAnnotationNames.TableIndexMappings)!; + var tableIndex = Assert.Single(tableIndexes); + Assert.True(tableIndex.IsUnique); + var column = Assert.Single(tableIndex.Columns); + Assert.Equal(expectedColumnName, column.Name); + Assert.Same(table, column.Table); + } + + [ConditionalFact] + public void GetIndex_resolves_index_on_json_mapped_complex_property() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Ignore(); + modelBuilder.Ignore(); + modelBuilder.Ignore(); + modelBuilder.Ignore
(); + modelBuilder.Entity(b => + { + b.ComplexProperty(e => e.ComplexProperty, cb => cb.ToJson()); + b.HasIndex(e => e.ComplexProperty); + }); + + var model = Finalize(modelBuilder); + var orderType = model.Model.FindEntityType(typeof(Order))!; + var index = orderType.GetIndexes().Single(); + var complexProperty = orderType.FindComplexProperty(nameof(Order.ComplexProperty))!; + + Assert.Same(complexProperty, Assert.Single(index.Properties)); + + // Simulates the lookup performed by compiled-model code generation, which uses property paths. +#pragma warning disable EF1001 // Internal EF Core API usage. + var resolved = Microsoft.EntityFrameworkCore.Metadata.Internal.RelationalModel.GetIndex( + model.Model, orderType.Name, [nameof(Order.ComplexProperty)]); +#pragma warning restore EF1001 // Internal EF Core API usage. + Assert.Same(index, resolved); + } + private static IRelationalModel Finalize(TestHelpers.TestModelBuilder modelBuilder) => modelBuilder.FinalizeModel(designTime: true).GetRelationalModel(); diff --git a/test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest.ComplexType.cs b/test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest.ComplexType.cs index 62f45f39f67..644f741e67c 100644 --- a/test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest.ComplexType.cs +++ b/test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest.ComplexType.cs @@ -2286,6 +2286,205 @@ public virtual void Can_specify_discriminator_value() Assert.Equal(BasicEnum.Two, complexType.GetDiscriminatorValue()); } + [ConditionalFact] + public virtual void Can_define_alternate_key_on_complex_property_via_lambda() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Ignore() + .Ignore() + .Entity(b => + { + b.Ignore(e => e.Customer); + b.Ignore(e => e.Customers); + b.Ignore(e => e.CollectionQuarks); + b.Ignore(e => e.QuarksCollection); + b.Ignore(e => e.DoubleProperty); + b.HasAlternateKey(e => e.Quarks.Up); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(ComplexProperties))!; + var quarksType = entityType.GetComplexProperties().Single(p => p.Name == nameof(ComplexProperties.Quarks)) + .ComplexType; + var upProperty = quarksType.FindProperty(nameof(Quarks.Up))!; + + var alternateKey = entityType.GetKeys().Single(k => !k.IsPrimaryKey()); + Assert.Same(upProperty, alternateKey.Properties.Single()); + Assert.Same(entityType, alternateKey.DeclaringEntityType); + } + + [ConditionalFact] + public virtual void Can_define_alternate_key_on_complex_property_via_string_dotted_path() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Ignore() + .Ignore() + .Entity(b => + { + b.Ignore(e => e.Customer); + b.Ignore(e => e.Customers); + b.Ignore(e => e.CollectionQuarks); + b.Ignore(e => e.QuarksCollection); + b.Ignore(e => e.DoubleProperty); + b.HasAlternateKey("Quarks.Up"); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(ComplexProperties))!; + var quarksType = entityType.GetComplexProperties().Single(p => p.Name == nameof(ComplexProperties.Quarks)) + .ComplexType; + var upProperty = quarksType.FindProperty(nameof(Quarks.Up))!; + + var alternateKey = entityType.GetKeys().Single(k => !k.IsPrimaryKey()); + Assert.Same(upProperty, alternateKey.Properties.Single()); + Assert.Same(entityType, alternateKey.DeclaringEntityType); + } + + [ConditionalFact] + public virtual void Can_define_index_on_complex_property_via_lambda() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Ignore() + .Ignore() + .Entity(b => + { + b.Ignore(e => e.Customer); + b.Ignore(e => e.Customers); + b.Ignore(e => e.CollectionQuarks); + b.Ignore(e => e.QuarksCollection); + b.Ignore(e => e.DoubleProperty); + b.HasIndex(e => e.Quarks.Up).IsUnique(); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(ComplexProperties))!; + var quarksType = entityType.GetComplexProperties().Single(p => p.Name == nameof(ComplexProperties.Quarks)) + .ComplexType; + var upProperty = quarksType.FindProperty(nameof(Quarks.Up))!; + + var index = entityType.GetIndexes().Single(); + Assert.Same(upProperty, index.Properties.Single()); + Assert.True(index.IsUnique); + Assert.Same(entityType, index.DeclaringEntityType); + } + + [ConditionalFact] + public virtual void Can_define_composite_index_mixing_entity_and_complex_property_via_lambda() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Ignore() + .Ignore() + .Entity(b => + { + b.Ignore(e => e.Customer); + b.Ignore(e => e.Customers); + b.Ignore(e => e.CollectionQuarks); + b.Ignore(e => e.QuarksCollection); + b.Ignore(e => e.DoubleProperty); + b.HasIndex(e => new { e.Id, e.Quarks.Up }); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(ComplexProperties))!; + var idProperty = entityType.FindProperty(nameof(ComplexPropertiesBase.Id))!; + var quarksType = entityType.GetComplexProperties().Single(p => p.Name == nameof(ComplexProperties.Quarks)) + .ComplexType; + var upProperty = quarksType.FindProperty(nameof(Quarks.Up))!; + + var index = entityType.GetIndexes().Single(); + Assert.Equal(2, index.Properties.Count); + Assert.Same(idProperty, index.Properties[0]); + Assert.Same(upProperty, index.Properties[1]); + Assert.Same(entityType, index.DeclaringEntityType); + } + + [ConditionalFact] + public virtual void Can_define_index_on_complex_property_via_string_dotted_path() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Ignore() + .Ignore() + .Entity(b => + { + b.Ignore(e => e.Customer); + b.Ignore(e => e.Customers); + b.Ignore(e => e.CollectionQuarks); + b.Ignore(e => e.QuarksCollection); + b.Ignore(e => e.DoubleProperty); + b.HasIndex("Quarks.Up"); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(ComplexProperties))!; + var quarksType = entityType.GetComplexProperties().Single(p => p.Name == nameof(ComplexProperties.Quarks)) + .ComplexType; + var upProperty = quarksType.FindProperty(nameof(Quarks.Up))!; + + var index = entityType.GetIndexes().Single(); + Assert.Same(upProperty, index.Properties.Single()); + Assert.Same(entityType, index.DeclaringEntityType); + } + + [ConditionalFact] + public virtual void Key_on_complex_property_marks_chain_as_required() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Ignore() + .Ignore() + .Entity(b => + { + b.Ignore(e => e.Customer); + b.Ignore(e => e.Customers); + b.Ignore(e => e.CollectionQuarks); + b.Ignore(e => e.QuarksCollection); + b.Ignore(e => e.DoubleProperty); + b.HasAlternateKey(e => e.Quarks.Up); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(ComplexProperties))!; + var quarksProperty = entityType.GetComplexProperties().Single(p => p.Name == nameof(ComplexProperties.Quarks)); + + Assert.False(quarksProperty.IsNullable); + } + + [ConditionalFact] + public virtual void Index_on_complex_property_marks_chain_as_required() + { + var modelBuilder = CreateModelBuilder(); + + modelBuilder + .Ignore() + .Ignore() + .Entity(b => + { + b.Ignore(e => e.Customer); + b.Ignore(e => e.Customers); + b.Ignore(e => e.CollectionQuarks); + b.Ignore(e => e.QuarksCollection); + b.Ignore(e => e.DoubleProperty); + b.HasIndex(e => e.Quarks.Up); + }); + + var model = modelBuilder.FinalizeModel(); + var entityType = model.FindEntityType(typeof(ComplexProperties))!; + var quarksProperty = entityType.GetComplexProperties().Single(p => p.Name == nameof(ComplexProperties.Quarks)); + + Assert.False(quarksProperty.IsNullable); + } + [ConditionalFact] public virtual void Chained_property_lambda_configures_nested_complex_property() { diff --git a/test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest.Inheritance.cs b/test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest.Inheritance.cs index e20a9fb5c7d..3493ff8154b 100644 --- a/test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest.Inheritance.cs +++ b/test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest.Inheritance.cs @@ -159,8 +159,8 @@ public virtual void Can_set_and_remove_base_type() return true; }); Fixture.TestHelpers.ModelAsserter.AssertEqual( - initialIndexes.SingleOrDefault()?.Properties ?? [], - pickle.GetIndexes().SingleOrDefault()?.Properties ?? []); + initialIndexes.SingleOrDefault()?.Properties.OfType().ToList() ?? [], + pickle.GetIndexes().SingleOrDefault()?.Properties.OfType().ToList() ?? []); Fixture.TestHelpers.ModelAsserter.AssertEqual( initialForeignKeys.Single().Properties, pickle.GetForeignKeys().Single().Properties); @@ -191,8 +191,8 @@ public virtual void Can_set_and_remove_base_type() return true; }); Fixture.TestHelpers.ModelAsserter.AssertEqual( - initialIndexes.SingleOrDefault()?.Properties ?? [], - ingredient.GetIndexes().SingleOrDefault()?.Properties ?? []); + initialIndexes.SingleOrDefault()?.Properties.OfType().ToList() ?? [], + ingredient.GetIndexes().SingleOrDefault()?.Properties.OfType().ToList() ?? []); Fixture.TestHelpers.ModelAsserter.AssertEqual( initialForeignKeys.Single().Properties, ingredient.GetForeignKeys().Single().Properties); diff --git a/test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest.NonGeneric.cs b/test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest.NonGeneric.cs index 8b2ef81ab84..57518e925b8 100644 --- a/test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest.NonGeneric.cs +++ b/test/EFCore.Specification.Tests/ModelBuilding/ModelBuilderTest.NonGeneric.cs @@ -82,7 +82,7 @@ public override TestEntityTypeBuilder HasBaseType(string? baseEntityTyp public override TestKeyBuilder HasKey(Expression> keyExpression) => new NonGenericTestKeyBuilder( - EntityTypeBuilder.HasKey(keyExpression.GetMemberAccessList().Select(p => p.GetSimpleMemberName()).ToArray())); + EntityTypeBuilder.HasKey(keyExpression.GetMemberAccessChainList().Select(ToDottedName).ToArray())); public override TestKeyBuilder HasKey(params string[] propertyNames) => new NonGenericTestKeyBuilder(EntityTypeBuilder.HasKey(propertyNames)); @@ -90,7 +90,7 @@ public override TestKeyBuilder HasKey(params string[] propertyNames) public override TestKeyBuilder HasAlternateKey(Expression> keyExpression) => new NonGenericTestKeyBuilder( EntityTypeBuilder.HasAlternateKey( - keyExpression.GetMemberAccessList().Select(p => p.GetSimpleMemberName()).ToArray())); + keyExpression.GetMemberAccessChainList().Select(ToDottedName).ToArray())); public override TestKeyBuilder HasAlternateKey(params string[] propertyNames) => new NonGenericTestKeyBuilder(EntityTypeBuilder.HasAlternateKey(propertyNames)); @@ -259,11 +259,11 @@ public override TestEntityTypeBuilder Ignore(string propertyName) public override TestIndexBuilder HasIndex(Expression> indexExpression) => new NonGenericTestIndexBuilder( - EntityTypeBuilder.HasIndex(indexExpression.GetMemberAccessList().Select(p => p.GetSimpleMemberName()).ToArray())); + EntityTypeBuilder.HasIndex(indexExpression.GetMemberAccessChainList().Select(ToDottedName).ToArray())); public override TestIndexBuilder HasIndex(Expression> indexExpression, string name) => new NonGenericTestIndexBuilder( - EntityTypeBuilder.HasIndex(indexExpression.GetMemberAccessList().Select(p => p.GetSimpleMemberName()).ToArray(), name)); + EntityTypeBuilder.HasIndex(indexExpression.GetMemberAccessChainList().Select(ToDottedName).ToArray(), name)); public override TestIndexBuilder HasIndex(params string[] propertyNames) => new NonGenericTestIndexBuilder(EntityTypeBuilder.HasIndex(propertyNames)); diff --git a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs index 6f42292c9bc..bb61fd09329 100644 --- a/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/AdHocComplexTypeQueryTestBase.cs @@ -692,6 +692,166 @@ public virtual async Task Update_entity_with_nullable_complex_type_and_discrimin #endregion Issue38105 + #region Issue31246 + + [ConditionalFact] + public virtual async Task Can_query_by_complex_type_property_with_index() + { + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + context.AddRange( + new Context31246.Person { Id = new Context31246.StronglyTypedId(1), Address = new Context31246.Address { City = "Seattle", PostalCode = "98101" } }, + new Context31246.Person { Id = new Context31246.StronglyTypedId(2), Address = new Context31246.Address { City = "Redmond", PostalCode = "98052" } }); + return context.SaveChangesAsync(); + }); + + await using var context = contextFactory.CreateDbContext(); + + // The primary key Id.Id reorders the entity's complex properties so that the chain containing + // the PK property comes first, and reorders the properties inside the Id complex type so that + // the PK property is listed first. + var personType = context.Model.FindEntityType(typeof(Context31246.Person))!; + Assert.Equal( + [nameof(Context31246.Person.Id), nameof(Context31246.Person.Address)], + personType.GetComplexProperties().Select(p => p.Name)); + var idComplexType = personType.FindComplexProperty(nameof(Context31246.Person.Id))!.ComplexType; + Assert.Equal( + [nameof(Context31246.StronglyTypedId.Id)], + idComplexType.GetProperties().Select(p => p.Name)); + + var found = await context.Set().SingleAsync(p => p.Address.City == "Seattle"); + Assert.Equal(new Context31246.StronglyTypedId(1), found.Id); + Assert.Equal("98101", found.Address.PostalCode); + } + + [ConditionalFact] + public virtual async Task Can_update_entity_with_index_on_complex_type_property() + { + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + context.Add( + new Context31246.Person { Id = new Context31246.StronglyTypedId(1), Address = new Context31246.Address { City = "Seattle", PostalCode = "98101" } }); + return context.SaveChangesAsync(); + }); + + await using (var context = contextFactory.CreateDbContext()) + { + var person = await context.Set().SingleAsync(); + person.Address.PostalCode = "98102"; + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateDbContext()) + { + var person = await context.Set().SingleAsync(); + Assert.Equal("98102", person.Address.PostalCode); + } + } + + [ConditionalFact] + public virtual async Task Can_delete_entity_with_index_on_complex_type_property() + { + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + context.Add( + new Context31246.Person { Id = new Context31246.StronglyTypedId(1), Address = new Context31246.Address { City = "Seattle", PostalCode = "98101" } }); + return context.SaveChangesAsync(); + }); + + await using (var context = contextFactory.CreateDbContext()) + { + var person = await context.Set().SingleAsync(); + context.Remove(person); + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateDbContext()) + { + Assert.Equal(0, await context.Set().CountAsync()); + } + } + + [ConditionalFact] + public virtual async Task Can_query_by_alternate_key_on_complex_type_property() + { + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + context.AddRange( + new Context31246.Person { Id = new Context31246.StronglyTypedId(1), Address = new Context31246.Address { City = "Seattle", PostalCode = "98101" } }, + new Context31246.Person { Id = new Context31246.StronglyTypedId(2), Address = new Context31246.Address { City = "Redmond", PostalCode = "98052" } }); + return context.SaveChangesAsync(); + }); + + await using var context = contextFactory.CreateDbContext(); + + var found = await context.Set().SingleAsync(p => p.Address.City == "Redmond"); + Assert.Equal(new Context31246.StronglyTypedId(2), found.Id); + } + + [ConditionalFact] + public virtual async Task Can_save_batch_swapping_alternate_key_values_on_complex_type_property() + { + var contextFactory = await InitializeNonSharedTest( + seed: context => + { + context.AddRange( + new Context31246.Person { Id = new Context31246.StronglyTypedId(1), Address = new Context31246.Address { City = "Seattle", PostalCode = "98101" } }, + new Context31246.Person { Id = new Context31246.StronglyTypedId(2), Address = new Context31246.Address { City = "Redmond", PostalCode = "98052" } }); + return context.SaveChangesAsync(); + }); + + // Update non-AK columns on multiple rows in the same batch. The CommandBatchPreparer + // still has to read the alternate-key column values (Address_City) for each command in + // order to build edges in the topological sort (AddUniqueValueEdges). + await using (var context = contextFactory.CreateDbContext()) + { + var people = await context.Set().OrderBy(p => p.Id.Id).ToListAsync(); + people[0].Address.PostalCode = "98103"; + people[1].Address.PostalCode = "98054"; + await context.SaveChangesAsync(); + } + + await using (var context = contextFactory.CreateDbContext()) + { + var people = await context.Set().OrderBy(p => p.Id.Id).ToListAsync(); + Assert.Equal("98103", people[0].Address.PostalCode); + Assert.Equal("98054", people[1].Address.PostalCode); + } + } + + protected class Context31246(DbContextOptions options) : DbContext(options) + { + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity(b => + { + b.ComplexProperty(p => p.Id); + b.HasKey(p => p.Id.Id); + b.Property(p => p.Id.Id).ValueGeneratedNever(); + b.HasAlternateKey(p => p.Address.City); + b.HasIndex(p => p.Address.PostalCode); + }); + + public readonly record struct StronglyTypedId(int Id); + + public class Person + { + public StronglyTypedId Id { get; set; } + public Address Address { get; set; } = null!; + } + + public class Address + { + public string City { get; set; } = null!; + public string PostalCode { get; set; } = null!; + } + } + + #endregion Issue31246 + protected override string NonSharedStoreName => "AdHocComplexTypeQueryTest"; } diff --git a/test/EFCore.Specification.Tests/Scaffolding/CompiledModelTestBase.cs b/test/EFCore.Specification.Tests/Scaffolding/CompiledModelTestBase.cs index 4131d897023..d55e2a1f195 100644 --- a/test/EFCore.Specification.Tests/Scaffolding/CompiledModelTestBase.cs +++ b/test/EFCore.Specification.Tests/Scaffolding/CompiledModelTestBase.cs @@ -1223,6 +1223,11 @@ protected virtual void BuildComplexTypesModel(ModelBuilder modelBuilder) cb.Ignore(e => e.Deriveds); }); }); + + if (SupportsIndexes) + { + eb.HasIndex(e => new { e.Id, e.Owned.Number }, "IX_PrincipalBase_Id_Owned_Number"); + } }); modelBuilder.Entity>>(eb => @@ -1249,7 +1254,12 @@ protected virtual void BuildComplexTypesModel(ModelBuilder modelBuilder) cb.Ignore(e => e.Deriveds); }); }); - eb.Ignore(p => p.Dependent); + eb.ComplexProperty( + p => p.Dependent, cb => + { + cb.Property("Id"); + cb.Ignore(d => d.Principal); + }); eb.Ignore(p => p.Principals); }); } @@ -1310,6 +1320,15 @@ protected virtual void AssertComplexTypes(IModel model) Assert.Equal(ExpectedComplexTypeProperties, nestedComplexType.GetProperties().Count()); + if (SupportsIndexes) + { + var index = principalBase.GetIndexes().Single(i => i.Name == "IX_PrincipalBase_Id_Owned_Number"); + Assert.Equal("IX_PrincipalBase_Id_Owned_Number", index.Name); + Assert.Equal(2, index.Properties.Count); + Assert.Same(principalBase.FindProperty(nameof(PrincipalBase.Id)), index.Properties[0]); + Assert.Same(complexType.FindProperty(nameof(OwnedType.Number)), index.Properties[1]); + } + var principalDerived = model.FindEntityType(typeof(PrincipalDerived>)); if (principalDerived == null) { @@ -1318,7 +1337,7 @@ protected virtual void AssertComplexTypes(IModel model) Assert.Equal(principalBase, principalDerived.BaseType); - var complexCollection = principalDerived.GetDeclaredComplexProperties().Single(); + var complexCollection = principalDerived.GetDeclaredComplexProperties().Single(p => p.IsCollection); Assert.Equal( ["goo"], complexCollection.GetAnnotations().Select(a => a.Name)); @@ -1367,6 +1386,20 @@ protected virtual void AssertComplexTypes(IModel model) Assert.Equal(ExpectedComplexTypeProperties, collectionNestedComplexType.GetProperties().Count()); + var dependentComplexProperty = principalDerived.FindComplexProperty(nameof(PrincipalDerived>.Dependent))!; + Assert.False(dependentComplexProperty.IsCollection); + Assert.True(dependentComplexProperty.IsNullable); + Assert.Equal(typeof(DependentBase), dependentComplexProperty.ClrType); + Assert.Same(principalDerived, dependentComplexProperty.DeclaringType); + + var dependentComplexType = dependentComplexProperty.ComplexType; + Assert.Equal(typeof(DependentBase), dependentComplexType.ClrType); + Assert.True(dependentComplexType.HasSharedClrType); + Assert.IsType(dependentComplexType.ConstructorBinding); + var dependentIdProperty = dependentComplexType.FindProperty("Id")!; + Assert.Equal(typeof(byte?), dependentIdProperty.ClrType); + Assert.Empty(dependentComplexType.GetComplexProperties()); + Assert.Equal( [principalBase, principalDerived], model.GetEntityTypes()); @@ -1378,6 +1411,9 @@ protected virtual int ExpectedComplexTypeProperties protected virtual bool SupportsNonAutoLoadedProperties => true; + protected virtual bool SupportsIndexes + => true; + public class CustomValueComparer() : ValueComparer(false); public class ManyTypesIdConverter() : ValueConverter(v => v.Id, v => new ManyTypesId(v)); diff --git a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/DbContextModelBuilder.cs b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/DbContextModelBuilder.cs index 32cfcc070fb..c6eb8e1bbfa 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/DbContextModelBuilder.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/DbContextModelBuilder.cs @@ -59,6 +59,11 @@ private IRelationalModel CreateRelationalModel() var defaultTableMappings = new List>(); principalBase.SetRuntimeAnnotation("Relational:DefaultMappings", defaultTableMappings); var microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase = new TableBase("Microsoft.EntityFrameworkCore.Scaffolding.CompiledModelTestBase+PrincipalBase", null, relationalModel); + var dependent_IdColumnBase = new ColumnBase("Dependent_Id", "tinyint", microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase) + { + IsNullable = true + }; + microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase.Columns.Add("Dependent_Id", dependent_IdColumnBase); var discriminatorColumnBase = new ColumnBase("Discriminator", "nvarchar(55)", microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase); microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase.Columns.Add("Discriminator", discriminatorColumnBase); var enum1ColumnBase = new ColumnBase("Enum1", "int", microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase); @@ -556,6 +561,12 @@ private IRelationalModel CreateRelationalModel() }; principalBaseTable.Columns.Add("Deets", deetsColumn); deetsColumn.Accessors = ColumnAccessorsFactory.CreateGeneric(deetsColumn); + var dependentColumn = new JsonColumn("Dependent", "nvarchar(450)", principalBaseTable) + { + IsNullable = true + }; + principalBaseTable.Columns.Add("Dependent", dependentColumn); + dependentColumn.Accessors = ColumnAccessorsFactory.CreateGeneric(dependentColumn); var discriminatorColumn = new Column("Discriminator", "nvarchar(55)", principalBaseTable); principalBaseTable.Columns.Add("Discriminator", discriminatorColumn); discriminatorColumn.Accessors = ColumnAccessorsFactory.CreateGeneric(discriminatorColumn); @@ -769,45 +780,49 @@ private IRelationalModel CreateRelationalModel() }; principalBaseTable.Columns.Add("ValueTypeList", valueTypeListColumn); valueTypeListColumn.Accessors = ColumnAccessorsFactory.CreateGeneric(valueTypeListColumn); + var elementJsonObject = new RelationalJsonObject(dependentColumn, true); + var idJsonScalar = new RelationalJsonScalar("Id", elementJsonObject, true); + elementJsonObject.AddProperty(idJsonScalar); + dependentColumn.JsonElement = elementJsonObject; var arrayJsonArray39 = new RelationalJsonArray(manyOwnedColumn, false); - var elementJsonObject = new RelationalJsonObject(arrayJsonArray39, false); - var detailsJsonScalar = new RelationalJsonScalar("Details", elementJsonObject, true); - elementJsonObject.AddProperty(detailsJsonScalar); - var numberJsonScalar = new RelationalJsonScalar("Number", elementJsonObject, false); - elementJsonObject.AddProperty(numberJsonScalar); - var refTypeArrayJsonArray = new RelationalJsonArray("RefTypeArray", elementJsonObject, true); + var elementJsonObject0 = new RelationalJsonObject(arrayJsonArray39, false); + var detailsJsonScalar = new RelationalJsonScalar("Details", elementJsonObject0, true); + elementJsonObject0.AddProperty(detailsJsonScalar); + var numberJsonScalar = new RelationalJsonScalar("Number", elementJsonObject0, false); + elementJsonObject0.AddProperty(numberJsonScalar); + var refTypeArrayJsonArray = new RelationalJsonArray("RefTypeArray", elementJsonObject0, true); var scalarJsonScalar39 = new RelationalJsonScalar(refTypeArrayJsonArray, false); refTypeArrayJsonArray.ElementType = scalarJsonScalar39; - elementJsonObject.AddProperty(refTypeArrayJsonArray); - var refTypeEnumerableJsonArray = new RelationalJsonArray("RefTypeEnumerable", elementJsonObject, true); + elementJsonObject0.AddProperty(refTypeArrayJsonArray); + var refTypeEnumerableJsonArray = new RelationalJsonArray("RefTypeEnumerable", elementJsonObject0, true); var scalarJsonScalar40 = new RelationalJsonScalar(refTypeEnumerableJsonArray, false); refTypeEnumerableJsonArray.ElementType = scalarJsonScalar40; - elementJsonObject.AddProperty(refTypeEnumerableJsonArray); - var refTypeIListJsonArray = new RelationalJsonArray("RefTypeIList", elementJsonObject, true); + elementJsonObject0.AddProperty(refTypeEnumerableJsonArray); + var refTypeIListJsonArray = new RelationalJsonArray("RefTypeIList", elementJsonObject0, true); var scalarJsonScalar41 = new RelationalJsonScalar(refTypeIListJsonArray, false); refTypeIListJsonArray.ElementType = scalarJsonScalar41; - elementJsonObject.AddProperty(refTypeIListJsonArray); - var refTypeListJsonArray = new RelationalJsonArray("RefTypeList", elementJsonObject, true); + elementJsonObject0.AddProperty(refTypeIListJsonArray); + var refTypeListJsonArray = new RelationalJsonArray("RefTypeList", elementJsonObject0, true); var scalarJsonScalar42 = new RelationalJsonScalar(refTypeListJsonArray, false); refTypeListJsonArray.ElementType = scalarJsonScalar42; - elementJsonObject.AddProperty(refTypeListJsonArray); - var valueTypeArrayJsonArray = new RelationalJsonArray("ValueTypeArray", elementJsonObject, true); + elementJsonObject0.AddProperty(refTypeListJsonArray); + var valueTypeArrayJsonArray = new RelationalJsonArray("ValueTypeArray", elementJsonObject0, true); var scalarJsonScalar43 = new RelationalJsonScalar(valueTypeArrayJsonArray, false); valueTypeArrayJsonArray.ElementType = scalarJsonScalar43; - elementJsonObject.AddProperty(valueTypeArrayJsonArray); - var valueTypeEnumerableJsonArray = new RelationalJsonArray("ValueTypeEnumerable", elementJsonObject, true); + elementJsonObject0.AddProperty(valueTypeArrayJsonArray); + var valueTypeEnumerableJsonArray = new RelationalJsonArray("ValueTypeEnumerable", elementJsonObject0, true); var scalarJsonScalar44 = new RelationalJsonScalar(valueTypeEnumerableJsonArray, false); valueTypeEnumerableJsonArray.ElementType = scalarJsonScalar44; - elementJsonObject.AddProperty(valueTypeEnumerableJsonArray); - var valueTypeIListJsonArray = new RelationalJsonArray("ValueTypeIList", elementJsonObject, true); + elementJsonObject0.AddProperty(valueTypeEnumerableJsonArray); + var valueTypeIListJsonArray = new RelationalJsonArray("ValueTypeIList", elementJsonObject0, true); var scalarJsonScalar45 = new RelationalJsonScalar(valueTypeIListJsonArray, false); valueTypeIListJsonArray.ElementType = scalarJsonScalar45; - elementJsonObject.AddProperty(valueTypeIListJsonArray); - var valueTypeListJsonArray = new RelationalJsonArray("ValueTypeList", elementJsonObject, true); + elementJsonObject0.AddProperty(valueTypeIListJsonArray); + var valueTypeListJsonArray = new RelationalJsonArray("ValueTypeList", elementJsonObject0, true); var scalarJsonScalar46 = new RelationalJsonScalar(valueTypeListJsonArray, false); valueTypeListJsonArray.ElementType = scalarJsonScalar46; - elementJsonObject.AddProperty(valueTypeListJsonArray); - var principalJsonObject = new RelationalJsonObject("Principal", elementJsonObject, true); + elementJsonObject0.AddProperty(valueTypeListJsonArray); + var principalJsonObject = new RelationalJsonObject("Principal", elementJsonObject0, true); var alternateIdJsonScalar = new RelationalJsonScalar("AlternateId", principalJsonObject, false); principalJsonObject.AddProperty(alternateIdJsonScalar); var enum1JsonScalar = new RelationalJsonScalar("Enum1", principalJsonObject, false); @@ -818,8 +833,8 @@ private IRelationalModel CreateRelationalModel() principalJsonObject.AddProperty(flagsEnum1JsonScalar); var flagsEnum2JsonScalar = new RelationalJsonScalar("FlagsEnum2", principalJsonObject, false); principalJsonObject.AddProperty(flagsEnum2JsonScalar); - var idJsonScalar = new RelationalJsonScalar("Id", principalJsonObject, true); - principalJsonObject.AddProperty(idJsonScalar); + var idJsonScalar0 = new RelationalJsonScalar("Id", principalJsonObject, true); + principalJsonObject.AddProperty(idJsonScalar0); var refTypeArrayJsonArray0 = new RelationalJsonArray("RefTypeArray", principalJsonObject, true); var scalarJsonScalar47 = new RelationalJsonScalar(refTypeArrayJsonArray0, false); refTypeArrayJsonArray0.ElementType = scalarJsonScalar47; @@ -852,8 +867,8 @@ private IRelationalModel CreateRelationalModel() var scalarJsonScalar54 = new RelationalJsonScalar(valueTypeListJsonArray0, false); valueTypeListJsonArray0.ElementType = scalarJsonScalar54; principalJsonObject.AddProperty(valueTypeListJsonArray0); - elementJsonObject.AddProperty(principalJsonObject); - arrayJsonArray39.ElementType = elementJsonObject; + elementJsonObject0.AddProperty(principalJsonObject); + arrayJsonArray39.ElementType = elementJsonObject0; manyOwnedColumn.JsonElement = arrayJsonArray39; var arrayJsonArray40 = new RelationalJsonArray(owned_Principal_RefTypeArrayColumn, true); var scalarJsonScalar55 = new RelationalJsonScalar(arrayJsonArray40, false); @@ -1857,6 +1872,15 @@ private IRelationalModel CreateRelationalModel() pK_PrincipalBase.MappedKeys.Add(pK_PrincipalBaseKey); RelationalModel.GetOrCreateUniqueConstraints(pK_PrincipalBaseKey).Add(pK_PrincipalBase); principalBaseTable.UniqueConstraints.Add("PK_PrincipalBase", pK_PrincipalBase); + var iX_PrincipalBase_Id_Owned_Number = new TableIndex( + "IX_PrincipalBase_Id_Owned_Number", principalBaseTable, new[] { idColumn, owned_NumberColumn }, false); + iX_PrincipalBase_Id_Owned_Number.SetRowIndexValueFactory(new CompositeRowIndexValueFactory(iX_PrincipalBase_Id_Owned_Number)); + var iX_PrincipalBase_Id_Owned_NumberIx = RelationalModel.GetIndex(this, + "Microsoft.EntityFrameworkCore.Scaffolding.CompiledModelTestBase+PrincipalBase", + "IX_PrincipalBase_Id_Owned_Number"); + iX_PrincipalBase_Id_Owned_Number.MappedIndexes.Add(iX_PrincipalBase_Id_Owned_NumberIx); + RelationalModel.GetOrCreateTableIndexes(iX_PrincipalBase_Id_Owned_NumberIx).Add(iX_PrincipalBase_Id_Owned_Number); + principalBaseTable.Indexes.Add("IX_PrincipalBase_Id_Owned_Number", iX_PrincipalBase_Id_Owned_Number); var iX_PrincipalBase_PrincipalBaseId = new TableIndex( "IX_PrincipalBase_PrincipalBaseId", principalBaseTable, new[] { principalBaseIdColumn }, false); iX_PrincipalBase_PrincipalBaseId.SetRowIndexValueFactory(new SimpleRowIndexValueFactory(iX_PrincipalBase_PrincipalBaseId)); @@ -1866,6 +1890,15 @@ private IRelationalModel CreateRelationalModel() iX_PrincipalBase_PrincipalBaseId.MappedIndexes.Add(iX_PrincipalBase_PrincipalBaseIdIx); RelationalModel.GetOrCreateTableIndexes(iX_PrincipalBase_PrincipalBaseIdIx).Add(iX_PrincipalBase_PrincipalBaseId); principalBaseTable.Indexes.Add("IX_PrincipalBase_PrincipalBaseId", iX_PrincipalBase_PrincipalBaseId); + var iX_PrincipalDerived_Dependent = new TableIndex( + "IX_PrincipalDerived_Dependent", principalBaseTable, new[] { dependentColumn }, false); + iX_PrincipalDerived_Dependent.SetRowIndexValueFactory(new SimpleRowIndexValueFactory(iX_PrincipalDerived_Dependent)); + var iX_PrincipalDerived_DependentIx = RelationalModel.GetIndex(this, + "Microsoft.EntityFrameworkCore.Scaffolding.CompiledModelTestBase+PrincipalDerived>", + "IX_PrincipalDerived_Dependent"); + iX_PrincipalDerived_Dependent.MappedIndexes.Add(iX_PrincipalDerived_DependentIx); + RelationalModel.GetOrCreateTableIndexes(iX_PrincipalDerived_DependentIx).Add(iX_PrincipalDerived_Dependent); + principalBaseTable.Indexes.Add("IX_PrincipalDerived_Dependent", iX_PrincipalDerived_Dependent); var sqlQueryMappings0 = new List(); principalDerived.SetRuntimeAnnotation("Relational:SqlQueryMappings", sqlQueryMappings0); @@ -1940,99 +1973,116 @@ private IRelationalModel CreateRelationalModel() RelationalModel.CreateStoredProcedureParameterMapping(valueTypeIListParameter0, principalBase_UpdateUSproc.FindParameter("ValueTypeIList")!, principalDerived.FindProperty("ValueTypeIList")!, principalBase_UpdateSprocMapping0); RelationalModel.CreateStoredProcedureParameterMapping(valueTypeListParameter0, principalBase_UpdateUSproc.FindParameter("ValueTypeList")!, principalDerived.FindProperty("ValueTypeList")!, principalBase_UpdateSprocMapping0); - var ownedCollection = principalDerived.FindComplexProperty("ManyOwned")!.ComplexType; + var dependentBasebyte = principalDerived.FindComplexProperty("Dependent")!.ComplexType; var defaultTableMappings3 = new List>(); - ownedCollection.SetRuntimeAnnotation("Relational:DefaultMappings", defaultTableMappings3); - var microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3 = new TableMappingBase(ownedCollection, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase, null); + dependentBasebyte.SetRuntimeAnnotation("Relational:DefaultMappings", defaultTableMappings3); + var microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3 = new TableMappingBase(dependentBasebyte, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase, null); microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase.AddTypeMapping(microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3, null); defaultTableMappings3.Add(microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3); - RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_DetailsColumnBase, ownedCollection.FindProperty("Details")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3); - RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_NumberColumnBase, ownedCollection.FindProperty("Number")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3); - RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_RefTypeArrayColumnBase, ownedCollection.FindProperty("RefTypeArray")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3); - RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_RefTypeEnumerableColumnBase, ownedCollection.FindProperty("RefTypeEnumerable")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3); - RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_RefTypeIListColumnBase, ownedCollection.FindProperty("RefTypeIList")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3); - RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_RefTypeListColumnBase, ownedCollection.FindProperty("RefTypeList")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3); - RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_ValueTypeArrayColumnBase, ownedCollection.FindProperty("ValueTypeArray")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3); - RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_ValueTypeEnumerableColumnBase, ownedCollection.FindProperty("ValueTypeEnumerable")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3); - RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_ValueTypeIListColumnBase, ownedCollection.FindProperty("ValueTypeIList")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3); - RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_ValueTypeListColumnBase, ownedCollection.FindProperty("ValueTypeList")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("RefTypeArray")!, arrayJsonArray7, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("RefTypeEnumerable")!, arrayJsonArray8, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("RefTypeIList")!, arrayJsonArray9, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("RefTypeList")!, arrayJsonArray10, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("ValueTypeArray")!, arrayJsonArray11, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("ValueTypeEnumerable")!, arrayJsonArray12, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("ValueTypeIList")!, arrayJsonArray13, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("ValueTypeList")!, arrayJsonArray14, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3); + RelationalModel.CreateColumnMapping((ColumnBase)dependent_IdColumnBase, dependentBasebyte.FindProperty("Id")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase3); var tableMappings3 = new List(); - ownedCollection.SetRuntimeAnnotation("Relational:TableMappings", tableMappings3); - var principalBaseTableMapping3 = new TableMapping(ownedCollection, principalBaseTable, true); + dependentBasebyte.SetRuntimeAnnotation("Relational:TableMappings", tableMappings3); + var principalBaseTableMapping3 = new TableMapping(dependentBasebyte, principalBaseTable, true); principalBaseTable.AddTypeMapping(principalBaseTableMapping3, null); tableMappings3.Add(principalBaseTableMapping3); - RelationalModel.CreateJsonElementMapping(principalDerived.FindComplexProperty("ManyOwned")!, arrayJsonArray39, principalBaseTableMapping3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("Details")!, detailsJsonScalar, principalBaseTableMapping3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("Number")!, numberJsonScalar, principalBaseTableMapping3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("RefTypeArray")!, refTypeArrayJsonArray, principalBaseTableMapping3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("RefTypeEnumerable")!, refTypeEnumerableJsonArray, principalBaseTableMapping3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("RefTypeIList")!, refTypeIListJsonArray, principalBaseTableMapping3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("RefTypeList")!, refTypeListJsonArray, principalBaseTableMapping3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("ValueTypeArray")!, valueTypeArrayJsonArray, principalBaseTableMapping3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("ValueTypeEnumerable")!, valueTypeEnumerableJsonArray, principalBaseTableMapping3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("ValueTypeIList")!, valueTypeIListJsonArray, principalBaseTableMapping3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("ValueTypeList")!, valueTypeListJsonArray, principalBaseTableMapping3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!, principalJsonObject, principalBaseTableMapping3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("AlternateId")!, alternateIdJsonScalar, principalBaseTableMapping3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("Enum1")!, enum1JsonScalar, principalBaseTableMapping3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("Enum2")!, enum2JsonScalar, principalBaseTableMapping3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("FlagsEnum1")!, flagsEnum1JsonScalar, principalBaseTableMapping3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("FlagsEnum2")!, flagsEnum2JsonScalar, principalBaseTableMapping3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("Id")!, idJsonScalar, principalBaseTableMapping3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("RefTypeArray")!, refTypeArrayJsonArray0, principalBaseTableMapping3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("RefTypeEnumerable")!, refTypeEnumerableJsonArray0, principalBaseTableMapping3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("RefTypeIList")!, refTypeIListJsonArray0, principalBaseTableMapping3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("RefTypeList")!, refTypeListJsonArray0, principalBaseTableMapping3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("ValueTypeArray")!, valueTypeArrayJsonArray0, principalBaseTableMapping3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("ValueTypeEnumerable")!, valueTypeEnumerableJsonArray0, principalBaseTableMapping3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("ValueTypeIList")!, valueTypeIListJsonArray0, principalBaseTableMapping3); - RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("ValueTypeList")!, valueTypeListJsonArray0, principalBaseTableMapping3); + RelationalModel.CreateJsonElementMapping(principalDerived.FindComplexProperty("Dependent")!, elementJsonObject, principalBaseTableMapping3); + RelationalModel.CreateJsonElementMapping(dependentBasebyte.FindProperty("Id")!, idJsonScalar, principalBaseTableMapping3); - var principalBase1 = ownedCollection.FindComplexProperty("Principal")!.ComplexType; + var ownedCollection = principalDerived.FindComplexProperty("ManyOwned")!.ComplexType; var defaultTableMappings4 = new List>(); - principalBase1.SetRuntimeAnnotation("Relational:DefaultMappings", defaultTableMappings4); - var microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4 = new TableMappingBase(principalBase1, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase, null); + ownedCollection.SetRuntimeAnnotation("Relational:DefaultMappings", defaultTableMappings4); + var microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4 = new TableMappingBase(ownedCollection, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase, null); microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase.AddTypeMapping(microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4, null); defaultTableMappings4.Add(microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); - RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_AlternateIdColumnBase, principalBase1.FindProperty("AlternateId")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); - RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_Enum1ColumnBase, principalBase1.FindProperty("Enum1")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); - RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_Enum2ColumnBase, principalBase1.FindProperty("Enum2")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); - RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_FlagsEnum1ColumnBase, principalBase1.FindProperty("FlagsEnum1")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); - RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_FlagsEnum2ColumnBase, principalBase1.FindProperty("FlagsEnum2")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); - RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_IdColumnBase, principalBase1.FindProperty("Id")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); - RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_RefTypeArrayColumnBase, principalBase1.FindProperty("RefTypeArray")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); - RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_RefTypeEnumerableColumnBase, principalBase1.FindProperty("RefTypeEnumerable")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); - RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_RefTypeIListColumnBase, principalBase1.FindProperty("RefTypeIList")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); - RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_RefTypeListColumnBase, principalBase1.FindProperty("RefTypeList")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); - RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_ValueTypeArrayColumnBase, principalBase1.FindProperty("ValueTypeArray")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); - RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_ValueTypeEnumerableColumnBase, principalBase1.FindProperty("ValueTypeEnumerable")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); - RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_ValueTypeIListColumnBase, principalBase1.FindProperty("ValueTypeIList")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); - RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_ValueTypeListColumnBase, principalBase1.FindProperty("ValueTypeList")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); - RelationalModel.CreateJsonElementMapping(principalBase1.FindProperty("RefTypeArray")!, arrayJsonArray, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); - RelationalModel.CreateJsonElementMapping(principalBase1.FindProperty("RefTypeEnumerable")!, arrayJsonArray0, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); - RelationalModel.CreateJsonElementMapping(principalBase1.FindProperty("RefTypeIList")!, arrayJsonArray1, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); - RelationalModel.CreateJsonElementMapping(principalBase1.FindProperty("RefTypeList")!, arrayJsonArray2, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); - RelationalModel.CreateJsonElementMapping(principalBase1.FindProperty("ValueTypeArray")!, arrayJsonArray3, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); - RelationalModel.CreateJsonElementMapping(principalBase1.FindProperty("ValueTypeEnumerable")!, arrayJsonArray4, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); - RelationalModel.CreateJsonElementMapping(principalBase1.FindProperty("ValueTypeIList")!, arrayJsonArray5, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); - RelationalModel.CreateJsonElementMapping(principalBase1.FindProperty("ValueTypeList")!, arrayJsonArray6, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); + RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_DetailsColumnBase, ownedCollection.FindProperty("Details")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); + RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_NumberColumnBase, ownedCollection.FindProperty("Number")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); + RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_RefTypeArrayColumnBase, ownedCollection.FindProperty("RefTypeArray")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); + RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_RefTypeEnumerableColumnBase, ownedCollection.FindProperty("RefTypeEnumerable")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); + RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_RefTypeIListColumnBase, ownedCollection.FindProperty("RefTypeIList")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); + RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_RefTypeListColumnBase, ownedCollection.FindProperty("RefTypeList")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); + RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_ValueTypeArrayColumnBase, ownedCollection.FindProperty("ValueTypeArray")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); + RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_ValueTypeEnumerableColumnBase, ownedCollection.FindProperty("ValueTypeEnumerable")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); + RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_ValueTypeIListColumnBase, ownedCollection.FindProperty("ValueTypeIList")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); + RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_ValueTypeListColumnBase, ownedCollection.FindProperty("ValueTypeList")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("RefTypeArray")!, arrayJsonArray7, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("RefTypeEnumerable")!, arrayJsonArray8, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("RefTypeIList")!, arrayJsonArray9, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("RefTypeList")!, arrayJsonArray10, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("ValueTypeArray")!, arrayJsonArray11, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("ValueTypeEnumerable")!, arrayJsonArray12, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("ValueTypeIList")!, arrayJsonArray13, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("ValueTypeList")!, arrayJsonArray14, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase4); var tableMappings4 = new List(); - principalBase1.SetRuntimeAnnotation("Relational:TableMappings", tableMappings4); - var principalBaseTableMapping4 = new TableMapping(principalBase1, principalBaseTable, true); + ownedCollection.SetRuntimeAnnotation("Relational:TableMappings", tableMappings4); + var principalBaseTableMapping4 = new TableMapping(ownedCollection, principalBaseTable, true); principalBaseTable.AddTypeMapping(principalBaseTableMapping4, null); tableMappings4.Add(principalBaseTableMapping4); + RelationalModel.CreateJsonElementMapping(principalDerived.FindComplexProperty("ManyOwned")!, arrayJsonArray39, principalBaseTableMapping4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("Details")!, detailsJsonScalar, principalBaseTableMapping4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("Number")!, numberJsonScalar, principalBaseTableMapping4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("RefTypeArray")!, refTypeArrayJsonArray, principalBaseTableMapping4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("RefTypeEnumerable")!, refTypeEnumerableJsonArray, principalBaseTableMapping4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("RefTypeIList")!, refTypeIListJsonArray, principalBaseTableMapping4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("RefTypeList")!, refTypeListJsonArray, principalBaseTableMapping4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("ValueTypeArray")!, valueTypeArrayJsonArray, principalBaseTableMapping4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("ValueTypeEnumerable")!, valueTypeEnumerableJsonArray, principalBaseTableMapping4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("ValueTypeIList")!, valueTypeIListJsonArray, principalBaseTableMapping4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindProperty("ValueTypeList")!, valueTypeListJsonArray, principalBaseTableMapping4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!, principalJsonObject, principalBaseTableMapping4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("AlternateId")!, alternateIdJsonScalar, principalBaseTableMapping4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("Enum1")!, enum1JsonScalar, principalBaseTableMapping4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("Enum2")!, enum2JsonScalar, principalBaseTableMapping4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("FlagsEnum1")!, flagsEnum1JsonScalar, principalBaseTableMapping4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("FlagsEnum2")!, flagsEnum2JsonScalar, principalBaseTableMapping4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("Id")!, idJsonScalar0, principalBaseTableMapping4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("RefTypeArray")!, refTypeArrayJsonArray0, principalBaseTableMapping4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("RefTypeEnumerable")!, refTypeEnumerableJsonArray0, principalBaseTableMapping4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("RefTypeIList")!, refTypeIListJsonArray0, principalBaseTableMapping4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("RefTypeList")!, refTypeListJsonArray0, principalBaseTableMapping4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("ValueTypeArray")!, valueTypeArrayJsonArray0, principalBaseTableMapping4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("ValueTypeEnumerable")!, valueTypeEnumerableJsonArray0, principalBaseTableMapping4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("ValueTypeIList")!, valueTypeIListJsonArray0, principalBaseTableMapping4); + RelationalModel.CreateJsonElementMapping(ownedCollection.FindComplexProperty("Principal")!.ComplexType.FindProperty("ValueTypeList")!, valueTypeListJsonArray0, principalBaseTableMapping4); + + var principalBase1 = ownedCollection.FindComplexProperty("Principal")!.ComplexType; + + var defaultTableMappings5 = new List>(); + principalBase1.SetRuntimeAnnotation("Relational:DefaultMappings", defaultTableMappings5); + var microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase5 = new TableMappingBase(principalBase1, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase, null); + microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseTableBase.AddTypeMapping(microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase5, null); + defaultTableMappings5.Add(microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase5); + RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_AlternateIdColumnBase, principalBase1.FindProperty("AlternateId")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase5); + RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_Enum1ColumnBase, principalBase1.FindProperty("Enum1")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase5); + RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_Enum2ColumnBase, principalBase1.FindProperty("Enum2")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase5); + RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_FlagsEnum1ColumnBase, principalBase1.FindProperty("FlagsEnum1")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase5); + RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_FlagsEnum2ColumnBase, principalBase1.FindProperty("FlagsEnum2")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase5); + RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_IdColumnBase, principalBase1.FindProperty("Id")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase5); + RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_RefTypeArrayColumnBase, principalBase1.FindProperty("RefTypeArray")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase5); + RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_RefTypeEnumerableColumnBase, principalBase1.FindProperty("RefTypeEnumerable")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase5); + RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_RefTypeIListColumnBase, principalBase1.FindProperty("RefTypeIList")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase5); + RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_RefTypeListColumnBase, principalBase1.FindProperty("RefTypeList")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase5); + RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_ValueTypeArrayColumnBase, principalBase1.FindProperty("ValueTypeArray")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase5); + RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_ValueTypeEnumerableColumnBase, principalBase1.FindProperty("ValueTypeEnumerable")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase5); + RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_ValueTypeIListColumnBase, principalBase1.FindProperty("ValueTypeIList")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase5); + RelationalModel.CreateColumnMapping((ColumnBase)manyOwned_Principal_ValueTypeListColumnBase, principalBase1.FindProperty("ValueTypeList")!, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase5); + RelationalModel.CreateJsonElementMapping(principalBase1.FindProperty("RefTypeArray")!, arrayJsonArray, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase5); + RelationalModel.CreateJsonElementMapping(principalBase1.FindProperty("RefTypeEnumerable")!, arrayJsonArray0, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase5); + RelationalModel.CreateJsonElementMapping(principalBase1.FindProperty("RefTypeIList")!, arrayJsonArray1, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase5); + RelationalModel.CreateJsonElementMapping(principalBase1.FindProperty("RefTypeList")!, arrayJsonArray2, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase5); + RelationalModel.CreateJsonElementMapping(principalBase1.FindProperty("ValueTypeArray")!, arrayJsonArray3, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase5); + RelationalModel.CreateJsonElementMapping(principalBase1.FindProperty("ValueTypeEnumerable")!, arrayJsonArray4, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase5); + RelationalModel.CreateJsonElementMapping(principalBase1.FindProperty("ValueTypeIList")!, arrayJsonArray5, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase5); + RelationalModel.CreateJsonElementMapping(principalBase1.FindProperty("ValueTypeList")!, arrayJsonArray6, microsoftEntityFrameworkCoreScaffoldingCompiledModelTestBasePrincipalBaseMappingBase5); + + var tableMappings5 = new List(); + principalBase1.SetRuntimeAnnotation("Relational:TableMappings", tableMappings5); + var principalBaseTableMapping5 = new TableMapping(principalBase1, principalBaseTable, true); + principalBaseTable.AddTypeMapping(principalBaseTableMapping5, null); + tableMappings5.Add(principalBaseTableMapping5); var fK_PrincipalBase_PrincipalBase_PrincipalBaseId = new ForeignKeyConstraint( "FK_PrincipalBase_PrincipalBase_PrincipalBaseId", principalBaseTable, principalBaseTable, new[] { principalBaseIdColumn }, diff --git a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/DependentBaseUnsafeAccessors.cs b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/DependentBaseUnsafeAccessors.cs new file mode 100644 index 00000000000..ae6e1779611 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/DependentBaseUnsafeAccessors.cs @@ -0,0 +1,15 @@ +// +using System.Runtime.CompilerServices; +using Microsoft.EntityFrameworkCore.Scaffolding; + +#pragma warning disable 219, 612, 618 +#nullable disable + +namespace TestNamespace +{ + public static class DependentBaseUnsafeAccessors + { + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "k__BackingField")] + public static extern ref TKey Id(CompiledModelTestBase.DependentBase @this); + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalBaseEntityType.cs b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalBaseEntityType.cs index 43d092f6fb1..135b5c2899a 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalBaseEntityType.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalBaseEntityType.cs @@ -38,6 +38,7 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas navigationCount: 1, foreignKeyCount: 1, unnamedIndexCount: 1, + namedIndexCount: 1, keyCount: 1); var id = runtimeEntityType.AddProperty( @@ -1011,6 +1012,10 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas var index = runtimeEntityType.AddIndex( new[] { principalBaseId }); + var iX_PrincipalBase_Id_Owned_Number = runtimeEntityType.AddIndex( + new[] { id, runtimeEntityType.FindComplexProperty("Owned").ComplexType.FindProperty("Number") }, + name: "IX_PrincipalBase_Id_Owned_Number"); + return runtimeEntityType; } diff --git a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedEntityType.cs b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedEntityType.cs index c063c753540..fb85e11b8ba 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedEntityType.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedEntityType.cs @@ -34,12 +34,131 @@ public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType bas discriminatorProperty: "Discriminator", discriminatorValue: "PrincipalDerived>", propertyCount: 0, - complexPropertyCount: 1); + complexPropertyCount: 2, + namedIndexCount: 1); + DependentComplexProperty.Create(runtimeEntityType); ManyOwnedComplexProperty.Create(runtimeEntityType); + var iX_PrincipalDerived_Dependent = runtimeEntityType.AddIndex( + new[] { runtimeEntityType.FindComplexProperty("Dependent") }, + name: "IX_PrincipalDerived_Dependent"); + return runtimeEntityType; } + public static class DependentComplexProperty + { + public static RuntimeComplexProperty Create(RuntimeEntityType declaringType) + { + var complexProperty = declaringType.AddComplexProperty("Dependent", + typeof(CompiledModelTestBase.DependentBase), + "Microsoft.EntityFrameworkCore.Scaffolding.CompiledModelTestBase+PrincipalDerived>.Dependent#DependentBase", + typeof(CompiledModelTestBase.DependentBase), + propertyInfo: typeof(CompiledModelTestBase.PrincipalDerived>).GetProperty("Dependent", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(CompiledModelTestBase.PrincipalDerived>).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true, + propertyCount: 1); + + var complexType = complexProperty.ComplexType; + complexProperty.SetGetter( + CompiledModelTestBase.DependentBase (CompiledModelTestBase.PrincipalDerived> instance) => PrincipalDerivedUnsafeAccessors>.Dependent(instance), + bool (CompiledModelTestBase.PrincipalDerived> instance) => PrincipalDerivedUnsafeAccessors>.Dependent(instance) == null); + complexProperty.SetSetter( + CompiledModelTestBase.PrincipalDerived> (CompiledModelTestBase.PrincipalDerived> instance, CompiledModelTestBase.DependentBase value) => + { + PrincipalDerivedUnsafeAccessors>.Dependent(instance) = value; + return instance; + }); + complexProperty.SetMaterializationSetter( + CompiledModelTestBase.PrincipalDerived> (CompiledModelTestBase.PrincipalDerived> instance, CompiledModelTestBase.DependentBase value) => + { + PrincipalDerivedUnsafeAccessors>.Dependent(instance) = value; + return instance; + }); + complexProperty.SetAccessors( + CompiledModelTestBase.DependentBase (IInternalEntry entry) => PrincipalDerivedUnsafeAccessors>.Dependent(((CompiledModelTestBase.PrincipalDerived>)(entry.Entity))), + CompiledModelTestBase.DependentBase (IInternalEntry entry) => PrincipalDerivedUnsafeAccessors>.Dependent(((CompiledModelTestBase.PrincipalDerived>)(entry.Entity))), + null, + CompiledModelTestBase.DependentBase (IInternalEntry entry) => entry.GetCurrentValue>(complexProperty)); + complexProperty.SetPropertyIndexes( + index: 2, + originalValueIndex: 40, + shadowIndex: -1, + relationshipIndex: -1, + storeGenerationIndex: -1); + var id = complexType.AddProperty( + "Id", + typeof(byte?), + propertyInfo: typeof(CompiledModelTestBase.DependentBase).GetProperty("Id", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + fieldInfo: typeof(CompiledModelTestBase.DependentBase).GetField("k__BackingField", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly), + nullable: true); + id.SetGetter( + byte? (CompiledModelTestBase.PrincipalDerived> entity, IReadOnlyList indices) => (PrincipalDerivedUnsafeAccessors>.Dependent(entity) == null ? default(byte? ) : DependentBaseUnsafeAccessors.Id(PrincipalDerivedUnsafeAccessors>.Dependent(entity))), + bool (CompiledModelTestBase.PrincipalDerived> entity, IReadOnlyList indices) => !((PrincipalDerivedUnsafeAccessors>.Dependent(entity) == null ? default(byte? ) : DependentBaseUnsafeAccessors.Id(PrincipalDerivedUnsafeAccessors>.Dependent(entity))).HasValue), + byte? (CompiledModelTestBase.DependentBase instance) => DependentBaseUnsafeAccessors.Id(instance), + bool (CompiledModelTestBase.DependentBase instance) => !(DependentBaseUnsafeAccessors.Id(instance).HasValue)); + id.SetSetter( + (CompiledModelTestBase.PrincipalDerived> entity, IReadOnlyList indices, byte? value) => + { + var level1 = PrincipalDerivedUnsafeAccessors>.Dependent(entity); + DependentBaseUnsafeAccessors.Id(level1) = value; + }, + CompiledModelTestBase.DependentBase (CompiledModelTestBase.DependentBase instance, byte? value) => + { + DependentBaseUnsafeAccessors.Id(instance) = value; + return instance; + }); + id.SetMaterializationSetter( + (CompiledModelTestBase.PrincipalDerived> entity, IReadOnlyList indices, byte? value) => + { + var level1 = PrincipalDerivedUnsafeAccessors>.Dependent(entity); + DependentBaseUnsafeAccessors.Id(level1) = value; + }, + CompiledModelTestBase.DependentBase (CompiledModelTestBase.DependentBase instance, byte? value) => + { + DependentBaseUnsafeAccessors.Id(instance) = value; + return instance; + }); + id.SetAccessors( + byte? (IInternalEntry entry) => (PrincipalDerivedUnsafeAccessors>.Dependent(((CompiledModelTestBase.PrincipalDerived>)(entry.Entity))) == null ? default(byte? ) : DependentBaseUnsafeAccessors.Id(PrincipalDerivedUnsafeAccessors>.Dependent(((CompiledModelTestBase.PrincipalDerived>)(entry.Entity))))), + byte? (IInternalEntry entry) => (PrincipalDerivedUnsafeAccessors>.Dependent(((CompiledModelTestBase.PrincipalDerived>)(entry.Entity))) == null ? default(byte? ) : DependentBaseUnsafeAccessors.Id(PrincipalDerivedUnsafeAccessors>.Dependent(((CompiledModelTestBase.PrincipalDerived>)(entry.Entity))))), + byte? (IInternalEntry entry) => entry.ReadOriginalValue(id, 41), + byte? (IInternalEntry entry) => entry.GetCurrentValue(id)); + id.SetPropertyIndexes( + index: 39, + originalValueIndex: 41, + shadowIndex: -1, + relationshipIndex: -1, + storeGenerationIndex: -1); + id.TypeMapping = SqlServerByteTypeMapping.Default.Clone( + comparer: new ValueComparer( + bool (byte v1, byte v2) => v1 == v2, + int (byte v) => ((int)v), + byte (byte v) => v), + keyComparer: new ValueComparer( + bool (byte v1, byte v2) => v1 == v2, + int (byte v) => ((int)v), + byte (byte v) => v), + providerValueComparer: new ValueComparer( + bool (byte v1, byte v2) => v1 == v2, + int (byte v) => ((int)v), + byte (byte v) => v)); + id.SetComparer(new NullableValueComparer(id.TypeMapping.Comparer)); + id.SetKeyComparer(new NullableValueComparer(id.TypeMapping.KeyComparer)); + id.AddAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.None); + + complexType.AddAnnotation("Relational:ContainerColumnName", "Dependent"); + complexType.AddAnnotation("Relational:ContainerColumnType", "nvarchar(450)"); + complexType.AddAnnotation("Relational:FunctionName", null); + complexType.AddAnnotation("Relational:Schema", null); + complexType.AddAnnotation("Relational:SqlQuery", "select * from PrincipalBase"); + complexType.AddAnnotation("Relational:TableName", "PrincipalBase"); + complexType.AddAnnotation("Relational:ViewName", null); + complexType.AddAnnotation("Relational:ViewSchema", null); + return complexProperty; + } + } + public static class ManyOwnedComplexProperty { public static RuntimeComplexProperty Create(RuntimeEntityType declaringType) @@ -83,7 +202,7 @@ public static RuntimeComplexProperty Create(RuntimeEntityType declaringType) IList (IInternalEntry entry) => entry.GetCurrentValue>(complexProperty)); complexProperty.SetPropertyIndexes( index: 0, - originalValueIndex: 40, + originalValueIndex: 42, shadowIndex: -1, relationshipIndex: -1, storeGenerationIndex: -1); @@ -2434,6 +2553,9 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) var valueTypeEnumerable1 = principalBase.FindProperty("ValueTypeEnumerable"); var valueTypeIList1 = principalBase.FindProperty("ValueTypeIList"); var valueTypeList1 = principalBase.FindProperty("ValueTypeList"); + var dependent = runtimeEntityType.FindComplexProperty("Dependent"); + var dependentBasebyte = dependent.ComplexType; + var id1 = dependentBasebyte.FindProperty("Id"); var manyOwned = runtimeEntityType.FindComplexProperty("ManyOwned"); var ownedCollection = manyOwned.ComplexType; var details0 = ownedCollection.FindProperty("Details"); @@ -2453,7 +2575,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) var enum21 = principalBase0.FindProperty("Enum2"); var flagsEnum11 = principalBase0.FindProperty("FlagsEnum1"); var flagsEnum21 = principalBase0.FindProperty("FlagsEnum2"); - var id1 = principalBase0.FindProperty("Id"); + var id2 = principalBase0.FindProperty("Id"); var refTypeArray3 = principalBase0.FindProperty("RefTypeArray"); var refTypeEnumerable3 = principalBase0.FindProperty("RefTypeEnumerable"); var refTypeIList3 = principalBase0.FindProperty("RefTypeIList"); @@ -2469,7 +2591,7 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) var structuralType1 = ((CompiledModelTestBase.PrincipalDerived>)(source.Entity)); var liftedArg0 = ((ISnapshot)(new Snapshot, IList, List, DateTime[], IEnumerable, IList, List, string, int, IPAddress[], IEnumerable, IList, List, DateTime[], IEnumerable, IList, List, object, Guid, CompiledModelTestBase.AnEnum, CompiledModelTestBase.AnEnum?, CompiledModelTestBase.AFlagsEnum>((source.GetCurrentValue(id) == null ? null : ((ValueComparer)(((IProperty)id).GetValueComparer())).Snapshot(source.GetCurrentValue(id))), (source.GetCurrentValue(discriminator) == null ? null : ((ValueComparer)(((IProperty)discriminator).GetValueComparer())).Snapshot(source.GetCurrentValue(discriminator))), ((ValueComparer)(((IProperty)enum1).GetValueComparer())).Snapshot(source.GetCurrentValue(enum1)), (source.GetCurrentValue(enum2) == null ? null : ((ValueComparer)(((IProperty)enum2).GetValueComparer())).Snapshot(source.GetCurrentValue(enum2))), ((ValueComparer)(((IProperty)flagsEnum1).GetValueComparer())).Snapshot(source.GetCurrentValue(flagsEnum1)), ((ValueComparer)(((IProperty)flagsEnum2).GetValueComparer())).Snapshot(source.GetCurrentValue(flagsEnum2)), (source.GetCurrentValue(principalBaseId) == null ? null : ((ValueComparer)(((IProperty)principalBaseId).GetValueComparer())).Snapshot(source.GetCurrentValue(principalBaseId))), (((object)(source.GetCurrentValue(refTypeArray))) == null ? null : ((IPAddress[])(((ValueComparer)(((IProperty)refTypeArray).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue(refTypeArray))))))), (((object)(source.GetCurrentValue>(refTypeEnumerable))) == null ? null : ((IEnumerable)(((ValueComparer)(((IProperty)refTypeEnumerable).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeEnumerable))))))), (((object)(source.GetCurrentValue>(refTypeIList))) == null ? null : ((IList)(((ValueComparer)(((IProperty)refTypeIList).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeIList))))))), (((object)(source.GetCurrentValue>(refTypeList))) == null ? null : ((List)(((ValueComparer)(((IProperty)refTypeList).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeList))))))), (((IEnumerable)(source.GetCurrentValue(valueTypeArray))) == null ? null : ((DateTime[])(((ValueComparer>)(((IProperty)valueTypeArray).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue(valueTypeArray))))))), (source.GetCurrentValue>(valueTypeEnumerable) == null ? null : ((ValueComparer>)(((IProperty)valueTypeEnumerable).GetValueComparer())).Snapshot(source.GetCurrentValue>(valueTypeEnumerable))), (((IEnumerable)(source.GetCurrentValue>(valueTypeIList))) == null ? null : ((IList)(((ValueComparer>)(((IProperty)valueTypeIList).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue>(valueTypeIList))))))), (((IEnumerable)(source.GetCurrentValue>(valueTypeList))) == null ? null : ((List)(((ValueComparer>)(((IProperty)valueTypeList).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue>(valueTypeList))))))), (source.GetCurrentValue(details) == null ? null : ((ValueComparer)(((IProperty)details).GetValueComparer())).Snapshot(source.GetCurrentValue(details))), ((ValueComparer)(((IProperty)number).GetValueComparer())).Snapshot(source.GetCurrentValue(number)), (((object)(source.GetCurrentValue(refTypeArray0))) == null ? null : ((IPAddress[])(((ValueComparer)(((IProperty)refTypeArray0).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue(refTypeArray0))))))), (((object)(source.GetCurrentValue>(refTypeEnumerable0))) == null ? null : ((IEnumerable)(((ValueComparer)(((IProperty)refTypeEnumerable0).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeEnumerable0))))))), (((object)(source.GetCurrentValue>(refTypeIList0))) == null ? null : ((IList)(((ValueComparer)(((IProperty)refTypeIList0).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeIList0))))))), (((object)(source.GetCurrentValue>(refTypeList0))) == null ? null : ((List)(((ValueComparer)(((IProperty)refTypeList0).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeList0))))))), (((IEnumerable)(source.GetCurrentValue(valueTypeArray0))) == null ? null : ((DateTime[])(((ValueComparer>)(((IProperty)valueTypeArray0).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue(valueTypeArray0))))))), (source.GetCurrentValue>(valueTypeEnumerable0) == null ? null : ((ValueComparer>)(((IProperty)valueTypeEnumerable0).GetValueComparer())).Snapshot(source.GetCurrentValue>(valueTypeEnumerable0))), (((IEnumerable)(source.GetCurrentValue>(valueTypeIList0))) == null ? null : ((IList)(((ValueComparer>)(((IProperty)valueTypeIList0).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue>(valueTypeIList0))))))), (((IEnumerable)(source.GetCurrentValue>(valueTypeList0))) == null ? null : ((List)(((ValueComparer>)(((IProperty)valueTypeList0).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue>(valueTypeList0))))))), ((object)(source.GetCurrentValue(principal))), ((ValueComparer)(((IProperty)alternateId).GetValueComparer())).Snapshot(source.GetCurrentValue(alternateId)), ((ValueComparer)(((IProperty)enum10).GetValueComparer())).Snapshot(source.GetCurrentValue(enum10)), (source.GetCurrentValue(enum20) == null ? null : ((ValueComparer)(((IProperty)enum20).GetValueComparer())).Snapshot(source.GetCurrentValue(enum20))), ((ValueComparer)(((IProperty)flagsEnum10).GetValueComparer())).Snapshot(source.GetCurrentValue(flagsEnum10))))); var structuralType2 = ((CompiledModelTestBase.PrincipalDerived>)(source.Entity)); - return ((ISnapshot)(new MultiSnapshot(new ISnapshot[] { liftedArg0, ((ISnapshot)(new Snapshot, IList, List, DateTime[], IEnumerable, IList, List, object>(((ValueComparer)(((IProperty)flagsEnum20).GetValueComparer())).Snapshot(source.GetCurrentValue(flagsEnum20)), (source.GetCurrentValue(id0) == null ? null : ((ValueComparer)(((IProperty)id0).GetValueComparer())).Snapshot(source.GetCurrentValue(id0))), (((object)(source.GetCurrentValue(refTypeArray1))) == null ? null : ((IPAddress[])(((ValueComparer)(((IProperty)refTypeArray1).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue(refTypeArray1))))))), (((object)(source.GetCurrentValue>(refTypeEnumerable1))) == null ? null : ((IEnumerable)(((ValueComparer)(((IProperty)refTypeEnumerable1).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeEnumerable1))))))), (((object)(source.GetCurrentValue>(refTypeIList1))) == null ? null : ((IList)(((ValueComparer)(((IProperty)refTypeIList1).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeIList1))))))), (((object)(source.GetCurrentValue>(refTypeList1))) == null ? null : ((List)(((ValueComparer)(((IProperty)refTypeList1).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeList1))))))), (((IEnumerable)(source.GetCurrentValue(valueTypeArray1))) == null ? null : ((DateTime[])(((ValueComparer>)(((IProperty)valueTypeArray1).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue(valueTypeArray1))))))), (source.GetCurrentValue>(valueTypeEnumerable1) == null ? null : ((ValueComparer>)(((IProperty)valueTypeEnumerable1).GetValueComparer())).Snapshot(source.GetCurrentValue>(valueTypeEnumerable1))), (((IEnumerable)(source.GetCurrentValue>(valueTypeIList1))) == null ? null : ((IList)(((ValueComparer>)(((IProperty)valueTypeIList1).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue>(valueTypeIList1))))))), (((IEnumerable)(source.GetCurrentValue>(valueTypeList1))) == null ? null : ((List)(((ValueComparer>)(((IProperty)valueTypeList1).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue>(valueTypeList1))))))), SnapshotFactoryFactory.SnapshotComplexCollection(((IList)(source.GetCurrentValue>(manyOwned))), manyOwned)))) }))); + return ((ISnapshot)(new MultiSnapshot(new ISnapshot[] { liftedArg0, ((ISnapshot)(new Snapshot, IList, List, DateTime[], IEnumerable, IList, List, object, byte?, object>(((ValueComparer)(((IProperty)flagsEnum20).GetValueComparer())).Snapshot(source.GetCurrentValue(flagsEnum20)), (source.GetCurrentValue(id0) == null ? null : ((ValueComparer)(((IProperty)id0).GetValueComparer())).Snapshot(source.GetCurrentValue(id0))), (((object)(source.GetCurrentValue(refTypeArray1))) == null ? null : ((IPAddress[])(((ValueComparer)(((IProperty)refTypeArray1).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue(refTypeArray1))))))), (((object)(source.GetCurrentValue>(refTypeEnumerable1))) == null ? null : ((IEnumerable)(((ValueComparer)(((IProperty)refTypeEnumerable1).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeEnumerable1))))))), (((object)(source.GetCurrentValue>(refTypeIList1))) == null ? null : ((IList)(((ValueComparer)(((IProperty)refTypeIList1).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeIList1))))))), (((object)(source.GetCurrentValue>(refTypeList1))) == null ? null : ((List)(((ValueComparer)(((IProperty)refTypeList1).GetValueComparer())).Snapshot(((object)(source.GetCurrentValue>(refTypeList1))))))), (((IEnumerable)(source.GetCurrentValue(valueTypeArray1))) == null ? null : ((DateTime[])(((ValueComparer>)(((IProperty)valueTypeArray1).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue(valueTypeArray1))))))), (source.GetCurrentValue>(valueTypeEnumerable1) == null ? null : ((ValueComparer>)(((IProperty)valueTypeEnumerable1).GetValueComparer())).Snapshot(source.GetCurrentValue>(valueTypeEnumerable1))), (((IEnumerable)(source.GetCurrentValue>(valueTypeIList1))) == null ? null : ((IList)(((ValueComparer>)(((IProperty)valueTypeIList1).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue>(valueTypeIList1))))))), (((IEnumerable)(source.GetCurrentValue>(valueTypeList1))) == null ? null : ((List)(((ValueComparer>)(((IProperty)valueTypeList1).GetValueComparer())).Snapshot(((IEnumerable)(source.GetCurrentValue>(valueTypeList1))))))), ((object)(source.GetCurrentValue>(dependent))), (source.GetCurrentValue(id1) == null ? null : ((ValueComparer)(((IProperty)id1).GetValueComparer())).Snapshot(source.GetCurrentValue(id1))), SnapshotFactoryFactory.SnapshotComplexCollection(((IList)(source.GetCurrentValue>(manyOwned))), manyOwned)))) }))); }); runtimeEntityType.SetStoreGeneratedValuesFactory( ISnapshot () => ((ISnapshot)(new Snapshot((default(long? ) == null ? null : ((ValueComparer)(((IProperty)id).GetValueComparer())).Snapshot(default(long? ))), (default(long? ) == null ? null : ((ValueComparer)(((IProperty)principalBaseId).GetValueComparer())).Snapshot(default(long? ))), (default(string) == null ? null : ((ValueComparer)(((IProperty)details).GetValueComparer())).Snapshot(default(string))))))); @@ -2486,11 +2608,11 @@ public static void CreateAnnotations(RuntimeEntityType runtimeEntityType) return ((ISnapshot)(new Snapshot((source.GetCurrentValue(id) == null ? null : ((ValueComparer)(((IProperty)id).GetKeyValueComparer())).Snapshot(source.GetCurrentValue(id))), (source.GetCurrentValue(principalBaseId) == null ? null : ((ValueComparer)(((IProperty)principalBaseId).GetKeyValueComparer())).Snapshot(source.GetCurrentValue(principalBaseId))), SnapshotFactoryFactory.SnapshotCollection(source.GetCurrentValue>(deriveds))))); }); runtimeEntityType.SetCounts(new PropertyCounts( - propertyCount: 39, + propertyCount: 40, navigationCount: 1, - complexPropertyCount: 2, + complexPropertyCount: 3, complexCollectionCount: 1, - originalValueCount: 41, + originalValueCount: 43, shadowCount: 2, relationshipCount: 3, storeGeneratedCount: 3)); diff --git a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedUnsafeAccessors.cs b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedUnsafeAccessors.cs index 8d3af16dcd9..a499bc54a83 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedUnsafeAccessors.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/ComplexTypes/PrincipalDerivedUnsafeAccessors.cs @@ -11,6 +11,9 @@ namespace TestNamespace public static class PrincipalDerivedUnsafeAccessors where TDependent : class { + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "k__BackingField")] + public static extern ref TDependent Dependent(CompiledModelTestBase.PrincipalDerived @this); + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "ManyOwned")] public static extern ref IList ManyOwned(CompiledModelTestBase.PrincipalDerived @this); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/CompiledModelSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/CompiledModelSqlServerTest.cs index d798f379d93..69ca7ae9e09 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/CompiledModelSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/CompiledModelSqlServerTest.cs @@ -354,6 +354,11 @@ protected override void BuildComplexTypesModel(ModelBuilder modelBuilder) .UseCollation("Latin1_General_CI_AI"); }); }); + + modelBuilder.Entity>>(eb => + { + eb.ComplexProperty(p => p.Dependent, cb => cb.HasColumnType("nvarchar(450)")); + }); } protected override void AssertComplexTypes(IModel model) diff --git a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs index 50fe67413e9..f69f997a300 100644 --- a/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs +++ b/test/EFCore.SqlServer.Tests/Infrastructure/SqlServerModelValidatorTest.cs @@ -542,6 +542,78 @@ public void Detects_indexed_include_properties() VerifyError(SqlServerStrings.IncludePropertyInIndex(nameof(Dog), nameof(Dog.Name), "{'Name'}"), modelBuilder); } + [ConditionalFact] + public void IncludeProperties_supports_dotted_paths_to_complex_type_properties() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.ComplexProperty(e => e.Address, cb => + { + cb.Property(a => a.City).IsRequired(); + cb.Property(a => a.Street).IsRequired(); + }); + b.HasIndex(e => e.Id).IncludeProperties("Address.City"); + }); + + var model = Validate(modelBuilder); + var index = model.FindEntityType(typeof(EntityWithIncludedComplex))!.GetIndexes().Single(); + Assert.Equal(["Address.City"], index.GetIncludeProperties()); + } + + [ConditionalFact] + public void IncludeProperties_lambda_supports_complex_property_chain() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.ComplexProperty(e => e.Address, cb => + { + cb.Property(a => a.City).IsRequired(); + cb.Property(a => a.Street).IsRequired(); + }); + b.HasIndex(e => e.Id).IncludeProperties(e => e.Address.City); + }); + + var model = Validate(modelBuilder); + var index = model.FindEntityType(typeof(EntityWithIncludedComplex))!.GetIndexes().Single(); + Assert.Equal(["Address.City"], index.GetIncludeProperties()); + } + + [ConditionalFact] + public void IncludeProperties_dotted_path_not_found_throws() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.ComplexProperty(e => e.Address, cb => + { + cb.Property(a => a.City).IsRequired(); + cb.Property(a => a.Street).IsRequired(); + }); + b.HasIndex(e => e.Id).IncludeProperties("Address.NotAProperty"); + }); + + VerifyError( + SqlServerStrings.IncludePropertyNotFound("Address.NotAProperty", "{'Id'}", nameof(EntityWithIncludedComplex)), + modelBuilder); + } + + protected class EntityWithIncludedComplex + { + public int Id { get; set; } + public required EntityWithIncludedComplexAddress Address { get; set; } + } + + protected class EntityWithIncludedComplexAddress + { + public required string City { get; set; } + public required string Street { get; set; } + } + [ConditionalFact] public virtual void Detects_incompatible_memory_optimized_shared_table() { diff --git a/test/EFCore.Tests/Diagnostics/CoreEventIdTest.cs b/test/EFCore.Tests/Diagnostics/CoreEventIdTest.cs index 9fb0883cdb2..239c2e6e194 100644 --- a/test/EFCore.Tests/Diagnostics/CoreEventIdTest.cs +++ b/test/EFCore.Tests/Diagnostics/CoreEventIdTest.cs @@ -114,6 +114,9 @@ public override bool IsCollection public override TypeBase DeclaringType { get; } = entityType; + public override bool IsInModel + => true; + public override Type ClrType => throw new NotImplementedException(); diff --git a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs index 15de51f6f04..c5f8268851e 100644 --- a/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs +++ b/test/EFCore.Tests/Infrastructure/ModelValidatorTest.cs @@ -1304,6 +1304,182 @@ public virtual void Detects_indexer_complex_properties() modelBuilder); } + [ConditionalFact] + public virtual void Detects_key_traversing_nullable_complex_property() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.Ignore(e => e.OtherSamples); + b.Ignore(e => e.AnotherReferencedEntity); + b.Ignore(e => e.Name); + b.Ignore(e => e.Number); + b.ComplexProperty(e => e.ReferencedEntity); + b.HasAlternateKey("ReferencedEntity.SampleEntityId"); + b.ComplexProperty(e => e.ReferencedEntity).IsRequired(false); + }); + + VerifyError( + CoreStrings.KeyOnNullableComplexProperty( + "{'SampleEntityId'}", nameof(SampleEntity), nameof(SampleEntity.ReferencedEntity)), + modelBuilder); + } + + [ConditionalFact] + public virtual void Passes_on_index_traversing_nullable_complex_property() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.Ignore(e => e.OtherSamples); + b.Ignore(e => e.AnotherReferencedEntity); + b.Ignore(e => e.Name); + b.Ignore(e => e.Number); + b.ComplexProperty(e => e.ReferencedEntity); + b.HasIndex("ReferencedEntity.SampleEntityId"); + b.ComplexProperty(e => e.ReferencedEntity).IsRequired(false); + }); + + Validate(modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_index_on_complex_collection_property() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.ComplexCollection(e => e.Items); + }); + + var entityType = (EntityType)modelBuilder.Model.FindEntityType(typeof(EntityWithComplexCollection))!; + var collectionProperty = entityType.FindComplexProperty(nameof(EntityWithComplexCollection.Items))!; + entityType.AddIndex([collectionProperty], ConfigurationSource.Explicit); + + VerifyError( + CoreStrings.IndexOnComplexProperty( + "{'Items'}", nameof(EntityWithComplexCollection), nameof(EntityWithComplexCollection.Items)), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_index_traversing_complex_collection() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.ComplexCollection(e => e.Items); + }); + + var entityType = (EntityType)modelBuilder.Model.FindEntityType(typeof(EntityWithComplexCollection))!; + var collectionProperty = entityType.FindComplexProperty(nameof(EntityWithComplexCollection.Items))!; + var leaf = collectionProperty.ComplexType.FindProperty(nameof(ComplexCollectionItem.Value))!; + entityType.AddIndex([leaf], ConfigurationSource.Explicit); + + VerifyError( + CoreStrings.IndexOnComplexCollection( + "{'Value'}", nameof(EntityWithComplexCollection), nameof(EntityWithComplexCollection.Items)), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_key_traversing_complex_collection() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.ComplexCollection(e => e.Items); + }); + + var entityType = (EntityType)modelBuilder.Model.FindEntityType(typeof(EntityWithComplexCollection))!; + var collectionProperty = entityType.FindComplexProperty(nameof(EntityWithComplexCollection.Items))!; + var leaf = collectionProperty.ComplexType.FindProperty(nameof(ComplexCollectionItem.Value))!; + entityType.AddKey([leaf], ConfigurationSource.Explicit); + + VerifyError( + CoreStrings.KeyOnComplexCollection( + "{'Value'}", nameof(EntityWithComplexCollection), nameof(EntityWithComplexCollection.Items)), + modelBuilder); + } + + protected class EntityWithComplexCollection + { + public int Id { get; set; } + public List Items { get; set; } = null!; + } + + protected class ComplexCollectionItem + { + public int Value { get; set; } + } + + [ConditionalFact] + public virtual void Detects_composite_index_with_scalar_and_complex_properties() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.Ignore(e => e.OtherSamples); + b.Ignore(e => e.AnotherReferencedEntity); + b.Ignore(e => e.Number); + b.ComplexProperty(e => e.ReferencedEntity); + b.HasIndex(e => new { e.Name, e.ReferencedEntity }); + }); + + VerifyError( + CoreStrings.IndexOnComplexProperty( + "{'Name', 'ReferencedEntity'}", + nameof(SampleEntity), + nameof(SampleEntity.ReferencedEntity)), + modelBuilder); + } + + [ConditionalFact] + public virtual void Detects_index_on_complex_property() + { + var modelBuilder = CreateConventionModelBuilder(); + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.Ignore(e => e.OtherSamples); + b.Ignore(e => e.AnotherReferencedEntity); + b.Ignore(e => e.Name); + b.Ignore(e => e.Number); + b.ComplexProperty(e => e.ReferencedEntity); + b.HasIndex(e => e.ReferencedEntity ); + }); + + VerifyError( + CoreStrings.IndexOnComplexProperty( + "{'ReferencedEntity'}", + nameof(SampleEntity), + nameof(SampleEntity.ReferencedEntity)), + modelBuilder); + } + + private class EntityWithNestedComplex + { + public int Id { get; set; } + public OuterComplex Outer { get; set; } = null!; + } + + private class OuterComplex + { + public int OuterValue { get; set; } + public InnerComplex Inner { get; set; } = null!; + } + + private class InnerComplex + { + public int InnerValue { get; set; } + } + [ConditionalFact] public virtual void Passes_on_valid_owned_entity_types() { diff --git a/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs b/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs index 60187f77bfa..3cf91daec79 100644 --- a/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs +++ b/test/EFCore.Tests/Metadata/Conventions/ConventionDispatcherTest.cs @@ -2974,7 +2974,7 @@ public void ProcessIndexAdded(IConventionIndexBuilder indexBuilder, IConventionC if (_terminate) { - indexBuilder.Metadata.DeclaringEntityType.RemoveIndex(indexBuilder.Metadata.Properties); + indexBuilder.Metadata.DeclaringEntityType.RemoveIndex(indexBuilder.Metadata.Properties.OfType().ToList()); context.StopProcessing(); } } diff --git a/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs b/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs index 7180c5b7039..db807418d43 100644 --- a/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/EntityTypeTest.cs @@ -1570,8 +1570,8 @@ public void Can_add_retrieve_and_remove_indexes() Assert.Same(index1, entityType.GetIndexes().First()); Assert.Same(index2, entityType.GetIndexes().Last()); - Assert.Same(index1, entityType.RemoveIndex(index1.Properties)); - Assert.Null(entityType.RemoveIndex(index1.Properties)); + Assert.Same(index1, entityType.RemoveIndex(index1.Properties.OfType().ToList())); + Assert.Null(entityType.RemoveIndex(index1.Properties.OfType().ToList())); Assert.Single(entityType.GetIndexes()); Assert.Same(index2, entityType.GetIndexes().Single()); diff --git a/test/EFCore.Tests/Metadata/Internal/InternalEntityTypeBuilderTest.cs b/test/EFCore.Tests/Metadata/Internal/InternalEntityTypeBuilderTest.cs index 7b56e19f017..ed5cb190303 100644 --- a/test/EFCore.Tests/Metadata/Internal/InternalEntityTypeBuilderTest.cs +++ b/test/EFCore.Tests/Metadata/Internal/InternalEntityTypeBuilderTest.cs @@ -769,7 +769,7 @@ public void Can_promote_index_to_base() var indexBuilder = entityBuilder.HasIndex([Order.IdProperty.Name], ConfigurationSource.Convention); Assert.Same(indexBuilder.Metadata.Properties.Single(), entityBuilder.Metadata.FindProperty(Order.IdProperty.Name)); - Assert.Same(indexBuilder.Metadata, entityBuilder.Metadata.FindIndex(indexBuilder.Metadata.Properties.Single())); + Assert.Same(indexBuilder.Metadata, entityBuilder.Metadata.FindIndex((IReadOnlyProperty)indexBuilder.Metadata.Properties.Single())); Assert.Empty(derivedEntityBuilder.Metadata.GetDeclaredIndexes()); } @@ -802,7 +802,7 @@ public void Can_promote_index_to_base_with_facets() var indexBuilder = entityBuilder.HasIndex([Order.IdProperty.Name], ConfigurationSource.Convention); Assert.Same(indexBuilder.Metadata.Properties.Single(), entityBuilder.Metadata.FindProperty(Order.IdProperty.Name)); - Assert.Same(indexBuilder.Metadata, entityBuilder.Metadata.FindIndex(indexBuilder.Metadata.Properties.Single())); + Assert.Same(indexBuilder.Metadata, entityBuilder.Metadata.FindIndex((IReadOnlyProperty)indexBuilder.Metadata.Properties.Single())); Assert.True(indexBuilder.Metadata.IsUnique); Assert.Empty(derivedEntityBuilder.Metadata.GetDeclaredIndexes()); } @@ -1563,6 +1563,44 @@ public void Property_returns_same_instance_if_type_matches() Assert.Equal([propertyBuilder.Metadata], entityBuilder.GetActualProperties([propertyBuilder.Metadata], null)); } + [ConditionalFact] + public void GetActualProperties_resolves_complex_property_no_longer_in_model() + { + var modelBuilder = CreateModelBuilder(); + var entityBuilder = modelBuilder.Entity(typeof(EntityWithComplexProperty), ConfigurationSource.Explicit); + + var originalComplexBuilder = entityBuilder.ComplexProperty( + EntityWithComplexProperty.DetailsProperty, complexTypeName: null, collection: false, ConfigurationSource.Explicit); + Assert.NotNull(originalComplexBuilder); + var stale = originalComplexBuilder.Metadata; + + Assert.NotNull(entityBuilder.HasNoComplexProperty(stale, ConfigurationSource.Explicit)); + Assert.False(stale.IsInModel); + + var newComplexBuilder = entityBuilder.ComplexProperty( + EntityWithComplexProperty.DetailsProperty, complexTypeName: null, collection: false, ConfigurationSource.Explicit); + Assert.NotNull(newComplexBuilder); + Assert.NotSame(stale, newComplexBuilder.Metadata); + + var resolved = entityBuilder.GetActualProperties([stale], null); + Assert.NotNull(resolved); + Assert.Same(newComplexBuilder.Metadata, resolved[0]); + } + + private class EntityWithComplexProperty + { + public static readonly PropertyInfo DetailsProperty = + typeof(EntityWithComplexProperty).GetProperty(nameof(Details)); + + public int Id { get; set; } + public ComplexDetails Details { get; set; } + } + + private class ComplexDetails + { + public string Name { get; set; } + } + [ConditionalFact] public void Can_add_indexed_property() { diff --git a/test/EFCore.Tests/Metadata/Internal/PropertyListComparerTest.cs b/test/EFCore.Tests/Metadata/Internal/PropertyListComparerTest.cs new file mode 100644 index 00000000000..511a8775960 --- /dev/null +++ b/test/EFCore.Tests/Metadata/Internal/PropertyListComparerTest.cs @@ -0,0 +1,162 @@ +// 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.Metadata.Internal; + +public class PropertyListComparerTest +{ + [ConditionalFact] + public void Distinguishes_properties_with_same_name_in_different_complex_types() + { + var model = new Model(); + var entityType = model.AddEntityType(typeof(Customer), owned: false, ConfigurationSource.Explicit); + + var homeAddress = entityType.AddComplexProperty( + nameof(Customer.HomeAddress), typeof(Address), typeof(Address), collection: false, ConfigurationSource.Explicit)!; + var workAddress = entityType.AddComplexProperty( + nameof(Customer.WorkAddress), typeof(Address), typeof(Address), collection: false, ConfigurationSource.Explicit)!; + + var homeCity = homeAddress.ComplexType.AddProperty( + nameof(Address.City), typeof(string), ConfigurationSource.Explicit, ConfigurationSource.Explicit)!; + var workCity = workAddress.ComplexType.AddProperty( + nameof(Address.City), typeof(string), ConfigurationSource.Explicit, ConfigurationSource.Explicit)!; + + var homeList = new IReadOnlyPropertyBase[] { homeCity }; + var workList = new IReadOnlyPropertyBase[] { workCity }; + + Assert.NotEqual(0, PropertyListComparer.Instance.Compare(homeList, workList)); + Assert.False(PropertyListComparer.Instance.Equals(homeList, workList)); + Assert.NotEqual( + PropertyListComparer.Instance.GetHashCode(homeList), + PropertyListComparer.Instance.GetHashCode(workList)); + + Assert.Equal(0, PropertyListComparer.Instance.Compare(homeList, new IReadOnlyPropertyBase[] { homeCity })); + Assert.True(PropertyListComparer.Instance.Equals(homeList, new IReadOnlyPropertyBase[] { homeCity })); + Assert.Equal( + PropertyListComparer.Instance.GetHashCode(homeList), + PropertyListComparer.Instance.GetHashCode(new IReadOnlyPropertyBase[] { homeCity })); + } + + [ConditionalFact] + public void Allows_index_with_same_property_names_through_different_complex_paths() + { + var model = new Model(); + var entityType = model.AddEntityType(typeof(Customer), owned: false, ConfigurationSource.Explicit); + + var homeAddress = entityType.AddComplexProperty( + nameof(Customer.HomeAddress), typeof(Address), typeof(Address), collection: false, ConfigurationSource.Explicit)!; + var workAddress = entityType.AddComplexProperty( + nameof(Customer.WorkAddress), typeof(Address), typeof(Address), collection: false, ConfigurationSource.Explicit)!; + + var homeCity = homeAddress.ComplexType.AddProperty( + nameof(Address.City), typeof(string), ConfigurationSource.Explicit, ConfigurationSource.Explicit)!; + var workCity = workAddress.ComplexType.AddProperty( + nameof(Address.City), typeof(string), ConfigurationSource.Explicit, ConfigurationSource.Explicit)!; + + var homeIndex = entityType.AddIndex([homeCity], ConfigurationSource.Explicit); + var workIndex = entityType.AddIndex([workCity], ConfigurationSource.Explicit); + + Assert.NotSame(homeIndex, workIndex); + Assert.Same(homeIndex, entityType.FindIndex([homeCity])); + Assert.Same(workIndex, entityType.FindIndex([workCity])); + } + + [ConditionalFact] + public void Distinguishes_properties_with_same_name_through_nested_complex_paths() + { + var model = new Model(); + var entityType = model.AddEntityType(typeof(Order), owned: false, ConfigurationSource.Explicit); + + var oldDetails = entityType.AddComplexProperty( + nameof(Order.OldDetails), typeof(Details), typeof(Details), collection: false, ConfigurationSource.Explicit)!; + var newDetails = entityType.AddComplexProperty( + nameof(Order.NewDetails), typeof(Details), typeof(Details), collection: false, ConfigurationSource.Explicit)!; + + var oldAddress = oldDetails.ComplexType.AddComplexProperty( + nameof(Details.Address), typeof(Address), typeof(Address), collection: false, ConfigurationSource.Explicit)!; + var newAddress = newDetails.ComplexType.AddComplexProperty( + nameof(Details.Address), typeof(Address), typeof(Address), collection: false, ConfigurationSource.Explicit)!; + + var oldStreet = oldAddress.ComplexType.AddProperty( + nameof(Address.Street), typeof(string), ConfigurationSource.Explicit, ConfigurationSource.Explicit)!; + var newStreet = newAddress.ComplexType.AddProperty( + nameof(Address.Street), typeof(string), ConfigurationSource.Explicit, ConfigurationSource.Explicit)!; + + var oldList = new IReadOnlyPropertyBase[] { oldStreet }; + var newList = new IReadOnlyPropertyBase[] { newStreet }; + + Assert.NotEqual(0, PropertyListComparer.Instance.Compare(oldList, newList)); + Assert.False(PropertyListComparer.Instance.Equals(oldList, newList)); + Assert.NotEqual( + PropertyListComparer.Instance.GetHashCode(oldList), + PropertyListComparer.Instance.GetHashCode(newList)); + } + + [ConditionalFact] + public void PropertyNameComparer_orders_keys_with_same_name_in_different_complex_types_independently() + { + var model = new Model(); + var entityType = model.AddEntityType(typeof(Customer), owned: false, ConfigurationSource.Explicit); + + var homeAddress = entityType.AddComplexProperty( + nameof(Customer.HomeAddress), typeof(Address), typeof(Address), collection: false, ConfigurationSource.Explicit)!; + homeAddress.IsNullable = false; + var workAddress = entityType.AddComplexProperty( + nameof(Customer.WorkAddress), typeof(Address), typeof(Address), collection: false, ConfigurationSource.Explicit)!; + workAddress.IsNullable = false; + + var homeCity = homeAddress.ComplexType.AddProperty( + nameof(Address.City), typeof(string), ConfigurationSource.Explicit, ConfigurationSource.Explicit)!; + homeCity.IsNullable = false; + homeAddress.ComplexType.AddProperty( + nameof(Address.Street), typeof(string), ConfigurationSource.Explicit, ConfigurationSource.Explicit); + var workCity = workAddress.ComplexType.AddProperty( + nameof(Address.City), typeof(string), ConfigurationSource.Explicit, ConfigurationSource.Explicit)!; + workCity.IsNullable = false; + workAddress.ComplexType.AddProperty( + nameof(Address.Street), typeof(string), ConfigurationSource.Explicit, ConfigurationSource.Explicit); + + // Composite primary key whose two parts have the same property name but live in different complex types. + entityType.SetPrimaryKey([homeCity, workCity], ConfigurationSource.Explicit); + + // Within each complex type, City must sort before Street because it is part of the primary key + // declared on that specific complex type. + Assert.Equal( + [nameof(Address.City), nameof(Address.Street)], + homeAddress.ComplexType.GetProperties().Select(p => p.Name)); + Assert.Equal( + [nameof(Address.City), nameof(Address.Street)], + workAddress.ComplexType.GetProperties().Select(p => p.Name)); + + var homeComparer = new PropertyNameComparer(homeAddress.ComplexType); + var workComparer = new PropertyNameComparer(workAddress.ComplexType); + + Assert.True(homeComparer.Compare(nameof(Address.City), nameof(Address.Street)) < 0); + Assert.True(workComparer.Compare(nameof(Address.City), nameof(Address.Street)) < 0); + } + + private class Order + { + public int Id { get; set; } + public Details OldDetails { get; set; } = null!; + public Details NewDetails { get; set; } = null!; + } + + private class Details + { + public Address Address { get; set; } = null!; + } + + private class Customer + { + public int Id { get; set; } + public Address HomeAddress { get; set; } = null!; + public Address WorkAddress { get; set; } = null!; + } + + private class Address + { + public string City { get; set; } = null!; + public string Street { get; set; } = null!; + } +}