Efcore: EnsureDeleted for InMemory databases does not clear entries

Created on 10 Aug 2016  路  5Comments  路  Source: dotnet/efcore

Steps to reproduce

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Metadata.Internal;
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace ConsoleApp1
{

    public class Author
    {
        [Key]
        public int Id { get; set; }

        public string Name { get; set; }
    }

    public class Book
    {
        [Key]
        public int Id { get; set; }

        public int AuthorId { get; set; }

        public string Name { get; set; }

        public Author Author { get; set; }
    }

    public class Context : DbContext
    {
        public Context(DbContextOptions options) : base(options)
        {

        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.Entity<Author>(b =>
            {
                b.Property(x => x.Id).ValueGeneratedNever();
            });

            modelBuilder.Entity<Book>(b =>
            {
                b.Property(x => x.Id).ValueGeneratedNever();
            });
        }

        public DbSet<Author> Authors { get; set; }
        public DbSet<Book> Books { get; set; }
    }

    public class Program
    {
        public static void Main(string[] args)
        {
            var sp = new ServiceCollection().AddEntityFrameworkInMemoryDatabase().BuildServiceProvider();

            var builder = new DbContextOptionsBuilder<Context>();
            builder.UseInMemoryDatabase().UseInternalServiceProvider(sp);

            using (var c1 = new Context(builder.Options))
            {
                c1.Books.Add(new Book { Id = 1, Author = new Author { Id = 1, Name = "Author 1" }, Name = "Book 1" });
                c1.SaveChanges();
                c1.Database.EnsureDeleted();

                using (var c2 = new Context(builder.Options))
                {
                    // THIS LINE WILL CRASH!
                    //c1.Authors.Add(new Author { Id = 1, Name = "Author 2" });
                    c2.Books.Add(new Book { Id = 1, Author = new Author { Id = 1, Name = "Author 2" }, Name = "Book 2" });
                    c2.SaveChanges();
                }

                var b = c1.Books.First();
                if (b.Name == "Book 1")
                    throw new Exception("Wrong book");
            }
        }
    }
}

The issue

EnsureDeleted does not clear the entries
We have a legacy database where primary keys are not automatically generated. While testing, one context instance is injected into services and fresh contexts are used to stage tests. If I reuse ids from test to test the singleton context instance in services picks entries which should have been deleted and not the fresh ones. Sometimes adding new instances with reused ids crashes - as demonstrated by the commented out line.

I can fix that by using unique test ids but I'm concerned that those "zombie" entries will somehow interfere.

Further technical details

EF Core version: 1.0.0
Operating system: Windows 10
Visual Studio version: VS 2015 Update 3

closed-by-design

Most helpful comment

@ajcvickers the common setup for xUnit is to create a provider per fixture which is then shared by a bunch of tests. It would be a waste to move the provider to the test level. Although, I fixed my problem by generating unique ids I still don't believe it is correct to leave entries tracked in a context when EnsureDeleted is called on it. I would understand if separate contexts didn't see each other EnsureDeletes, but with just one?

All 5 comments

@MaximBalaganskiy EnsureDeleted deletes the underlying database, but it doesn't do anything to entities that are being tracked by the current context instance, The code in the commented out line is using the first context, which is still tracking the entity that was added and saved. This is all by design.

It's also worth noting that the default behavior for the in-memory database is a single database per service provider, shared by all contexts. However, you can change this by using a named in-memory database:

C# UseInMemoryDatabase("MyDbName")

This can help isolate tests from each other without using a separate service provider per test.

Closing this as by design.

@ajcvickers the common setup for xUnit is to create a provider per fixture which is then shared by a bunch of tests. It would be a waste to move the provider to the test level. Although, I fixed my problem by generating unique ids I still don't believe it is correct to leave entries tracked in a context when EnsureDeleted is called on it. I would understand if separate contexts didn't see each other EnsureDeletes, but with just one?

Encountered problems with this just now - detaching all the entities fixes it for me. So what I did is this:

        context.ChangeTracker
            .Entries()
            .ToList()
            .ForEach(e => e.State = EntityState.Detached);

        context.Database.EnsureDeleted();
        context.Database.EnsureCreated();

The examples here achieve this through RemoveRange: https://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-3.1

db.<Entity>.RemoveRange(db.<entity>);

Was this page helpful?
0 / 5 - 0 ratings