Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EF9 Cosmos provider does not map a property with HasKey() to the db id property #35325

Open
ErikPilsits-RJW opened this issue Dec 12, 2024 · 3 comments · May be fixed by #35347
Open

EF9 Cosmos provider does not map a property with HasKey() to the db id property #35325

ErikPilsits-RJW opened this issue Dec 12, 2024 · 3 comments · May be fixed by #35347
Labels
area-cosmos area-model-building closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported type-bug
Milestone

Comments

@ErikPilsits-RJW
Copy link

File a bug

Prior to EF9, the Cosmos provider would map an entity property that was set with .HasKey() to the cosmos document's id property, as long as another Id or <entity>Id property did not exist. In EF9 this has changed and we're getting an exception for entities that don't have an explicit Id property.

The entity type 'PlanningMovement' does not have a property mapped to the 'id' property in the database. Add a property mapped to 'id'.

Include your code

The entity in question does not have an explicit Id property. Here is the db context configuration we've been using prior to EF9. Our partition key is configured as /movementId which is the reason for the json name mapping.

modelBuilder.Entity<PlanningMovement>(builder =>
{
    builder.ToContainer(PlanningMovementContainerName)
        .HasNoDiscriminator()
        .HasPartitionKey(m => m.MovementId)
        .HasKey(m => m.MovementId);

    builder.UseETagConcurrency();

    builder.Property(m => m.MovementId)
        .ToJsonProperty("movementId");
});

Include stack traces

System.InvalidOperationException:
   at Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal.CosmosModelValidator.ValidateKeys (Microsoft.EntityFrameworkCore.Cosmos, Version=9.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)
   at Microsoft.EntityFrameworkCore.Cosmos.Infrastructure.Internal.CosmosModelValidator.Validate (Microsoft.EntityFrameworkCore.Cosmos, Version=9.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelRuntimeInitializer.Initialize (Microsoft.EntityFrameworkCore, Version=9.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.GetModel (Microsoft.EntityFrameworkCore, Version=9.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.CreateModel (Microsoft.EntityFrameworkCore, Version=9.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.get_Model (Microsoft.EntityFrameworkCore, Version=9.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)
   at Microsoft.EntityFrameworkCore.Infrastructure.EntityFrameworkServicesBuilder+<>c.<TryAddCoreServices>b__8_4 (Microsoft.EntityFrameworkCore, Version=9.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60)

Include provider and version information

EF Core version: 9.0.0
Database provider: Microsoft.EntityFrameworkCore.Cosmos
Target framework: .NET 8.0
Operating system: Windows 10
IDE: Visual Studio 2022 17.12.3

@roji
Copy link
Member

roji commented Dec 13, 2024

I can't reproduce the problem. Please see the below minimal console program which works for me for this scenario. Please tweak it to show it failing, or submit a similar code sample which illustrates the problem.

Attempted repro
await using var context = new BlogContext();
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();

context.PlanningMovements.Add(new PlanningMovement
{
    MovementId = new Guid("a456bf06-89c3-458d-93d8-ce21eeb1790b"),
    Name = "foo",
});
await context.SaveChangesAsync();

_ = await context.PlanningMovements.ToListAsync();

public class BlogContext : DbContext
{
    public DbSet<PlanningMovement> PlanningMovements { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseCosmos(
                "https://192.168.64.6:8081",
                "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
                "Test",
                o => o.HttpClientFactory(() => new HttpClient(
                        new HttpClientHandler
                        {
                            ServerCertificateCustomValidationCallback =
                                HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
                        }))
                    .ConnectionMode(ConnectionMode.Gateway))
            .LogTo(Console.WriteLine, LogLevel.Information)
            .EnableSensitiveDataLogging();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<PlanningMovement>(builder =>
        {
            builder.ToContainer("test")
                .HasNoDiscriminator()
                .HasPartitionKey(m => m.MovementId)
                .HasKey(m => m.MovementId);

            builder.UseETagConcurrency();
        });
    }
}

public class PlanningMovement
{
    public Guid MovementId { get; set; }
    public string Name { get; set; }
}

@ErikPilsits-RJW
Copy link
Author

ErikPilsits-RJW commented Dec 13, 2024

This is strange behavior (for me) coming from EF8. Here's the repro that breaks. Only changed your example to add a .ToJsonProperty() mapping on the partition key.

Repro
using Microsoft.Azure.Cosmos;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System.Text.Json;

await using var context = new BlogContext();
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();

context.PlanningMovements.Add(new PlanningMovement
{
    MovementId = new Guid("a456bf06-89c3-458d-93d8-ce21eeb1790b"),
    Name = "foo",
});
await context.SaveChangesAsync();

var movements = await context.PlanningMovements.ToListAsync();
Console.WriteLine(JsonSerializer.Serialize(movements));

public class BlogContext : DbContext
{
    public DbSet<PlanningMovement> PlanningMovements { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseCosmos(
                "https://localhost:8081",
                "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
                "Test",
                o => o.HttpClientFactory(() => new HttpClient(
                        new HttpClientHandler
                        {
                            ServerCertificateCustomValidationCallback =
                                HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
                        }))
                    .ConnectionMode(ConnectionMode.Gateway))
            .LogTo(Console.WriteLine, LogLevel.Information)
            .EnableSensitiveDataLogging();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<PlanningMovement>(builder =>
        {
            builder.ToContainer("test")
                .HasNoDiscriminator()
                .HasPartitionKey(m => m.MovementId)
                .HasKey(m => m.MovementId);

            builder.UseETagConcurrency();

            builder.Property(m => m.MovementId)
                .ToJsonProperty("movementId");
        });
    }
}

public class PlanningMovement
{
    public Guid MovementId { get; set; }
    public string Name { get; set; }
}

From your example, here's what the saved entity looks like. There's no MovementId property, and the partition key is /id.

Image

It looks OK when serialized however, but doesn't work with our existing data model.

Image

Incidentally we did manage to workaround the issue yesterday by applying the .HasShadowId() method in our model building. Based on the IntelliSense this does seem to be either an EF9 breaking change or an unintended consequence / bug.

Image

In our model, MovementId is a string, but I saw the same behavior in the repro code whether it was a string or guid. Also the partition key is /movementId.

@AndriySvyryd
Copy link
Member

@ErikPilsits-RJW Add builder.HasShadowId(); to your configuration:

modelBuilder.Entity<PlanningMovement>(builder =>
{
    builder.ToContainer(PlanningMovementContainerName)
        .HasNoDiscriminator()
        .HasPartitionKey(m => m.MovementId)
        .HasKey(m => m.MovementId);

    builder.UseETagConcurrency();
    builder.HasShadowId();

    builder.Property(m => m.MovementId)
        .ToJsonProperty("movementId");
});

@AndriySvyryd AndriySvyryd closed this as not planned Won't fix, can't repro, duplicate, stale Dec 18, 2024
@AndriySvyryd AndriySvyryd added closed-no-further-action The issue is closed and no further action is planned. closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. and removed closed-no-further-action The issue is closed and no further action is planned. labels Dec 18, 2024
@AndriySvyryd AndriySvyryd added this to the 10.0.0 milestone Dec 18, 2024
@AndriySvyryd AndriySvyryd reopened this Dec 18, 2024
AndriySvyryd added a commit that referenced this issue Dec 18, 2024
AndriySvyryd added a commit that referenced this issue Dec 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-cosmos area-model-building closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported type-bug
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants