using System.Text.Json;
using LudosData.Api.Domain;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
namespace LudosData.Api.Data;
public class SeedOptions
{
public const string SectionName = "Seed";
/// When false, migrations still run but no user or games are created.
public bool Enabled { get; set; } = true;
public string UserName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
}
///
/// Applies migrations and, on a genuinely empty database, creates the initial user
/// and imports the 105 games recovered from the 2018 MySQL dump.
///
/// The dump predates the multi-user work, so it has no users table and no per-game
/// owner: every imported game is assigned to the seed user.
///
public static class DbSeeder
{
public static async Task MigrateAndSeedAsync(IServiceProvider services, CancellationToken ct = default)
{
using var scope = services.CreateScope();
var sp = scope.ServiceProvider;
var logger = sp.GetRequiredService().CreateLogger("DbSeeder");
var db = sp.GetRequiredService();
await db.Database.MigrateAsync(ct);
var options = sp.GetRequiredService>().Value;
if (!options.Enabled)
{
logger.LogInformation("Seeding disabled; skipping.");
return;
}
if (await db.Users.AnyAsync(ct))
{
logger.LogInformation("Database already has users; skipping seed.");
return;
}
if (string.IsNullOrWhiteSpace(options.UserName) || string.IsNullOrWhiteSpace(options.Password))
{
logger.LogWarning(
"Seeding is enabled but Seed:UserName / Seed:Password are not set, so no initial user was " +
"created and the {Count} games from the 2018 dump were not imported. Set SEED__USERNAME, " +
"SEED__EMAIL and SEED__PASSWORD and restart, or register a user and import manually.",
await CountSeedGamesAsync(ct));
return;
}
var userManager = sp.GetRequiredService>();
var user = new AppUser
{
UserName = options.UserName,
Email = string.IsNullOrWhiteSpace(options.Email) ? $"{options.UserName}@localhost" : options.Email,
EmailConfirmed = true,
};
var created = await userManager.CreateAsync(user, options.Password);
if (!created.Succeeded)
{
var errors = string.Join("; ", created.Errors.Select(e => e.Description));
logger.LogError("Could not create the seed user: {Errors}", errors);
return;
}
var games = await LoadSeedGamesAsync(ct);
foreach (var game in games)
{
game.OwnerId = user.Id;
db.Games.Add(game);
}
await db.SaveChangesAsync(ct);
logger.LogInformation(
"Seeded user {UserName} with {Count} games from the 2018 dump.", user.UserName, games.Count);
}
private static async Task> LoadSeedGamesAsync(CancellationToken ct)
{
var path = Path.Combine(AppContext.BaseDirectory, "Data", "Seed", "games.json");
if (!File.Exists(path))
{
return [];
}
await using var stream = File.OpenRead(path);
var records = await JsonSerializer.DeserializeAsync>(
stream, new JsonSerializerOptions(JsonSerializerDefaults.Web), ct) ?? [];
return records.Select(r => new Game
{
Title = r.Title,
System = r.System,
Genre = r.Genre,
Year = r.Year,
Developer = r.Developer,
Publisher = r.Publisher,
Art = r.Art,
Description = r.Description,
Own = r.Own,
Dumped = r.Dumped,
Played = r.Played,
Finished = r.Finished,
}).ToList();
}
private static async Task CountSeedGamesAsync(CancellationToken ct) =>
(await LoadSeedGamesAsync(ct)).Count;
private sealed record SeedGame(
string Title,
string? System,
string? Genre,
string? Year,
string? Developer,
string? Publisher,
string? Art,
string? Description,
bool Own,
bool Dumped,
bool Played,
bool Finished);
}