Discriminator on derived type is being incorrectly updated with integer value of enum (EF Core CosmosDB provider)
I have a problem with a C# application that uses the Entity Framework Core CosmosDB provider.
I am seeing some strange behaviour relating to a derived type that uses an enum as the discriminator.
I have been able to re-create the issue in a simple console app.
Using the code below after the first 'SaveChangesAsync' the document in the database looks like this:
After the second 'SaveChangesAsync' the document in the database now looks like this:
As you can see the "Type" has been changed to 1 not "ThingyTwo" so it looks like the type has been updated using the integer value of the enum rather that the string version.
This causes a problem in our app because the record can no longer be loaded because the query is looking for 'Type = "ThingyTwo"'.
I did notice that calling 'context.ChangeTracker.Clear()' between the saves does make the type save correctly the second time so this might have something to do with the change tracking.
It would be great if someone could tell me if there is something wrong in my mapping that is causing this or if this is some sort of bug.
Thanks.
EF Core version: 8.0.8 Database provider: Microsoft.EntityFrameworkCore.Cosmos Target framework: (e.g. .NET 8.0) Operating system: MacOS 15.0
// dotnet 8.0 console app with package reference
// to Microsoft.EntityFrameworkCore.Cosmos version 8.0.8
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Microsoft.Extensions.Logging;
namespace CosmosDbTestApp3
{
public class Program
{
public static async Task Main(string[] args)
{
using (var context = new MyDbContext())
{
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();
var a = new DerivedThingy2()
{
Id = "A",
Name = "A",
Type = ThingyType.ThingyTwo
};
context.Things.Add(a);
await context.SaveChangesAsync();
// After this save the CosmosDB record looks like this:
//
// {
// "Id": "A",
// "Name": "A",
// "Type": "ThingyTwo",
// "id": "ThingyTwo|A",
// "_etag": "\"000049e0-0000-1100-0000-670549100000\"",
// "SomethingForB": null,
// "_rid": "NDxoAICHERwBAAAAAAAAAA==",
// "_self": "dbs/NDxoAA==/colls/NDxoAICHERw=/docs/NDxoAICHERwBAAAAAAAAAA==/",
// "_attachments": "attachments/",
// "_ts": 1728399632
// }
// with this line uncommented the 'Type' gets saved correctly
//context.ChangeTracker.Clear();
var a2 = context.Things.Single(x => x.Id == "A");
a2.Name = "A updated";
await context.SaveChangesAsync();
// After this save the CosmosDB record looks like this:
// (notice the Type has been changed to the integer value of the enum):
//
// {
// "Id": "A",
// "Name": "A updated",
// "Type": 1,
// "id": "ThingyTwo|A",
// "_etag": "\"00005ae0-0000-1100-0000-670549310000\"",
// "SomethingForB": null,
// "_rid": "NDxoAICHERwBAAAAAAAAAA==",
// "_self": "dbs/NDxoAA==/colls/NDxoAICHERw=/docs/NDxoAICHERwBAAAAAAAAAA==/",
// "_attachments": "attachments/",
// "_ts": 1728399665
// }
}
}
}
public class MyDbContext : DbContext
{
public DbSet<Thingy> Things { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseCosmos(
"https://REDACTED.documents.azure.com:443/",
"REDACTED",
"TestDb").LogTo(Console.WriteLine, LogLevel.Debug).EnableSensitiveDataLogging();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Thingy>()
.ToContainer(nameof(Thingy))
.HasPartitionKey(x => x.Id)
.HasKey(x => x.Id);
modelBuilder.Entity<Thingy>().Property(e => e.Type)
.HasConversion(new EnumToStringConverter<ThingyType>());
modelBuilder.Entity<Thingy>().UseETagConcurrency();
modelBuilder.Entity<Thingy>()
.HasDiscriminator(x => x.Type)
.HasValue<DerivedThingy1>(ThingyType.ThingyOne)
.HasValue<DerivedThingy2>(ThingyType.ThingyTwo);
modelBuilder.Entity<DerivedThingy1>()
.HasPartitionKey(x => x.Id)
.HasBaseType<Thingy>();
modelBuilder.Entity<DerivedThingy2>()
.HasPartitionKey(x => x.Id)
.HasBaseType<Thingy>();
}
}
public abstract record Thingy
{
public string Id { get; set; }
public string Name { get; set; } = default!;
public ThingyType Type { get; set; }
}
public enum ThingyType
{
ThingyOne,
ThingyTwo
}
public record DerivedThingy1 : Thingy
{
public string SomethingForA { get; set; } = default!;
}
public record DerivedThingy2 : Thingy
{
public string SomethingForB { get; set; } = default!;
}
}
log:
warn: 08/10/2024 16:29:28.645 CoreEventId.SensitiveDataLoggingEnabledWarning[10400] (Microsoft.EntityFrameworkCore.Infrastructure)
Sensitive data logging is enabled. Log entries and exception messages may include sensitive application data; this mode should only be enabled during development.
dbug: 08/10/2024 16:29:28.660 CoreEventId.ShadowPropertyCreated[10600] (Microsoft.EntityFrameworkCore.Model.Validation)
The property 'Thingy.__id' was created in shadow state because there are no eligible CLR members with a matching name.
dbug: 08/10/2024 16:29:28.660 CoreEventId.ShadowPropertyCreated[10600] (Microsoft.EntityFrameworkCore.Model.Validation)
The property 'Thingy.__jObject' was created in shadow state because there are no eligible CLR members with a matching name.
dbug: 08/10/2024 16:29:28.685 CoreEventId.ContextInitialized[10403] (Microsoft.EntityFrameworkCore.Infrastructure)
Entity Framework Core 8.0.8 initialized 'MyDbContext' using provider 'Microsoft.EntityFrameworkCore.Cosmos:8.0.8' with options: ServiceEndPoint=https://REDACTED.documents.azure.com:443/ Database=TestDb SensitiveDataLoggingEnabled
dbug: 08/10/2024 16:29:29.427 CoreEventId.ShadowPropertyCreated[10600] (Microsoft.EntityFrameworkCore.Model.Validation)
The property 'Thingy.__id' was created in shadow state because there are no eligible CLR members with a matching name.
dbug: 08/10/2024 16:29:29.427 CoreEventId.ShadowPropertyCreated[10600] (Microsoft.EntityFrameworkCore.Model.Validation)
The property 'Thingy.__jObject' was created in shadow state because there are no eligible CLR members with a matching name.
dbug: 08/10/2024 16:29:30.441 CoreEventId.ValueGenerated[10808] (Microsoft.EntityFrameworkCore.ChangeTracking)
'MyDbContext' generated value 'ThingyTwo|A' for the property 'DerivedThingy2.__id'.
dbug: 08/10/2024 16:29:30.492 CoreEventId.StartedTracking[10806] (Microsoft.EntityFrameworkCore.ChangeTracking)
Context 'MyDbContext' started tracking 'DerivedThingy2' entity with key '{Id: A}'.
dbug: 08/10/2024 16:29:30.496 CoreEventId.SaveChangesStarting[10004] (Microsoft.EntityFrameworkCore.Update)
SaveChanges starting for 'MyDbContext'.
dbug: 08/10/2024 16:29:30.500 CoreEventId.DetectChangesStarting[10800] (Microsoft.EntityFrameworkCore.ChangeTracking)
DetectChanges starting for 'MyDbContext'.
dbug: 08/10/2024 16:29:30.506 CoreEventId.DetectChangesCompleted[10801] (Microsoft.EntityFrameworkCore.ChangeTracking)
DetectChanges completed for 'MyDbContext'.
info: 08/10/2024 16:29:30.975 CosmosEventId.ExecutedCreateItem[30104] (Microsoft.EntityFrameworkCore.Database.Command)
Executed CreateItem (439 ms, 6.86 RU) ActivityId='c02c8bb0-f9a3-480b-ae3d-bbc985596dec', Container='Thingy', Id='ThingyTwo|A', Partition='A'
dbug: 08/10/2024 16:29:30.982 CoreEventId.StateChanged[10807] (Microsoft.EntityFrameworkCore.ChangeTracking)
The 'DerivedThingy2' entity with key '{Id: A}' tracked by 'MyDbContext' changed state from 'Added' to 'Unchanged'.
dbug: 08/10/2024 16:29:30.988 CoreEventId.SaveChangesCompleted[10005] (Microsoft.EntityFrameworkCore.Update)
SaveChanges completed for 'MyDbContext' with 1 entities written to the database.
dbug: 08/10/2024 16:29:31.017 CoreEventId.QueryCompilationStarting[10111] (Microsoft.EntityFrameworkCore.Query)
Compiling query expression:
'DbSet<Thingy>()
.Single(x => x.Id == "A")'
dbug: 08/10/2024 16:29:31.139 CoreEventId.QueryExecutionPlanned[10107] (Microsoft.EntityFrameworkCore.Query)
Generated query execution expression:
'queryContext => new QueryingEnumerable<Thingy>(
(CosmosQueryContext)queryContext,
SqlExpressionFactory,
QuerySqlGeneratorFactory,
[Cosmos.Query.Internal.SelectExpression],
Func<QueryContext, JObject, Thingy>,
CosmosDbTestApp3.MyDbContext,
null,
False,
True
)
.Single()'
info: 08/10/2024 16:29:31.154 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command)
Executing SQL query for container 'Thingy' in partition 'A' [Parameters=[]]
SELECT c
FROM root c
WHERE c["Type"] IN ("ThingyOne", "ThingyTwo")
OFFSET 0 LIMIT 2
info: 08/10/2024 16:29:31.483 CosmosEventId.ExecutedReadNext[30102] (Microsoft.EntityFrameworkCore.Database.Command)
Executed ReadNext (315.0336 ms, 3.02 RU) ActivityId='214f74d2-7b73-4446-a104-4c70966d8109', Container='Thingy', Partition='A', Parameters=[]
SELECT c
FROM root c
WHERE c["Type"] IN ("ThingyOne", "ThingyTwo")
OFFSET 0 LIMIT 2
dbug: 08/10/2024 16:29:31.500 CoreEventId.SaveChangesStarting[10004] (Microsoft.EntityFrameworkCore.Update)
SaveChanges starting for 'MyDbContext'.
dbug: 08/10/2024 16:29:31.500 CoreEventId.DetectChangesStarting[10800] (Microsoft.EntityFrameworkCore.ChangeTracking)
DetectChanges starting for 'MyDbContext'.
dbug: 08/10/2024 16:29:31.502 CoreEventId.PropertyChangeDetected[10802] (Microsoft.EntityFrameworkCore.ChangeTracking)
The unchanged property 'Thingy.Name' was detected as changed from 'A' to 'A updated' and will be marked as modified for entity with key '{Id: A}'.
dbug: 08/10/2024 16:29:31.502 CoreEventId.StateChanged[10807] (Microsoft.EntityFrameworkCore.ChangeTracking)
The 'DerivedThingy2' entity with key '{Id: A}' tracked by 'MyDbContext' changed state from 'Unchanged' to 'Modified'.
dbug: 08/10/2024 16:29:31.502 CoreEventId.DetectChangesCompleted[10801] (Microsoft.EntityFrameworkCore.ChangeTracking)
DetectChanges completed for 'MyDbContext'.
info: 08/10/2024 16:29:31.542 CosmosEventId.ExecutedReplaceItem[30105] (Microsoft.EntityFrameworkCore.Database.Command)
Executed ReplaceItem (32 ms, 11.43 RU) ActivityId='1a037cd7-de20-4676-b4db-b2d1072da668', Container='Thingy', Id='ThingyTwo|A', Partition='A'
dbug: 08/10/2024 16:29:31.542 CoreEventId.StateChanged[10807] (Microsoft.EntityFrameworkCore.ChangeTracking)
The 'DerivedThingy2' entity with key '{Id: A}' tracked by 'MyDbContext' changed state from 'Modified' to 'Unchanged'.
dbug: 08/10/2024 16:29:31.543 CoreEventId.SaveChangesCompleted[10005] (Microsoft.EntityFrameworkCore.Update)
SaveChanges completed for 'MyDbContext' with 1 entities written to the database.
dbug: 08/10/2024 16:29:31.544 CoreEventId.ContextDisposed[10407] (Microsoft.EntityFrameworkCore.Infrastructure)
'MyDbContext' disposed.
@AndriySvyryd are you the right person to look into this?
Might be related to #35092 (/cc @ajcvickers)
Poaching. Confirmed bug, still repros on 9.
Just wondering if there is an ETA on the fix for this?
The issue is still reproducible with Microsoft.EntityFrameworkCore.Cosmos 9.0.6.