ℹ️ This is another article in a series showing how you can use IHostedService.

You may find that you need to do some initialization before your ASP.NET Core service starts (before the service starts responding to queries). For example, you need to run database migrations, create some records, create storage, load data into the local cache, and so on.

⚠️ Beware of database migrations and data initialization. For a more complex system it is recommended to do this in the CI/CD process and not at service start (complications with multiple service instances, slowing down the application start, …).
In the case of Entity Framework it is possible for example to create a bundle.exe dotnet ef migrations bundle and run this in the CD process.

However, this is suitable for simple services or for local development needs.

A simple and quite nice way to do this is to use IHostedService.

Let’s create an interface for such an application initializer:

public interface IAppInitializer
{
    Task InitializeAsync(CancellationToken cancellationToken = default);
}

We then create a handler that implements IHostedService and handles all the initializers:

internal class AppInitializerHandler(IServiceScopeFactory scopeFactory) 
    : IHostedService
{
    private readonly IServiceScopeFactory _scopeFactory = scopeFactory;

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        // 👇 Create a new scope to retrieve scoped services
        using var scope = _scopeFactory.CreateScope();
        // 👇 Get all initializers
        var initializers = scope.ServiceProvider.GetServices<IAppInitializer>();

        foreach (var initializer in initializers)
        {
            // 👇 Run the initializer (choose your async strategy)
            await initializer.InitializeAsync(cancellationToken);
        }
    }

    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

An example of an initializer that will populate the local cache:

public class CacheInitializer(
    IMemoryCache memoryCache, 
    IProductRepository productRepository) : IAppInitializer
{
    private readonly IMemoryCache _memoryCache = memoryCache;
    private readonly IProductRepository productRepository = productRepository;

    public Task InitializeAsync(CancellationToken cancellationToken = default)
    {
        // Your logic for initializing the cache can go here
        return Task.CompletedTask;
    }
}

In case we want to run database migrations, we can directly implement this interface in DbContext:

public class ProductsDbContext : DbContext, IAppInitializer
{
    public DbSet<Product> Products { get; set; }

    public ProductsDbContext(DbContextOptions options) : base(options)
    {
    }

    public async Task InitializeAsync(CancellationToken cancellationToken = default)
    {
        // 👇 Apply pending migrations
        await Database.MigrateAsync(cancellationToken);
    }
}

Finally, we register all initializers and handler in DI containers:

// 👇 Register the initializers
builder.Services.AddScoped<IAppInitializer, ProductsDbContext>();
builder.Services.AddScoped<IAppInitializer, CacheInitializer>();

// 👇 register AppInitializerHandler as a hosted service
builder.Services.AddHostedService<AppInitializerHandler>();

This way we can elegantly initialize our application before it starts answering queries.

⚠️ Remember that initialization should be fast.

To simplify registration to the DI container, we can create an extension:

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddAppInitializer<T>(
        this IServiceCollection services) 
        where T : class, IAppInitializer
    {
        services.AddScoped<IAppInitializer, T>();
        return services;
    }
}

// 👇 Register the initializers
builder.Services.AddAppInitializer<ProductsDbContext>();
builder.Services.AddAppInitializer<CacheInitializer>();