From 41bc53bb4c3e59b28c608f96cb0b21152d5957b0 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Thu, 6 Aug 2020 16:30:46 -0700 Subject: [PATCH] Make required dependent columns in shared table non-nullable. Part of #12100 --- .../Design/CSharpSnapshotGenerator.cs | 138 +++++++++++ .../Internal/CSharpEntityTypeGenerator.cs | 1 - .../Design/AnnotationCodeGenerator.cs | 66 +++++- .../Design/IAnnotationCodeGenerator.cs | 20 ++ .../RelationalForeignKeyExtensions.cs | 12 +- .../RelationalPropertyExtensions.cs | 20 +- .../Storage/ParameterNameGenerator.cs | 2 +- .../Migrations/ModelSnapshotSqlServerTest.cs | 219 ++++++++++++++++++ .../TableSplittingTestBase.cs | 3 +- .../Internal/MigrationsModelDifferTest.cs | 30 +++ .../TransportationContext.cs | 5 + .../TPTTableSplittingSqlServerTest.cs | 13 +- .../TableSplittingSqlServerTest.cs | 9 +- 13 files changed, 509 insertions(+), 29 deletions(-) diff --git a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs index f1eab6589f3..57fabbe2e0d 100644 --- a/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs +++ b/src/EFCore.Design/Migrations/Design/CSharpSnapshotGenerator.cs @@ -137,6 +137,16 @@ protected virtual void GenerateEntityTypes( GenerateEntityTypeRelationships(builderName, entityType, stringBuilder); } + + foreach (var entityType in entityTypes.Where( + e => !e.HasDefiningNavigation() + && e.FindOwnership() == null + && e.GetDeclaredNavigations().Any(n => !n.IsOnDependent && !n.ForeignKey.IsOwnership))) + { + stringBuilder.AppendLine(); + + GenerateEntityTypeNavigations(builderName, entityType, stringBuilder); + } } /// @@ -215,6 +225,9 @@ protected virtual void GenerateEntityType( if (ownerNavigation != null) { GenerateRelationships(builderName, entityType, stringBuilder); + + GenerateNavigations(builderName, entityType.GetDeclaredNavigations() + .Where(n => !n.IsOnDependent && !n.ForeignKey.IsOwnership), stringBuilder); } GenerateData(builderName, entityType.GetProperties(), entityType.GetSeedData(providerValues: true), stringBuilder); @@ -318,6 +331,9 @@ protected virtual void GenerateRelationships( GenerateForeignKeys(builderName, entityType.GetDeclaredForeignKeys(), stringBuilder); GenerateOwnedTypes(builderName, entityType.GetDeclaredReferencingForeignKeys().Where(fk => fk.IsOwnership), stringBuilder); + + GenerateNavigations(builderName, entityType.GetDeclaredNavigations() + .Where(n => n.IsOnDependent || (!n.IsOnDependent && n.ForeignKey.IsOwnership)), stringBuilder); } /// @@ -1188,6 +1204,128 @@ protected virtual void GenerateForeignKeyAnnotations( GenerateAnnotations(annotations.Values, stringBuilder); } + /// + /// Generates code for the navigations of an . + /// + /// The name of the builder variable. + /// The entity type. + /// The builder code is added to. + protected virtual void GenerateEntityTypeNavigations( + [NotNull] string builderName, + [NotNull] IEntityType entityType, + [NotNull] IndentedStringBuilder stringBuilder) + { + Check.NotEmpty(builderName, nameof(builderName)); + Check.NotNull(entityType, nameof(entityType)); + Check.NotNull(stringBuilder, nameof(stringBuilder)); + + stringBuilder + .Append(builderName) + .Append(".Entity(") + .Append(Code.Literal(entityType.Name)) + .AppendLine(", b =>"); + + using (stringBuilder.Indent()) + { + stringBuilder.Append("{"); + + using (stringBuilder.Indent()) + { + GenerateNavigations("b", entityType.GetDeclaredNavigations() + .Where(n => !n.IsOnDependent && !n.ForeignKey.IsOwnership), stringBuilder); + } + + stringBuilder.AppendLine("});"); + } + } + + /// + /// Generates code for objects. + /// + /// The name of the builder variable. + /// The navigations. + /// The builder code is added to. + protected virtual void GenerateNavigations( + [NotNull] string builderName, + [NotNull] IEnumerable navigations, + [NotNull] IndentedStringBuilder stringBuilder) + { + Check.NotNull(builderName, nameof(builderName)); + Check.NotNull(navigations, nameof(navigations)); + Check.NotNull(stringBuilder, nameof(stringBuilder)); + + foreach (var navigation in navigations) + { + stringBuilder.AppendLine(); + + GenerateNavigation(builderName, navigation, stringBuilder); + } + } + + /// + /// Generates code for an . + /// + /// The name of the builder variable. + /// The navigation. + /// The builder code is added to. + protected virtual void GenerateNavigation( + [NotNull] string builderName, + [NotNull] INavigation navigation, + [NotNull] IndentedStringBuilder stringBuilder) + { + Check.NotNull(builderName, nameof(builderName)); + Check.NotNull(navigation, nameof(navigation)); + Check.NotNull(stringBuilder, nameof(stringBuilder)); + + stringBuilder + .Append(builderName) + .Append(".Navigation(") + .Append(Code.Literal(navigation.Name)) + .Append(")"); + + using (stringBuilder.Indent()) + { + if (!navigation.IsOnDependent + && !navigation.IsCollection + && navigation.ForeignKey.IsRequiredDependent) + { + stringBuilder + .AppendLine() + .Append(".IsRequired()"); + } + + GenerateNavigationAnnotations(navigation, stringBuilder); + } + + stringBuilder.AppendLine(";"); + } + + /// + /// Generates code for the annotations on a navigation. + /// + /// The navigation. + /// The builder code is added to. + protected virtual void GenerateNavigationAnnotations( + [NotNull] INavigation navigation, [NotNull] IndentedStringBuilder stringBuilder) + { + Check.NotNull(navigation, nameof(navigation)); + Check.NotNull(stringBuilder, nameof(stringBuilder)); + + var annotations = Dependencies.AnnotationCodeGenerator + .FilterIgnoredAnnotations(navigation.GetAnnotations()) + .ToDictionary(a => a.Name, a => a); + + foreach (var methodCallCodeFragment in + Dependencies.AnnotationCodeGenerator.GenerateFluentApiCalls(navigation, annotations)) + { + stringBuilder + .AppendLine() + .Append(Code.Fragment(methodCallCodeFragment)); + } + + GenerateAnnotations(annotations.Values, stringBuilder); + } + /// /// Generates code for annotations. /// diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs index b61d87262f2..48f73b85fb4 100644 --- a/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs +++ b/src/EFCore.Design/Scaffolding/Internal/CSharpEntityTypeGenerator.cs @@ -10,7 +10,6 @@ using Microsoft.EntityFrameworkCore.Design; using Microsoft.EntityFrameworkCore.Design.Internal; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Utilities; diff --git a/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs b/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs index 64964d6878b..22ac9ad57c9 100644 --- a/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs +++ b/src/EFCore.Relational/Design/AnnotationCodeGenerator.cs @@ -214,7 +214,7 @@ public virtual IReadOnlyList GenerateFluentApiCalls( /// public virtual IReadOnlyList GenerateFluentApiCalls( - IForeignKey foreignKey, IDictionary annotations) + IForeignKey navigation, IDictionary annotations) { var methodCallCodeFragments = new List(); @@ -222,7 +222,29 @@ public virtual IReadOnlyList GenerateFluentApiCalls( annotations, RelationalAnnotationNames.Name, nameof(RelationalForeignKeyBuilderExtensions.HasConstraintName), methodCallCodeFragments); - methodCallCodeFragments.AddRange(GenerateFluentApiCallsHelper(foreignKey, annotations, GenerateFluentApi)); + methodCallCodeFragments.AddRange(GenerateFluentApiCallsHelper(navigation, annotations, GenerateFluentApi)); + + return methodCallCodeFragments; + } + + /// + public virtual IReadOnlyList GenerateFluentApiCalls( + INavigation navigation, IDictionary annotations) + { + var methodCallCodeFragments = new List(); + + methodCallCodeFragments.AddRange(GenerateFluentApiCallsHelper(navigation, annotations, GenerateFluentApi)); + + return methodCallCodeFragments; + } + + /// + public virtual IReadOnlyList GenerateFluentApiCalls( + ISkipNavigation navigation, IDictionary annotations) + { + var methodCallCodeFragments = new List(); + + methodCallCodeFragments.AddRange(GenerateFluentApiCallsHelper(navigation, annotations, GenerateFluentApi)); return methodCallCodeFragments; } @@ -501,6 +523,46 @@ protected virtual MethodCallCodeFragment GenerateFluentApi([NotNull] IForeignKey return null; } + /// + /// + /// Returns a fluent API call for the given , or + /// if no fluent API call exists for it. + /// + /// + /// The default implementation always returns . + /// + /// + /// The . + /// The . + /// . + protected virtual MethodCallCodeFragment GenerateFluentApi([NotNull] INavigation navigation, [NotNull] IAnnotation annotation) + { + Check.NotNull(navigation, nameof(navigation)); + Check.NotNull(annotation, nameof(annotation)); + + return null; + } + + /// + /// + /// Returns a fluent API call for the given , or + /// if no fluent API call exists for it. + /// + /// + /// The default implementation always returns . + /// + /// + /// The . + /// The . + /// . + protected virtual MethodCallCodeFragment GenerateFluentApi([NotNull] ISkipNavigation navigation, [NotNull] IAnnotation annotation) + { + Check.NotNull(navigation, nameof(navigation)); + Check.NotNull(annotation, nameof(annotation)); + + return null; + } + /// /// /// Returns a fluent API call for the given , or diff --git a/src/EFCore.Relational/Design/IAnnotationCodeGenerator.cs b/src/EFCore.Relational/Design/IAnnotationCodeGenerator.cs index ccca4a05ec1..2d8f06fcaad 100644 --- a/src/EFCore.Relational/Design/IAnnotationCodeGenerator.cs +++ b/src/EFCore.Relational/Design/IAnnotationCodeGenerator.cs @@ -121,6 +121,26 @@ IReadOnlyList GenerateFluentApiCalls( [NotNull] IForeignKey foreignKey, [NotNull] IDictionary annotations) => Array.Empty(); + /// + /// For the given annotations which have corresponding fluent API calls, returns those fluent API calls + /// and removes the annotations. + /// + /// The navigation to which the annotations are applied. + /// The set of annotations from which to generate fluent API calls. + IReadOnlyList GenerateFluentApiCalls( + [NotNull] INavigation navigation, [NotNull] IDictionary annotations) + => Array.Empty(); + + /// + /// For the given annotations which have corresponding fluent API calls, returns those fluent API calls + /// and removes the annotations. + /// + /// The skip navigation to which the annotations are applied. + /// The set of annotations from which to generate fluent API calls. + IReadOnlyList GenerateFluentApiCalls( + [NotNull] ISkipNavigation navigation, [NotNull] IDictionary annotations) + => Array.Empty(); + /// /// For the given annotations which have corresponding fluent API calls, returns those fluent API calls /// and removes the annotations. diff --git a/src/EFCore.Relational/Extensions/RelationalForeignKeyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalForeignKeyExtensions.cs index aa1f64fc255..7aa5d13a0f5 100644 --- a/src/EFCore.Relational/Extensions/RelationalForeignKeyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalForeignKeyExtensions.cs @@ -88,8 +88,8 @@ public static string GetDefaultName( { IForeignKey linkedForeignKey = null; foreach (var otherForeignKey in rootForeignKey.DeclaringEntityType - .FindRowInternalForeignKeys(storeObject) - .SelectMany(fk => fk.PrincipalEntityType.GetForeignKeys())) + .FindRowInternalForeignKeys(storeObject) + .SelectMany(fk => fk.PrincipalEntityType.GetForeignKeys())) { if (principalStoreObject.Name == otherForeignKey.PrincipalEntityType.GetTableName() && principalStoreObject.Schema == otherForeignKey.PrincipalEntityType.GetSchema() @@ -196,7 +196,7 @@ public static IForeignKey FindSharedObjectRootForeignKey([NotNull] this IForeign // Using a hashset is detrimental to the perf when there are no cycles for (var i = 0; i < Metadata.Internal.RelationalEntityTypeExtensions.MaxEntityTypesSharingTable; i++) { - IForeignKey linkedKey = null; + IForeignKey linkedForeignKey = null; foreach (var otherForeignKey in rootForeignKey.DeclaringEntityType .FindRowInternalForeignKeys(storeObject) .SelectMany(fk => fk.PrincipalEntityType.GetForeignKeys())) @@ -207,17 +207,17 @@ public static IForeignKey FindSharedObjectRootForeignKey([NotNull] this IForeign otherForeignKey.PrincipalEntityType.GetSchema())) == foreignKeyName) { - linkedKey = otherForeignKey; + linkedForeignKey = otherForeignKey; break; } } - if (linkedKey == null) + if (linkedForeignKey == null) { break; } - rootForeignKey = linkedKey; + rootForeignKey = linkedForeignKey; } return rootForeignKey == foreignKey ? null : rootForeignKey; diff --git a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs index 4b9ba74e622..dd3d11b7b65 100644 --- a/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalPropertyExtensions.cs @@ -825,7 +825,25 @@ public static bool IsColumnNullable([NotNull] this IProperty property, in StoreO return property.IsNullable || (property.DeclaringEntityType.BaseType != null && property.DeclaringEntityType.GetDiscriminatorProperty() != null) - || property.DeclaringEntityType.FindRowInternalForeignKeys(storeObject).Any(); + || IsOptionalSharingDependent(property.DeclaringEntityType, storeObject, 0); + } + + private static bool IsOptionalSharingDependent(IEntityType entityType, in StoreObjectIdentifier storeObject, int recursionDepth) + { + if (recursionDepth++ == Metadata.Internal.RelationalEntityTypeExtensions.MaxEntityTypesSharingTable) + { + return true; + } + + bool? optional = null; + foreach (var linkingForeignKey in entityType.FindRowInternalForeignKeys(storeObject)) + { + optional = (optional ?? true) + && (!linkingForeignKey.IsRequiredDependent + || IsOptionalSharingDependent(linkingForeignKey.PrincipalEntityType, storeObject, recursionDepth)); + } + + return optional ?? false; } /// diff --git a/src/EFCore.Relational/Storage/ParameterNameGenerator.cs b/src/EFCore.Relational/Storage/ParameterNameGenerator.cs index 52cdc7f54c8..64977294644 100644 --- a/src/EFCore.Relational/Storage/ParameterNameGenerator.cs +++ b/src/EFCore.Relational/Storage/ParameterNameGenerator.cs @@ -22,7 +22,7 @@ public class ParameterNameGenerator /// Generates the next unique parameter name. /// /// The generated name. - public virtual string GenerateNext() => string.Format(CultureInfo.InvariantCulture, "p{0}", _count++); + public virtual string GenerateNext() => "p" + _count++; /// /// Resets the generator, meaning it can reuse previously generated names. diff --git a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs index 317608b18cb..3a6f5367b03 100644 --- a/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs +++ b/test/EFCore.Design.Tests/Migrations/ModelSnapshotSqlServerTest.cs @@ -1098,6 +1098,13 @@ public virtual void Foreign_keys_are_stored_in_snapshot() .HasForeignKey(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithTwoProperties"", ""AlternateId"") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation(""EntityWithOneProperty""); + }); + + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithOneProperty"", b => + { + b.Navigation(""EntityWithTwoProperties""); });"), o => { @@ -1658,6 +1665,10 @@ public virtual void Owned_types_are_stored_in_snapshot() .WithOne() .HasForeignKey(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithTwoProperties"", ""EntityWithStringKeyId""); + b1.Navigation(""EntityWithOneProperty""); + + b1.Navigation(""EntityWithStringKey""); + b1.HasData( new { @@ -1665,6 +1676,8 @@ public virtual void Owned_types_are_stored_in_snapshot() Id = -1 }); }); + + b.Navigation(""EntityWithTwoProperties""); }); modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithStringKey"", b => @@ -1702,7 +1715,11 @@ public virtual void Owned_types_are_stored_in_snapshot() b1.WithOwner() .HasForeignKey(""EntityWithStringKeyId""); + + b1.Navigation(""EntityWithOneProperty""); }); + + b.Navigation(""Properties""); });", usingSystem: true), o => { @@ -1817,6 +1834,8 @@ public virtual void Weak_owned_types_are_stored_in_snapshot() b2.WithOwner() .HasForeignKey(""OrderDetailsOrderId""); }); + + b1.Navigation(""StreetAddress""); }); b.OwnsOne(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+OrderDetails"", ""OrderShippingDetails"", b1 => @@ -1850,6 +1869,8 @@ public virtual void Weak_owned_types_are_stored_in_snapshot() b2.WithOwner() .HasForeignKey(""OrderDetailsOrderId""); }); + + b1.Navigation(""StreetAddress""); }); b.OwnsOne(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+OrderInfo"", ""OrderInfo"", b1 => @@ -1883,7 +1904,15 @@ public virtual void Weak_owned_types_are_stored_in_snapshot() b2.WithOwner() .HasForeignKey(""OrderInfoOrderId""); }); + + b1.Navigation(""StreetAddress""); }); + + b.Navigation(""OrderBillingDetails""); + + b.Navigation(""OrderInfo""); + + b.Navigation(""OrderShippingDetails""); });"), o => { @@ -1979,6 +2008,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.WithOwner() .HasForeignKey(""TestOwnerId""); }); + + b.Navigation(""OwnedEntities""); }); #pragma warning restore 612, 618 } @@ -3331,6 +3362,13 @@ public virtual void ForeignKey_annotations_are_stored_in_snapshot() .HasAnnotation(""AnnotationName"", ""AnnotationValue"") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation(""EntityWithOneProperty""); + }); + + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithOneProperty"", b => + { + b.Navigation(""EntityWithTwoProperties""); });"), o => Assert.Equal( "AnnotationValue", o.FindEntityType(typeof(EntityWithTwoProperties)).GetForeignKeys().First()["AnnotationName"])); @@ -3438,6 +3476,11 @@ public virtual void ForeignKey_isUnique_is_stored_in_snapshot() b.HasOne(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithStringKey"", null) .WithMany(""Properties"") .HasForeignKey(""Name""); + }); + + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithStringKey"", b => + { + b.Navigation(""Properties""); });"), o => Assert.False(o.FindEntityType(typeof(EntityWithStringProperty)).GetForeignKeys().First().IsUnique)); } @@ -3489,6 +3532,8 @@ public virtual void ForeignKey_deleteBehavior_is_stored_in_snapshot() .HasForeignKey(""Id"") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation(""EntityWithTwoProperties""); });"), o => Assert.Equal( DeleteBehavior.Cascade, o.FindEntityType(typeof(EntityWithOneProperty)).GetForeignKeys().First().DeleteBehavior)); @@ -3540,6 +3585,13 @@ public virtual void ForeignKey_deleteBehavior_is_stored_in_snapshot_for_one_to_o .HasForeignKey(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithOneProperty"", ""Id"") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation(""EntityWithTwoProperties""); + }); + + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithTwoProperties"", b => + { + b.Navigation(""EntityWithOneProperty""); });"), o => Assert.Equal( DeleteBehavior.Cascade, o.FindEntityType(typeof(EntityWithOneProperty)).GetForeignKeys().First().DeleteBehavior)); @@ -3678,6 +3730,13 @@ public virtual void ForeignKey_constraint_name_is_stored_in_snapshot_as_fluent_a .HasConstraintName(""Constraint"") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation(""EntityWithOneProperty""); + }); + + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithOneProperty"", b => + { + b.Navigation(""EntityWithTwoProperties""); });"), o => Assert.Equal( "Constraint", o.FindEntityType(typeof(EntityWithTwoProperties)).GetForeignKeys().First()["Relational:Name"])); @@ -3738,6 +3797,13 @@ public virtual void ForeignKey_multiple_annotations_are_stored_in_snapshot() .HasAnnotation(""AnnotationName"", ""AnnotationValue"") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation(""EntityWithOneProperty""); + }); + + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithOneProperty"", b => + { + b.Navigation(""EntityWithTwoProperties""); });"), o => { @@ -3808,6 +3874,8 @@ public virtual void Do_not_generate_entity_type_builder_again_if_no_foreign_key_ b.HasOne(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithOneProperty"", ""Navigation"") .WithMany() .HasForeignKey(""NavigationId""); + + b.Navigation(""Navigation""); });", usingSystem: true), o => { }); } @@ -3860,6 +3928,13 @@ public virtual void ForeignKey_principal_key_is_stored_in_snapshot() .HasPrincipalKey(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithTwoProperties"", ""AlternateId"") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation(""EntityWithTwoProperties""); + }); + + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithTwoProperties"", b => + { + b.Navigation(""EntityWithOneProperty""); });"), o => { @@ -3921,6 +3996,13 @@ public virtual void ForeignKey_principal_key_with_non_default_name_is_stored_in_ .HasPrincipalKey(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithTwoProperties"", ""AlternateId"") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + + b.Navigation(""EntityWithTwoProperties""); + }); + + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithTwoProperties"", b => + { + b.Navigation(""EntityWithOneProperty""); });"), o => { @@ -3933,6 +4015,143 @@ public virtual void ForeignKey_principal_key_with_non_default_name_is_stored_in_ #endregion + #region Navigation + + [ConditionalFact] + public virtual void Navigation_annotations_are_stored_in_snapshot() + { + Test( + builder => + { + builder.Entity() + .HasOne(e => e.EntityWithOneProperty) + .WithOne(e => e.EntityWithTwoProperties) + .HasForeignKey(e => e.AlternateId); + + builder.Entity().Navigation(e => e.EntityWithOneProperty) + .HasAnnotation("AnnotationName", "AnnotationValue"); + }, + AddBoilerPlate( + GetHeading() + + @" + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithOneProperty"", b => + { + b.Property(""Id"") + .ValueGeneratedOnAdd() + .HasColumnType(""int"") + .UseIdentityColumn(); + + b.HasKey(""Id""); + + b.ToTable(""EntityWithOneProperty""); + }); + + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithTwoProperties"", b => + { + b.Property(""Id"") + .ValueGeneratedOnAdd() + .HasColumnType(""int"") + .UseIdentityColumn(); + + b.Property(""AlternateId"") + .HasColumnType(""int""); + + b.HasKey(""Id""); + + b.HasIndex(""AlternateId"") + .IsUnique(); + + b.ToTable(""EntityWithTwoProperties""); + }); + + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithTwoProperties"", b => + { + b.HasOne(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithOneProperty"", ""EntityWithOneProperty"") + .WithOne(""EntityWithTwoProperties"") + .HasForeignKey(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithTwoProperties"", ""AlternateId"") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation(""EntityWithOneProperty"") + .HasAnnotation(""AnnotationName"", ""AnnotationValue""); + }); + + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithOneProperty"", b => + { + b.Navigation(""EntityWithTwoProperties""); + });"), + o => Assert.Equal( + "AnnotationValue", o.FindEntityType(typeof(EntityWithTwoProperties)).GetNavigations().First()["AnnotationName"])); + } + + [ConditionalFact] + public virtual void Navigation_isRequired_is_stored_in_snapshot() + { + Test( + builder => + { + builder.Entity() + .HasOne(e => e.EntityWithOneProperty) + .WithOne(e => e.EntityWithTwoProperties) + .HasForeignKey(e => e.AlternateId); + + builder.Entity().Navigation(e => e.EntityWithTwoProperties) + .IsRequired(); + }, + AddBoilerPlate( + GetHeading() + + @" + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithOneProperty"", b => + { + b.Property(""Id"") + .ValueGeneratedOnAdd() + .HasColumnType(""int"") + .UseIdentityColumn(); + + b.HasKey(""Id""); + + b.ToTable(""EntityWithOneProperty""); + }); + + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithTwoProperties"", b => + { + b.Property(""Id"") + .ValueGeneratedOnAdd() + .HasColumnType(""int"") + .UseIdentityColumn(); + + b.Property(""AlternateId"") + .HasColumnType(""int""); + + b.HasKey(""Id""); + + b.HasIndex(""AlternateId"") + .IsUnique(); + + b.ToTable(""EntityWithTwoProperties""); + }); + + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithTwoProperties"", b => + { + b.HasOne(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithOneProperty"", ""EntityWithOneProperty"") + .WithOne(""EntityWithTwoProperties"") + .HasForeignKey(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithTwoProperties"", ""AlternateId"") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation(""EntityWithOneProperty""); + }); + + modelBuilder.Entity(""Microsoft.EntityFrameworkCore.Migrations.ModelSnapshotSqlServerTest+EntityWithOneProperty"", b => + { + b.Navigation(""EntityWithTwoProperties"") + .IsRequired(); + });"), + o => Assert.True(o.FindEntityType(typeof(EntityWithOneProperty)).GetNavigations().First().ForeignKey.IsRequiredDependent)); + } + + #endregion + #region SeedData [ConditionalFact] diff --git a/test/EFCore.Relational.Specification.Tests/TableSplittingTestBase.cs b/test/EFCore.Relational.Specification.Tests/TableSplittingTestBase.cs index 9be9645e690..d4dab4cdc40 100644 --- a/test/EFCore.Relational.Specification.Tests/TableSplittingTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/TableSplittingTestBase.cs @@ -193,7 +193,8 @@ public virtual void Can_share_required_columns() { Name = "Electric scooter", SeatingCapacity = 1, - Engine = new Engine() + Engine = new Engine(), + Operator = new Operator { Name = "Kai Saunders" } }); scooterEntry.Reference(v => v.Engine).TargetEntry.Property("SeatingCapacity").CurrentValue = 1; diff --git a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs index d215d3d6d02..8897ebcef10 100644 --- a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs +++ b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs @@ -6594,6 +6594,36 @@ public void Create_shared_table_with_two_entity_types() }); } + [ConditionalFact] + public void Create_shared_table_with_required_dependent() + { + Execute( + _ => { }, + modelBuilder => + { + modelBuilder.Entity("Order").ToTable("Orders").Property("Id"); + modelBuilder.Entity( + "OrderDetails", eb => + { + eb.Property("Id"); + eb.Property("Time"); + eb.HasOne("Order").WithOne("OrderDetails").HasForeignKey("OrderDetails", "Id"); + eb.ToTable("Orders"); + }); + modelBuilder.Entity("Order").Navigation("OrderDetails").IsRequired(); + }, + operations => + { + Assert.Equal(1, operations.Count); + + var createTableOperation = Assert.IsType(operations[0]); + Assert.Equal(2, createTableOperation.Columns.Count); + var timeColumn = createTableOperation.Columns[1]; + Assert.Equal("Time", timeColumn.Name); + Assert.False(timeColumn.IsNullable); + }); + } + [ConditionalFact] public void Create_shared_table_with_inheritance_and_three_entity_types() { diff --git a/test/EFCore.Specification.Tests/TestModels/TransportationModel/TransportationContext.cs b/test/EFCore.Specification.Tests/TestModels/TransportationModel/TransportationContext.cs index 20a5dab7556..1ac83096175 100644 --- a/test/EFCore.Specification.Tests/TestModels/TransportationModel/TransportationContext.cs +++ b/test/EFCore.Specification.Tests/TestModels/TransportationModel/TransportationContext.cs @@ -48,6 +48,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) }); modelBuilder.Entity(); + modelBuilder.Entity(vb => + { + vb.Navigation(v => v.Operator).IsRequired(); + }); + modelBuilder.Entity( eb => { diff --git a/test/EFCore.SqlServer.FunctionalTests/TPTTableSplittingSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/TPTTableSplittingSqlServerTest.cs index c29a45e593e..88ea36ea3f1 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TPTTableSplittingSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TPTTableSplittingSqlServerTest.cs @@ -38,7 +38,6 @@ END AS [Discriminator] FROM [Vehicles] AS [v1] LEFT JOIN [PoweredVehicles] AS [p0] ON [v1].[Name] = [p0].[Name] ) AS [t] ON [v0].[Name] = [t].[Name] - WHERE [v0].[RequiredInt] IS NOT NULL ) AS [t0] ON [v].[Name] = [t0].[Name] LEFT JOIN ( SELECT [v2].[Name], [v2].[Type], [t2].[Name] AS [Name0], [t2].[Name0] AS [Name00] @@ -56,7 +55,6 @@ END AS [Discriminator] FROM [Vehicles] AS [v4] LEFT JOIN [PoweredVehicles] AS [p1] ON [v4].[Name] = [p1].[Name] ) AS [t1] ON [v3].[Name] = [t1].[Name] - WHERE [v3].[RequiredInt] IS NOT NULL ) AS [t2] ON [v2].[Name] = [t2].[Name] WHERE [v2].[Type] IS NOT NULL ) AS [t3] ON [t0].[Name] = [t3].[Name] @@ -117,8 +115,7 @@ WHEN [p].[Name] IS NOT NULL THEN N'PoweredVehicle' END AS [Discriminator] FROM [Vehicles] AS [v0] LEFT JOIN [PoweredVehicles] AS [p] ON [v0].[Name] = [p].[Name] -) AS [t] ON [v].[Name] = [t].[Name] -WHERE [v].[RequiredInt] IS NOT NULL"); +) AS [t] ON [v].[Name] = [t].[Name]"); } public override void Can_query_shared_nonhierarchy() @@ -134,8 +131,7 @@ WHEN [p].[Name] IS NOT NULL THEN N'PoweredVehicle' END AS [Discriminator] FROM [Vehicles] AS [v0] LEFT JOIN [PoweredVehicles] AS [p] ON [v0].[Name] = [p].[Name] -) AS [t] ON [v].[Name] = [t].[Name] -WHERE [v].[RequiredInt] IS NOT NULL"); +) AS [t] ON [v].[Name] = [t].[Name]"); } public override void Can_query_shared_nonhierarchy_with_nonshared_dependent() @@ -151,8 +147,7 @@ WHEN [p].[Name] IS NOT NULL THEN N'PoweredVehicle' END AS [Discriminator] FROM [Vehicles] AS [v0] LEFT JOIN [PoweredVehicles] AS [p] ON [v0].[Name] = [p].[Name] -) AS [t] ON [v].[Name] = [t].[Name] -WHERE [v].[RequiredInt] IS NOT NULL"); +) AS [t] ON [v].[Name] = [t].[Name]"); } public override void Can_query_shared_derived_hierarchy() @@ -264,7 +259,6 @@ END AS [Discriminator] FROM [Vehicles] AS [v1] LEFT JOIN [PoweredVehicles] AS [p0] ON [v1].[Name] = [p0].[Name] ) AS [t] ON [v0].[Name] = [t].[Name] - WHERE [v0].[RequiredInt] IS NOT NULL ) AS [t0] ON [v].[Name] = [t0].[Name] WHERE [v].[Name] = N'Trek Pro Fit Madone 6 Series'"); } @@ -300,7 +294,6 @@ END AS [Discriminator] FROM [Vehicles] AS [v1] LEFT JOIN [PoweredVehicles] AS [p0] ON [v1].[Name] = [p0].[Name] ) AS [t] ON [v0].[Name] = [t].[Name] - WHERE [v0].[RequiredInt] IS NOT NULL ) AS [t0] ON [v].[Name] = [t0].[Name] WHERE [v].[Name] = N'Trek Pro Fit Madone 6 Series'"); } diff --git a/test/EFCore.SqlServer.FunctionalTests/TableSplittingSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/TableSplittingSqlServerTest.cs index 53eb32a7e62..33ab79614c0 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TableSplittingSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TableSplittingSqlServerTest.cs @@ -28,7 +28,6 @@ LEFT JOIN ( SELECT [v0].[Name], [v0].[Operator_Discriminator], [v0].[Operator_Name], [v0].[LicenseType], [v1].[Name] AS [Name0] FROM [Vehicles] AS [v0] INNER JOIN [Vehicles] AS [v1] ON [v0].[Name] = [v1].[Name] - WHERE [v0].[Operator_Discriminator] IS NOT NULL ) AS [t] ON [v].[Name] = [t].[Name] LEFT JOIN ( SELECT [v2].[Name], [v2].[Type], [t0].[Name] AS [Name0], [t0].[Name0] AS [Name00] @@ -37,7 +36,6 @@ INNER JOIN ( SELECT [v3].[Name], [v3].[Operator_Discriminator], [v3].[Operator_Name], [v3].[LicenseType], [v4].[Name] AS [Name0] FROM [Vehicles] AS [v3] INNER JOIN [Vehicles] AS [v4] ON [v3].[Name] = [v4].[Name] - WHERE [v3].[Operator_Discriminator] IS NOT NULL ) AS [t0] ON [v2].[Name] = [t0].[Name] WHERE [v2].[Type] IS NOT NULL ) AS [t1] ON [t].[Name] = [t1].[Name] @@ -85,8 +83,7 @@ public override void Can_query_shared() AssertSql( @"SELECT [v].[Name], [v].[Operator_Discriminator], [v].[Operator_Name], [v].[LicenseType] FROM [Vehicles] AS [v] -INNER JOIN [Vehicles] AS [v0] ON [v].[Name] = [v0].[Name] -WHERE [v].[Operator_Discriminator] IS NOT NULL"); +INNER JOIN [Vehicles] AS [v0] ON [v].[Name] = [v0].[Name]"); } public override void Can_query_shared_nonhierarchy() @@ -222,7 +219,7 @@ public override void Can_change_dependent_instance_non_derived() AssertSql( @"@p3='Trek Pro Fit Madone 6 Series' (Nullable = false) (Size = 450) -@p0='LicensedOperator' (Size = 4000) +@p0='LicensedOperator' (Nullable = false) (Size = 4000) @p1='Repair' (Size = 4000) @p2='repairman' (Size = 4000) @@ -237,7 +234,6 @@ LEFT JOIN ( SELECT [v0].[Name], [v0].[Operator_Discriminator], [v0].[Operator_Name], [v0].[LicenseType], [v1].[Name] AS [Name0] FROM [Vehicles] AS [v0] INNER JOIN [Vehicles] AS [v1] ON [v0].[Name] = [v1].[Name] - WHERE [v0].[Operator_Discriminator] IS NOT NULL ) AS [t] ON [v].[Name] = [t].[Name] WHERE [v].[Name] = N'Trek Pro Fit Madone 6 Series'"); } @@ -261,7 +257,6 @@ LEFT JOIN ( SELECT [v0].[Name], [v0].[Operator_Discriminator], [v0].[Operator_Name], [v0].[LicenseType], [v1].[Name] AS [Name0] FROM [Vehicles] AS [v0] INNER JOIN [Vehicles] AS [v1] ON [v0].[Name] = [v1].[Name] - WHERE [v0].[Operator_Discriminator] IS NOT NULL ) AS [t] ON [v].[Name] = [t].[Name] WHERE [v].[Name] = N'Trek Pro Fit Madone 6 Series'"); }