-
Notifications
You must be signed in to change notification settings - Fork 3.2k
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
InMemory: Improve in-memory key generation #6872
Comments
I don't know if I agree with this exact behavior, mostly because I'm having a hard time envisioning a unit test or operational scenario where you'd want to delete an existing item, then add a new item with the same ID. HOWEVER, I do see the need for a command like |
The repel shows one test. The problem exhibits itself when you have many tests. Every test will end up with different IDs - there is now way to really reset the in memory db. Steve Smith
|
repro not repel |
Note for triage: dupe of #4096. |
The InMemory provider doesn't the Identity pattern you would get in a relational database. When a column is configured as If you want to have a database that acts like a relational database, but doesn't have the overhead of I/O, then we'd recommend using an In-Memory SQLite database - http://www.sqlite.org/inmemorydb.html. We're going to update our testing docs to make the SQLite option more prominent. |
Thanks, @rowanmiller 2 things I'd like to see that I think would be useful:
Thanks! |
@ardalis fair point on point 1, I'm re-opening this issue for us to re-discuss. Regarding point 2, our docs show an example that tests a |
I do want to point out that I made a suggestion on how to handle what @ardalis is asking for in point 1 with a command like |
@rowanmiller You're right, unit testing an individual controller action works just like your BlogService sample. To clarify, I'm talking about integration testing using WebHostBuilder, as shown here: https://docs.asp.net/en/latest/testing/integration-testing.html. In this case, there's no direct access to the controller action. The web host is constructed for each test and the request is made such that it goes through the full MVC stack (routing, filters, etc). This is an awesome new feature of ASP.NET Core but getting the data reset for each test has proven much more difficult than I would like. To be fair, I haven't yet resorted to using SqlLite for this purpose, but my hope has been to avoid the need for this since it adds complexity to the process. Thanks. |
...and regarding that link, you'll see it doesn't include any EF examples. That's because I couldn't figure a great way to include them such that they were reset between each test. |
Here's a complete example showing how to (try to) configure an ASP.NET Core app using WebHostBuilder and InMemory for integration tests: https://github.com/ardalis/CleanArchitecture/blob/master/src/CleanArchitecture.Web/Startup.cs#L73-L104 The sample shown works, but starts to fall down as you add more entities and tests to it, as test data starts to step on other test data. One workaround I was able to use was hardcoding the IDs of test data, as suggested in another issue. However, any guidance or assistance with how to reset data for each integration test like this would be appreciated. |
@ardalis what if you used a throw away instance of the InMemory database for each test? There is an overload of |
I tried that but the numbers still incremented every time. I ended up working around it by hard-coding the initial ID. I had tried this:
But even with that in place, and with disposing of the TestServer after every test, the ID numbers would continue incrementing between tests (which made it hard for me to construct API calls like /item/{id} when I couldn't count on a valid test ID to use). Again this was fixed by specifying the ID in my test data method. A working version of what I have currently can be found here: |
Note for triage: One idea we had in triage was replacing some services to make the value generator resettable, but it involves copying a lot of code because the value generator is cached - and can't be replaced. You have to re-implement |
@rowanmiller Does this work: modelBuilder
.Entity<Blog>()
.Property(e => e.Id)
.HasValueGenerator<InMemoryIntegerValueGenerator<int>>(); |
@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 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. |
Moving to backlog to consider:
|
FYI I ran into this today where the state of the in-mem database is preserved across each run of my unit tests (and each test creates a whole new DI system per test with a new ServiceCollection, etc.). I can't imagine I'd ever want to preserve the old data across each test, since unit tests don't run in a consistent order, and they might run in parallel. This behavior seems like a strange choice by default if this is being positioned for unit testing. Why isn't the in-mem database just a singleton when added to DI? As a workaround I'm using Guid.NewGuid() to set the database name on each run, but it feels uncomfortable to have to do so. |
@brockallen The InMemory provider is not a relational database provider, current recommendation is to use SQLite (or maybe SQL Compact?) instead: https://docs.microsoft.com/en-us/ef/core/miscellaneous/testing/ |
@ErikEJ I'm confused how that relates. I don't care what DB is used -- IMO, between tests it should start from scratch/empty. |
bump |
I started with the SQLite user guide but quickly stopped once I learned schemas are not supported. |
I ran into this issue and was led down a rabbit hole that culminated in this thread. I agree with everyone else here - unit tests are supposed to be completely independent of each other and therefore able to run in any order without any change in test results. There absolutely should be a way to completely reset all EF Core state between unit tests for this reason, independent of what sort of database provider you're trying to simulate. In my case, I'm trying to verify my application service logic by performing deep equality comparisons between entities. The persistence of primary key auto-generation state between unit tests is screwing up these comparisons when comparing the entity IDs:
|
I tried to test this issue using 3.0.0 preview5 but I ran into the issue described in #14906 (Could not load type 'System.Collections.Generic.IAsyncEnumerable'1' from assembly 'System.Interactive.Async, Version=4.0.0.0), so in my case testing was not possible. |
@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). @ajcvickers, is Microsoft still looking at possible a long-term solution for this? |
@denmitchell As indicated by the |
what would I go around using this snippet? |
@eskensberg Can you file a new issue describing the problem when running tests in parallel? |
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
|
…ite insted of InMemory
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: 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 |
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: |
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 |
@vasont That's likely a duplicate of #17672 |
@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. Would not you know solution for EF Core 3.1? Thanks |
@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: #19854 |
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.
Further technical details
Project.json:
VS2015
The text was updated successfully, but these errors were encountered: