Creating Seed Data for ASP.NET Applications

When creating a new application I almost always need to have some initial seed data created when I first run or begin developing an application. This data helps create a baseline for all developers on an application and makes it easier to bring on new coders to the team.

Unlike database schema migrations I may or may not want this data to exist in upstream environments like staging, QA or production.  So I need to find another way to create this data and make it easily available.

Creating Seed Data

I will usually encapsulate seed data behind a dedicated "SeedDataService" in my project.  Each project may have different initial requirement but almost every project will contain an initial admin user and roles. The following example seed data service is similar to ones I've used on many projects.

public class SeedDataService
{
    private readonly ILogger<SeedDataService> _logger;
    private readonly ApplicationDbContext _dataContext;
    private readonly RoleManager<ApplicationRole> _roleManager;
    private readonly UserManager<ApplicationUser> _userManager;

    public SeedDataService(ILogger<SeedDataService> logger
                            , ApplicationDbContext dataContext
                            , RoleManager<ApplicationRole> roleManager
                            , UserManager<ApplicationUser> userManager
                            )
    {
        _logger = logger;
        _dataContext = dataContext;
        _roleManager = roleManager;
        _userManager = userManager;
    }

    public async Task EnsureSeedData()
    {
        _logger.LogInformation("Starting to Ensure Seed Data");

        await MigrateSchema();
        await EnsureRoles();
        await EnsureUsers();
    }

    protected async Task MigrateSchema(){
        _logger.LogInformation("Migrating Database Schema");
        await _dataContext.Database.MigrateAsync();
    }

    protected async Task EnsureRoles(){
        _logger.LogInformation("Ensure Seed Roles");

        var seedRoles = new []{"admin", "orgadmin"};

        var roles = await _dataContext.Roles.Select(x => x.Name.ToLower()).ToListAsync();

        foreach(var r in seedRoles.Where(x => !roles.Contains(x))){
            _logger.LogInformation($"Creating role '{r}'");
            await _roleManager.CreateAsync(new ApplicationRole(){Id = Guid.NewGuid().ToString(), Name = r});
        }
    }

    protected async Task EnsureUsers(){
        _logger.LogInformation("Ensure Seed Users");

        var seedUsers = new []{
            new {Id = "f2cdb728-b176-46d3-9546-131328a061be", Email = "admin@inertiafx.local", Password = "P@ssword2", Roles = new[]{"admin", "orgadmin"} }
            //Add additional users here
        };

        foreach(var u in seedUsers){
            var user = await _userManager.FindByEmailAsync(u.Email);
            if(user == null){
                _logger.LogInformation($"Creating user '{u.Email}'");

                user = new ApplicationUser(){
                    Id = u.Id,
                    Email = u.Email,
                    EmailConfirmed = true,
                    UserName = u.Email
                };
                await _userManager.CreateAsync(user, u.Password);
                await _userManager.AddToRolesAsync(user, u.Roles);
            }
        }
    }
}

This service is pretty simple.  It requires instances of the "ApplicationDbContext" and the Identity User and Role Managers.  Then it makes sure the current database is migrated before creating the default system roles and a single admin user.

I placed the seed data in simple inline arrays but if the requirements were more complex I could extract these arrays to dedicated classes or even embedded JSON resource files.

To make the service available to other parts of my application I need to register it in my "ConfigureServices" method in "Startup.cs" just like any other service my application uses.

public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddScoped<SeedDataService>();
}

Running the Seed Data Service

The seed data service can easily be injected into a controller or a page to run on demand but I like to take it a step further and make it available from the command line with a "/seed" argument so I can run it using "dotnet run /seed".   I like to work from the command line. This pattern also makes it easy to run seed data from a task runner, as part of my CI/CD pipeline or even debug the service directly.

To support the "/seed" argument I had to update my "Program.cs" file to look for a "/seed" argument and then execute the seed data service instead of running the application.  To make that happen I made two changes.

  1. Look for the "/seed" argument
  2. Defer the call to run the application

Before this update my Program.cs file looked liked this.

public class Program
{
    public static void Main(string[] args)
    {
    	CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });
}
Progam.cs before

To run my seed service I needed to update the "Main" method to the following.

public static void Main(string[] args)
{
    var builder = CreateHostBuilder(args);

    if(args.Contains("/seed")){
        builder.EnsureSeedData(args).Wait();
        return;
    }

    builder.Build().Run();
}

The call to "CreateHostBuilder" didn't change but I added a check for the "/seed" argument and if it is there then I called a new "EnsureSeedData" extension method, wait for it to finish and then exit.  

I chose not to call "IHostBuilder.Build"  before the call to ensure seed data so that my extension method had an opportunity to extend the context if needed.  In my case I wanted to ensure that console logging was setup when running via the extension method.

public static class SeedDataServiceExtensions
{
    public static async Task EnsureSeedData(this IHostBuilder builder, string[] args)
    {
        builder.ConfigureLogging(x => {
            x.AddConsole();
        });

        var host = builder.Build();

        var services =  host.Services.CreateScope();
        var service = services.ServiceProvider.GetService<SeedDataService>();

        await service.EnsureSeedData();
    }
}
SeedDataServiceExtensions class

The extension method is pretty simple. It adds console logging and then builds the host.  Because I wanted to make use of scoped services in my seed service (ApplicationDbContext) I also need to create a services scope in order to create my service using dependency injection.  

Conclusion

I have found the ability to have some easily accessed seed data to be really helpful and it's something I add to most project shortly after starting them.

In a future post I'll cover how I generate demo data using Faker.Net which is another library I've been enjoying lately.

The complete application code for this post can be found in my Paging Example repository.

Other Scenarios

  • What other data would we likely add with seeded user accounts and roles?
  • Could we replace the seed data service with entity framework migrations?
  • Are there other features or switches we could add to the "donet run /seed" command?

Cover image by Joshua Sortino on Unsplash