From 7d9746ecedef730080d002b722506f67b563a895 Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Wed, 30 Nov 2022 10:53:30 +0000 Subject: [PATCH 1/2] Handle row-version columns in owned types with conversions Fixes #29689 Also add more end-to-end tests for optimistic concurrency, converters, owned types, inheritance, and table sharing. --- .../TableSharingConcurrencyTokenConvention.cs | 33 ++- .../F1RelationalFixture.cs | 57 ++++ .../RelationalModelValidatorTest.cs | 2 +- .../F1FixtureBase.cs | 27 ++ .../F1MaterializationInterceptor.cs | 32 +++ .../TestModels/ConcurrencyModel/Circuit.cs | 50 ++++ .../TestModels/ConcurrencyModel/CircuitTpc.cs | 44 ++++ .../TestModels/ConcurrencyModel/CircuitTpt.cs | 44 ++++ .../TestModels/ConcurrencyModel/City.cs | 56 ++++ .../TestModels/ConcurrencyModel/F1Context.cs | 22 ++ .../TestModels/ConcurrencyModel/Fan.cs | 48 ++++ .../TestModels/ConcurrencyModel/FanTpc.cs | 42 +++ .../TestModels/ConcurrencyModel/FanTpt.cs | 42 +++ .../TestModels/ConcurrencyModel/SwagBag.cs | 20 ++ .../F1SqlServerFixture.cs | 117 ++++++++ .../OptimisticConcurrencySqlServerTest.cs | 249 +++++++++++++++++- 16 files changed, 876 insertions(+), 9 deletions(-) create mode 100644 test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Circuit.cs create mode 100644 test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/CircuitTpc.cs create mode 100644 test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/CircuitTpt.cs create mode 100644 test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/City.cs create mode 100644 test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Fan.cs create mode 100644 test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/FanTpc.cs create mode 100644 test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/FanTpt.cs create mode 100644 test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/SwagBag.cs diff --git a/src/EFCore.Relational/Metadata/Conventions/TableSharingConcurrencyTokenConvention.cs b/src/EFCore.Relational/Metadata/Conventions/TableSharingConcurrencyTokenConvention.cs index 1c78f979437..6d09e1af470 100644 --- a/src/EFCore.Relational/Metadata/Conventions/TableSharingConcurrencyTokenConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/TableSharingConcurrencyTokenConvention.cs @@ -103,17 +103,38 @@ public virtual void ProcessModelFinalizing( foreach (var (conventionEntityType, exampleProperty) in entityTypesMissingConcurrencyColumn) { - var providerType = exampleProperty.GetProviderClrType() - ?? (exampleProperty.GetValueConverter() ?? exampleProperty.FindTypeMapping()?.Converter)?.ProviderClrType - ?? exampleProperty.ClrType; - conventionEntityType.Builder.CreateUniqueProperty( - providerType, + var propertyBuilder = conventionEntityType.Builder.CreateUniqueProperty( + exampleProperty.ClrType, ConcurrencyPropertyPrefix + exampleProperty.Name, !exampleProperty.IsNullable)! .HasColumnName(concurrencyColumnName)! .HasColumnType(exampleProperty.GetColumnType())! .IsConcurrencyToken(true)! - .ValueGenerated(exampleProperty.ValueGenerated); + .ValueGenerated(exampleProperty.ValueGenerated)!; + + var typeMapping = exampleProperty.FindTypeMapping(); + if (typeMapping != null) + { + propertyBuilder = propertyBuilder.HasTypeMapping(typeMapping)!; + } + + var converter = exampleProperty.GetValueConverter(); + if (converter != null) + { + propertyBuilder = propertyBuilder.HasConversion(converter)!; + } + + var providerType = exampleProperty.GetProviderClrType(); + if (providerType != propertyBuilder.Metadata.GetProviderClrType()) + { + propertyBuilder = propertyBuilder.HasConversion(providerType)!; + } + + var comparer = exampleProperty.GetValueComparer(); + if (comparer != null) + { + propertyBuilder.HasValueComparer(comparer); + } } } } diff --git a/test/EFCore.Relational.Specification.Tests/F1RelationalFixture.cs b/test/EFCore.Relational.Specification.Tests/F1RelationalFixture.cs index 65219465adb..8a6b3ccc703 100644 --- a/test/EFCore.Relational.Specification.Tests/F1RelationalFixture.cs +++ b/test/EFCore.Relational.Specification.Tests/F1RelationalFixture.cs @@ -25,5 +25,62 @@ protected override void BuildModelExternal(ModelBuilder modelBuilder) modelBuilder.Entity().ToTable("EngineSuppliers"); modelBuilder.Entity().ToTable("Gearboxes"); modelBuilder.Entity().ToTable("Sponsors"); + + modelBuilder.Entity().UseTptMappingStrategy(); + modelBuilder.Entity().UseTpcMappingStrategy(); + + modelBuilder.Entity( + b => + { + b.ToTable("Circuits"); + b.Property(e => e.Name).HasColumnName("Name"); + }); + + modelBuilder.Entity( + b => + { + b.ToTable("Circuits"); + b.Property(e => e.Name).HasColumnName("Name"); + }); + + modelBuilder.Entity( + b => + { + b.UseTptMappingStrategy(); + b.Property(e => e.Name).HasColumnName("Name"); + }); + + modelBuilder.Entity( + b => + { + b.ToTable("StreetCircuitsTpt"); + }); + + modelBuilder.Entity( + b => + { + b.ToTable("StreetCircuitsTpt"); + b.Property(e => e.Name).HasColumnName("Name"); + }); + + modelBuilder.Entity( + b => + { + b.UseTpcMappingStrategy(); + b.Property(e => e.Name).HasColumnName("Name"); + }); + + modelBuilder.Entity( + b => + { + b.ToTable("StreetCircuitsTpc"); + }); + + modelBuilder.Entity( + b => + { + b.ToTable("StreetCircuitsTpc"); + b.Property(e => e.Name).HasColumnName("Name"); + }); } } diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs index 265143cc7e6..4689fbe81ff 100644 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalModelValidatorTest.cs @@ -1834,7 +1834,7 @@ public virtual void Passes_for_missing_concurrency_token_property_on_the_sharing var personType = model.FindEntityType(typeof(Person))!; var concurrencyProperty = personType.GetDeclaredProperties().Single(p => p.IsConcurrencyToken); Assert.Equal("Version", concurrencyProperty.GetColumnName()); - Assert.Equal(typeof(byte[]), concurrencyProperty.ClrType); + Assert.Equal(typeof(ulong), concurrencyProperty.ClrType); } [ConditionalFact] diff --git a/test/EFCore.Specification.Tests/F1FixtureBase.cs b/test/EFCore.Specification.Tests/F1FixtureBase.cs index f7912268d09..8532c43715b 100644 --- a/test/EFCore.Specification.Tests/F1FixtureBase.cs +++ b/test/EFCore.Specification.Tests/F1FixtureBase.cs @@ -192,6 +192,33 @@ protected virtual void BuildModelExternal(ModelBuilder modelBuilder) eb.Property("Version").IsRowVersion(); eb.Property(Sponsor.ClientTokenPropertyName).IsConcurrencyToken(); }); + + modelBuilder.Entity(); + modelBuilder.Entity(); + modelBuilder.Entity(); + + modelBuilder.Entity(); + modelBuilder.Entity(); + modelBuilder.Entity(); + + modelBuilder.Entity(); + modelBuilder.Entity(); + modelBuilder.Entity(); + + modelBuilder.Entity(); + modelBuilder.Entity().HasOne(e => e.City).WithOne().HasForeignKey(e => e.Id); + modelBuilder.Entity(); + modelBuilder.Entity(); + + modelBuilder.Entity(); + modelBuilder.Entity().HasOne(e => e.City).WithOne().HasForeignKey(e => e.Id); + modelBuilder.Entity(); + modelBuilder.Entity(); + + modelBuilder.Entity(); + modelBuilder.Entity().HasOne(e => e.City).WithOne().HasForeignKey(e => e.Id); + modelBuilder.Entity(); + modelBuilder.Entity(); } private static void ConfigureConstructorBinding(IMutableEntityType mutableEntityType, params string[] propertyNames) diff --git a/test/EFCore.Specification.Tests/F1MaterializationInterceptor.cs b/test/EFCore.Specification.Tests/F1MaterializationInterceptor.cs index 162f6c8d183..c7262fe8f7a 100644 --- a/test/EFCore.Specification.Tests/F1MaterializationInterceptor.cs +++ b/test/EFCore.Specification.Tests/F1MaterializationInterceptor.cs @@ -84,6 +84,38 @@ public InterceptionResult CreatingInstance( nameof(TitleSponsor) => InterceptionResult.SuppressWithResult( new TitleSponsor.TitleSponsorProxy( materializationData.GetPropertyValue("_loader"))), + nameof(SuperFan) => InterceptionResult.SuppressWithResult( + new SuperFan.SuperFanProxy()), + nameof(MegaFan) => InterceptionResult.SuppressWithResult( + new MegaFan.MegaFanProxy()), + nameof(SuperFanTpt) => InterceptionResult.SuppressWithResult( + new SuperFanTpt.SuperFanTptProxy()), + nameof(MegaFanTpt) => InterceptionResult.SuppressWithResult( + new MegaFanTpt.MegaFanTptProxy()), + nameof(SuperFanTpc) => InterceptionResult.SuppressWithResult( + new SuperFanTpc.SuperFanTpcProxy()), + nameof(MegaFanTpc) => InterceptionResult.SuppressWithResult( + new MegaFanTpc.MegaFanTpcProxy()), + nameof(SwagBag) => InterceptionResult.SuppressWithResult( + new SwagBag.SwagBagProxy()), + nameof(StreetCircuit) => InterceptionResult.SuppressWithResult( + new StreetCircuit.StreetCircuitProxy()), + nameof(OvalCircuit) => InterceptionResult.SuppressWithResult( + new OvalCircuit.OvalCircuitProxy()), + nameof(City) => InterceptionResult.SuppressWithResult( + new City.CityProxy()), + nameof(StreetCircuitTpt) => InterceptionResult.SuppressWithResult( + new StreetCircuitTpt.StreetCircuitTptProxy()), + nameof(OvalCircuitTpt) => InterceptionResult.SuppressWithResult( + new OvalCircuitTpt.OvalCircuitTptProxy()), + nameof(CityTpt) => InterceptionResult.SuppressWithResult( + new CityTpt.CityTptProxy()), + nameof(StreetCircuitTpc) => InterceptionResult.SuppressWithResult( + new StreetCircuitTpc.StreetCircuitTpcProxy()), + nameof(OvalCircuitTpc) => InterceptionResult.SuppressWithResult( + new OvalCircuit.OvalCircuitProxy()), + nameof(CityTpc) => InterceptionResult.SuppressWithResult( + new CityTpc.CityTpcProxy()), _ => result }; diff --git a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Circuit.cs b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Circuit.cs new file mode 100644 index 00000000000..ed7a09285c1 --- /dev/null +++ b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Circuit.cs @@ -0,0 +1,50 @@ +// 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; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel; + +public interface IStreetCircuit +{ + string Name { get; set; } + public TCity City { get; set; } +} + +public abstract class Circuit +{ + public int Id { get; set; } + public string Name { get; set; } + public ulong ULongVersion { get; init; } + + [NotMapped] + public List BinaryVersion { get; init; } +} + +public class StreetCircuit : Circuit, IStreetCircuit +{ + public class StreetCircuitProxy : StreetCircuit, IF1Proxy + { + public bool CreatedCalled { get; set; } + public bool InitializingCalled { get; set; } + public bool InitializedCalled { get; set; } + } + + public int Length { get; set; } + + [Required] + public City City { get; set; } +} + +public class OvalCircuit : Circuit +{ + public class OvalCircuitProxy : OvalCircuit, IF1Proxy + { + public bool CreatedCalled { get; set; } + public bool InitializingCalled { get; set; } + public bool InitializedCalled { get; set; } + } + + public double Banking { get; set; } +} diff --git a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/CircuitTpc.cs b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/CircuitTpc.cs new file mode 100644 index 00000000000..0ad949f76b4 --- /dev/null +++ b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/CircuitTpc.cs @@ -0,0 +1,44 @@ +// 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; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel; + +public abstract class CircuitTpc +{ + public int Id { get; set; } + public string Name { get; set; } + public ulong ULongVersion { get; init; } + + [NotMapped] + public List BinaryVersion { get; init; } +} + +public class StreetCircuitTpc : CircuitTpc, IStreetCircuit +{ + public class StreetCircuitTpcProxy : StreetCircuitTpc, IF1Proxy + { + public bool CreatedCalled { get; set; } + public bool InitializingCalled { get; set; } + public bool InitializedCalled { get; set; } + } + + public int Length { get; set; } + + [Required] + public CityTpc City { get; set; } +} + +public class OvalCircuitTpc : CircuitTpc +{ + public class OvalCircuitTpcProxy : OvalCircuitTpc, IF1Proxy + { + public bool CreatedCalled { get; set; } + public bool InitializingCalled { get; set; } + public bool InitializedCalled { get; set; } + } + + public double Banking { get; set; } +} diff --git a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/CircuitTpt.cs b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/CircuitTpt.cs new file mode 100644 index 00000000000..856b8bec690 --- /dev/null +++ b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/CircuitTpt.cs @@ -0,0 +1,44 @@ +// 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; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel; + +public abstract class CircuitTpt +{ + public int Id { get; set; } + public string Name { get; set; } + public ulong ULongVersion { get; init; } + + [NotMapped] + public List BinaryVersion { get; init; } +} + +public class StreetCircuitTpt : CircuitTpt, IStreetCircuit +{ + public class StreetCircuitTptProxy : StreetCircuitTpt, IF1Proxy + { + public bool CreatedCalled { get; set; } + public bool InitializingCalled { get; set; } + public bool InitializedCalled { get; set; } + } + + public int Length { get; set; } + + [Required] + public CityTpt City { get; set; } +} + +public class OvalCircuitTpt : CircuitTpt +{ + public class OvalCircuitTptProxy : OvalCircuitTpt, IF1Proxy + { + public bool CreatedCalled { get; set; } + public bool InitializingCalled { get; set; } + public bool InitializedCalled { get; set; } + } + + public double Banking { get; set; } +} diff --git a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/City.cs b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/City.cs new file mode 100644 index 00000000000..366571fe7ce --- /dev/null +++ b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/City.cs @@ -0,0 +1,56 @@ +// 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; + +namespace Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel; + +public interface ICity +{ + public string Name { get; set; } +} + +public class City : ICity +{ + public class CityProxy : City, IF1Proxy + { + public bool CreatedCalled { get; set; } + public bool InitializingCalled { get; set; } + public bool InitializedCalled { get; set; } + } + + public int Id { get; set; } + + [Required] + public string Name { get; set; } +} + +public class CityTpt : ICity +{ + public class CityTptProxy : CityTpt, IF1Proxy + { + public bool CreatedCalled { get; set; } + public bool InitializingCalled { get; set; } + public bool InitializedCalled { get; set; } + } + + public int Id { get; set; } + + [Required] + public string Name { get; set; } +} + +public class CityTpc : ICity +{ + public class CityTpcProxy : CityTpc, IF1Proxy + { + public bool CreatedCalled { get; set; } + public bool InitializingCalled { get; set; } + public bool InitializedCalled { get; set; } + } + + public int Id { get; set; } + + [Required] + public string Name { get; set; } +} diff --git a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/F1Context.cs b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/F1Context.cs index a6a1389127a..d28a57d3342 100644 --- a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/F1Context.cs +++ b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/F1Context.cs @@ -15,6 +15,11 @@ public F1Context(DbContextOptions options) public DbSet Sponsors { get; set; } public DbSet Engines { get; set; } public DbSet EngineSuppliers { get; set; } + public DbSet Fans { get; set; } + public DbSet FanTpts { get; set; } + public DbSet FanTpcs { get; set; } + + public DbSet Circuits { get; set; } public static void Seed(F1Context context) { @@ -864,5 +869,22 @@ private static void AddEntities(F1Context context) teams.Single(t => t.Id == Team.McLaren).Sponsors.Add(vodafone); teams.Single(t => t.Id == Team.Ferrari).Sponsors.Add(shell); + + context.AddRange( + new SuperFan { Id = 1, Name = "Alice", Swag = new() { Stuff = "SuperStuff" }}, + new MegaFan { Id = 2, Name = "Toast", Swag = new() { Stuff = "MegaStuff" } }, + new SuperFanTpt { Id = 1, Name = "Alice", Swag = new() { Stuff = "SuperStuff" }}, + new MegaFanTpt { Id = 2, Name = "Toast", Swag = new() { Stuff = "MegaStuff" } }, + new SuperFanTpc { Id = 1, Name = "Alice", Swag = new() { Stuff = "SuperStuff" }}, + new MegaFanTpc { Id = 2, Name = "Toast", Swag = new() { Stuff = "MegaStuff" } }, + new StreetCircuit { Id = 1, Name = "Monaco" }, + new City { Id = 1, Name = "Monaco" }, + new OvalCircuit { Id = 2, Name = "Indy" }, + new StreetCircuitTpt { Id = 1, Name = "Monaco" }, + new CityTpt { Id = 1, Name = "Monaco" }, + new OvalCircuitTpt { Id = 2, Name = "Indy" }, + new StreetCircuitTpc { Id = 1, Name = "Monaco" }, + new CityTpc { Id = 1, Name = "Monaco" }, + new OvalCircuitTpc { Id = 2, Name = "Indy" }); } } diff --git a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Fan.cs b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Fan.cs new file mode 100644 index 00000000000..c2f31d644ca --- /dev/null +++ b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/Fan.cs @@ -0,0 +1,48 @@ +// 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.Schema; + +namespace Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel; + +public interface ISuperFan +{ + string Name { get; set; } + SwagBag Swag { get; set; } +} + +public abstract class Fan +{ + public int Id { get; set; } + public string Name { get; set; } + public ulong ULongVersion { get; init; } + + [NotMapped] + public List BinaryVersion { get; init; } +} + +public class MegaFan : Fan +{ + public class MegaFanProxy : MegaFan, IF1Proxy + { + public bool CreatedCalled { get; set; } + public bool InitializingCalled { get; set; } + public bool InitializedCalled { get; set; } + } + + public string MegaStatus { get; set; } + public SwagBag Swag { get; set; } +} + +public class SuperFan : Fan, ISuperFan +{ + public class SuperFanProxy : SuperFan, IF1Proxy + { + public bool CreatedCalled { get; set; } + public bool InitializingCalled { get; set; } + public bool InitializedCalled { get; set; } + } + + public string SuperStatus { get; set; } + public SwagBag Swag { get; set; } +} diff --git a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/FanTpc.cs b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/FanTpc.cs new file mode 100644 index 00000000000..c5f3e657cb0 --- /dev/null +++ b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/FanTpc.cs @@ -0,0 +1,42 @@ +// 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.Schema; + +namespace Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel; + +public abstract class FanTpc +{ + public int Id { get; set; } + public string Name { get; set; } + public ulong ULongVersion { get; init; } + + [NotMapped] + public List BinaryVersion { get; init; } +} + +public class MegaFanTpc : FanTpc +{ + public class MegaFanTpcProxy : MegaFanTpc, IF1Proxy + { + public bool CreatedCalled { get; set; } + public bool InitializingCalled { get; set; } + public bool InitializedCalled { get; set; } + } + + public string MegaStatus { get; set; } + public SwagBag Swag { get; set; } +} + +public class SuperFanTpc : FanTpc, ISuperFan +{ + public class SuperFanTpcProxy : SuperFanTpc, IF1Proxy + { + public bool CreatedCalled { get; set; } + public bool InitializingCalled { get; set; } + public bool InitializedCalled { get; set; } + } + + public string SuperStatus { get; set; } + public SwagBag Swag { get; set; } +} diff --git a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/FanTpt.cs b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/FanTpt.cs new file mode 100644 index 00000000000..123936c4574 --- /dev/null +++ b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/FanTpt.cs @@ -0,0 +1,42 @@ +// 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.Schema; + +namespace Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel; + +public abstract class FanTpt +{ + public int Id { get; set; } + public string Name { get; set; } + public ulong ULongVersion { get; init; } + + [NotMapped] + public List BinaryVersion { get; init; } +} + +public class MegaFanTpt : FanTpt +{ + public class MegaFanTptProxy : MegaFanTpt, IF1Proxy + { + public bool CreatedCalled { get; set; } + public bool InitializingCalled { get; set; } + public bool InitializedCalled { get; set; } + } + + public string MegaStatus { get; set; } + public SwagBag Swag { get; set; } +} + +public class SuperFanTpt : FanTpt, ISuperFan +{ + public class SuperFanTptProxy : SuperFanTpt, IF1Proxy + { + public bool CreatedCalled { get; set; } + public bool InitializingCalled { get; set; } + public bool InitializedCalled { get; set; } + } + + public string SuperStatus { get; set; } + public SwagBag Swag { get; set; } +} diff --git a/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/SwagBag.cs b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/SwagBag.cs new file mode 100644 index 00000000000..6ff5b5390ff --- /dev/null +++ b/test/EFCore.Specification.Tests/TestModels/ConcurrencyModel/SwagBag.cs @@ -0,0 +1,20 @@ +// 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; + +namespace Microsoft.EntityFrameworkCore.TestModels.ConcurrencyModel; + +[Owned] +public class SwagBag +{ + public class SwagBagProxy : SwagBag, IF1Proxy + { + public bool CreatedCalled { get; set; } + public bool InitializingCalled { get; set; } + public bool InitializedCalled { get; set; } + } + + [Required] + public string Stuff { get; set; } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/F1SqlServerFixture.cs b/test/EFCore.SqlServer.FunctionalTests/F1SqlServerFixture.cs index a27a2143622..3a22ad8db9c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/F1SqlServerFixture.cs +++ b/test/EFCore.SqlServer.FunctionalTests/F1SqlServerFixture.cs @@ -36,6 +36,42 @@ protected override void BuildModelExternal(ModelBuilder modelBuilder) .HasData( new OptimisticParent { Id = new Guid("AF8451C3-61CB-4EDA-8282-92250D85EF03"), } ); + + modelBuilder + .Entity() + .Property(e => e.ULongVersion) + .IsRowVersion() + .HasConversion(); + + modelBuilder + .Entity() + .Property(e => e.ULongVersion) + .IsRowVersion() + .HasConversion(); + + modelBuilder + .Entity() + .Property(e => e.ULongVersion) + .IsRowVersion() + .HasConversion(); + + modelBuilder + .Entity() + .Property(e => e.ULongVersion) + .IsRowVersion() + .HasConversion(); + + modelBuilder + .Entity() + .Property(e => e.ULongVersion) + .IsRowVersion() + .HasConversion(); + + modelBuilder + .Entity() + .Property(e => e.ULongVersion) + .IsRowVersion() + .HasConversion(); } public class OptimisticOptionalChild @@ -54,6 +90,87 @@ public class OptimisticParent public class F1SqlServerFixture : F1SqlServerFixtureBase { + protected override void BuildModelExternal(ModelBuilder modelBuilder) + { + base.BuildModelExternal(modelBuilder); + + var converter = new BinaryVersionConverter(); + var comparer = new BinaryVersionComparer(); + + modelBuilder + .Entity() + .Property(e => e.BinaryVersion) + .HasConversion(converter, comparer) + .IsRowVersion(); + + modelBuilder + .Entity() + .Property(e => e.BinaryVersion) + .HasConversion(converter, comparer) + .IsRowVersion(); + + modelBuilder + .Entity() + .Property(e => e.BinaryVersion) + .HasConversion(converter, comparer) + .IsRowVersion(); + + modelBuilder + .Entity() + .Property(e => e.BinaryVersion) + .HasConversion(converter, comparer) + .IsRowVersion(); + + modelBuilder + .Entity() + .Property(e => e.BinaryVersion) + .HasConversion(converter, comparer) + .IsRowVersion(); + + modelBuilder + .Entity() + .Property(e => e.BinaryVersion) + .HasConversion(converter, comparer) + .IsRowVersion(); + } + + private class BinaryVersionConverter : ValueConverter, byte[]> + { + public BinaryVersionConverter() + : base( + v => v == null ? null : v.ToArray(), + v => v == null ? null : v.ToList()) + { + } + } + + private class BinaryVersionComparer : ValueComparer> + { + public BinaryVersionComparer() + : base( + (l, r) => (l == null && r == null) || (l != null && r != null && l.SequenceEqual(r)), + v => CalculateHashCode(v), + v => v == null ? null : v.ToList()) + { + } + + private static int CalculateHashCode(List source) + { + if (source == null) + { + return 0; + } + + var hash = new HashCode(); + foreach (var el in source) + { + hash.Add(el); + } + + return hash.ToHashCode(); + } + + } } public abstract class F1SqlServerFixtureBase : F1RelationalFixture diff --git a/test/EFCore.SqlServer.FunctionalTests/OptimisticConcurrencySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/OptimisticConcurrencySqlServerTest.cs index c242ab38149..1d2b1a6566d 100644 --- a/test/EFCore.SqlServer.FunctionalTests/OptimisticConcurrencySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/OptimisticConcurrencySqlServerTest.cs @@ -32,6 +32,42 @@ await context } ).ToArrayAsync(); } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public Task Ulong_row_version_with_TPH_and_owned_types(bool updateOwned) + => Row_version_with_owned_types(updateOwned, Mapping.Tph, "ULongVersion"); + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public Task Ulong_row_version_with_TPT_and_owned_types(bool updateOwned) + => Row_version_with_owned_types(updateOwned, Mapping.Tpt, "ULongVersion"); + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public Task Ulong_row_version_with_TPC_and_owned_types(bool updateOwned) + => Row_version_with_owned_types(updateOwned, Mapping.Tpc, "ULongVersion"); + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public Task Ulong_row_version_with_TPH_and_table_splitting(bool updateDependent) + => Row_version_with_table_splitting(updateDependent, Mapping.Tph, "ULongVersion"); + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public Task Ulong_row_version_with_TPT_and_table_splitting(bool updateDependent) + => Row_version_with_table_splitting(updateDependent, Mapping.Tpt, "ULongVersion"); + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public Task Ulong_row_version_with_TPC_and_table_splitting(bool updateDependent) + => Row_version_with_table_splitting(updateDependent, Mapping.Tpc, "ULongVersion"); } public class OptimisticConcurrencySqlServerTest : OptimisticConcurrencySqlServerTestBase @@ -40,6 +76,42 @@ public OptimisticConcurrencySqlServerTest(F1SqlServerFixture fixture) : base(fixture) { } + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public Task Row_version_with_TPH_and_owned_types(bool updateOwned) + => Row_version_with_owned_types>(updateOwned, Mapping.Tph, "BinaryVersion"); + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public Task Row_version_with_TPT_and_owned_types(bool updateOwned) + => Row_version_with_owned_types>(updateOwned, Mapping.Tpt, "BinaryVersion"); + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public Task Row_version_with_TPC_and_owned_types(bool updateOwned) + => Row_version_with_owned_types>(updateOwned, Mapping.Tpc, "BinaryVersion"); + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public Task Ulong_row_version_with_TPH_and_table_splitting(bool updateDependent) + => Row_version_with_table_splitting>(updateDependent, Mapping.Tph, "BinaryVersion"); + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public Task Ulong_row_version_with_TPT_and_table_splitting(bool updateDependent) + => Row_version_with_table_splitting>(updateDependent, Mapping.Tpt, "BinaryVersion"); + + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public Task Ulong_row_version_with_TPC_and_table_splitting(bool updateDependent) + => Row_version_with_table_splitting>(updateDependent, Mapping.Tpc, "BinaryVersion"); } public abstract class OptimisticConcurrencySqlServerTestBase @@ -51,6 +123,179 @@ protected OptimisticConcurrencySqlServerTestBase(TFixture fixture) { } + protected enum Mapping + { + Tph, + Tpt, + Tpc + } + + protected async Task Row_version_with_owned_types(bool updateOwned, Mapping mapping, string propertyName) + where TEntity : class, ISuperFan + { + await using var c = CreateF1Context(); + + await c.Database.CreateExecutionStrategy().ExecuteAsync( + c, async context => + { + var synthesizedPropertyName = $"_TableSharingConcurrencyTokenConvention_{propertyName}"; + + await using var transaction = BeginTransaction(context.Database); + + var fan = context.Set().Single(e => e.Name == "Alice"); + + var fanEntry = c.Entry(fan); + var swagEntry = fanEntry.Reference(s => s.Swag).TargetEntry!; + var originalFanVersion = fanEntry.Property(propertyName).CurrentValue; + var originalSwagVersion = default(TVersion); + + if (mapping == Mapping.Tph) + { + originalSwagVersion + = swagEntry.Property(synthesizedPropertyName).CurrentValue; + + Assert.Equal(originalFanVersion, originalSwagVersion); + } + + if (updateOwned) + { + fan.Swag.Stuff += "+"; + } + else + { + fan.Name += "+"; + } + + await using var innerContext = CreateF1Context(); + UseTransaction(innerContext.Database, transaction); + var fanInner = innerContext.Set().Single(e => e.Name == "Alice"); + + if (updateOwned) + { + fanInner.Swag.Stuff += "-"; + } + else + { + fanInner.Name += "-"; + } + + await innerContext.SaveChangesAsync(); + + if (!updateOwned || mapping != Mapping.Tpt) // Issue #22060 + { + await Assert.ThrowsAnyAsync(() => context.SaveChangesAsync()); + + await fanEntry.ReloadAsync(); + await swagEntry.ReloadAsync(); + + await context.SaveChangesAsync(); + + var fanVersion = fanEntry.Property(propertyName).CurrentValue; + Assert.NotEqual(originalFanVersion, fanVersion); + + if (mapping == Mapping.Tph) + { + var swagVersion = swagEntry.Property(synthesizedPropertyName).CurrentValue; + Assert.Equal(fanVersion, swagVersion); + Assert.NotEqual(originalSwagVersion, swagVersion); + } + } + else + { + await context.SaveChangesAsync(); + } + }); + } + + protected async Task Row_version_with_table_splitting( + bool updateDependent, + Mapping mapping, + string propertyName) + where TEntity : class, IStreetCircuit + where TCity : class, ICity + { + await using var c = CreateF1Context(); + + await c.Database.CreateExecutionStrategy().ExecuteAsync( + c, async context => + { + var synthesizedPropertyName = $"_TableSharingConcurrencyTokenConvention_{propertyName}"; + + await using var transaction = BeginTransaction(context.Database); + + var circuit = context.Set().Include(e => e.City).Single(e => e.Name == "Monaco"); + + var circuitEntry = c.Entry(circuit); + var cityEntry = circuitEntry.Reference(s => s.City).TargetEntry!; + var originalCircuitVersion = circuitEntry.Property(propertyName).CurrentValue; + var originalCityVersion = default(TVersion); + + if (mapping == Mapping.Tph) + { + originalCityVersion + = cityEntry.Property(synthesizedPropertyName).CurrentValue; + + Assert.Equal(originalCircuitVersion, originalCityVersion); + } + + if (updateDependent) + { + circuit.City.Name += "+"; + } + else + { + circuit.Name += "+"; + } + + await using var innerContext = CreateF1Context(); + UseTransaction(innerContext.Database, transaction); + var fanInner = innerContext.Set().Include(e => e.City).Single(e => e.Name == "Monaco"); + + if (updateDependent) + { + fanInner.City.Name += "-"; + } + else + { + fanInner.Name += "-"; + } + + if (mapping == Mapping.Tpc) + { + await Assert.ThrowsAsync(() => innerContext.SaveChangesAsync()); + + } + else + { + await innerContext.SaveChangesAsync(); + + if (!updateDependent || mapping != Mapping.Tpt) // Issue #22060 + { + await Assert.ThrowsAnyAsync(() => context.SaveChangesAsync()); + + await circuitEntry.ReloadAsync(); + await cityEntry.ReloadAsync(); + + await context.SaveChangesAsync(); + + var circuitVersion = circuitEntry.Property(propertyName).CurrentValue; + Assert.NotEqual(originalCircuitVersion, circuitVersion); + + if (mapping == Mapping.Tph) + { + var cityVersion = cityEntry.Property(synthesizedPropertyName).CurrentValue; + Assert.Equal(circuitVersion, cityVersion); + Assert.NotEqual(originalCityVersion, cityVersion); + } + } + else + { + await context.SaveChangesAsync(); + } + } + }); + } + [ConditionalFact] public async Task Modifying_concurrency_token_only_is_noop() { @@ -155,13 +400,13 @@ public override void Property_entry_original_value_is_set() base.Property_entry_original_value_is_set(); AssertSql( -""" + """ SELECT TOP(1) [e].[Id], [e].[EngineSupplierId], [e].[Name], [e].[StorageLocation_Latitude], [e].[StorageLocation_Longitude] FROM [Engines] AS [e] ORDER BY [e].[Id] """, // -""" + """ @p1='1' @p2='Mercedes' (Size = 450) @p0='FO 108X' (Size = 4000) From 0de13fda6ce2646a13b93dd5633fe67192843adf Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Sat, 3 Dec 2022 22:01:06 +0000 Subject: [PATCH 2/2] Review updates. --- .../OptimisticConcurrencySqlServerTest.cs | 244 +++++++++++------- 1 file changed, 152 insertions(+), 92 deletions(-) diff --git a/test/EFCore.SqlServer.FunctionalTests/OptimisticConcurrencySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/OptimisticConcurrencySqlServerTest.cs index 1d2b1a6566d..a45c0fec087 100644 --- a/test/EFCore.SqlServer.FunctionalTests/OptimisticConcurrencySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/OptimisticConcurrencySqlServerTest.cs @@ -36,38 +36,38 @@ await context [ConditionalTheory] [InlineData(true)] [InlineData(false)] - public Task Ulong_row_version_with_TPH_and_owned_types(bool updateOwned) - => Row_version_with_owned_types(updateOwned, Mapping.Tph, "ULongVersion"); + public Task Ulong_row_version_with_TPH_and_owned_types(bool updateOwnedFirst) + => Row_version_with_owned_types(updateOwnedFirst, Mapping.Tph, "ULongVersion"); [ConditionalTheory] [InlineData(true)] [InlineData(false)] - public Task Ulong_row_version_with_TPT_and_owned_types(bool updateOwned) - => Row_version_with_owned_types(updateOwned, Mapping.Tpt, "ULongVersion"); + public Task Ulong_row_version_with_TPT_and_owned_types(bool updateOwnedFirst) + => Row_version_with_owned_types(updateOwnedFirst, Mapping.Tpt, "ULongVersion"); [ConditionalTheory] [InlineData(true)] [InlineData(false)] - public Task Ulong_row_version_with_TPC_and_owned_types(bool updateOwned) - => Row_version_with_owned_types(updateOwned, Mapping.Tpc, "ULongVersion"); + public Task Ulong_row_version_with_TPC_and_owned_types(bool updateOwnedFirst) + => Row_version_with_owned_types(updateOwnedFirst, Mapping.Tpc, "ULongVersion"); [ConditionalTheory] [InlineData(true)] [InlineData(false)] - public Task Ulong_row_version_with_TPH_and_table_splitting(bool updateDependent) - => Row_version_with_table_splitting(updateDependent, Mapping.Tph, "ULongVersion"); + public Task Ulong_row_version_with_TPH_and_table_splitting(bool updateDependentFirst) + => Row_version_with_table_splitting(updateDependentFirst, Mapping.Tph, "ULongVersion"); [ConditionalTheory] [InlineData(true)] [InlineData(false)] - public Task Ulong_row_version_with_TPT_and_table_splitting(bool updateDependent) - => Row_version_with_table_splitting(updateDependent, Mapping.Tpt, "ULongVersion"); + public Task Ulong_row_version_with_TPT_and_table_splitting(bool updateDependentFirst) + => Row_version_with_table_splitting(updateDependentFirst, Mapping.Tpt, "ULongVersion"); [ConditionalTheory] [InlineData(true)] [InlineData(false)] - public Task Ulong_row_version_with_TPC_and_table_splitting(bool updateDependent) - => Row_version_with_table_splitting(updateDependent, Mapping.Tpc, "ULongVersion"); + public Task Ulong_row_version_with_TPC_and_table_splitting(bool updateDependentFirst) + => Row_version_with_table_splitting(updateDependentFirst, Mapping.Tpc, "ULongVersion"); } public class OptimisticConcurrencySqlServerTest : OptimisticConcurrencySqlServerTestBase @@ -80,38 +80,38 @@ public OptimisticConcurrencySqlServerTest(F1SqlServerFixture fixture) [ConditionalTheory] [InlineData(true)] [InlineData(false)] - public Task Row_version_with_TPH_and_owned_types(bool updateOwned) - => Row_version_with_owned_types>(updateOwned, Mapping.Tph, "BinaryVersion"); + public Task Row_version_with_TPH_and_owned_types(bool updateOwnedFirst) + => Row_version_with_owned_types>(updateOwnedFirst, Mapping.Tph, "BinaryVersion"); [ConditionalTheory] [InlineData(true)] [InlineData(false)] - public Task Row_version_with_TPT_and_owned_types(bool updateOwned) - => Row_version_with_owned_types>(updateOwned, Mapping.Tpt, "BinaryVersion"); + public Task Row_version_with_TPT_and_owned_types(bool updateOwnedFirst) + => Row_version_with_owned_types>(updateOwnedFirst, Mapping.Tpt, "BinaryVersion"); [ConditionalTheory] [InlineData(true)] [InlineData(false)] - public Task Row_version_with_TPC_and_owned_types(bool updateOwned) - => Row_version_with_owned_types>(updateOwned, Mapping.Tpc, "BinaryVersion"); + public Task Row_version_with_TPC_and_owned_types(bool updateOwnedFirst) + => Row_version_with_owned_types>(updateOwnedFirst, Mapping.Tpc, "BinaryVersion"); [ConditionalTheory] [InlineData(true)] [InlineData(false)] - public Task Ulong_row_version_with_TPH_and_table_splitting(bool updateDependent) - => Row_version_with_table_splitting>(updateDependent, Mapping.Tph, "BinaryVersion"); + public Task Ulong_row_version_with_TPH_and_table_splitting(bool updateDependentFirst) + => Row_version_with_table_splitting>(updateDependentFirst, Mapping.Tph, "BinaryVersion"); [ConditionalTheory] [InlineData(true)] [InlineData(false)] - public Task Ulong_row_version_with_TPT_and_table_splitting(bool updateDependent) - => Row_version_with_table_splitting>(updateDependent, Mapping.Tpt, "BinaryVersion"); + public Task Ulong_row_version_with_TPT_and_table_splitting(bool updateDependentFirst) + => Row_version_with_table_splitting>(updateDependentFirst, Mapping.Tpt, "BinaryVersion"); [ConditionalTheory] [InlineData(true)] [InlineData(false)] - public Task Ulong_row_version_with_TPC_and_table_splitting(bool updateDependent) - => Row_version_with_table_splitting>(updateDependent, Mapping.Tpc, "BinaryVersion"); + public Task Ulong_row_version_with_TPC_and_table_splitting(bool updateDependentFirst) + => Row_version_with_table_splitting>(updateDependentFirst, Mapping.Tpc, "BinaryVersion"); } public abstract class OptimisticConcurrencySqlServerTestBase @@ -130,7 +130,7 @@ protected enum Mapping Tpc } - protected async Task Row_version_with_owned_types(bool updateOwned, Mapping mapping, string propertyName) + protected async Task Row_version_with_owned_types(bool updateOwnedFirst, Mapping mapping, string propertyName) where TEntity : class, ISuperFan { await using var c = CreateF1Context(); @@ -146,69 +146,100 @@ await c.Database.CreateExecutionStrategy().ExecuteAsync( var fanEntry = c.Entry(fan); var swagEntry = fanEntry.Reference(s => s.Swag).TargetEntry!; - var originalFanVersion = fanEntry.Property(propertyName).CurrentValue; - var originalSwagVersion = default(TVersion); + var fanVersion1 = fanEntry.Property(propertyName).CurrentValue; + var swagVersion1 = default(TVersion); - if (mapping == Mapping.Tph) + if (mapping == Mapping.Tph) // Issue #29750 { - originalSwagVersion - = swagEntry.Property(synthesizedPropertyName).CurrentValue; + swagVersion1 = swagEntry.Property(synthesizedPropertyName).CurrentValue; - Assert.Equal(originalFanVersion, originalSwagVersion); - } - - if (updateOwned) - { - fan.Swag.Stuff += "+"; - } - else - { - fan.Name += "+"; + Assert.Equal(fanVersion1, swagVersion1); } await using var innerContext = CreateF1Context(); UseTransaction(innerContext.Database, transaction); var fanInner = innerContext.Set().Single(e => e.Name == "Alice"); - if (updateOwned) + if (updateOwnedFirst) { + fan.Swag.Stuff += "+"; fanInner.Swag.Stuff += "-"; } else { fanInner.Name += "-"; + fan.Name += "+"; } await innerContext.SaveChangesAsync(); - if (!updateOwned || mapping != Mapping.Tpt) // Issue #22060 + if (updateOwnedFirst && mapping == Mapping.Tpt) // Issue #22060 { - await Assert.ThrowsAnyAsync(() => context.SaveChangesAsync()); + await context.SaveChangesAsync(); + return; + } - await fanEntry.ReloadAsync(); - await swagEntry.ReloadAsync(); + await Assert.ThrowsAnyAsync(() => context.SaveChangesAsync()); - await context.SaveChangesAsync(); + await fanEntry.ReloadAsync(); + await swagEntry.ReloadAsync(); - var fanVersion = fanEntry.Property(propertyName).CurrentValue; - Assert.NotEqual(originalFanVersion, fanVersion); + await context.SaveChangesAsync(); - if (mapping == Mapping.Tph) - { - var swagVersion = swagEntry.Property(synthesizedPropertyName).CurrentValue; - Assert.Equal(fanVersion, swagVersion); - Assert.NotEqual(originalSwagVersion, swagVersion); - } + var fanVersion2 = fanEntry.Property(propertyName).CurrentValue; + Assert.NotEqual(fanVersion1, fanVersion2); + + var swagVersion2 = default(TVersion); + if (mapping == Mapping.Tph) // Issue #29750 + { + swagVersion2 = swagEntry.Property(synthesizedPropertyName).CurrentValue; + Assert.Equal(fanVersion2, swagVersion2); + Assert.NotEqual(swagVersion1, swagVersion2); + } + + await innerContext.Entry(fanInner).ReloadAsync(); + await innerContext.Entry(fanInner.Swag).ReloadAsync(); + + if (updateOwnedFirst) + { + fanInner.Name += "-"; + fan.Name += "+"; } else + { + fan.Swag.Stuff += "+"; + fanInner.Swag.Stuff += "-"; + } + + await innerContext.SaveChangesAsync(); + + if (!updateOwnedFirst && mapping == Mapping.Tpt) // Issue #22060 { await context.SaveChangesAsync(); + return; + } + + await Assert.ThrowsAnyAsync(() => context.SaveChangesAsync()); + + await fanEntry.ReloadAsync(); + await swagEntry.ReloadAsync(); + + await context.SaveChangesAsync(); + + var fanVersion3 = fanEntry.Property(propertyName).CurrentValue; + Assert.NotEqual(fanVersion2, fanVersion3); + + if (mapping == Mapping.Tph) // Issue #29750 + { + var swagVersion3 = swagEntry.Property(synthesizedPropertyName).CurrentValue; + Assert.Equal(fanVersion3, swagVersion3); + Assert.NotEqual(swagVersion2, swagVersion3); } }); } protected async Task Row_version_with_table_splitting( - bool updateDependent, + bool updateDependentFirst, Mapping mapping, string propertyName) where TEntity : class, IStreetCircuit @@ -227,71 +258,100 @@ await c.Database.CreateExecutionStrategy().ExecuteAsync( var circuitEntry = c.Entry(circuit); var cityEntry = circuitEntry.Reference(s => s.City).TargetEntry!; - var originalCircuitVersion = circuitEntry.Property(propertyName).CurrentValue; - var originalCityVersion = default(TVersion); + var circuitVersion1 = circuitEntry.Property(propertyName).CurrentValue; + var cityVersion1 = default(TVersion); - if (mapping == Mapping.Tph) + if (mapping == Mapping.Tph) // Issue #29750 { - originalCityVersion - = cityEntry.Property(synthesizedPropertyName).CurrentValue; + cityVersion1 = cityEntry.Property(synthesizedPropertyName).CurrentValue; - Assert.Equal(originalCircuitVersion, originalCityVersion); + Assert.Equal(circuitVersion1, cityVersion1); } - if (updateDependent) + await using var innerContext = CreateF1Context(); + UseTransaction(innerContext.Database, transaction); + var circuitInner = innerContext.Set().Include(e => e.City).Single(e => e.Name == "Monaco"); + + if (updateDependentFirst) { circuit.City.Name += "+"; + circuitInner.City.Name += "-"; } else { circuit.Name += "+"; + circuitInner.Name += "-"; } - await using var innerContext = CreateF1Context(); - UseTransaction(innerContext.Database, transaction); - var fanInner = innerContext.Set().Include(e => e.City).Single(e => e.Name == "Monaco"); - - if (updateDependent) + if (mapping == Mapping.Tpc) // Issue #29751. { - fanInner.City.Name += "-"; + await Assert.ThrowsAsync(() => innerContext.SaveChangesAsync()); + return; } - else + + await innerContext.SaveChangesAsync(); + + if (updateDependentFirst && mapping == Mapping.Tpt) // Issue #22060 { - fanInner.Name += "-"; + await context.SaveChangesAsync(); + return; } - if (mapping == Mapping.Tpc) + await Assert.ThrowsAnyAsync(() => context.SaveChangesAsync()); + + await circuitEntry.ReloadAsync(); + await cityEntry.ReloadAsync(); + + await context.SaveChangesAsync(); + + var circuitVersion2 = circuitEntry.Property(propertyName).CurrentValue; + Assert.NotEqual(circuitVersion1, circuitVersion2); + + var cityVersion2 = default(TVersion); + if (mapping == Mapping.Tph) // Issue #29750 { - await Assert.ThrowsAsync(() => innerContext.SaveChangesAsync()); + cityVersion2 = cityEntry.Property(synthesizedPropertyName).CurrentValue; + Assert.Equal(circuitVersion2, cityVersion2); + Assert.NotEqual(cityVersion1, cityVersion2); + } + + await innerContext.Entry(circuitInner).ReloadAsync(); + await innerContext.Entry(circuitInner.City).ReloadAsync(); + if (updateDependentFirst) + { + circuit.Name += "+"; + circuitInner.Name += "-"; } else { - await innerContext.SaveChangesAsync(); + circuit.City.Name += "+"; + circuitInner.City.Name += "-"; + } - if (!updateDependent || mapping != Mapping.Tpt) // Issue #22060 - { - await Assert.ThrowsAnyAsync(() => context.SaveChangesAsync()); + await innerContext.SaveChangesAsync(); - await circuitEntry.ReloadAsync(); - await cityEntry.ReloadAsync(); + if (!updateDependentFirst && mapping == Mapping.Tpt) // Issue #22060 + { + await context.SaveChangesAsync(); + return; + } - await context.SaveChangesAsync(); + await Assert.ThrowsAnyAsync(() => context.SaveChangesAsync()); - var circuitVersion = circuitEntry.Property(propertyName).CurrentValue; - Assert.NotEqual(originalCircuitVersion, circuitVersion); + await circuitEntry.ReloadAsync(); + await cityEntry.ReloadAsync(); - if (mapping == Mapping.Tph) - { - var cityVersion = cityEntry.Property(synthesizedPropertyName).CurrentValue; - Assert.Equal(circuitVersion, cityVersion); - Assert.NotEqual(originalCityVersion, cityVersion); - } - } - else - { - await context.SaveChangesAsync(); - } + await context.SaveChangesAsync(); + + var circuitVersion3 = circuitEntry.Property(propertyName).CurrentValue; + Assert.NotEqual(circuitVersion2, circuitVersion3); + + if (mapping == Mapping.Tph) // Issue #29750 + { + var cityVersion3 = cityEntry.Property(synthesizedPropertyName).CurrentValue; + Assert.Equal(circuitVersion3, cityVersion3); + Assert.NotEqual(cityVersion2, cityVersion3); } }); }