From 8a13af557fd83cf5ee0234d8a7b2747d81a1de0b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 22:13:19 +0000 Subject: [PATCH 1/2] Initial plan From 0bdb76ba1c5281a35afbf33f0af2b6a23dd32b30 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 22:23:38 +0000 Subject: [PATCH 2/2] Fix TableSharingConcurrencyTokenConvention to skip JSON-mapped entities Skip adding shadow concurrency token properties to entities that are mapped to JSON columns. JSON-mapped entities use JSON property names, not column names, and adding a column-mapped property causes model validation to fail. Fixes #37274 Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../TableSharingConcurrencyTokenConvention.cs | 7 +++ ...leSharingConcurrencyTokenConventionTest.cs | 49 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/EFCore.Relational/Metadata/Conventions/TableSharingConcurrencyTokenConvention.cs b/src/EFCore.Relational/Metadata/Conventions/TableSharingConcurrencyTokenConvention.cs index cf9b6f99cdb..095fe6cb6e5 100644 --- a/src/EFCore.Relational/Metadata/Conventions/TableSharingConcurrencyTokenConvention.cs +++ b/src/EFCore.Relational/Metadata/Conventions/TableSharingConcurrencyTokenConvention.cs @@ -78,6 +78,13 @@ public virtual void ProcessModelFinalizing( { Check.DebugAssert(readOnlyProperties.Count != 0, $"No properties mapped to column '{concurrencyColumnName}'"); + // JSON-mapped entities don't have column names for their properties, + // so we skip them as they participate in the owner's concurrency token + if (entityType.IsMappedToJson()) + { + continue; + } + var foundMappedProperty = !IsConcurrencyTokenMissing(readOnlyProperties, entityType, mappedTypes) || entityType.GetProperties() .Any(p => p.GetColumnName(table) == concurrencyColumnName); diff --git a/test/EFCore.Relational.Tests/Metadata/Conventions/TableSharingConcurrencyTokenConventionTest.cs b/test/EFCore.Relational.Tests/Metadata/Conventions/TableSharingConcurrencyTokenConventionTest.cs index cc1197ba992..4bd293a4b58 100644 --- a/test/EFCore.Relational.Tests/Metadata/Conventions/TableSharingConcurrencyTokenConventionTest.cs +++ b/test/EFCore.Relational.Tests/Metadata/Conventions/TableSharingConcurrencyTokenConventionTest.cs @@ -202,6 +202,55 @@ public virtual void Concurrency_token_property_is_not_created_on_the_sharing_whe Assert.All(animalEntityType.GetProperties(), p => Assert.NotEqual(typeof(byte[]), p.ClrType)); } + [ConditionalFact] + public virtual void Missing_concurrency_token_property_is_not_created_for_json_mapped_entity() + { + var modelBuilder = GetModelBuilder(); + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.Property(e => e.RowVersion).IsRowVersion().IsConcurrencyToken(); + b.Property(e => e.Name).HasMaxLength(100).IsRequired(); + b.HasDiscriminator("Type") + .HasValue(nameof(DerivedEntity)); + }); + modelBuilder.Entity(b => + { + b.OwnsOne(x => x.Owned, ob => + { + ob.ToJson(); + ob.Property(o => o.Description).HasMaxLength(200).IsRequired(); + }); + }); + + var model = modelBuilder.Model; + model.FinalizeModel(); + + var ownedEntity = model.FindEntityType(typeof(OwnedEntity)); + Assert.NotNull(ownedEntity); + Assert.True(ownedEntity.IsMappedToJson()); + Assert.DoesNotContain( + ownedEntity.GetProperties(), + p => p.Name.StartsWith("_TableSharingConcurrencyTokenConvention")); + } + + protected abstract class BaseEntity + { + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public long RowVersion { get; set; } + } + + protected class DerivedEntity : BaseEntity + { + public OwnedEntity Owned { get; set; } = new(); + } + + protected class OwnedEntity + { + public string Description { get; set; } = "Any"; + } + protected class Animal { public int Id { get; set; }