Efcore: In-memory database not persisted across service providers

Created on 29 Aug 2017  路  11Comments  路  Source: dotnet/efcore

Each named in-memory database is rooted in the internal service provider. This allows it to persist across multiple uses of a context without being rooted in the app-domain, which could cause leaks. Most of the time this works as expected, but sometimes an options change can cause EF to build a new internal service provider. This causes the two contexts to not be sharing the in-memory database even when it is likely expected they will.

For example:
```C#
using (var context = new FooContext(
new DbContextOptionsBuilder()
.UseInMemoryDatabase(nameof(FooContext))
.Options))
{
context.Add(new Foo());
context.SaveChanges();
}

using (var context = new FooContext(
new DbContextOptionsBuilder()
.UseInMemoryDatabase(nameof(FooContext))
.EnableSensitiveDataLogging()
.Options))
{
// Fails because one context has EnableSensitiveDataLogging while the other doesn't
Debug.Assert(context.Foos.Count() == 1);
}
```

closed-fixed type-bug

Most helpful comment

Note for others finding this issue. There are two ways to resolve this:

  • Either pass a InMemoryDatabaseRoot object to UseInMemoryDatabase--see below
  • Or ensure the context instance is created the same way in each place so that the same internal service provider is used everywhere

Example use of InMemoryDatabaseRoot. First, create an instance of this object with an appropriate location and scope for managing the lifetime of the database. Often, this just means having a static somewhere:
```C#
public static readonly InMemoryDatabaseRoot InMemoryDatabaseRoot = new InMemoryDatabaseRoot();

Then pass this instance to `UseInMemoryDatabase`:
```C#
    optionsBuilder.UseInMemoryDatabase("MyDb", InMemoryDatabaseRoot);

All 11 comments

Note that this can happen if one DbContext is resolved from an IServiceProvider after being registered with AddDbContext, and the other DbContext instance is created with new'. One workaround is to always resolve the context instance from the service provider. For example: ```C# using (var scope = serviceProvider.CreateScope()) { var db = scope.ServiceProvider.GetService<MyContext>(); } ``` In the case of a web app, the IServiceProvider can be obtained by callingIWebHost.Services`.

@ajcvickers
I am using .NET Core 2.1 RC and have the same issue. My code looks like this:
```C#
public class TestStartup : Startup
{
public TestStartup(IConfiguration env) : base(env)
{
}
public override void SetUpDataBase(IServiceCollection services)
{
services
.AddEntityFrameworkInMemoryDatabase()
.AddDbContext(options =>
options.UseInMemoryDatabase("MyTestDatabase"))
;

        SeedData(services);
    }

    private void SeedData(IServiceCollection services)
    {

        using (var scope = services.BuildServiceProvider().CreateScope())
        {
            var dataContext = scope.ServiceProvider.GetService<ApplicationDbContext>();

```

Even when using CreateScope when run Integration Tests I cannot get seed data in .NET Core 2.1 Web Application.
One note. Main class in which repository is injected is registered as singleton.
Any suggestions?

@radenkozec Please file a new issue including a runnable project/solution or complete code listing that demonstrates the behavior you are seeing.

@ajcvickers I manages to solve the issues by following this tutorial http://www.dotnetcurry.com/aspnet-core/1420/integration-testing-aspnet-core
It seams that my issue was related to wrong dependency injection configuration.

Note for others finding this issue. There are two ways to resolve this:

  • Either pass a InMemoryDatabaseRoot object to UseInMemoryDatabase--see below
  • Or ensure the context instance is created the same way in each place so that the same internal service provider is used everywhere

Example use of InMemoryDatabaseRoot. First, create an instance of this object with an appropriate location and scope for managing the lifetime of the database. Often, this just means having a static somewhere:
```C#
public static readonly InMemoryDatabaseRoot InMemoryDatabaseRoot = new InMemoryDatabaseRoot();

Then pass this instance to `UseInMemoryDatabase`:
```C#
    optionsBuilder.UseInMemoryDatabase("MyDb", InMemoryDatabaseRoot);

@ajcvickers Have you tested this with EntityFrameworkCore 2.2.0? I tried your example and it isn't working.
I can seed data below, but afterwards the User is not found in the database.
Note that this works when using SQL Server.
Let me know if you see anything that looks incorrect.

        private static readonly InMemoryDatabaseRoot InMemoryDatabaseRoot = new InMemoryDatabaseRoot();

        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<CookiePolicyOptions>(options =>
            {
            services.AddDbContext<ApplicationDbContext>(
                options => 
                options.UseInMemoryDatabase("MyInMemoryDatabase", InMemoryDatabaseRoot));
                //options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
            services.AddDefaultIdentity<IdentityUser>()
                .AddDefaultUI(UIFramework.Bootstrap4)
                .AddEntityFrameworkStores<ApplicationDbContext>();

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
        }
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseCookiePolicy();

            app.UseAuthentication();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });

            SampleData.Initialize(app);
        }
    public class SampleData
    {
        public static void Initialize(IApplicationBuilder app)
        {
            using (var serviceScope = app.ApplicationServices.CreateScope())
            //using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope())
            //using (var context = serviceScope.ServiceProvider.GetService<ApplicationDbContext>())
            {
                //var context = new ApplicationDbContext(options);
                var context = serviceScope.ServiceProvider.GetService<ApplicationDbContext>();
                context.Database.EnsureCreated();

                var user = new IdentityUser
                {
                    Email = "[email protected]",
                    NormalizedEmail = "[email protected]",
                    UserName = "[email protected]",
                    NormalizedUserName = "[email protected]",
                    PhoneNumber = "+2481234444",
                    EmailConfirmed = true,
                    PhoneNumberConfirmed = true,
                    SecurityStamp = Guid.NewGuid().ToString("D")
                };

                if (!context.Users.Any(u => u.UserName == user.UserName))
                {
                    var password = new PasswordHasher<IdentityUser>();
                    var hashed = password.HashPassword(user, "test");
                    user.PasswordHash = hashed;

                    var userStore = new UserStore<IdentityUser>(context);
                    userStore.CreateAsync(user).Wait();
                }

                context.SaveChangesAsync();
            }
        }

    }

@radenkozec It should work. It might be because you're calling an async method (SaveChangesAsync) in a fire-and-forget way. Use SaveChanges instead, or make the method async and await.

If it still doesn't work, then please open a new issue and include a project/solution or complete code listing that demonstrates the behavior you are seeing.

@ajcvickers Can you be more detailed about

Or ensure the context instance is created the same way in each place so that the same internal service provider is used everywhere

I have the same issue as here with .NET Core 2.2 and EntityFrameworkCore 2.2.0.

This is my test factory...

    public class ToDoAppApiFactory<T> : WebApplicationFactory<T> where T : class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                ConfigureServices(services);
                ServiceProvider = services.BuildServiceProvider();
            });
        }

        public ServiceProvider ServiceProvider { get; private set; }

        protected void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ToDoAppDbContext>(options =>
            {
                ServiceProvider serviceProvider = new ServiceCollection()
                    .AddEntityFrameworkInMemoryDatabase()
                    .BuildServiceProvider();

                options.UseInMemoryDatabase("ToDoAppDb");
                options.UseInternalServiceProvider(serviceProvider);
            });
        }
    }

...and this is my method to configure services in Startup class

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
            services.AddDbContext<ToDoAppDbContext>
                (options => options.UseSqlServer(Configuration.GetConnectionString("ToDoAppDbConnectionString")));

            // Register the Swagger generator, defining 1 or more Swagger documents
            services.AddSwaggerGen(swaggerGenOptions =>
            {
                swaggerGenOptions.SwaggerDoc(VersionName, new Info {Title = ApiName, Version = VersionName});

                // Set the comments path for the Swagger JSON and UI
                List<string> xmlFiles = Directory
                    .GetFiles(AppContext.BaseDirectory, "*.xml", SearchOption.TopDirectoryOnly).ToList();
                xmlFiles.ForEach(file => swaggerGenOptions.IncludeXmlComments(file));
            });

            services.AddAutoMapper(Api.Configuration.AutoMapper.Configure);
        }

As you can see I'm using the same configuration and still have difference instances in memory database.

I was able to fix it with InMemoryDatabaseRoot as you can see below but still wondering why is this necessary.

    public class ToDoAppApiFactory<T> : WebApplicationFactory<T> where T : class
    {
        private static readonly InMemoryDatabaseRoot InMemoryDatabaseRoot = new InMemoryDatabaseRoot();

        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                ConfigureServices(services);
                ServiceProvider = services.BuildServiceProvider();
            });
        }

        public ServiceProvider ServiceProvider { get; private set; }

        protected void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ToDoAppDbContext>(options =>
            {
                ServiceProvider serviceProvider = new ServiceCollection()
                    .AddEntityFrameworkInMemoryDatabase()
                    .BuildServiceProvider();

                options.UseInMemoryDatabase("ToDoAppDb", InMemoryDatabaseRoot);
                options.UseInternalServiceProvider(serviceProvider);
            });
        }
    }

@akalcik The configuration for the in-memory database builds and uses a new internal service provider for every context instance. This is where the in-memory database is rooted by default, so using a new one every context instance results in a new in-memory database for every instance.

@ajcvickers Really surprising for me as this isn't mentioned anywhere in the documentation. So the code I provided in my example isn't correct without InMemoryDatabaseRoot, right? Shouldn't be this updated anywhere in the documentation? https://docs.microsoft.com/en-us/ef/core/miscellaneous/testing/in-memory

Was this page helpful?
0 / 5 - 0 ratings