How to seed in Entity Framework Core 2?
Solution 1:
As of Entity Framework Core 2.1 there is now a new method of seeding data. In your DbContext
class override OnModelCreating
:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>().HasData(new Blog { BlogId = 1, Url = "http://sample.com" });
}
And for related entities, use anonymous classes and specify the foreign key of the related entity:
modelBuilder.Entity<Post>().HasData(
new {BlogId = 1, PostId = 1, Title = "First post", Content = "Test 1"},
new {BlogId = 1, PostId = 2, Title = "Second post", Content = "Test 2"});
Important: Please note you will need to run an add-migration after you enter this data in your OnModelCreating method and Update-Database to update your data.
The official docs have been updated.
Solution 2:
This is my solution for EF Core 2.0, adapted from https://docs.microsoft.com/en-us/aspnet/core/migration/1x-to-2x/#move-database-initialization-code
In program.cs
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Seed().Run();
}
....
Then my seeder class
public static class DatabaseSeedInitializer
{
public static IWebHost Seed(this IWebHost host)
{
using (var scope = host.Services.CreateScope())
{
var serviceProvider = scope.ServiceProvider;
try
{
Task.Run(async () =>
{
var dataseed = new DataInitializer();
await dataseed.InitializeDataAsync(serviceProvider);
}).Wait();
}
catch (Exception ex)
{
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
return host;
}
}
Solution 3:
tl;dr: Take a look through my dwCheckApi project to see how I've implemented it.
As others have said, you can read your seed data from JSON or similar (that way it can be source controlled, if you want).
The way that I've implemented it in my projects is to have a method which is called in the Configure
method in the Startup class (only when in development):
if (env.IsDevelopment())
{
app.EnsureDatabaseIsSeeded(false);
}
which calls the following:
public static int EnsureDatabaseIsSeeded(this IApplicationBuilder applicationBuilder,
bool autoMigrateDatabase)
{
// seed the database using an extension method
using (var serviceScope = applicationBuilder.ApplicationServices
.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
var context = serviceScope.ServiceProvider.GetService<DwContext>();
if (autoMigrateDatabase)
{
context.Database.Migrate();
}
return context.EnsureSeedData();
}
}
My DbContext is of type DwContext
which is a class which extends the EF Core DbContext
type
The EnsureSeedData
extension method looks like this:
public static int EnsureSeedData(this DwContext context)
{
var bookCount = default(int);
var characterCount = default(int);
var bookSeriesCount = default(int);
// Because each of the following seed method needs to do a save
// (the data they're importing is relational), we need to call
// SaveAsync within each method.
// So let's keep tabs on the counts as they come back
var dbSeeder = new DatabaseSeeder(context);
if (!context.Books.Any())
{
var pathToSeedData = Path.Combine(Directory.GetCurrentDirectory(), "SeedData", "BookSeedData.json");
bookCount = dbSeeder.SeedBookEntitiesFromJson(pathToSeedData).Result;
}
if (!context.BookCharacters.Any())
{
characterCount = dbSeeder.SeedBookCharacterEntriesFromJson().Result;
}
if (!context.BookSeries.Any())
{
bookSeriesCount = dbSeeder.SeedBookSeriesEntriesFromJson().Result;
}
return bookCount + characterCount + bookSeriesCount;
}
This application is meant to show the relationships between books, characters and series. Which is why there are three seeders.
And one of those seeder methods looks like this:
public async Task<int> SeedBookEntitiesFromJson(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath))
{
throw new ArgumentException($"Value of {filePath} must be supplied to {nameof(SeedBookEntitiesFromJson)}");
}
if (!File.Exists(filePath))
{
throw new ArgumentException($"The file { filePath} does not exist");
}
var dataSet = File.ReadAllText(filePath);
var seedData = JsonConvert.DeserializeObject<List<Book>>(dataSet);
// ensure that we only get the distinct books (based on their name)
var distinctSeedData = seedData.GroupBy(b => b.BookName).Select(b => b.First());
_context.Books.AddRange(distinctSeedData);
return await _context.SaveChangesAsync();
}
There's probably some code here which isn't great, but it could be a starting point for you to bounce off of.
Because the seeders are only called when in the development environment, you'll need to ensure that your application starts that way (if starting from the command line you can use ASPNETCORE_ENVIRONMENT=Development dotnet run
to ensure that it starts in development).
It also means that you'll need a different approach to seeding your database in production. In dwCheckApi, I have a controller which can be called to seed the database (take a look at the DatabaseController's SeedData method to see how I do that).
Solution 4:
I don't like the HasData approach than has been written in Microsoft documentation because I cannot keep my migrations clean this way & because OnModelCreating()
in my DbContext
starts to depend on data which feels a bit wrong and causes issues with random data generator.
For me the most efficient and comfortable way is to create a seed class for each of my DbSets that looks like this. (With Bogus library it's as easy as breathing)
using Bogus;
// namespace, class, etc.
// CategorySeeder seed method
public int Seed(AppDbContext context)
{
var faker = new Faker<Category>()
.RuleFor(r => r.IsGroup, () => true)
.RuleFor(r => r.Parent, () => null)
.RuleFor(r => r.UniversalTimeTicks, () => DateTime.Now.ToUniversalTime().Ticks)
.RuleFor(r => r.Title, f => "Folder: " + f.Random.Word());
var folders1 = faker.Generate(5);
faker.RuleFor(r => r.Parent, () => folders1.OrderBy(r => Guid.NewGuid()).First());
var folders2 = faker.Generate(10);
var folders3 = folders1.Concat(folders2).ToArray();
faker.RuleFor(r => r.Parent, () => folders3.OrderBy(r => Guid.NewGuid()).First());
faker.RuleFor(r => r.Title, f => f.Random.Word());
faker.RuleFor(r => r.IsGroup, () => false);
var elements = faker.Generate(20);
var allSeeds = elements.Concat(folders3).ToArray();
context.AddRange(allSeeds);
context.SaveChanges();
return allSeeds.Length;
}
// ProductSeeder Seed method
public int Seed(AppDbContext context)
{
var faker = new Faker<Product>()
.RuleFor(r => r.Sku, f => f.Random.AlphaNumeric(8))
.RuleFor(r => r.Title, f => f.Random.Word())
.RuleFor(r => r.Category, () => context.Categories.Where(c => !c.IsGroup).OrderBy(o => Guid.NewGuid()).First());
var prod = faker.Generate(50);
context.AddRange(prod);
context.SaveChanges();
return prod.Count;
}
Then create the service controller, that works only in development environment.
public class DataGeneratorController : BaseController
{
public DataGeneratorController(IServiceProvider sp) : base(sp) { }
public IActionResult SeedData()
{
var lst = new List<string>();
if (!_dbContext.Categories.Any())
{
var count = new CategoryConfiguration().Seed(_dbContext);
lst.Add($"{count} Categories have been seeded.");
}
if (!_dbContext.Products.Any())
{
var count = new ProductConfiguration().Seed(_dbContext);
lst.Add($"{count} Products have been seeded.");
}
if (lst.Count == 0)
{
lst.Add("Nothing has been seeded.");
}
return Json(lst);
}
}
And call it from Insomnia\Postman whenever I want.
Solution 5:
Create seed data static class like
public static class SeedData
{
public static void Initialize(IServiceProvider serviceProvider)
{
var context = serviceProvider.GetRequiredService<YourDbContext>();
context.Database.EnsureCreated();
if (!context.Items.Any())
{
context.Items.Add(entity: new Item() { Name = "Green Thunder" });
context.Items.Add(entity: new Item() { Name = "Berry Pomegranate" });
context.Items.Add(entity: new Item() { Name = "Betty Crocker" });
context.Items.Add(entity: new Item() { Name = "Pizza Crust Mix" });
context.SaveChanges();
}
if (!context.Shoppings.Any()) {
context.Shoppings.Add(entity:new Shopping() { Name="Defualt" });
}
}
}
update your program.cs code for inserting your seed data like below
public class Program
{
public static void Main(string[] args)
{
//CreateWebHostBuilder(args).Build().Run();
var host = CreateWebHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<YourDbContext>();
context.Database.Migrate(); // apply all migrations
SeedData.Initialize(services); // Insert default data
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred seeding the DB.");
}
}
host.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}