diff --git a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs index 26305af61aa..555ff93715b 100644 --- a/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs +++ b/src/EFCore.Relational/Migrations/Internal/MigrationsModelDiffer.cs @@ -595,7 +595,7 @@ protected virtual IEnumerable Diff( var sourceMigrationsAnnotations = source.GetAnnotations(); var targetMigrationsAnnotations = target.GetAnnotations(); - if (source.Comment != target.Comment + if (!MultilineEquals(source.Comment, target.Comment) || HasDifferences(sourceMigrationsAnnotations, targetMigrationsAnnotations)) { var alterTableOperation = new AlterTableOperation @@ -986,11 +986,11 @@ private static bool ColumnStructureEquals(IColumn source, IColumn target) && source.MaxLength == target.MaxLength && source.IsFixedLength == target.IsFixedLength && source.Collation == target.Collation - && source.Comment == target.Comment + && MultilineEquals(source.Comment, target.Comment) && source.IsStored == target.IsStored - && source.ComputedColumnSql == target.ComputedColumnSql + && MultilineEquals(source.ComputedColumnSql, target.ComputedColumnSql) && Equals(sourceDefault, targetDefault) - && source.DefaultValueSql == target.DefaultValueSql; + && MultilineEquals(source.DefaultValueSql, target.DefaultValueSql); } private static bool EntityTypePathEquals(ITypeBase source, ITypeBase target, DiffContext diffContext) @@ -1079,12 +1079,12 @@ protected virtual IEnumerable Diff( if (isNullableChanged || columnTypeChanged - || source.DefaultValueSql != target.DefaultValueSql - || source.ComputedColumnSql != target.ComputedColumnSql + || !MultilineEquals(source.DefaultValueSql, target.DefaultValueSql) + || !MultilineEquals(source.ComputedColumnSql, target.ComputedColumnSql) || source.IsStored != target.IsStored || sourceDefault?.GetType() != targetDefault?.GetType() || (sourceDefault != DBNull.Value && !target.ProviderValueComparer.Equals(sourceDefault, targetDefault)) - || source.Comment != target.Comment + || !MultilineEquals(source.Comment, target.Comment) || source.Collation != target.Collation || source.Order != target.Order || HasDifferences(sourceMigrationsAnnotations, targetMigrationsAnnotations)) @@ -1507,7 +1507,7 @@ private bool IndexStructureEquals(ITableIndex source, ITableIndex target, DiffCo || (source.IsDescending is not null && target.IsDescending is not null && source.IsDescending.SequenceEqual(target.IsDescending))) - && source.Filter == target.Filter + && MultilineEquals(source.Filter, target.Filter) && !HasDifferences(source.GetAnnotations(), target.GetAnnotations()) && source.Columns.Select(p => p.Name).SequenceEqual( target.Columns.Select(p => diffContext.FindSource(p)?.Name)); @@ -1600,7 +1600,7 @@ protected virtual IEnumerable Diff( Remove, (s, t, c) => c.FindTable(s.EntityType) == c.FindSource(c.FindTable(t.EntityType)) && string.Equals(s.Name, t.Name, StringComparison.OrdinalIgnoreCase) - && string.Equals(s.Sql, t.Sql, StringComparison.OrdinalIgnoreCase)); + && MultilineEquals(s.Sql, t.Sql)); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -2481,7 +2481,7 @@ protected virtual bool HasDifferences(IEnumerable source, IEnumerab foreach (var annotation in source) { var index = unmatched.FindIndex(a - => a.Name == annotation.Name && StructuralComparisons.StructuralEqualityComparer.Equals(a.Value, annotation.Value)); + => a.Name == annotation.Name && AnnotationValuesEqual(a, annotation)); if (index == -1) { return true; @@ -2491,8 +2491,19 @@ protected virtual bool HasDifferences(IEnumerable source, IEnumerab } return unmatched.Count != 0; + + static bool AnnotationValuesEqual(IAnnotation left, IAnnotation right) + => left.Value is string leftString && right.Value is string rightString + ? MultilineEquals(leftString, rightString) + : StructuralComparisons.StructuralEqualityComparer.Equals(left.Value, right.Value); } + private static bool MultilineEquals(string? sourceString, string? targetString, StringComparison comparisonType = StringComparison.Ordinal) + => ReferenceEquals(sourceString, targetString) + || (sourceString is not null + && targetString is not null + && string.Equals(sourceString.ReplaceLineEndings(), targetString.ReplaceLineEndings(), comparisonType)); + /// /// 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/ChangeTracking/PropertyValues.cs b/src/EFCore/ChangeTracking/PropertyValues.cs index 79a53634f9c..95777844184 100644 --- a/src/EFCore/ChangeTracking/PropertyValues.cs +++ b/src/EFCore/ChangeTracking/PropertyValues.cs @@ -226,7 +226,7 @@ internal virtual IComplexProperty CheckCollection(IComplexProperty complexProper /// The type of the property. /// The property name. /// The property value if any. - /// True if the property exists, otherwise false. + /// if the property exists, otherwise . public virtual bool TryGetValue(string propertyName, out TValue value) { var property = Properties.FirstOrDefault(p => p.Name == propertyName); diff --git a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs index 5c7f2f3310f..4553967aeb3 100644 --- a/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs +++ b/test/EFCore.Relational.Tests/Migrations/Internal/MigrationsModelDifferTest.cs @@ -12318,6 +12318,127 @@ public void Model_differ_does_not_detect_entity_type_mapped_to_TVF() result => Assert.Equal(0, result.Count), skipSourceConventions: true); + [ConditionalFact] + public void Model_differ_ignores_newline_differences_in_DefaultValueSql() + => Execute( + source => source.Entity("Cat", x => x.Property("Meow").HasDefaultValueSql("SELECT\r\n'test'")), + target => target.Entity("Cat", x => x.Property("Meow").HasDefaultValueSql("SELECT\n'test'")), + result => Assert.Empty(result)); + + [ConditionalFact] + public void Model_differ_ignores_newline_differences_in_ComputedColumnSql() + => Execute( + source => source.Entity("Cat", x => x.Property("Meow").HasComputedColumnSql("UPPER(\r\nName)")), + target => target.Entity("Cat", x => x.Property("Meow").HasComputedColumnSql("UPPER(\nName)")), + result => Assert.Empty(result)); + + [ConditionalFact] + public void Model_differ_ignores_newline_differences_in_index_filter() + => Execute( + source => source.Entity("Cat", x => + { + x.Property("Name"); + x.HasIndex("Name").HasFilter("Name IS NOT\r\nNULL"); + }), + target => target.Entity("Cat", x => + { + x.Property("Name"); + x.HasIndex("Name").HasFilter("Name IS NOT\nNULL"); + }), + result => Assert.Empty(result)); + + [ConditionalFact] + public void Model_differ_ignores_newline_differences_in_check_constraint() + => Execute( + source => source.Entity("Cat", x => + { + x.Property("Age"); + x.ToTable(t => t.HasCheckConstraint("CK_Cat_Age", "Age >\r\n0")); + }), + target => target.Entity("Cat", x => + { + x.Property("Age"); + x.ToTable(t => t.HasCheckConstraint("CK_Cat_Age", "Age >\n0")); + }), + result => Assert.Empty(result)); + + [ConditionalFact] + public void Model_differ_ignores_carriage_return_differences_in_DefaultValueSql() + => Execute( + source => source.Entity("Cat", x => x.Property("Meow").HasDefaultValueSql("SELECT\r'test'")), + target => target.Entity("Cat", x => x.Property("Meow").HasDefaultValueSql("SELECT\n'test'")), + result => Assert.Empty(result)); + + [ConditionalFact] + public void Model_differ_ignores_complex_newline_differences_in_ComputedColumnSql() + => Execute( + source => source.Entity("Cat", x => x.Property("Meow").HasComputedColumnSql("CASE\r\nWHEN Age > 5\r\nTHEN 'Old'\r\nELSE 'Young'\r\nEND")), + target => target.Entity("Cat", x => x.Property("Meow").HasComputedColumnSql("CASE\nWHEN Age > 5\nTHEN 'Old'\nELSE 'Young'\nEND")), + result => Assert.Empty(result)); + + [ConditionalFact] + public void Model_differ_ignores_newline_differences_in_table_comments() + => Execute( + source => source.Entity("Cat", x => x.ToTable(t => t.HasComment("Table for storing\r\ncat information"))), + target => target.Entity("Cat", x => x.ToTable(t => t.HasComment("Table for storing\ncat information"))), + result => Assert.Empty(result)); + + [ConditionalFact] + public void Model_differ_ignores_newline_differences_in_column_comments() + => Execute( + source => source.Entity("Cat", x => x.Property("Name").HasComment("Cat name\r\nfield")), + target => target.Entity("Cat", x => x.Property("Name").HasComment("Cat name\nfield")), + result => Assert.Empty(result)); + + [ConditionalFact] + public void Model_differ_ignores_newline_differences_in_DefaultValueSql_annotations() + => Execute( + source => source.Entity("Cat", x => x.Property("Name").HasAnnotation("Relational:DefaultValueSql", "SELECT\r\n'test'")), + target => target.Entity("Cat", x => x.Property("Name").HasAnnotation("Relational:DefaultValueSql", "SELECT\n'test'")), + result => Assert.Empty(result)); + + [ConditionalFact] + public void Model_differ_ignores_newline_differences_in_ComputedColumnSql_annotations() + => Execute( + source => source.Entity("Cat", x => x.Property("Name").HasAnnotation("Relational:ComputedColumnSql", "UPPER(\r\nName)")), + target => target.Entity("Cat", x => x.Property("Name").HasAnnotation("Relational:ComputedColumnSql", "UPPER(\nName)")), + result => Assert.Empty(result)); + + [ConditionalFact] + public void Model_differ_ignores_newline_differences_in_Comment_annotations() + => Execute( + source => source.Entity("Cat", x => x.Property("Name").HasAnnotation("Relational:Comment", "Multi-line\r\ncomment")), + target => target.Entity("Cat", x => x.Property("Name").HasAnnotation("Relational:Comment", "Multi-line\ncomment")), + result => Assert.Empty(result)); + + [ConditionalFact] + public void Model_differ_ignores_newline_differences_in_ViewDefinitionSql_annotations() + => Execute( + source => source.Entity("Cat", x => x.HasAnnotation("Relational:ViewDefinitionSql", "SELECT Id,\r\nName FROM Cats")), + target => target.Entity("Cat", x => x.HasAnnotation("Relational:ViewDefinitionSql", "SELECT Id,\nName FROM Cats")), + result => Assert.Empty(result)); + + [ConditionalFact] + public void Model_differ_ignores_newline_differences_in_Filter_annotations() + => Execute( + source => source.Entity("Cat", x => x.Property("Name").HasAnnotation("Relational:Filter", "Name IS NOT\r\nNULL")), + target => target.Entity("Cat", x => x.Property("Name").HasAnnotation("Relational:Filter", "Name IS NOT\nNULL")), + result => Assert.Empty(result)); + + [ConditionalFact] + public void Model_differ_detects_actual_annotation_sql_changes() + => Execute( + source => source.Entity("Cat", x => x.Property("Name").HasAnnotation("Relational:DefaultValueSql", "SELECT 'old'")), + target => target.Entity("Cat", x => x.Property("Name").HasAnnotation("Relational:DefaultValueSql", "SELECT 'new'")), + result => Assert.Single(result)); + + [ConditionalFact] + public void Model_differ_ignores_newline_differences_in_non_relational_annotations() + => Execute( + source => source.Entity("Cat", x => x.Property("Name").HasAnnotation("CustomAnnotation", "Value with\r\nnewlines")), + target => target.Entity("Cat", x => x.Property("Name").HasAnnotation("CustomAnnotation", "Value with\nnewlines")), + result => Assert.Empty(result)); + protected override TestHelpers TestHelpers => FakeRelationalTestHelpers.Instance; } diff --git a/test/EFCore.Specification.Tests/TestUtilities/ListLoggerFactory.cs b/test/EFCore.Specification.Tests/TestUtilities/ListLoggerFactory.cs index f4acf95c785..ac0e443c97d 100644 --- a/test/EFCore.Specification.Tests/TestUtilities/ListLoggerFactory.cs +++ b/test/EFCore.Specification.Tests/TestUtilities/ListLoggerFactory.cs @@ -57,7 +57,7 @@ public void Dispose() => _disposed = true; public static string NormalizeLineEndings(string expectedString) - => expectedString.Replace("\r", string.Empty).Replace("\n", Environment.NewLine); + => expectedString.ReplaceLineEndings(); protected class ListLogger : ILogger {