question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

InMemory: Cascade Delete Support

See original GitHub issue

Dear all

During my tests with EF RC1, I found, that the EF.InMemory is lacking a quite important feature (IMHO). Cascading deletes are not supported by EF.InMemory!

Having the following context and classes, you can reproduce the issue quite easily:

    public class DatabaseContext : DbContext
    {
        public DatabaseContext(DbContextOptions options)
            : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            var factory = Database.GetService<ILoggerFactory>();
            factory.AddDebug(LogLevel.Verbose);

            modelBuilder.Entity<Apple>().HasKey(x => x.Identity);
            modelBuilder.Entity<Apple>().Property(x => x.Identity).ValueGeneratedNever();
            modelBuilder.Entity<Apple>().Property(x => x.Version).IsConcurrencyToken();

            modelBuilder.Entity<AggregateIndexes<Apple>>().HasKey(x => new {x.AggregateIdentity, x.AggregateVersion});

            modelBuilder.Entity(typeof (MapIndex)).HasKey("AggregateIdentity", "AggregateVersion");
            modelBuilder.Entity(typeof (MapIndex)).Property<Guid>("AggregateIdentity").ValueGeneratedNever();
            modelBuilder.Entity(typeof (MapIndex)).Property<Guid>("AggregateVersion").ValueGeneratedNever();

            modelBuilder.Entity(typeof (MapIndex))
                .HasOne(typeof (AggregateIndexes<Apple>))
                .WithOne()
                .HasForeignKey(typeof (MapIndex), "AggregateIdentity", "AggregateVersion")
                .IsRequired()
                .OnDelete(DeleteBehavior.Cascade);

            modelBuilder.Entity<ComplexMapIndex>().HasKey(x => x.Key);
            modelBuilder.Entity<ComplexMapIndex>().Property(x => x.Key).ValueGeneratedNever();

            modelBuilder.Entity(typeof (ComplexMapIndex)).HasKey("Key");
            modelBuilder.Entity(typeof (ComplexMapIndex)).Property<Guid>("Key").ValueGeneratedNever();

            modelBuilder.Entity(typeof (ComplexMapIndex))
                .HasOne(typeof (AggregateIndexes<Apple>))
                .WithMany()
                .HasForeignKey("AggregateIdentity", "AggregateVersion")
                .IsRequired()
                .OnDelete(DeleteBehavior.Cascade);
        }
    }

    public abstract class Aggregate
    {
        public Guid Identity { get; set; }
        public Guid Version { get; set; }
    }

    public class Apple : Aggregate
    {
        public string Color { get; set; }        
    }

    public class AggregateIndexes<T> where T: Aggregate
    {
        public Guid AggregateIdentity { get; set; }
        public Guid AggregateVersion { get; set; } 
    }

    public class MapIndex
    {
        public Guid AggregateIdentity { get; set; }
        public Guid AggregateVersion { get; set; }

        public string IndexValue { get; set; }
    }

    public class ComplexMapIndex
    {
        public Guid Key { get; set; }

        public Guid AggregateIdentity { get; set; }
        public Guid AggregateVersion { get; set; }

        public string IndexValue { get; set; }
    }

With the following lines of code, you can see the results:

        private static void Main(string[] args)
        {
            var builder = new DbContextOptionsBuilder();
//            builder.UseInMemoryDatabase();
            builder.UseSqlCe(@"Data Source=AggregatesIndexingWithDeleteCascade.sdf");

            var appleIdentity = Guid.NewGuid();

            // fill the context with some data
            using (var context = new DatabaseContext(builder.Options))
            {
                context.Database.EnsureCreated();

                var apple = new Apple
                {
                    Identity = appleIdentity,
                    Version = Guid.NewGuid(),
                    Color = "Red"
                };

                var indexes = new AggregateIndexes<Apple>()
                {
                    AggregateIdentity = apple.Identity,
                    AggregateVersion = apple.Version
                };


                context.Set<Aggregate>().Add(apple);
                context.Set<AggregateIndexes<Apple>>().Add(indexes);

                var mapIndex = new MapIndex
                {
                    AggregateIdentity = apple.Identity,
                    AggregateVersion = apple.Version,
                    IndexValue = "index-value-for-the-red-apple"
                };
                context.Set<MapIndex>().Add(mapIndex);

                for (int i = 0; i < 5; i++)
                {
                    var index = new ComplexMapIndex()
                    {
                        Key = Guid.NewGuid(),
                        AggregateIdentity = apple.Identity,
                        AggregateVersion = apple.Version,
                        IndexValue = "complex-index-value-for-the-red-apple"
                    };
                    context.Set<ComplexMapIndex>().Add(index);
                }

                context.SaveChanges();
            }

            // now we delete the AggregateIndexes<Apple> again,
            // the cascade should remove all entries now (map-index, complex-map-index)
            using (var context = new DatabaseContext(builder.Options))
            {
                var appleIndexes = context.Set<AggregateIndexes<Apple>>().Single(x => x.AggregateIdentity == appleIdentity);
                context.Remove(appleIndexes);
                context.SaveChanges();
            }

            // all entries should now be gone, as they are deleted by the cascade
            using (var context = new DatabaseContext(builder.Options))
            {
                var appleIndexes = 
                    context.Set<AggregateIndexes<Apple>>().SingleOrDefault(x => x.AggregateIdentity == appleIdentity);
                Console.WriteLine("AggregateIndexes<Apple> gone: " + (appleIndexes == null));

                var mapIndex = context.Set<MapIndex>().SingleOrDefault(x => x.AggregateIdentity == appleIdentity);
                Console.WriteLine("MapIndex gone: " + (mapIndex == null));

                var complexMapIndex = context.Set<ComplexMapIndex>().Where(x => x.AggregateIdentity == appleIdentity);
                Console.WriteLine("ComplexMapIndex gone: " + (!complexMapIndex.Any()));
            }

            Console.WriteLine("Finished ... Press any key to continue");
            Console.ReadLine();
        }

Output for SQL CE Provider (and all other relational providers)

AggregateIndexes<Apple> gone: True
MapIndex gone: True
ComplexMapIndex gone: True
Finished ... Press any key to continue

Output for EF.InMemory

AggregateIndexes<Apple> gone: True
MapIndex gone: False
ComplexMapIndex gone: False
Finished ... Press any key to continue

The results for the actual providers is correct (and as expected). The output for EF.InMemory however isn’t - or at least to me, it is surprising.

In your documentation I found the following:

Note This cascading behavior is only applied to entities that are being tracked by the context. A corresponding cascade behavior should be setup in the database to ensure data that is not being tracked by the context has the same action applied. If you use EF to create the database, this cascade behavior will be setup for you.

Whereas I totally get this for a real context, which would need to evaluate these FK relations in-memory and therefore fetch the entries from the store, I actually cannot understand, why EF.InMemory can’t do this for me. It already has the whole graph in-memory and would only need to evaluate the FK relations, which are already in place.

Actually, the EF.Core infrastructure almost does all the things, and handles the delete in the graph correctly. It propagates the DELETE calls along the graph-relations.

Unfortunately, EF.InMemory does not really support this, as shown in these tests.

Why can’t EF.InMemory handle this? It would be quite easy to implement I guess.

And why can it delete entries, if they are in the current context? At that point, it already CAN evaluate the FK relations and the cascading deletes. So it could also do for all entries in the store. For the in-memory case, all entries are always in memory (and thus close to the context). It would be easy to fetch them, in case of a delete call to a cascading FK relation.

Any input to this topic?

Thanks a lot, Harald

Issue Analytics

  • State:closed
  • Created 8 years ago
  • Reactions:8
  • Comments:11 (7 by maintainers)

github_iconTop GitHub Comments

12reactions
ajcvickerscommented, Sep 1, 2016

@zpbappi The support for cascade delete should be implemented purely in the in-memory database. It should not change the StateManager in any way. It should not change which entities are being loaded. This means that the in-memory database needs to understand the FK constraints that have been created and what their cascade behavior is defined as. It should then follow these constraints, independently of the state manager, when an entity is deleted and make appropriate changes or throw appropriate exceptions based on the constraint and cascade behavior. You would effectively be implementing some part of the FK support that a relational database has.

4reactions
PascalHoneggercommented, Jun 15, 2018

Any updates? I would really appreciate this feature to ensure my test in memory database behaves (more) like my production database.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Cascade Delete - EF Core
Configuring cascading behaviors triggered when an entity is deleted or severed from its principal/parent.
Read more >
Cascade deleting with EF Core
1 Answer. Cascade delete always works in one direction - from principal entity to dependent entity, i.e. deleting the principal entity deletes ......
Read more >
EF Core Advanced Topics - Cascade Delete
Cascade delete allows the deletion of a row to trigger the deletion of related rows automatically. EF Core covers a closely related concept...
Read more >
Cascade delete
I have a scenario where some tables I would like cascade delete and some not. I have a relationship setup in SQL with...
Read more >
If foreign keys/cascade deletes are bad, why use a ...
Lets start with your first link. It says clearly: Working on large databases with referential integrity just does not perform well.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found