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

CosmosDB: Nested embedded entities get rearranged on deletion #27272

Closed
sephnewman opened this issue Jan 25, 2022 · 1 comment · Fixed by #29028
Closed

CosmosDB: Nested embedded entities get rearranged on deletion #27272

sephnewman opened this issue Jan 25, 2022 · 1 comment · Fixed by #29028
Labels
area-cosmos 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

@sephnewman
Copy link

sephnewman commented Jan 25, 2022

Overview

I'm working on a project that has a Cosmos DB container that contains multiple levels of nested embedded entities. We started to notice strange behavior that I've ultimately tracked down to an interaction between EF and CosmosDB. This behavior occurs when deleting middle level embedded entities from a CosmosDB record. When the middle level embedded entity is deleted, its inner child entities which we would expect to also be deleted are instead rearranged to point to a different middle level entity.

Example

We have three levels of entities:

  • OuterEntity
  • MiddleEntity
  • InnerEntity

The OuterEntity owns many MiddleEntities and the MiddleEntity owns many InnerEntities

After we delete the first MiddleEntity, the one remaining MiddleEntity now contains the wrong InnerEntity

EFCosmosBugDiagram

Code Example

This example is easily reproducible with a small program run against the local cosmos db emulator

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;

namespace CosmosDbBugDemo
{
    class Program
    {
        public class OuterEntity
        {
            public string OuterName { get; set; }
            public IList<MiddleEntity> MiddleEntities { get; set; }

            public override string ToString()
            {
                var outputStr = $"{OuterName}";
                foreach (var middleEntity in MiddleEntities)
                {
                    outputStr += $"\n\t{middleEntity.ToString()}";
                }
                return outputStr;
            }
        }

        public class MiddleEntity
        {
            public string MiddleName { get; set; }
            public IList<InnerEntity> InnerEntities { get; set; }

            public override string ToString()
            {
                var outputStr = $"{MiddleName}";
                foreach (var innerEntity in InnerEntities)
                {
                    outputStr += $"\n\t\t{innerEntity.ToString()}";
                }
                return outputStr;
            }
        }

        public class InnerEntity
        {
            public string InnerName { get; set; }

            public override string ToString()
            {
                return InnerName;
            }
        }

        // fixed key common to all local cosmos db emulators
        private const string LocalCosmosDbKey =
            "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";

        public class BugDemoContext : DbContext
        {
            protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
                => optionsBuilder.UseCosmos(
                    "https://localhost:8081",
                    LocalCosmosDbKey,
                    databaseName: "BugDemoDatabase");

            public DbSet<OuterEntity> OuterEntities { get; set; }

            protected override void OnModelCreating(ModelBuilder modelBuilder)
            {
                modelBuilder.Entity<OuterEntity>().HasKey(o => o.OuterName);
                modelBuilder.Entity<OuterEntity>()
                    .OwnsMany(o => o.MiddleEntities,
                        middleEntity =>
                        {
                            middleEntity.OwnsMany(m => m.InnerEntities);
                        });
            }
        }

        static void Main(string[] args)
        {
            // setup the DB
            using (var context = new BugDemoContext())
            {
                context.Database.EnsureDeleted();
                context.Database.EnsureCreated();
            }

            // add a test item to the DB
            using (var context = new BugDemoContext())
            {
                var outerEntity = new OuterEntity()
                {
                    OuterName = "Outer1",
                    MiddleEntities = new List<MiddleEntity>()
                    {
                        new MiddleEntity()
                        {
                            MiddleName = "Middle1",
                            InnerEntities = new List<InnerEntity>()
                            {
                                new InnerEntity()
                                {
                                    InnerName = "Inner1",
                                }
                            }
                        },
                        new MiddleEntity()
                        {
                            MiddleName = "Middle2",
                            InnerEntities = new List<InnerEntity>()
                            {
                                new InnerEntity()
                                {
                                    InnerName = "Inner2"
                                }
                            }
                        }
                    }
                };
                context.OuterEntities.Add(outerEntity);
                context.SaveChanges();
            }

            // get Outer1 from the DB and print its state
            Console.WriteLine("Initial State:");
            using (var context = new BugDemoContext())
            {
                var outer1 = context.OuterEntities.Find("Outer1");
                Console.WriteLine(outer1.ToString());
            }

            // delete Middle1 from the DB
            using (var context = new BugDemoContext())
            {
                var outer1 = context.OuterEntities.Find("Outer1");
                var middle1 = outer1.MiddleEntities.Single(m => m.MiddleName == "Middle1");
                outer1.MiddleEntities.Remove(middle1);
                context.Update(outer1);
                context.SaveChanges();
            }

            // get Outer1 from the DB and print its state
            Console.WriteLine("\nAfter Deletion:");
            using (var context = new BugDemoContext())
            {
                var outer1 = context.OuterEntities.Find("Outer1");
                Console.WriteLine(outer1.ToString());
            }
        }
    }
}

Code Output

The code above produces the following ouput to the console:

Initial State:
Outer1
        Middle1
                Inner1
        Middle2
                Inner2

After Deletion:
Outer1
        Middle2
                Inner1

Clearly, this is not expected. The output we would expect if the Middle1 record was properly deleted would be:

Outer1
        Middle2
                Inner2

EF provider and version information

I have tested and confirmed this behavior with two configurations to prove that this bug is present in both EF 5 and EF 6:

EF 5 config:

EF Core version: 5.0.13
Database provider: Microsoft.EntityFrameworkCore.Cosmos (5.0.13)
Target framework: netcoreapp3.1
Operating system: Windows 10 Pro 21H2 19044.1466
IDE: Microsoft Visual Studio Community 2019 (Version 16.11.3)

EF 6 config:

EF Core version: 6.0.1
Database provider: Microsoft.EntityFrameworkCore.Cosmos (6.0.1)
Target framework: net6.0
Operating system: Windows 10 Pro 21H2 19044.1466
IDE: Microsoft Visual Studio Community 2022 (64-bit) (Version 17.0.5)
@AndriySvyryd
Copy link
Member

As a workaround detach all nested embedded entities for the entity being deleted right before SaveChanges():

foreach (var inner in middle1.InnerEntities)
{
	context.Entry(inner).State = EntityState.Detached;
}

@AndriySvyryd AndriySvyryd changed the title EntityFramework with CosmosDB bug: Nested embedded entities get rearranged on deletion CosmosDB: Nested embedded entities get rearranged on deletion Feb 2, 2022
@AndriySvyryd AndriySvyryd removed their assignment Sep 9, 2022
@AndriySvyryd AndriySvyryd added the closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. label Sep 9, 2022
AndriySvyryd added a commit that referenced this issue Sep 9, 2022
Re-set ordinal values when the principal is added, not only when it's modified
Mark the ordinal property as ValueGenerated.OnAddOrUpdate
Override all JSON values for a replaced dependent

Fixes #27918
Fixes #27272
Fixes #24828
AndriySvyryd added a commit that referenced this issue Sep 9, 2022
Re-set ordinal values when the principal is added, not only when it's modified
Mark the ordinal property as ValueGenerated.OnAddOrUpdate
Override all JSON values for a replaced dependent

Fixes #27918
Fixes #27272
Fixes #24828
AndriySvyryd added a commit that referenced this issue Sep 9, 2022
Re-set ordinal values when the principal is added, not only when it's modified
Mark the ordinal property as ValueGenerated.OnAddOrUpdate
Override all JSON values for a replaced dependent

Fixes #27918
Fixes #27272
Fixes #24828
@ajcvickers ajcvickers modified the milestones: 7.0.0, 7.0.0-rc2 Sep 10, 2022
@ajcvickers ajcvickers modified the milestones: 7.0.0-rc2, 7.0.0 Nov 5, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-cosmos 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