dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.72k stars 3.17k forks source link

InMemory: Improve in-memory key generation #6872

Closed ardalis closed 5 years ago

ardalis commented 7 years ago

The Issue

For testing purposes, you should be able to delete, recreate, and reseed InMemory databases and the result should be the same for each test. Currently identity columns do not reset, so IDs increment with each test iteration.

Repro Steps

This test fails. It should pass.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Xunit;

namespace EfCoreInMemory
{
    public class Item
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
    public class AppDbContext : DbContext
    {
        public DbSet<Item> Items { get; set; }

        public AppDbContext(DbContextOptions<AppDbContext> options ):base(options)
        {
        }
    }
    public class Tests
    {
        private static DbContextOptions<AppDbContext> CreateNewContextOptions()
        {
            // Create a fresh service provider, and therefore a fresh 
            // InMemory database instance.
            var serviceProvider = new ServiceCollection()
                .AddEntityFrameworkInMemoryDatabase()
                .BuildServiceProvider();

            // Create a new options instance telling the context to use an
            // InMemory database and the new service provider.
            var builder = new DbContextOptionsBuilder<AppDbContext>();
            builder.UseInMemoryDatabase()
                   .UseInternalServiceProvider(serviceProvider);

            return builder.Options;
        }

        [Fact]
        public void Test1()
        {
            // create a brand new dbContext
            var dbContext = new AppDbContext(CreateNewContextOptions());

            // add one item
            var item = new Item();
            dbContext.Items.Add(item);
            dbContext.SaveChanges();

            // ID should be 1
            Assert.Equal(1, item.Id);

            dbContext.Database.EnsureDeleted();

            Assert.False(dbContext.Items.Any());

            var item2 = new Item();
            dbContext.Items.Add(item2);
            dbContext.SaveChanges();

            // ID should STILL be 1
            Assert.Equal(1, item2.Id);

        }
    }
}

Further technical details

Project.json:

{
  "version": "1.0.0-*",
  "testRunner": "xunit",
  "dependencies": {
    "NETStandard.Library": "1.6.0",
    "xunit": "2.2.0-beta2-build3300",
    "dotnet-test-xunit": "2.2.0-preview2-build1029",
    "Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
    "Microsoft.AspNetCore.TestHost": "1.0.0",
    "Microsoft.EntityFrameworkCore": "1.0.0",
    "Microsoft.EntityFrameworkCore.InMemory": "1.0.0"
  },
  "frameworks": {
    "netcoreapp1.0": {
      "imports": [
        "dotnet5.6"
      ],
      "dependencies": {
        "Microsoft.NETCore.App": {
          "type": "platform",
          "version": "1.0.0"
        }
      }
    }
  }
}

VS2015

wmcainsh commented 5 years ago

@dannsam, I like your workaround. I encountered a similar issue and ended up creating a MaxPlusOneValueGenerator that works with Entities that implement IHasIntegerId (a public int Id property).

For anyone experiencing the auto-increment issue who doesn't use a column called Id on every table, I slightly modified the solution provided by @denmitchell

    public override int Next(EntityEntry entry)
    {
        var context = entry.Context;
        var qry = generic.Invoke(context, null) as DbSet<TEntity>;

        var key1Name = entry.Metadata
                            .FindPrimaryKey()
                            .Properties
                            .First()
                            .Name;

        var currentMax = qry.Max(e =>
            (int)e.GetType()
                  .GetProperty(key1Name)
                  .GetValue(e));

        //return max plus one
        return currentMax + 1;
    }
ardalis commented 5 years ago

This still is not working 100% for me when I try to reproduce the original behavior at the top of this issue. That is, trying to add an item, deleting the database, and then trying to add a separate new instance of the same entity. I get an InvalidOperationException saying the item is already being tracked:

image

The good news is that the new entity's ID is 1 as it should be. In my test I'm using the same dbContext before and after calling EnsureDeleted - maybe that's the unsupported scenario because the identitymap in the dbcontext still has a reference to the now-deleted entity from before I called EnsureDeleted.

Repro is here: https://github.com/ardalis/EFCoreFeatureTests/blob/ced23657c3c9257758d6734104fb3fc0f0562c25/EFCoreFeatureTests/UnitTest1.cs

smitpatel commented 5 years ago

@ardalis - Filed #18103 because cause of that issue is different from value generation.

aherrick commented 5 years ago

Is this broken in EF Core 3? Not sure I'm seeing this working and having trouble resetting my in memory DB. I'm wondering if similar to here:

https://github.com/aspnet/EntityFrameworkCore/issues/18187

RobK410 commented 5 years ago

I see usage of TryAddSingleton in the bowels of the InMemory code. I think the problem as I can understand it, is that internally the in memory database functionality uses singletons. Are they not thread safe? Therefore, for parallel tests, or tests that execute multi-threaded, run risk of non-thread safe issues. Is this correct?

I found this to be the case when I wrote my own inmemory db abstraction for .NET Framework EF6

AndriySvyryd commented 5 years ago

@vasont That's likely a duplicate of https://github.com/aspnet/EntityFrameworkCore/issues/17672

saliksaly commented 4 years ago

@ardalis @robertmclaws Here is some code that might work for you:

public static class DbContextExtensions
{
    public static void ResetValueGenerators(this DbContext context)
    {
        var cache = context.GetService<IValueGeneratorCache>();

        foreach (var keyProperty in context.Model.GetEntityTypes()
            .Select(e => e.FindPrimaryKey().Properties[0])
            .Where(p => p.ClrType == typeof(int)
                        && p.ValueGenerated == ValueGenerated.OnAdd))
        {
            var generator = (ResettableValueGenerator)cache.GetOrAdd(
                keyProperty,
                keyProperty.DeclaringEntityType,
                (p, e) => new ResettableValueGenerator());

            generator.Reset();
        }
    }
}

public class ResettableValueGenerator : ValueGenerator<int>
{
    private int _current;

    public override bool GeneratesTemporaryValues => false;

    public override int Next(EntityEntry entry)
        => Interlocked.Increment(ref _current);

    public void Reset() => _current = 0;
}

To use, call context.ResetValueGenerators(); before the context is used for the first time and any time that EnsureDeleted is called. For example:

using (var context = new BlogContext())
{
    context.ResetValueGenerators();
    context.Database.EnsureDeleted();

    context.Posts.Add(new Post {Title = "Open source FTW", Blog = new Blog {Title = "One Unicorn"}});
    context.SaveChanges();
}

No matter how many times I call this code, Blog.Id and Post.Id will always get the value 1.

The code works by finding every int primary key in the model and setting the cached value generator to a ResettableValueGenerator, or resetting that value generator if it has already been created. It can be easily adapted for other key types.

@ajcvickers, please - I have been using your solution to set id value generators for my in-emory tests with seed data. Now, in EF Core 3.1, it stoped working. It seems that ResettableValueGenerator.Next() is newer called. Again, I get error: System.InvalidOperationException: 'The instance of entity type X cannot be tracked because another instance with the key value '{Id: 1}' is already being tracked.

Would not you know solution for EF Core 3.1?

Thanks

ajcvickers commented 4 years ago

@saliksaly It should no longer be necessary to use a workaround here since the underlying issues were fixed in 3.0. If you're running into issues with in-memory keys, then please open a new issue and include a small, runnable project or complete code listing so that we can investigate.

saliksaly commented 4 years ago

@saliksaly It should no longer be necessary to use a workaround here since the underlying issues were fixed in 3.0. If you're running into issues with in-memory keys, then please open a new issue and include a small, runnable project or complete code listing so that we can investigate.

Thanks, here is the issue: https://github.com/dotnet/efcore/issues/19854