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);
}
| Property | Description |
|---|---|
Order | Execution priority — lower numbers run first |
InitializeAsync | Called 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 runs | After DB connection + table creation | After all services are constructed |
| Purpose | Seed database data | General initialization (subscribe to events, load files, etc.) |
| Access | Full IServiceProvider | Injected dependencies only |
| Discovery | Auto-discovered by implementing the interface | Auto-discovered by attribute on any [Service] method |
| Ordering | Explicit Order property | Explicit 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.