How to use IHostedService to initialize an application

How to use IHostedService to initialize an application
How to use IHostedService to initialize an application

ℹ️ Toto je ďalší článok zo série ukazújucej ako možno použiť IHostedService.

Možno narazíš na to, že pred štartom tvojej ASP.NET Core služby (skôr ako služba začne odpovedať na dotazy) potrebuješ vykonať nejakú inicializáciu.
Napríklad potrebuješ spustiť databázové migrácie, vytvoriť nejaké záznamy, vytvoriť storage, načítať dáta do lokálnej cache a podobne.

⚠️ Pozor na databázové migrácie a inicializáciu dát.
Pri komplexnejšom systéme je odporúčané toto robiť v CI/CD procese a nie pri štarte služby (komplikácie pri viacnásobných inštanciach služby, spomalenie štartu aplikácie, ...).
V prípade Entity Frameworku je možné napríklad vytvoriť bundle.exe dotnet ef migrations bundle a tento spustiť v CD procese.

Je to však vhodné pri jednoduchých službách, alebo pre potreby lokálneho vývoja.

Jednoduchý a celkom pekný spôsob ako to dosiahnuť je použiť IHostedService.

Vytvorme si rozhranie pre takýto aplikačný inicializátor:

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

Následne si vytvoríme handler, ktorý implementuje IHostedService a spracuje všetky inicializátory:

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;
}

Príklad initializátora, ktorý bude napĺňať lokálnu 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;
    }
}

V prípade, že chceme spustiť databázové migrácie, tak toto rozhranie môžeme priamo implementovať do DbContextu:

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);
    }
}

Nakoniec si zaregistrujeme všetky inicializátory a handler v DI kontajnery:

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

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

Takýmto spôsobom môžeme elegantne inicializovať našu aplikáciu predtým, ako začne odpovedať na dotazy.

⚠️ Nezabudni, že inicializácia by mala byť rýchla.

Pre zjednodušenie registrácie do DI kontajnera môžeme vytvoriť 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>();