Data seeding is the process of inserting predefined data during application setup or initialization.
It’s especially valuable for testing and for ensuring the essential tables and records our application needs are available from the start. This approach guarantees consistency across environments and speeds up development.
If you’re using EF Core, you’re in luck, it recently introduced yet another way to perform seeding and I love it!
Data Seeding with EF Core
EF Core offers 4 ways to add seeding data:
- Custom initialization logic
- Model managed data
- Manual migration
- Data seeding through configuration options
In this post, we’ll focus solely on data seeding using configuration options, a new feature introduced in EF Core 9.
If you’re on an older version and don’t plan to upgrade soon, don’t worry, I'll be covering the other seeding methods shortly as well.
Getting Started
For this example, we’ll use a simple .NET 9 API that performs CRUD operations on a Product entity:
public sealed class Product
{
public Guid Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? ModifiedAt { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
}
UseSeeding and UseSeedingAsync
EF Core 9 introduces two new methods to simplify data seeding:
- UseSeeding
- UseAsyncSeeding
Both serve the same purpose, the only difference is whether the seeding logic runs synchronously or asynchronously.
You can configure them wherever you set up your DbContext (e.g. Program.cs or extension methods):
services.AddDbContext<AppDbContext>(options =>
options
.UseNpgsql(configuration.GetConnectionString("Postgres"))
.UseAsyncSeeding(async (dbContext, _, cancellationToken) =>
{
if (!(await dbContext.Set<Product>().AnyAsync(cancellationToken)))
{
var products = GenerateProducts();
dbContext.Set<Product>().AddRange(products);
await dbContext.SaveChangesAsync(cancellationToken);
}
}));
To keep things tidy, I created the helper method that generates fake products using Bogus:
private static IEnumerable<Product> GenerateProducts()
{
var faker = new Faker<Product>()
.RuleFor(x => x.Id, f => f.Random.Guid())
.RuleFor(x => x.CreatedAt, f => f.Date.Past())
.RuleFor(x => x.Name, f => f.Commerce.ProductName())
.RuleFor(x => x.Description, f => f.Commerce.ProductDescription())
.RuleFor(x => x.Price, f => f.Random.Decimal());
return faker.Generate(10);
}
Bogus helps me generate realistic test data. If you want to learn more about Bogus, check my blog post on it: Generate Realistic Fake Data in C#
First, we check if the table already contains any records, preventing duplicate seeding.
If no records exist, we generate a list of products and insert them.
Notice how flexible this approach is, this approach defitely gives you flexibility covering more complex seeding scenarios as well.
The primary key is generated here by Bogus, but you could also let the database generate it if you prefer.
Triggering Seeding
To trigger the seeding logic we’ll use EnsureCreatedAsync() in the app:
await using var scope = builder.ApplicationServices.CreateAsyncScope();
await using var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await dbContext.Database.EnsureCreatedAsync();
It's important to note that EnsureCreated() triggers the synchronous seeding method (UseSeeding).
EnsureCreatedAsync() triggers the asynchronous seeding method (UseAsyncSeeding).
Mixing synchronous and asynchronous methods will prevent your seeding logic from running, so be careful to use the matching pair.
Additionaly, for production environments, it’s highly recommended to separate seeding into its own application or container rather than running it inside your main app.
This approach helps avoid concurrency issues and potential deadlocks when multiple instances start simultaneously.
Conclusion
With EF Core 9’s new UseSeeding and UseAsyncSeeding methods, seeding has become more flexible and straightforward than ever.
You can write normal C# code to seed your data asynchronously or synchronously, query the database during seeding, and even integrate complex logic if needed.
This approach is by far my favorite way of seeding data.
Just remember to match your seeding method with the corresponding EnsureCreated() or EnsureCreatedAsync() call.
If you want to check out examples I created, you can find the source code here:
Source CodeI hope you enjoyed it, subscribe and get a notification when a new blog is up!
