refactor(database): update seed pipeline to use dapper (#277)

This commit is contained in:
Aaron Po
2026-09-05 00:37:14 -04:00
committed by GitHub
parent 78ad32e93a
commit d9411eb00e
43 changed files with 1023 additions and 735 deletions

View File

@@ -1,34 +1,26 @@
using Database.Connection.DependencyInjection;
using Database.Seed.DatabaseHelpers;
using Database.Seed.PipelineData;
using Database.Seed.Sqlite;
using Domain.Entities;
using Features.Auth.Commands.Authentication.RegisterUser;
using Features.Auth.Commands.Profile.UpdateBiography;
using Features.Auth.Commands.Profile.UploadAvatar;
using Features.Users.DependencyInjection;
using Features.Auth.Dtos;
using Features.Auth.Services;
using Features.Breweries.Commands.CreateBrewery;
using Features.Breweries.DependencyInjection;
using Features.ImageUploads.Commands.UploadPhoto;
using Features.ImageUploads.DependencyInjection;
using Features.Locations.Commands.GetOrCreateCity;
using Features.Locations.DependencyInjection;
using Features.Locations.Dtos;
using idunno.Password;
using Features.Locations.Queries.GetCountry;
using Features.Users.DependencyInjection;
using Infrastructure.FileUpload;
using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Spectre.Console;
return await RunAsync();
namespace Database.Seed;
static async Task<int> RunAsync()
internal class Program
{
try
private static IMediator CreateMediator()
{
IConfiguration configuration = new ConfigurationBuilder().AddEnvironmentVariables().Build();
@@ -43,260 +35,41 @@ static async Task<int> RunAsync()
.AddScoped<ITokenService, NoOpTokenService>()
.AddMediatR(cfg =>
cfg.RegisterServicesFromAssemblyContaining<CreateBreweryCommand>()
.RegisterServicesFromAssemblyContaining<GetOrCreateCityCommand>()
.RegisterServicesFromAssemblyContaining<GetCountryQuery>()
.RegisterServicesFromAssemblyContaining<UploadAvatarCommand>()
.RegisterServicesFromAssemblyContaining<UploadPhotoCommand>()
);
await using ServiceProvider provider = services.BuildServiceProvider();
IMediator mediator = provider.GetRequiredService<IMediator>();
AnsiConsole.Write(new Rule("[bold green]Database Seeder[/]").LeftJustified());
AnsiConsole.MarkupLine("[grey]Connecting to SQLite source and loading seed data...[/]");
AnsiConsole.WriteLine();
PipelineSeedDataReader reader = new(ConnectionStrings.SqliteConnectionString);
SeedData seedData = null!;
IReadOnlyList<Guid> userIds = [];
await AnsiConsole
.Status()
.Spinner(Spinner.Known.Dots)
.SpinnerStyle(Style.Parse("green"))
.StartAsync(
"Loading seed data...",
async ctx =>
{
seedData = await reader.ReadSeedDataAsync();
ctx.Status(
$"Loaded {seedData.Breweries.Count} breweries and {seedData.Users.Count} users."
);
}
);
AnsiConsole.MarkupLine(
$"[green]✓[/] Loaded [bold]{seedData.Breweries.Count}[/] breweries."
);
AnsiConsole.Write(BuildBreweryTable(seedData.Breweries));
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine($"[green]✓[/] Loaded [bold]{seedData.Users.Count}[/] users.");
AnsiConsole.Write(BuildUserTable(seedData.Users));
AnsiConsole.WriteLine();
AnsiConsole.WriteLine("Seed data loaded successfully.");
AnsiConsole.Write(
new Rule("[bold green]Loading seed data into target database.[/]").LeftJustified()
);
await AnsiConsole
.Status()
.Spinner(Spinner.Known.Dots)
.SpinnerStyle(Style.Parse("green"))
.StartAsync(
"Loading user data into target database...",
async ctx =>
{
userIds = await LoadUsersIntoDatabaseAsync(mediator, seedData.Users);
ctx.Status("User data loaded into target database.");
}
);
await AnsiConsole
.Status()
.Spinner(Spinner.Known.Dots)
.SpinnerStyle(Style.Parse("green"))
.StartAsync(
"Loading avatars into target database...",
async ctx =>
{
await LoadAvatarsIntoDatabaseAsync(mediator, userIds, seedData.Users, ctx);
ctx.Status("Avatar data loaded into target database.");
}
);
await AnsiConsole
.Status()
.Spinner(Spinner.Known.Dots)
.SpinnerStyle(Style.Parse("green"))
.StartAsync(
"Loading brewery data into target database...",
async ctx =>
{
await LoadBreweriesIntoDatabaseAsync(
mediator,
seedData.Breweries,
userIds,
ctx
);
ctx.Status("Brewery data loaded into target database.");
}
);
return 0;
}
catch (Exception ex)
{
AnsiConsole.MarkupLine("[red]Seeding failed.[/]");
AnsiConsole.WriteException(ex);
return 1;
}
}
static async Task<IReadOnlyList<Guid>> LoadUsersIntoDatabaseAsync(
IMediator mediator,
IReadOnlyList<UserRecord> users
)
{
List<Guid> userAccountIds = [];
foreach (UserRecord userRecord in users)
{
// allowRepeatedCharacters: true -- 12 unique digits was requested from only 10 possible
// digit characters (0-9), which idunno.Password rejects outright.
string password = PasswordGenerator.Generate(64, 12, 12, allowRepeatedCharacters: true);
RegistrationPayload registration = await mediator.Send(
new RegisterUserCommand(
userRecord.User.Username,
userRecord.User.FirstName,
userRecord.User.LastName,
userRecord.Email,
DateTime.Parse(userRecord.DateOfBirth),
password
)
);
userAccountIds.Add(registration.UserAccountId);
using ServiceProvider provider = services.BuildServiceProvider();
return provider.GetRequiredService<IMediator>();
}
return userAccountIds;
}
static async Task LoadAvatarsIntoDatabaseAsync(
IMediator mediator,
IReadOnlyList<Guid> userIds,
IReadOnlyList<UserRecord> users,
StatusContext ctx
)
{
for (int i = 0; i < userIds.Count; i++)
public static async Task Main()
{
ctx.Status($"Loading avatar {i + 1}/{userIds.Count} into target database...");
Guid userId = userIds[i];
await mediator.Send(new UpdateBiographyCommand(userId, users[i].User.Bio));
byte[] avatarPng = AvatarGenerator.GeneratePng(userId);
await using MemoryStream stream = new(avatarPng);
IFormFile file = new FormFile(stream, 0, stream.Length, "file", $"{userId}.png")
using CancellationTokenSource cts = new();
Console.CancelKeyPress += (_, e) =>
{
Headers = new HeaderDictionary(),
ContentType = "image/png",
e.Cancel = true;
// ReSharper disable once AccessToDisposedClosure
cts.Cancel();
};
await mediator.Send(new UploadAvatarCommand(userId, file));
try
{
IMediator mediator = CreateMediator();
SeedRepository reader = new SeedRepository(
connectionString: "Data Source=SeedData/biergarten_seed_2026-08-25T20-45-50.697244Z.sqlite"
);
await new BiergartenDataSeeder(mediator, reader).Run(cts.Token);
}
catch (OperationCanceledException)
{
AnsiConsole.MarkupLine("[yellow]Seeding cancelled.[/]");
}
catch (Exception ex)
{
AnsiConsole.MarkupLine("[red]Seeding failed.[/]");
AnsiConsole.WriteException(ex);
}
}
}
static async Task LoadBreweriesIntoDatabaseAsync(
IMediator mediator,
IReadOnlyList<BreweryRecord> breweries,
IReadOnlyList<Guid> posterUserIds,
StatusContext ctx
)
{
if (posterUserIds.Count == 0)
throw new InvalidOperationException("Cannot load breweries without any registered users.");
for (int i = 0; i < breweries.Count; i++)
{
BreweryRecord breweryRecord = breweries[i];
ctx.Status($"Loading brewery {i + 1}/{breweries.Count} into target database...");
Guid cityId = await mediator.Send(
new GetOrCreateCityCommand(
new CityLocation(
breweryRecord.Address.City.CityName,
breweryRecord.Address.City.StateProvince,
breweryRecord.Address.City.Iso31662,
breweryRecord.Address.City.Country,
breweryRecord.Address.City.Iso31661
)
)
);
await mediator.Send(
new CreateBreweryCommand(
posterUserIds[i % posterUserIds.Count],
breweryRecord.Brewery.NameEn,
breweryRecord.Brewery.DescriptionEn,
new CreateBreweryLocation(
cityId,
breweryRecord.Address.AddressLine1,
null,
breweryRecord.Address.PostalCode,
new CoordinateData(
breweryRecord.Address.Latitude,
breweryRecord.Address.Longitude
)
)
)
);
}
}
static Table BuildBreweryTable(IReadOnlyList<BreweryRecord> breweries)
{
Table table = new Table()
.AddColumn("Brewery Name (EN)")
.AddColumn("Brewery Name (Local)")
.AddColumn("City")
.AddColumn("State/Province")
.AddColumn("Country")
.AddColumn("Longitude")
.AddColumn("Latitude");
foreach (BreweryRecord brewery in breweries)
{
table.AddRow(
brewery.Brewery.NameEn,
brewery.Brewery.NameLocal,
brewery.Address.City.CityName,
brewery.Address.City.StateProvince,
brewery.Address.City.Country,
brewery.Address.Longitude.ToString("F6"),
brewery.Address.Latitude.ToString("F6")
);
}
return table;
}
static Table BuildUserTable(IReadOnlyList<UserRecord> users)
{
Table table = new Table()
.AddColumn("Username")
.AddColumn("First Name")
.AddColumn("Last Name")
.AddColumn("Email")
.AddColumn("Date of Birth");
foreach (UserRecord user in users)
{
table.AddRow(
user.User.Username,
user.User.FirstName,
user.User.LastName,
user.Email,
user.DateOfBirth
);
}
return table;
}