From e709663b49d23754a86489a17f61c3b3e43e98cc Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Wed, 14 Sep 2022 00:58:37 -0700 Subject: [PATCH] Convert to provider values when comparing shared columns (#29064) --- .../Update/ColumnModification.cs | 25 +- .../Update/ModificationCommand.cs | 2 +- .../UpdatesRelationalTestBase.cs | 9 + .../SharedStoreFixtureBase.cs | 4 +- .../TestModels/UpdatesModel/Address.cs | 12 + .../TestModels/UpdatesModel/Country.cs | 15 ++ .../TestModels/UpdatesModel/Person.cs | 2 + .../UpdatesTestBase.cs | 35 ++- .../TestUtilities/SqlServerTestStore.cs | 2 +- .../UpdatesSqlServerTPCTest.cs | 122 +-------- .../UpdatesSqlServerTPTTest.cs | 125 +-------- .../UpdatesSqlServerTest.cs | 133 +--------- .../UpdatesSqlServerTestBase.cs | 250 ++++++++++++++++++ 13 files changed, 360 insertions(+), 376 deletions(-) create mode 100644 test/EFCore.Specification.Tests/TestModels/UpdatesModel/Address.cs create mode 100644 test/EFCore.Specification.Tests/TestModels/UpdatesModel/Country.cs create mode 100644 test/EFCore.SqlServer.FunctionalTests/UpdatesSqlServerTestBase.cs diff --git a/src/EFCore.Relational/Update/ColumnModification.cs b/src/EFCore.Relational/Update/ColumnModification.cs index 4837e314799..594c177d64a 100644 --- a/src/EFCore.Relational/Update/ColumnModification.cs +++ b/src/EFCore.Relational/Update/ColumnModification.cs @@ -191,7 +191,9 @@ public virtual void AddSharedColumnModification(IColumnModification modification _sharedColumnModifications ??= new List(); if (UseCurrentValueParameter - && !modification.Property.GetValueComparer().Equals(Value, modification.Value)) + && !Property.GetProviderValueComparer().Equals( + Entry.GetCurrentProviderValue(Property), + modification.Entry.GetCurrentProviderValue(modification.Property))) { if (_sensitiveLoggingEnabled) { @@ -215,13 +217,30 @@ public virtual void AddSharedColumnModification(IColumnModification modification } if (UseOriginalValueParameter - && !modification.Property.GetValueComparer().Equals(OriginalValue, modification.OriginalValue)) + && !Property.GetProviderValueComparer().Equals( + Entry.SharedIdentityEntry == null + ? Entry.GetOriginalProviderValue(Property) + : Entry.SharedIdentityEntry.GetOriginalProviderValue(Property), + modification.Entry.SharedIdentityEntry == null + ? modification.Entry.GetOriginalProviderValue(modification.Property) + : modification.Entry.SharedIdentityEntry.GetOriginalProviderValue(modification.Property))) { if (Entry.EntityState == EntityState.Modified && modification.Entry.EntityState == EntityState.Added && modification.Entry.SharedIdentityEntry == null) { - modification.Entry.SetOriginalValue(modification.Property, OriginalValue); + var originalValue = Entry.SharedIdentityEntry == null + ? Entry.GetOriginalProviderValue(Property) + : Entry.SharedIdentityEntry.GetOriginalProviderValue(Property); + + var typeMapping = modification.Property.GetTypeMapping(); + var converter = typeMapping.Converter; + if (converter != null) + { + originalValue = converter.ConvertFromProvider(originalValue); + } + + modification.Entry.SetOriginalValue(modification.Property, originalValue); } else { diff --git a/src/EFCore.Relational/Update/ModificationCommand.cs b/src/EFCore.Relational/Update/ModificationCommand.cs index 687fd7186bf..02b937c0465 100644 --- a/src/EFCore.Relational/Update/ModificationCommand.cs +++ b/src/EFCore.Relational/Update/ModificationCommand.cs @@ -976,7 +976,7 @@ public bool TryPropagate(IColumnMappingBase mapping, IUpdateEntry entry) && (entry.EntityState == EntityState.Unchanged || (entry.EntityState == EntityState.Modified && !entry.IsModified(property)) || (entry.EntityState == EntityState.Added - && mapping.Column.ProviderValueComparer.Equals(_originalValue, entry.GetCurrentValue(property))))) + && mapping.Column.ProviderValueComparer.Equals(_originalValue, entry.GetCurrentProviderValue(property))))) { if (property.GetAfterSaveBehavior() == PropertySaveBehavior.Save || entry.EntityState == EntityState.Added) diff --git a/test/EFCore.Relational.Specification.Tests/UpdatesRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/UpdatesRelationalTestBase.cs index 82e9f637e03..d732a305aef 100644 --- a/test/EFCore.Relational.Specification.Tests/UpdatesRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/UpdatesRelationalTestBase.cs @@ -174,6 +174,15 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con modelBuilder.Entity().HasIndex(p => new { p.Name, p.Price }).IsUnique().HasFilter("Name IS NOT NULL"); + modelBuilder.Entity() + .Property(p => p.Country) + .HasColumnName("Country"); + + modelBuilder.Entity() + .OwnsOne(p => p.Address) + .Property(p => p.Country) + .HasColumnName("Country"); + modelBuilder .Entity< LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkingCorrectlyDetails diff --git a/test/EFCore.Specification.Tests/SharedStoreFixtureBase.cs b/test/EFCore.Specification.Tests/SharedStoreFixtureBase.cs index d518535e4cb..d79ce61cdb0 100644 --- a/test/EFCore.Specification.Tests/SharedStoreFixtureBase.cs +++ b/test/EFCore.Specification.Tests/SharedStoreFixtureBase.cs @@ -89,16 +89,16 @@ protected virtual bool ShouldLogCategory(string logCategory) public virtual void Reseed() { using var context = CreateContext(); - Clean(context); TestStore.Clean(context); + Clean(context); Seed(context); } public virtual async Task ReseedAsync() { using var context = CreateContext(); - await CleanAsync(context); await TestStore.CleanAsync(context); + await CleanAsync(context); await SeedAsync(context); } diff --git a/test/EFCore.Specification.Tests/TestModels/UpdatesModel/Address.cs b/test/EFCore.Specification.Tests/TestModels/UpdatesModel/Address.cs new file mode 100644 index 00000000000..426a99c9ffb --- /dev/null +++ b/test/EFCore.Specification.Tests/TestModels/UpdatesModel/Address.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +# nullable enable + +namespace Microsoft.EntityFrameworkCore.TestModels.UpdatesModel; + +public class Address +{ + public string City { get; set; } = null!; + public Country Country { get; set; } +} diff --git a/test/EFCore.Specification.Tests/TestModels/UpdatesModel/Country.cs b/test/EFCore.Specification.Tests/TestModels/UpdatesModel/Country.cs new file mode 100644 index 00000000000..67dbdbac407 --- /dev/null +++ b/test/EFCore.Specification.Tests/TestModels/UpdatesModel/Country.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +# nullable enable + +namespace Microsoft.EntityFrameworkCore.TestModels.UpdatesModel; + +public enum Country +{ + Türkiye, + Czechia, + Montenegro, + Serbia, + Eswatini +} diff --git a/test/EFCore.Specification.Tests/TestModels/UpdatesModel/Person.cs b/test/EFCore.Specification.Tests/TestModels/UpdatesModel/Person.cs index 2912aee94c2..7074270dbb9 100644 --- a/test/EFCore.Specification.Tests/TestModels/UpdatesModel/Person.cs +++ b/test/EFCore.Specification.Tests/TestModels/UpdatesModel/Person.cs @@ -21,5 +21,7 @@ public Person(string name, Person? parent) public int PersonId { get; set; } public string Name { get; set; } public int? ParentId { get; set; } + public string? Country { get; set; } public Person? Parent { get; set; } + public Address? Address { get; set; } } diff --git a/test/EFCore.Specification.Tests/UpdatesTestBase.cs b/test/EFCore.Specification.Tests/UpdatesTestBase.cs index 6e8de2f4fde..90a74e1e10a 100644 --- a/test/EFCore.Specification.Tests/UpdatesTestBase.cs +++ b/test/EFCore.Specification.Tests/UpdatesTestBase.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. # nullable enable @@ -371,6 +371,34 @@ public virtual void Can_add_and_remove_self_refs() Assert.Equal("1", people.Single(p => p.Parent == null).Name); }); + [ConditionalFact] + public virtual void Can_change_enums_with_conversion() + => ExecuteWithStrategyInTransaction( + context => + { + var person = new Person("1", null) { Address = new Address { Country = Country.Eswatini, City = "Bulembu" }, Country = "Eswatini" }; + + context.Add(person); + + context.SaveChanges(); + }, + context => + { + var person = context.Set().Single(); + person.Address = new Address { Country = Country.Türkiye, City = "Konya" }; + person.Country = "Türkiye"; + + context.SaveChanges(); + }, + context => + { + var person = context.Set().Single(); + + Assert.Equal(Country.Türkiye, person.Address!.Country); + Assert.Equal("Konya", person.Address.City); + Assert.Equal("Türkiye", person.Country); + }); + [ConditionalFact] public virtual void Can_remove_partial() { @@ -621,6 +649,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext con .WithMany() .OnDelete(DeleteBehavior.Restrict); + modelBuilder.Entity() + .OwnsOne(p => p.Address) + .Property(p => p.Country) + .HasConversion(); + modelBuilder.Entity().HasMany(e => e.ProductCategories).WithOne(e => e.Category) .HasForeignKey(e => e.CategoryId); diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerTestStore.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerTestStore.cs index 627568331c0..824bea4f0b4 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerTestStore.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerTestStore.cs @@ -130,8 +130,8 @@ private bool CreateDatabase(Action clean) new DbContextOptionsBuilder() .EnableServiceProviderCaching(false)) .Options); - clean?.Invoke(context); Clean(context); + clean?.Invoke(context); return true; } diff --git a/test/EFCore.SqlServer.FunctionalTests/UpdatesSqlServerTPCTest.cs b/test/EFCore.SqlServer.FunctionalTests/UpdatesSqlServerTPCTest.cs index 6399f49091f..74443bbafca 100644 --- a/test/EFCore.SqlServer.FunctionalTests/UpdatesSqlServerTPCTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/UpdatesSqlServerTPCTest.cs @@ -2,23 +2,19 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.EntityFrameworkCore.TestModels.UpdatesModel; -using static Microsoft.EntityFrameworkCore.UpdatesSqlServerTPCTest; #nullable enable namespace Microsoft.EntityFrameworkCore; -public class UpdatesSqlServerTPCTest : UpdatesRelationalTestBase +public class UpdatesSqlServerTPCTest : UpdatesSqlServerTestBase { // ReSharper disable once UnusedParameter.Local - public UpdatesSqlServerTPCTest(UpdatesSqlServerTPTFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) + public UpdatesSqlServerTPCTest(UpdatesSqlServerTPCFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture, testOutputHelper) { - //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - Fixture.TestSqlLoggerFactory.Clear(); } - [ConditionalFact] public override void Save_with_shared_foreign_key() { base.Save_with_shared_foreign_key(); @@ -43,58 +39,6 @@ OUTPUT INSERTED.[Id] VALUES (@p0, @p1);"); } - [ConditionalFact] - public override void Can_add_and_remove_self_refs() - { - base.Can_add_and_remove_self_refs(); - - AssertContainsSql( - @"@p0='1' (Nullable = false) (Size = 4000) -@p1=NULL (DbType = Int32) - -SET IMPLICIT_TRANSACTIONS OFF; -SET NOCOUNT ON; -INSERT INTO [Person] ([Name], [ParentId]) -OUTPUT INSERTED.[PersonId] -VALUES (@p0, @p1);", - // - @"@p2='2' (Nullable = false) (Size = 4000) -@p3='1' (Nullable = true) -@p4='3' (Nullable = false) (Size = 4000) -@p5='1' (Nullable = true) - -SET IMPLICIT_TRANSACTIONS OFF; -SET NOCOUNT ON; -MERGE [Person] USING ( -VALUES (@p2, @p3, 0), -(@p4, @p5, 1)) AS i ([Name], [ParentId], _Position) ON 1=0 -WHEN NOT MATCHED THEN -INSERT ([Name], [ParentId]) -VALUES (i.[Name], i.[ParentId]) -OUTPUT INSERTED.[PersonId], i._Position;", - // - @"@p6='4' (Nullable = false) (Size = 4000) -@p7='2' (Nullable = true) -@p8='5' (Nullable = false) (Size = 4000) -@p9='2' (Nullable = true) -@p10='6' (Nullable = false) (Size = 4000) -@p11='3' (Nullable = true) -@p12='7' (Nullable = false) (Size = 4000) -@p13='3' (Nullable = true) - -SET IMPLICIT_TRANSACTIONS OFF; -SET NOCOUNT ON; -MERGE [Person] USING ( -VALUES (@p6, @p7, 0), -(@p8, @p9, 1), -(@p10, @p11, 2), -(@p12, @p13, 3)) AS i ([Name], [ParentId], _Position) ON 1=0 -WHEN NOT MATCHED THEN -INSERT ([Name], [ParentId]) -VALUES (i.[Name], i.[ParentId]) -OUTPUT INSERTED.[PersonId], i._Position;"); - } - public override void Save_replaced_principal() { base.Save_replaced_principal(); @@ -140,59 +84,8 @@ FROM [ProductBase] AS [p] WHERE [p].[Discriminator] = N'Product' AND [p].[DependentId] = @__category_PrincipalId_0"); } - public override void Identifiers_are_generated_correctly() - { - using var context = CreateContext(); - var entityType = context.Model.FindEntityType( - typeof( - LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkingCorrectly - ))!; - Assert.Equal( - "LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorking~", - entityType.GetTableName()); - Assert.Equal( - "PK_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWork~", - entityType.GetKeys().Single().GetName()); - Assert.Equal( - "FK_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWork~", - entityType.GetForeignKeys().Single().GetConstraintName()); - Assert.Equal( - "IX_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWork~", - entityType.GetIndexes().Single().GetDatabaseName()); - - var entityType2 = context.Model.FindEntityType( - typeof( - LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkingCorrectlyDetails - ))!; - - Assert.Equal( - "LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkin~1", - entityType2.GetTableName()); - Assert.Equal( - "PK_LoginDetails", - entityType2.GetKeys().Single().GetName()); - Assert.Equal( - "ExtraPropertyWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkingCo~", - entityType2.GetProperties().ElementAt(1).GetColumnName(StoreObjectIdentifier.Table(entityType2.GetTableName()!))); - Assert.Equal( - "ExtraPropertyWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkingC~1", - entityType2.GetProperties().ElementAt(2).GetColumnName(StoreObjectIdentifier.Table(entityType2.GetTableName()!))); - Assert.Equal( - "IX_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWork~", - entityType2.GetIndexes().Single().GetDatabaseName()); - } - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - protected void AssertContainsSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected, assertOrder: false); - - public class UpdatesSqlServerTPTFixture : UpdatesRelationalFixture + public class UpdatesSqlServerTPCFixture : UpdatesSqlServerFixtureBase { - protected override ITestStoreFactory TestStoreFactory - => SqlServerTestStoreFactory.Instance; - protected override string StoreName => "UpdateTestTPC"; @@ -200,20 +93,13 @@ public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder build => base.AddOptions(builder).ConfigureWarnings( w => { - w.Log(SqlServerEventId.DecimalTypeKeyWarning); w.Log(RelationalEventId.ForeignKeyTpcPrincipalWarning); }); - protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) - => configurationBuilder.Properties().HaveColumnType("decimal(18, 2)"); - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) { base.OnModelCreating(modelBuilder, context); - modelBuilder.Entity() - .Property(p => p.Id).HasDefaultValueSql("NEWID()"); - modelBuilder.Entity() .UseTpcMappingStrategy(); } diff --git a/test/EFCore.SqlServer.FunctionalTests/UpdatesSqlServerTPTTest.cs b/test/EFCore.SqlServer.FunctionalTests/UpdatesSqlServerTPTTest.cs index 5eb72e81fd8..cd9c1ac5b86 100644 --- a/test/EFCore.SqlServer.FunctionalTests/UpdatesSqlServerTPTTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/UpdatesSqlServerTPTTest.cs @@ -7,17 +7,14 @@ namespace Microsoft.EntityFrameworkCore; -public class UpdatesSqlServerTPTTest : UpdatesRelationalTestBase +public class UpdatesSqlServerTPTTest : UpdatesSqlServerTestBase { // ReSharper disable once UnusedParameter.Local public UpdatesSqlServerTPTTest(UpdatesSqlServerTPTFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) + : base(fixture, testOutputHelper) { - //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - Fixture.TestSqlLoggerFactory.Clear(); } - [ConditionalFact] public override void Save_with_shared_foreign_key() { base.Save_with_shared_foreign_key(); @@ -42,58 +39,6 @@ OUTPUT INSERTED.[Id] VALUES (@p0, @p1);"); } - [ConditionalFact] - public override void Can_add_and_remove_self_refs() - { - base.Can_add_and_remove_self_refs(); - - AssertContainsSql( - @"@p0='1' (Nullable = false) (Size = 4000) -@p1=NULL (DbType = Int32) - -SET IMPLICIT_TRANSACTIONS OFF; -SET NOCOUNT ON; -INSERT INTO [Person] ([Name], [ParentId]) -OUTPUT INSERTED.[PersonId] -VALUES (@p0, @p1);", - // - @"@p2='2' (Nullable = false) (Size = 4000) -@p3='1' (Nullable = true) -@p4='3' (Nullable = false) (Size = 4000) -@p5='1' (Nullable = true) - -SET IMPLICIT_TRANSACTIONS OFF; -SET NOCOUNT ON; -MERGE [Person] USING ( -VALUES (@p2, @p3, 0), -(@p4, @p5, 1)) AS i ([Name], [ParentId], _Position) ON 1=0 -WHEN NOT MATCHED THEN -INSERT ([Name], [ParentId]) -VALUES (i.[Name], i.[ParentId]) -OUTPUT INSERTED.[PersonId], i._Position;", - // - @"@p6='4' (Nullable = false) (Size = 4000) -@p7='2' (Nullable = true) -@p8='5' (Nullable = false) (Size = 4000) -@p9='2' (Nullable = true) -@p10='6' (Nullable = false) (Size = 4000) -@p11='3' (Nullable = true) -@p12='7' (Nullable = false) (Size = 4000) -@p13='3' (Nullable = true) - -SET IMPLICIT_TRANSACTIONS OFF; -SET NOCOUNT ON; -MERGE [Person] USING ( -VALUES (@p6, @p7, 0), -(@p8, @p9, 1), -(@p10, @p11, 2), -(@p12, @p13, 3)) AS i ([Name], [ParentId], _Position) ON 1=0 -WHEN NOT MATCHED THEN -INSERT ([Name], [ParentId]) -VALUES (i.[Name], i.[ParentId]) -OUTPUT INSERTED.[PersonId], i._Position;"); - } - public override void Save_replaced_principal() { base.Save_replaced_principal(); @@ -133,79 +78,15 @@ FROM [ProductBase] AS [p] WHERE [p].[Discriminator] = N'Product' AND [p].[DependentId] = @__category_PrincipalId_0"); } - public override void Identifiers_are_generated_correctly() - { - using var context = CreateContext(); - var entityType = context.Model.FindEntityType( - typeof( - LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkingCorrectly - ))!; - Assert.Equal( - "LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorking~", - entityType.GetTableName()); - Assert.Equal( - "PK_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWork~", - entityType.GetKeys().Single().GetName()); - Assert.Equal( - "FK_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWork~", - entityType.GetForeignKeys().Single().GetConstraintName()); - Assert.Equal( - "IX_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWork~", - entityType.GetIndexes().Single().GetDatabaseName()); - - var entityType2 = context.Model.FindEntityType( - typeof( - LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkingCorrectlyDetails - ))!; - - Assert.Equal( - "LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkin~1", - entityType2.GetTableName()); - Assert.Equal( - "PK_LoginDetails", - entityType2.GetKeys().Single().GetName()); - Assert.Equal( - "ExtraPropertyWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkingCo~", - entityType2.GetProperties().ElementAt(1).GetColumnName(StoreObjectIdentifier.Table(entityType2.GetTableName()!))); - Assert.Equal( - "ExtraPropertyWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkingC~1", - entityType2.GetProperties().ElementAt(2).GetColumnName(StoreObjectIdentifier.Table(entityType2.GetTableName()!))); - Assert.Equal( - "IX_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWork~", - entityType2.GetIndexes().Single().GetDatabaseName()); - } - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - protected void AssertContainsSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected, assertOrder: false); - - public class UpdatesSqlServerTPTFixture : UpdatesRelationalFixture + public class UpdatesSqlServerTPTFixture : UpdatesSqlServerFixtureBase { - protected override ITestStoreFactory TestStoreFactory - => SqlServerTestStoreFactory.Instance; - protected override string StoreName => "UpdateTestTPT"; - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - => base.AddOptions(builder).ConfigureWarnings( - w => - { - w.Log(SqlServerEventId.DecimalTypeKeyWarning); - }); - - protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) - => configurationBuilder.Properties().HaveColumnType("decimal(18, 2)"); - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) { base.OnModelCreating(modelBuilder, context); - modelBuilder.Entity() - .Property(p => p.Id).HasDefaultValueSql("NEWID()"); - modelBuilder.Entity() .UseTptMappingStrategy(); } diff --git a/test/EFCore.SqlServer.FunctionalTests/UpdatesSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/UpdatesSqlServerTest.cs index 18153e9d7ab..026f204f317 100644 --- a/test/EFCore.SqlServer.FunctionalTests/UpdatesSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/UpdatesSqlServerTest.cs @@ -3,21 +3,16 @@ #nullable enable -using Microsoft.EntityFrameworkCore.TestModels.UpdatesModel; - namespace Microsoft.EntityFrameworkCore; -public class UpdatesSqlServerTest : UpdatesRelationalTestBase +public class UpdatesSqlServerTest : UpdatesSqlServerTestBase { // ReSharper disable once UnusedParameter.Local public UpdatesSqlServerTest(UpdatesSqlServerFixture fixture, ITestOutputHelper testOutputHelper) - : base(fixture) + : base(fixture, testOutputHelper) { - //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); - Fixture.TestSqlLoggerFactory.Clear(); } - [ConditionalFact] public override void Save_with_shared_foreign_key() { base.Save_with_shared_foreign_key(); @@ -43,58 +38,6 @@ OUTPUT INSERTED.[Id] VALUES (@p0, @p1, @p2);"); } - [ConditionalFact] - public override void Can_add_and_remove_self_refs() - { - base.Can_add_and_remove_self_refs(); - - AssertContainsSql( - @"@p0='1' (Nullable = false) (Size = 4000) -@p1=NULL (DbType = Int32) - -SET IMPLICIT_TRANSACTIONS OFF; -SET NOCOUNT ON; -INSERT INTO [Person] ([Name], [ParentId]) -OUTPUT INSERTED.[PersonId] -VALUES (@p0, @p1);", - // - @"@p2='2' (Nullable = false) (Size = 4000) -@p3='1' (Nullable = true) -@p4='3' (Nullable = false) (Size = 4000) -@p5='1' (Nullable = true) - -SET IMPLICIT_TRANSACTIONS OFF; -SET NOCOUNT ON; -MERGE [Person] USING ( -VALUES (@p2, @p3, 0), -(@p4, @p5, 1)) AS i ([Name], [ParentId], _Position) ON 1=0 -WHEN NOT MATCHED THEN -INSERT ([Name], [ParentId]) -VALUES (i.[Name], i.[ParentId]) -OUTPUT INSERTED.[PersonId], i._Position;", - // - @"@p6='4' (Nullable = false) (Size = 4000) -@p7='2' (Nullable = true) -@p8='5' (Nullable = false) (Size = 4000) -@p9='2' (Nullable = true) -@p10='6' (Nullable = false) (Size = 4000) -@p11='3' (Nullable = true) -@p12='7' (Nullable = false) (Size = 4000) -@p13='3' (Nullable = true) - -SET IMPLICIT_TRANSACTIONS OFF; -SET NOCOUNT ON; -MERGE [Person] USING ( -VALUES (@p6, @p7, 0), -(@p8, @p9, 1), -(@p10, @p11, 2), -(@p12, @p13, 3)) AS i ([Name], [ParentId], _Position) ON 1=0 -WHEN NOT MATCHED THEN -INSERT ([Name], [ParentId]) -VALUES (i.[Name], i.[ParentId]) -OUTPUT INSERTED.[PersonId], i._Position;"); - } - public override void Save_replaced_principal() { base.Save_replaced_principal(); @@ -128,75 +71,9 @@ FROM [ProductBase] AS [p] WHERE [p].[Discriminator] = N'Product' AND [p].[DependentId] = @__category_PrincipalId_0"); } - public override void Identifiers_are_generated_correctly() - { - using var context = CreateContext(); - var entityType = context.Model.FindEntityType( - typeof( - LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkingCorrectly - ))!; - Assert.Equal( - "LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorking~", - entityType.GetTableName()); - Assert.Equal( - "PK_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWork~", - entityType.GetKeys().Single().GetName()); - Assert.Equal( - "FK_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWork~", - entityType.GetForeignKeys().Single().GetConstraintName()); - Assert.Equal( - "IX_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWork~", - entityType.GetIndexes().Single().GetDatabaseName()); - - var entityType2 = context.Model.FindEntityType( - typeof( - LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkingCorrectlyDetails - ))!; - - Assert.Equal( - "LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkin~1", - entityType2.GetTableName()); - Assert.Equal( - "PK_LoginDetails", - entityType2.GetKeys().Single().GetName()); - Assert.Equal( - "ExtraPropertyWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkingCo~", - entityType2.GetProperties().ElementAt(1).GetColumnName(StoreObjectIdentifier.Table(entityType2.GetTableName()!))); - Assert.Equal( - "ExtraPropertyWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkingC~1", - entityType2.GetProperties().ElementAt(2).GetColumnName(StoreObjectIdentifier.Table(entityType2.GetTableName()!))); - Assert.Equal( - "IX_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWork~", - entityType2.GetIndexes().Single().GetDatabaseName()); - } - - private void AssertSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); - - protected void AssertContainsSql(params string[] expected) - => Fixture.TestSqlLoggerFactory.AssertBaseline(expected, assertOrder: false); - - public class UpdatesSqlServerFixture : UpdatesRelationalFixture + public class UpdatesSqlServerFixture : UpdatesSqlServerFixtureBase { - protected override ITestStoreFactory TestStoreFactory - => SqlServerTestStoreFactory.Instance; - - public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) - => base.AddOptions(builder).ConfigureWarnings( - w => - { - w.Log(SqlServerEventId.DecimalTypeKeyWarning); - }); - - protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) - => configurationBuilder.Properties().HaveColumnType("decimal(18, 2)"); - - protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) - { - base.OnModelCreating(modelBuilder, context); - - modelBuilder.Entity() - .Property(p => p.Id).HasDefaultValueSql("NEWID()"); - } + protected override string StoreName + => "UpdateTest"; } } diff --git a/test/EFCore.SqlServer.FunctionalTests/UpdatesSqlServerTestBase.cs b/test/EFCore.SqlServer.FunctionalTests/UpdatesSqlServerTestBase.cs new file mode 100644 index 00000000000..c60b59077dc --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/UpdatesSqlServerTestBase.cs @@ -0,0 +1,250 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using Microsoft.EntityFrameworkCore.TestModels.UpdatesModel; + +namespace Microsoft.EntityFrameworkCore; + +public abstract class UpdatesSqlServerTestBase : UpdatesRelationalTestBase + where TFixture : UpdatesSqlServerTestBase.UpdatesSqlServerFixtureBase +{ + // ReSharper disable once UnusedParameter.Local + public UpdatesSqlServerTestBase(TFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + //Fixture.TestSqlLoggerFactory.SetTestOutputHelper(testOutputHelper); + Fixture.TestSqlLoggerFactory.Clear(); + } + + public override void Can_add_and_remove_self_refs() + { + Fixture.ResetIdentity(); + + base.Can_add_and_remove_self_refs(); + + AssertSql( + @"@p0=NULL (Size = 4000) +@p1='1' (Nullable = false) (Size = 4000) +@p2=NULL (DbType = Int32) + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +INSERT INTO [Person] ([Country], [Name], [ParentId]) +OUTPUT INSERTED.[PersonId] +VALUES (@p0, @p1, @p2);", + // + @"@p3=NULL (Size = 4000) +@p4='2' (Nullable = false) (Size = 4000) +@p5='1' (Nullable = true) +@p6=NULL (Size = 4000) +@p7='3' (Nullable = false) (Size = 4000) +@p8='1' (Nullable = true) + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +MERGE [Person] USING ( +VALUES (@p3, @p4, @p5, 0), +(@p6, @p7, @p8, 1)) AS i ([Country], [Name], [ParentId], _Position) ON 1=0 +WHEN NOT MATCHED THEN +INSERT ([Country], [Name], [ParentId]) +VALUES (i.[Country], i.[Name], i.[ParentId]) +OUTPUT INSERTED.[PersonId], i._Position;", + // + @"@p9=NULL (Size = 4000) +@p10='4' (Nullable = false) (Size = 4000) +@p11='2' (Nullable = true) +@p12=NULL (Size = 4000) +@p13='5' (Nullable = false) (Size = 4000) +@p14='2' (Nullable = true) +@p15=NULL (Size = 4000) +@p16='6' (Nullable = false) (Size = 4000) +@p17='3' (Nullable = true) +@p18=NULL (Size = 4000) +@p19='7' (Nullable = false) (Size = 4000) +@p20='3' (Nullable = true) + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +MERGE [Person] USING ( +VALUES (@p9, @p10, @p11, 0), +(@p12, @p13, @p14, 1), +(@p15, @p16, @p17, 2), +(@p18, @p19, @p20, 3)) AS i ([Country], [Name], [ParentId], _Position) ON 1=0 +WHEN NOT MATCHED THEN +INSERT ([Country], [Name], [ParentId]) +VALUES (i.[Country], i.[Name], i.[ParentId]) +OUTPUT INSERTED.[PersonId], i._Position;", + // + @"@p0='4' +@p1='5' +@p2='6' +@p3='2' +@p4='7' +@p5='3' +@p6=NULL (Size = 4000) +@p7='1' (Nullable = false) (Size = 4000) +@p8=NULL (DbType = Int32) + +SET NOCOUNT ON; +DELETE FROM [Person] +OUTPUT 1 +WHERE [PersonId] = @p0; +DELETE FROM [Person] +OUTPUT 1 +WHERE [PersonId] = @p1; +DELETE FROM [Person] +OUTPUT 1 +WHERE [PersonId] = @p2; +DELETE FROM [Person] +OUTPUT 1 +WHERE [PersonId] = @p3; +DELETE FROM [Person] +OUTPUT 1 +WHERE [PersonId] = @p4; +DELETE FROM [Person] +OUTPUT 1 +WHERE [PersonId] = @p5; +INSERT INTO [Person] ([Country], [Name], [ParentId]) +OUTPUT INSERTED.[PersonId] +VALUES (@p6, @p7, @p8);", + // + @"@p9='1' +@p10=NULL (Size = 4000) +@p11='2' (Nullable = false) (Size = 4000) +@p12='8' (Nullable = true) +@p13=NULL (Size = 4000) +@p14='3' (Nullable = false) (Size = 4000) +@p15='8' (Nullable = true) + +SET NOCOUNT ON; +DELETE FROM [Person] +OUTPUT 1 +WHERE [PersonId] = @p9; +MERGE [Person] USING ( +VALUES (@p10, @p11, @p12, 0), +(@p13, @p14, @p15, 1)) AS i ([Country], [Name], [ParentId], _Position) ON 1=0 +WHEN NOT MATCHED THEN +INSERT ([Country], [Name], [ParentId]) +VALUES (i.[Country], i.[Name], i.[ParentId]) +OUTPUT INSERTED.[PersonId], i._Position;", + // + @"@p16=NULL (Size = 4000) +@p17='4' (Nullable = false) (Size = 4000) +@p18='9' (Nullable = true) +@p19=NULL (Size = 4000) +@p20='5' (Nullable = false) (Size = 4000) +@p21='9' (Nullable = true) +@p22=NULL (Size = 4000) +@p23='6' (Nullable = false) (Size = 4000) +@p24='10' (Nullable = true) +@p25=NULL (Size = 4000) +@p26='7' (Nullable = false) (Size = 4000) +@p27='10' (Nullable = true) + +SET IMPLICIT_TRANSACTIONS OFF; +SET NOCOUNT ON; +MERGE [Person] USING ( +VALUES (@p16, @p17, @p18, 0), +(@p19, @p20, @p21, 1), +(@p22, @p23, @p24, 2), +(@p25, @p26, @p27, 3)) AS i ([Country], [Name], [ParentId], _Position) ON 1=0 +WHEN NOT MATCHED THEN +INSERT ([Country], [Name], [ParentId]) +VALUES (i.[Country], i.[Name], i.[ParentId]) +OUTPUT INSERTED.[PersonId], i._Position;", + // + @"SELECT [p].[PersonId], [p].[Country], [p].[Name], [p].[ParentId], [p].[Address_City], [p].[Country], [p0].[PersonId], [p0].[Country], [p0].[Name], [p0].[ParentId], [p0].[Address_City], [p0].[Country], [p1].[PersonId], [p1].[Country], [p1].[Name], [p1].[ParentId], [p1].[Address_City], [p1].[Country], [p2].[PersonId], [p2].[Country], [p2].[Name], [p2].[ParentId], [p2].[Address_City], [p2].[Country] +FROM [Person] AS [p] +LEFT JOIN [Person] AS [p0] ON [p].[ParentId] = [p0].[PersonId] +LEFT JOIN [Person] AS [p1] ON [p0].[ParentId] = [p1].[PersonId] +LEFT JOIN [Person] AS [p2] ON [p1].[ParentId] = [p2].[PersonId]"); + } + + public override void Identifiers_are_generated_correctly() + { + using var context = CreateContext(); + var entityType = context.Model.FindEntityType( + typeof( + LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkingCorrectly + ))!; + Assert.Equal( + "LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorking~", + entityType.GetTableName()); + Assert.Equal( + "PK_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWork~", + entityType.GetKeys().Single().GetName()); + Assert.Equal( + "FK_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWork~", + entityType.GetForeignKeys().Single().GetConstraintName()); + Assert.Equal( + "IX_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWork~", + entityType.GetIndexes().Single().GetDatabaseName()); + + var entityType2 = context.Model.FindEntityType( + typeof( + LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkingCorrectlyDetails + ))!; + + Assert.Equal( + "LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkin~1", + entityType2.GetTableName()); + Assert.Equal( + "PK_LoginDetails", + entityType2.GetKeys().Single().GetName()); + Assert.Equal( + "ExtraPropertyWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkingCo~", + entityType2.GetProperties().ElementAt(1).GetColumnName(StoreObjectIdentifier.Table(entityType2.GetTableName()!))); + Assert.Equal( + "ExtraPropertyWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWorkingC~1", + entityType2.GetProperties().ElementAt(2).GetColumnName(StoreObjectIdentifier.Table(entityType2.GetTableName()!))); + Assert.Equal( + "IX_LoginEntityTypeWithAnExtremelyLongAndOverlyConvolutedNameThatIsUsedToVerifyThatTheStoreIdentifierGenerationLengthLimitIsWork~", + entityType2.GetIndexes().Single().GetDatabaseName()); + } + + protected void AssertSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); + + protected void AssertContainsSql(params string[] expected) + => Fixture.TestSqlLoggerFactory.AssertBaseline(expected, assertOrder: false); + + public abstract class UpdatesSqlServerFixtureBase : UpdatesRelationalFixture + { + protected override ITestStoreFactory TestStoreFactory + => SqlServerTestStoreFactory.Instance; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => base.AddOptions(builder).ConfigureWarnings( + w => + { + w.Log(SqlServerEventId.DecimalTypeKeyWarning); + }); + + protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) + => configurationBuilder.Properties().HaveColumnType("decimal(18, 2)"); + + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + { + base.OnModelCreating(modelBuilder, context); + + modelBuilder.Entity() + .Property(p => p.Id).HasDefaultValueSql("NEWID()"); + } + public virtual void ResetIdentity() + { + var context = CreateContext(); + context.Database.ExecuteSqlRaw(ResetIdentitySql); + TestSqlLoggerFactory.Clear(); + } + + private const string ResetIdentitySql = @" +-- We can't use TRUNCATE on tables with foreign keys, so we DELETE and reset IDENTITY manually. +-- DBCC CHECKIDENT resets IDENTITY, but behaves differently based on whether whether rows were ever inserted (seed+1) or not (seed). +-- So we insert a dummy row before deleting everything to make sure we get the seed value 1. +INSERT INTO [Person] ([Name]) VALUES (''); +DELETE FROM [Person]; +DBCC CHECKIDENT ('[Person]', RESEED, 0);"; + } +}