Closed rowanmiller closed 8 years ago
Should also cover this point that was mentioned in #105
Keep DbContext's isolated; don't re-use the same context to seed the database and then query.
I'm trying to write a test that uses two DbContext's, sharing the same in-memory DB. The idea is to create the entities using the first context, and pass a clean context to the code that I'm testing. That way I can validate that I'm loading navigation properties correctly, etc, in my EF7 code. The best I could find was to register my DbContext as a transient:
public class TestClass
{
private readonly ServiceCollection _serviceCollection;
private readonly DbContextOptions<MyDbContext> _options;
public TestClass()
{
var optionsBuilder = new DbContextOptionsBuilder<MyDbContext>();
optionsBuilder.UseInMemoryDatabase();
_options = optionsBuilder.Options;
var _serviceCollection = new ServiceCollection();
_serviceCollection
.AddEntityFramework()
.AddInMemoryDatabase();
_serviceCollection.AddTransient<MyDbContext>(provider => new MyDbContext(provider, _options));
}
[Fact]
public Test1()
{
IServiceProvider provider = _serviceCollection.BuildServiceProvider();
using (var context = provider.GetService<MyDbContext>())
{
context.Add(new Thing { Id = 1 });
context.SaveChanges();
}
using (var context = provider.GetService<ApplicationDbContext>())
{
// test with a new context but same DB
}
}
}
It seems to work, but is there a better way to achieve this?
@MikeWasson it's a bit of a "level up" on DI skills... but you can create scopes within a service provider to get new instances of scoped services... but the non-scoped services (such as the InMemory store) are shared between all scopes on the provider.
This actually emulates nicely what happens in ASP.NET - where a new scope is created for each request. So Test1()
now approximates some data being inserted on one request and then some other logic happening on a second request that accesses the same database.
public class TestClass
{
private readonly ServiceCollection _serviceCollection;
public TestClass()
{
_serviceCollection = new ServiceCollection();
_serviceCollection
.AddEntityFramework()
.AddInMemoryDatabase()
.AddDbContext<SampleContext>(c => c.UseInMemoryDatabase());
}
[Fact]
public Test1()
{
IServiceProvider provider = _serviceCollection.BuildServiceProvider();
using (var scope = provider.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
var context = scope.ServiceProvider.GetService<MyDbContext>())
context.Add(new Thing { Id = 1 });
context.SaveChanges();
}
using (var scope = provider.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
var context = scope.ServiceProvider.GetService<MyDbContext>())
// test with a new context but same DB
}
}
}
Page is now available at http://docs.efproject.net/en/latest/miscellaneous/testing.html
@MikeWasson I came up with some patterns to make things a little simpler (though we still have plenty of room for improvement - mostly implementing https://github.com/aspnet/EntityFramework/issues/3253) so you should take a look at the final article.
Awesome Rowan! :) That was exactly what I needed to understand how to scope my contexts :)
@rowanmiller Thanks Rowan. Your doc was helpful !
inheriting from IdentityDbContext throws a bit of spanner in the works with this: (http://docs.efproject.net/en/latest/miscellaneous/testing.html) - it doesnt contain a constructor that takes two arguments
@funzeye this is all changing in the upcoming RC2 and IdentityDbContext will have a constructor that allows you to pass in all this stuff.
cool, great work guys. EF FTW.
@rowanmiller Hi. I reported this a while ago and since then the named dbcontext can be passed in but -- I find that the context is still shared.
I probably use a weird edge-case, but I am performing a bunch of integration tests in xunit (which work better than I could have ever hoped, with the exception of this problem) that spins up a TestServer with UseInMemoryDatabase for each test class.
The problem is, even with separate test servers, with separate dbcontexts, the underlying in memory database was still shared (each was created with a new Guid for a name)
I also get a "A second operation on this context....." type error that I'll never see in actual production all the time due to apparent context sharing.
@mgallig Please file a new issue on https://github.com/aspnet/EntityFramework including a full code listing or project that will let us reproduce what you are seeing.
where is ServiceCollection class
Following code works perfectly for me,
// Requires separate DI Service provider to isolate db instance as InMemory database is also shared within
// the class scope since the App Service Factory is configured to per scope instance for dbcontext
var internalServiceProviderForDbContext = new ServiceCollection()
.AddEntityFrameworkInMemoryDatabase()
.BuildServiceProvider();
var options = new DbContextOptionsBuilder
@logamanig It's generally best to avoid calling UseInternalServiceProvider
if you don't really need to.
@logamanig It's generally best to avoid calling
UseInternalServiceProvider
if you don't really need to.
without calling UseInternalServiceProvider, not able to isolate the InMemoryDatabase instance between test cases. Just a unique database name is not enough to isolate.
Following code works perfectly for me, // Requires separate DI Service provider to isolate db instance as InMemory database is also shared within // the class scope since the App Service Factory is configured to per scope instance for dbcontext var internalServiceProviderForDbContext = new ServiceCollection() .AddEntityFrameworkInMemoryDatabase() .BuildServiceProvider(); var options = new DbContextOptionsBuilder() .UseInternalServiceProvider(internalServiceProviderForDbContext) .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options;
As pointed out by @ajcvickers , internal service provider is not required. specifying the inMemoryRoot in optionbuilder is the best way to do it.
The most important thing is how to scope the database to a single test...