From 6a0170a7a853321b1fd8707559216ca2d2f659b6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Sep 2025 23:22:52 +0000 Subject: [PATCH 1/2] Initial plan From e4d3b82aed6b689007adcac1e124c187af4a70cb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Sep 2025 23:40:52 +0000 Subject: [PATCH 2/2] Fix enum discriminator serialization issue in Cosmos provider Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com> --- .../Update/Internal/DocumentSource.cs | 25 +++++- .../EnumDiscriminatorCosmosTest.cs | 90 +++++++++++++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 test/EFCore.Cosmos.FunctionalTests/EnumDiscriminatorCosmosTest.cs diff --git a/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs b/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs index 75b9a7f415a..0b00b0122b3 100644 --- a/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs +++ b/src/EFCore.Cosmos/Update/Internal/DocumentSource.cs @@ -325,7 +325,30 @@ private static void SetTemporaryOrdinals( private static JToken? ConvertPropertyValue(IProperty property, IUpdateEntry entry) { - var value = entry.GetCurrentProviderValue(property); + object? value; + + // For discriminator properties, use the discriminator value from the entity type + // to ensure consistency, rather than relying on the tracked property value + var discriminatorProperty = entry.EntityType.FindDiscriminatorProperty(); + if (discriminatorProperty != null && property == discriminatorProperty) + { + var discriminatorValue = entry.EntityType.GetDiscriminatorValue(); + if (discriminatorValue != null) + { + // If there's a converter, apply it to convert the discriminator value to the provider value + var converter = property.GetTypeMapping().Converter; + value = converter?.ConvertToProvider(discriminatorValue) ?? discriminatorValue; + } + else + { + value = entry.GetCurrentProviderValue(property); + } + } + else + { + value = entry.GetCurrentProviderValue(property); + } + return value == null ? null : (value as JToken) ?? JToken.FromObject(value, CosmosClientWrapper.Serializer); diff --git a/test/EFCore.Cosmos.FunctionalTests/EnumDiscriminatorCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/EnumDiscriminatorCosmosTest.cs new file mode 100644 index 00000000000..9093b47022a --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/EnumDiscriminatorCosmosTest.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Microsoft.EntityFrameworkCore.TestUtilities; + +namespace Microsoft.EntityFrameworkCore; + +#nullable disable + +public class EnumDiscriminatorCosmosTest(NonSharedFixture fixture) : NonSharedModelTestBase(fixture), IClassFixture +{ + protected override string StoreName + => "EnumDiscriminatorCosmosTest"; + + protected override ITestStoreFactory TestStoreFactory + => CosmosTestStoreFactory.Instance; + [ConditionalFact] + public async Task Enum_discriminator_saved_as_string_consistently() + { + var contextFactory = await InitializeAsync( + shouldLogCategory: _ => true); + + var thingy = new DerivedThingy2 + { + Id = "A", + Name = "A", + Type = ThingyType.ThingyTwo + }; + + using (var context = contextFactory.CreateContext()) + { + context.Things.Add(thingy); + await context.SaveChangesAsync(); + } + + using (var context = contextFactory.CreateContext()) + { + var entity = context.Things.Single(x => x.Id == "A"); + entity.Name = "A updated"; + await context.SaveChangesAsync(); + + // Verify entity can still be found after update + var reloadedEntity = context.Things.Single(x => x.Id == "A"); + Assert.Equal("A updated", reloadedEntity.Name); + Assert.Equal(ThingyType.ThingyTwo, reloadedEntity.Type); + } + } + + public class MyDbContext : DbContext + { + public MyDbContext(DbContextOptions options) : base(options) + { + } + + public DbSet Things { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(b => + { + b.HasKey(e => e.Id); + b.Property(e => e.Type) + .HasConversion(); + b.HasDiscriminator(e => e.Type) + .HasValue(ThingyType.ThingyTwo); + }); + + modelBuilder.Entity(); + } + } + + public enum ThingyType + { + ThingyOne = 0, + ThingyTwo = 1 + } + + public class Thingy + { + public string Id { get; set; } + public string Name { get; set; } + public ThingyType Type { get; set; } + } + + public class DerivedThingy2 : Thingy + { + public string SomethingForB { get; set; } + } +} \ No newline at end of file