Seeding Related Entities in EF Core 2.1's HasData()


Julie LermanIf you didn't notice, Entity Framework Core 2.1 has a new way to support seeding your databases with a method called HasData. Julie Lerman has a great new Data Points column in MSDN that explains how a lot of it works.

Go read that article first. It really covers the basics. Unfortunately, for my use, her article missed a tiny detail that I think is useful. But let's start with a brief overview of how HasData works.

Brief Overview of HasData

In Entity Framework before .NET Core, entity framework had a way to create seed data but that method had a number of issues so they decided not to bring it over to Entity Framework Core. Now that we're into version 2.1 of Entity Framework Core, they wanted to allow for a way to seed the data with certain types of data.

After discussing it with Julie, we seemed to agree that it wasn't an all-in-one solution, but it did handle a number of useful scenarios.

The way it works is to override the OnModelCreating method of the DbContext. The OnModelCreating method is for mapping your entities to the database types. E.g.:

protected override void OnModelCreating(ModelBuilder bldr)
{
  base.OnModelCreating(bldr);

  bldr.Entity<Person>()
    .Property(p => p.Name)
    .HasMaxLength(100);
}

In this example, I'm just setting the max length of the name to be 100 characters long (and we could have done this with an attribute too if that's your kind of thing).

But this is where we can use HasData to add seeded data:

protected override void OnModelCreating(ModelBuilder bldr)
{
  base.OnModelCreating(bldr);

  bldr.Entity<Person>()
    .Property(p => p.Name)
    .HasMaxLength(100);

  bldr.Entity<Person>()
    .HasData(new Person
    {
      Id = 1,
      Name = "Shawn Wildermuth",
      Birthdate = DateTime.Parse("1969-04-05")
    },
    new Person
    {
      Id = 2,
      Name = "Julie Lerman",
      Birthdate = DateTime.Parse("1975-07-05")
    }
    );

}

You'll see that I can simply add new people by specifying the primary key and the data I want to seed. This works great.

One drawback to think about is that this is created every time that a context is created, so you wouldn't want to use it for large amounts of data (e.g. if you're reading from a file to seed the database). It's great for things like lookup tables or state/country lists.

My Problem

So my problem started when I wanted to seed related entities too. So, my Person class looks like this:

  public class Person
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public DateTime Birthdate { get; set; }
    public ICollection<Toy> Toys { get; set; }
  }

So I thought I might be able to just add the toys here and HasData would fix it:

  bldr.Entity<Person>()
    .HasData(new Person
    {
      Id = 1,
      Name = "Shawn Wildermuth",
      Birthdate = DateTime.Parse("1969-04-05"),
      Toys = new List<Toy>()
      {
        new Toy()
        {
          Id = 1,
          Name = "Tonka Truck"
        }
      }
    },
    new Person
    {
      Id = 2,
      Name = "Julie Lerman",
      Birthdate = DateTime.Parse("1975-07-05")
    }
    );
    

Nope! Related entities aren't that easy. The error message was clear, I needed to add these on the Toy entity. My Toy entity looks like this

  public class Toy
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public Person Owner { set; get; }
  }

So I thought that I'd create it via the Entity<Toy>:

  bldr.Entity<Toy>()
    .HasData(new Toy()
    {
      Id = 1,
      Name = "Tonka Truck",
      Owner = person1 // Nope!
    }
    );

Even if I was saving the person1 to specify the owner, this doesn't work.

One Way to Solve It

Initially this implied that I needed to change my entity to expose the PersonId as a property:

      bldr.Entity<Toy>()
        .HasData(new Toy()
        {
          Id = 1,
          Name = "Tonka Truck",
          OwnerId = 1 // Works but yuck
        }
        );

There has to be a more elegant way.

A Better Solution

In Julie's article, she mentions that you can use anonymous types and that's where the magic happens. Even if I don't have a PersonId, the HasData can infer it from the model that is needed under the covers to make sense of it. For example, here is my Toy entity again:

  public class Toy
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public Person Owner { set; get; }
    // NO OwnerID necessary
  }

And if I change my HasData to use an anonymous type, it just works:

      bldr.Entity<Toy>()
        // Anonymous Type, not Toy Type
        .HasData(new 
        {
          Id = 1,
          Name = "Tonka Truck",
          OwnerId = 1 // Works but yuck
        }
        );

In fact, I might argue that anonymous types for all HasData has benefits of simplicity where the shape of the seeded data is more important than the type:

      bldr.Entity<Person>()
        .HasData(new
        {
          Id = 1,
          Name = "Shawn Wildermuth",
          Birthdate = DateTime.Parse("1969-04-05")
        },
        new
        {
          Id = 2,
          Name = "Julie Lerman",
          Birthdate = DateTime.Parse("1975-07-05")
        }
        );

      bldr.Entity<Toy>()
        // Anonymous Type, not Toy Type
        .HasData(new 
        {
          Id = 1,
          Name = "Tonka Truck",
          OwnerId = 1 // Just Works Now
        }
        );


You can see this is the ultimate migration from this seeding:


  migrationBuilder.InsertData(
      table: "People",
      columns: new[] { "Id", "Birthdate", "Name" },
      values: new object[] { 
        1, 
        new DateTime(1969, 4, 5, 0, 0, 0, 0, DateTimeKind.Unspecified), 
        "Shawn Wildermuth" });

  migrationBuilder.InsertData(
      table: "People",
      columns: new[] { "Id", "Birthdate", "Name" },
      values: new object[] { 
        2, 
        new DateTime(1975, 7, 5, 0, 0, 0, 0, DateTimeKind.Unspecified), 
        "Julie Lerman" });

  migrationBuilder.InsertData(
      table: "Toy",
      columns: new[] { "Id", "Description", "Name", "OwnerId" },
      values: new object[] { 1, null, "Tonka Truck", 1 });

You can see the whole solution in my github repo:

https://github.com/shawnwildermuth/wilderexamples/tree/master/HasDataRelatedEntity

What do you think?


Ready to Learn Vue with ASP.NET Core?

My new Wilder Minds' course is available as an Early Access for only $79. It will be released on a weekly basis. The first module is now available:

Enroll Today



Application Name WilderBlog Environment Name Production
Application Ver v4.0.30319 Runtime Framework x86
App Path D:\home\site\wwwroot\ Runtime Version .NET Core 4.6.26628.05
Operating System Microsoft Windows 10.0.14393 Runtime Arch X86