Database Initializers

Database Initializers are classes that run automatically after the database connection is established and tables are created. They're the recommended way to seed data, load catalogs, or perform one-time setup.

IDatabaseInitializer Interface

public interface IDatabaseInitializer
{
    int Order { get; }
    Task InitializeAsync(IServiceProvider services);
}
PropertyDescription
OrderExecution priority — lower numbers run first
InitializeAsyncCalled once at startup with access to the full DI container

Note:

Just implement IDatabaseInitializer — no registration needed. The framework runs it automatically after the database is ready.

Basic Example: Seed a Default Record

public sealed class ServerInfoInitializer : IDatabaseInitializer
{
    public int Order => 0;

    public async Task InitializeAsync(IServiceProvider services)
    {
        var vault = services.GetRequiredService<IVault<ServerInfoVault>>();

        // Only seed if table is empty
        if (await vault.CountAsync() > 0)
            return;

        await vault.SaveAsync(new ServerInfoVault
        {
            Name = "Main Server",
            Host = "localhost",
            Port = 8000,
            Status = "online",
            Capacity = 2000,
        });
    }
}

Note:

Always check if data already exists before inserting. Initializers run on every startup — without a guard check, you'll insert duplicate rows each time the server starts.

Loading Catalog Data from JSON

A common pattern is loading game data (item definitions, NPC catalogs, etc.) from JSON files into the database:

public sealed class ItemCatalogInitializer : IDatabaseInitializer
{
    public int Order => 0;

    public async Task InitializeAsync(IServiceProvider services)
    {
        var vault = services.GetRequiredService<IVault<ItemCatalogVault>>();

        if (await vault.CountAsync() > 0)
            return;

        var jsonOptions = services.GetRequiredService<JsonSerializerOptions>();
        var filePath = Path.Combine(AppContext.BaseDirectory, "Resources", "items.json");

        if (!File.Exists(filePath))
            return;

        var json = await File.ReadAllTextAsync(filePath);
        var items = JsonSerializer.Deserialize<List<ItemCatalogVault>>(json, jsonOptions);

        if (items == null || items.Count == 0)
            return;

        // Batch insert for performance
        await vault.SaveBatchAsync(items);
    }
}

Note:

Use SaveBatchAsync when inserting many records. It sends a single batched SQL query instead of one query per item — significantly faster for catalog data with hundreds or thousands of entries.

Execution Order

Multiple initializers run in Order ascending, then by type name alphabetically for ties:

public sealed class SchemaInitializer : IDatabaseInitializer
{
    public int Order => 0;  // Runs first — creates lookup tables
    // ...
}

public sealed class CatalogInitializer : IDatabaseInitializer
{
    public int Order => 1;  // Runs second — populates catalogs
    // ...
}

public sealed class AdminAccountInitializer : IDatabaseInitializer
{
    public int Order => 2;  // Runs third — creates admin user (may reference catalogs)
    // ...
}

Note:

Use ordering when initializers have dependencies. For example, if AdminAccountInitializer references character classes loaded by CatalogInitializer, give the catalog a lower order number.

Initializer with DI Dependencies

Constructor injection works — just add parameters:

public sealed class WorldMetadataInitializer : IDatabaseInitializer
{
    private readonly ILogger<WorldMetadataInitializer> _logger;

    public WorldMetadataInitializer(ILogger<WorldMetadataInitializer> logger)
    {
        _logger = logger;
    }

    public int Order => 0;

    public async Task InitializeAsync(IServiceProvider services)
    {
        var vault = services.GetRequiredService<IVault<WorldMetaVault>>();

        if (await vault.CountAsync() > 0)
        {
            _logger.LogInformation("World metadata already exists, skipping");
            return;
        }

        // Load and seed world metadata...
        _logger.LogInformation("Seeded world metadata");
    }
}

Note:

If an initializer throws an exception, the error is logged but startup continues. Other initializers still run. Check your logs if seed data is missing.

IDatabaseInitializer vs PostConstruct

Both run at startup, but they serve different purposes:

IDatabaseInitializer[PostConstruct]
When it runsAfter DB connection + table creationAfter all services are constructed
PurposeSeed database dataGeneral initialization (subscribe to events, load files, etc.)
AccessFull IServiceProviderInjected dependencies only
DiscoveryAuto-discovered by implementing the interfaceAuto-discovered by attribute on any [Service] method
OrderingExplicit Order propertyExplicit Order parameter

Note:

Use IDatabaseInitializer for anything that writes to the database at startup. Use [PostConstruct] for non-database initialization like registering event handlers, loading config files, or setting up in-memory state.