Skip to content
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

Seed InMemory database automatically (i.e. without calling EnsureCreated) #11666

Closed
jsacapdev opened this issue Apr 13, 2018 · 19 comments
Closed

Comments

@jsacapdev
Copy link
Contributor

jsacapdev commented Apr 13, 2018

In "Announcing Entity Framework Core 2.1 Preview 2" it states "Data seeding now works with in-memory databases."

For my unit tests, the In-Memory store does not get data populated with the data set up in OnModelCreating. So in the unit test method Seed_It i get nothing returned back. Am I doing something incorrectly?

Steps to reproduce

BloggingContext.cs

namespace TwoOnePreview.Data
{
    using Microsoft.EntityFrameworkCore;

    public class BloggingContext : DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
        public DbSet<Post> Posts { get; set; }

        public BloggingContext(DbContextOptions<BloggingContext> options)
        : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Blog>().HasData(new Blog(1, "Url1"));

            modelBuilder.Entity<Blog>().HasData(new Blog(2, "Url2"));

            modelBuilder.Entity<Post>().HasData(new Post(1, "Title1", "Content1"){ BlogId = 1 });
            modelBuilder.Entity<Post>().HasData(new Post(2, "Title2", "Content2"){ BlogId = 1 });
            modelBuilder.Entity<Post>().HasData(new Post(3, "Title3", "Content3"){ BlogId = 1 });

            modelBuilder.Entity<Post>().HasData(new Post(4, "Title4", "Content4"){ BlogId = 2 });
        }
    }
	
    public class Blog
    {
        public Blog(int blogId, string url)
        {
            BlogId = blogId;
            Url = url;
        }

        public int BlogId { get; private set; }

        public string Url { get; private set; }

        public virtual ICollection<Post> Posts { get; set; }
    }

    public class Post
    {
        public Post(int postId, string title, string content)
        {
            PostId = postId;
            Title = title;
            Content = content;
        }

        public int PostId { get; private set; }

        public string Title { get; private set; }
        
        public string Content { get; private set; }

        public int BlogId { get; set; }

        public virtual Blog Blog { get; set; }
    }	
}

ValuesController.cs

namespace TwoOnePreview.Api.Controllers
{
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.Extensions.Logging;
    using TwoOnePreview.Api.ServiceClients;
    using TwoOnePreview.Data;

    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private readonly BloggingContext _context;

        private readonly ILogger<BloggingContext> _logger;

        public ValuesController(ILogger<BloggingContext> logger, BloggingContext context)
        {
            _logger = logger;

            _context = context;
        }


        [HttpGet]
        public ActionResult<IEnumerable<Blog>> Get()
        {
            return _context.Blogs;
        }

        [HttpPost]
        [ProducesResponseType(201)]
        public ActionResult<Blog> Post(Blog blog)
        {
            _context.Blogs.Add(blog);
            return CreatedAtAction(nameof(Get), new { id = blog.BlogId }, blog);
        }
    }
}

ValuesControllerTests.cs

namespace TwoOnePreview.Api.Tests
{
    using System;
    using System.Linq;
    using System.Net.Http;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Mvc.Testing;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.Extensions.Logging;
    using Moq;
    using TwoOnePreview.Api.Controllers;
    using TwoOnePreview.Data;
    using Xunit;

    public class ValuesControllerTests : IClassFixture<TwoOnePreviewApiTestFixture<Startup>>
    {
        public ValuesControllerTests(TwoOnePreviewApiTestFixture<Startup> fixture)
        {
            Client = fixture.CreateDefaultClient();
        }

        public HttpClient Client { get; }

        [Fact]
        public async Task GetById()
        {
            var response = await Client.GetAsync("http://localhost/api/values/5");

            Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
        }

        [Fact]
        public void Seed_It()
        {
            var options = new DbContextOptionsBuilder<BloggingContext>()
                            .UseInMemoryDatabase(databaseName: "Get")
                            .Options;

            using (var context = new BloggingContext(options))
            {
                var controller = new ValuesController(new Mock<ILogger<BloggingContext>>().Object, context);

                var response = controller.Get();
            }
        }
    }
}

Further technical details

EF Core version: 2.1.0-preview2-final
Database Provider: Microsoft.EntityFrameworkCore.SqlServer
Operating system: Windows Server 2016
IDE: Visual Studio Code
Complete code listing: https://github.com/jsacapdev/asp-net-core-2-1-preview.git

@ajcvickers
Copy link
Contributor

@jsacapdev Currently there needs to be a call to context.Database.EnsureCreated() to trigger the seeding to happen. We will discuss whether or not we can make a change to not require this.

@ErikEJ
Copy link
Contributor

ErikEJ commented Apr 13, 2018

Keep it...

@ajcvickers
Copy link
Contributor

@ErikEJ More details please...

@ErikEJ
Copy link
Contributor

ErikEJ commented Apr 13, 2018

Seems logical that this only happens during migrations being applied

@jsacapdev
Copy link
Contributor Author

@ajcvickers thanks for the guidance.

@ajcvickers ajcvickers reopened this Apr 16, 2018
@ajcvickers
Copy link
Contributor

Putting this on the backlog for now to consider for later release based on feedback.

@ajcvickers ajcvickers changed the title 2.1 Preview 2 Data HasData() Not Working when Unit Testing for In-Memory Seed InMemory database automatically (i.e. without calling EnsureCreated) Apr 16, 2018
@ajcvickers ajcvickers added this to the Backlog milestone Apr 16, 2018
@anorborg
Copy link

When should one call context.Database.EnsureCreated when using HasData for seeding scenarios?

@AndriySvyryd
Copy link
Member

AndriySvyryd commented Jun 11, 2018

@anorborg You can just call it before using the context:

using (var context = new MyDbContext())
{
    context.Database.EnsureCreated();
    ...
}

@gojanpaolo
Copy link

I vote for explicitly calling context.Database.EnsureCreated.

Imagine having a test which needs the Count() value.

service.AddRangeIfValid(validItems);
Assert.Equal(validItems.Count(), dbContext.Items.Count());

If seeding is implicitly called then this may lead to unexpected results.

@victormarante
Copy link

Is this still valid? Do you still have call EnsureCreated()?

@ajcvickers
Copy link
Contributor

@victormarante Yes.

@JuergenGutsch
Copy link

Why? Doesn't makes sense to me, that EnsureCreated needs to be called explicitly.
I'd like to switch the context to a in-memory DB while running integration tests. This works, but the seeding didn't happen. So I need to add this additional line only to get the integration tests on an in-memory DB running.

@AndriySvyryd
Copy link
Member

@JuergenGutsch Seeding is performed as part of database initialization. To trigger database initialization you need to call EnsureCreated(). Relational providers can also use migrations tools to do this, but otherwise it works the same way.
If you use a shared (named) in-memory database you only need to do this once.

@JuergenGutsch
Copy link

Hi @AndriySvyryd
That means the behavior of the EF interface is dependent on the provider the context runs on? Does this makes sense? Anyway, I see your point. I'm just not sure whether this is good or bad. This seems to be confusing as we can see in this thread or other issues like this.
Currently I added EnsureCreated() to the application to test just to get the integration tests running, even if the DbContext is running on a relational database in production. This would be a pretty bad option. So I need to find a way to call EnsureCreated() from within the test project instead.

@ajcvickers
Copy link
Contributor

We discussed this again as part of planning and agreed that requiring a call to EnsureCreated is desirable here since this type of seeding is inherently associated with the database creation and migration.

@Mike-E-angelo
Copy link

Mike-E-angelo commented Mar 27, 2020

Hello All,

I have been utilizing this in my tests and it has been working great. However, I realized today after making additional tests that it seems that when DbContext.Database.EnsureCreated is called, the OnModelCreating method only along with its corresponding initializing data via HasData gets called once statically across my xUnit domain. This happens even though I have multiple instances of my DbContext created in this domain (one per test, scoped, for a total of four).

So it would seem now that there is a race condition that the first test that calls with OnModelCreating is the one that all tests use.

Is this expected behavior? I can verify that all four contexts are scoped appropriately and only get created once per test, each with the Database.EnsureCreated called returning true. It's just that the first context created has its OnModelCreating called once and that is the only time is fires.

I am still learning the ropes here, so there might be user error as well, but wanted to do a quick sanity check to explore options if not before diving in.

@ajcvickers
Copy link
Contributor

@Mike-E-angelo By default, EF builds a single model per context type and then caches the model. This is because model building can be slow. OnModelCreating is only being called once because the model is only being built once.

@Mike-E-angelo
Copy link

Thank you for the confirmation, @ajcvickers. I suspected as such and have adjusted my initialization components accordingly, making use of the Upsert package found here. 👍

@17521121
Copy link

17521121 commented Apr 4, 2021

I still have no idea how to seed existing data into context,

  • put the creating code in controller, it will be double after times

  • put the creating code in OnModelCreating, no data added, for example I put this code in ItemContext
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
    this.Database.EnsureCreated();
    this.Items.AddRange(items);
    this.SaveChanges();
    }

  • put the creating code in Program then
    using (var context = new ItemContext())
    {
    context.Database.EnsureCreated();
    context.Items.AddRange(context.items);
    context.SaveChanges();
    }
    errror CS7036 appears

@ajcvickers ajcvickers reopened this Oct 16, 2022
@ajcvickers ajcvickers closed this as not planned Won't fix, can't repro, duplicate, stale Oct 16, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants