using Database.Seed.DatabaseHelpers; using Database.Seed.Geocoding; using Database.Seed.PipelineData; using Database.Seed.Sqlite; using Domain.Entities; using Features.Auth.DependencyInjection; using Features.Auth.Identity; using Features.Breweries.DependencyInjection; using Features.Breweries.Repository; using Features.Locations.DependencyInjection; using Features.Locations.Dtos; using Features.Locations.Repository; using idunno.Password; using Infrastructure.Sql.DependencyInjection; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Spectre.Console; const string NominatimUserAgent = "TheBiergartenApp-Seeder/1.0 (+https://github.com/aaronpo97/the-biergarten-app)"; return await RunAsync(); static async Task RunAsync() { try { IConfiguration configuration = new ConfigurationBuilder().AddEnvironmentVariables().Build(); ServiceCollection services = []; services.AddSingleton(configuration); services.AddInfrastructureSql(); services.AddFeaturesBreweries(); services.AddFeaturesLocations(); services.AddFeaturesAuth(); await using ServiceProvider provider = services.BuildServiceProvider(); IBreweryRepository breweryRepository = provider.GetRequiredService(); ILocationRepository locationRepository = provider.GetRequiredService(); UserManager userManager = provider.GetRequiredService< UserManager >(); 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 postedByIds = []; 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 => { postedByIds = await LoadUsersIntoDatabaseAsync(userManager, seedData.Users); ctx.Status("User data loaded into target database."); } ); List geocodingWarnings = []; using (NominatimReverseGeocoder geocoder = new(NominatimUserAgent)) { await AnsiConsole .Status() .Spinner(Spinner.Known.Dots) .SpinnerStyle(Style.Parse("green")) .StartAsync( "Loading brewery data into target database...", async ctx => { await LoadBreweriesIntoDatabaseAsync( breweryRepository, locationRepository, geocoder, seedData.Breweries, postedByIds, geocodingWarnings, ctx ); ctx.Status("Brewery data loaded into target database."); } ); } foreach (string warning in geocodingWarnings) AnsiConsole.MarkupLine($"[yellow]![/] {warning.EscapeMarkup()}"); return 0; } catch (Exception ex) { AnsiConsole.MarkupLine("[red]Seeding failed.[/]"); AnsiConsole.WriteException(ex); return 1; } } static async Task> LoadUsersIntoDatabaseAsync( UserManager userManager, IReadOnlyList users ) { List userAccountIds = []; foreach (UserRecord userRecord in users) { ApplicationUser user = new() { FirstName = userRecord.User.FirstName, LastName = userRecord.User.LastName, DateOfBirth = DateTime.Parse(userRecord.DateOfBirth), UserName = userRecord.User.Username, Email = userRecord.Email, }; // allowRepeatedCharacters: true -- 12 unique digits was requested from only 10 possible // digit characters (0-9), which idunno.Password rejects outright. IdentityResult result = await userManager.CreateAsync( user, PasswordGenerator.Generate(64, 12, 12, allowRepeatedCharacters: true) ); if (!result.Succeeded) throw new InvalidOperationException( $"Failed to seed user '{userRecord.User.Username}': " + string.Join("; ", result.Errors.Select(e => e.Description)) ); userAccountIds.Add(user.Id); } return userAccountIds; } static async Task LoadBreweriesIntoDatabaseAsync( IBreweryRepository breweryRepository, ILocationRepository locationRepository, NominatimReverseGeocoder geocoder, IReadOnlyList breweries, IReadOnlyList posterUserIds, List geocodingWarnings, StatusContext ctx ) { const string placeholderAddressLine1 = "Address unavailable"; 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( $"Geocoding and loading brewery {i + 1}/{breweries.Count} into target database..." ); Guid cityId = await locationRepository.GetOrCreateCityIdAsync( new CityLocation( breweryRecord.Address.City.CityName, breweryRecord.Address.City.StateProvince, breweryRecord.Address.City.Iso31662, breweryRecord.Address.City.Country, breweryRecord.Address.City.Iso31661 ) ); ReverseGeocodeResult? geocoded = await geocoder.ReverseGeocodeAsync( breweryRecord.Address.Longitude, breweryRecord.Address.Latitude ); if (geocoded is null) geocodingWarnings.Add( $"Could not reverse geocode '{breweryRecord.Brewery.NameEn}' " + $"({breweryRecord.Address.Latitude}, {breweryRecord.Address.Longitude}); " + "using a placeholder address." ); await breweryRepository.CreateAsync( new BreweryPost { BreweryPostId = Guid.NewGuid(), BreweryName = breweryRecord.Brewery.NameEn, Description = breweryRecord.Brewery.DescriptionEn, PostedById = posterUserIds[i % posterUserIds.Count], // TODO: Populate Coordinates from breweryRecord.Address.Latitude/Longitude once it goes through NetTopologySuite (NTS). Location = new BreweryPostLocation { BreweryPostLocationId = Guid.NewGuid(), CityId = cityId, AddressLine1 = geocoded?.AddressLine1 ?? placeholderAddressLine1, PostalCode = geocoded?.PostalCode ?? string.Empty, }, } ); } } static Table BuildBreweryTable(IReadOnlyList 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 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; }