mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-07-16 17:47:22 +00:00
add xmldoc comments
This commit is contained in:
@@ -2,7 +2,18 @@ using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace Database.Seed;
|
||||
|
||||
/// <summary>
|
||||
/// Defines a unit of seed data that can be applied to the database. Implementations
|
||||
/// should be safe to run against an already-seeded database (e.g. by checking for
|
||||
/// existing data before inserting) and may depend on data created by seeders that run
|
||||
/// before them.
|
||||
/// </summary>
|
||||
internal interface ISeeder
|
||||
{
|
||||
/// <summary>
|
||||
/// Inserts this seeder's data into the database using the supplied open connection.
|
||||
/// </summary>
|
||||
/// <param name="connection">An open connection to the target database.</param>
|
||||
/// <returns>A task that completes when seeding is finished.</returns>
|
||||
Task SeedAsync(SqlConnection connection);
|
||||
}
|
||||
@@ -3,8 +3,18 @@ using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace Database.Seed;
|
||||
|
||||
/// <summary>
|
||||
/// Seeds the location hierarchy (countries, states/provinces, and cities) used to
|
||||
/// associate other entities (e.g. breweries, users) with a geographic location.
|
||||
/// Countries must be seeded before states/provinces, which must be seeded before
|
||||
/// cities, since each level references its parent by code. The underlying stored
|
||||
/// procedures (<c>USP_CreateCountry</c>, <c>USP_CreateStateProvince</c>,
|
||||
/// <c>USP_CreateCity</c>) are expected to be idempotent, so re-running this seeder
|
||||
/// against an already-seeded database is safe.
|
||||
/// </summary>
|
||||
internal class LocationSeeder : ISeeder
|
||||
{
|
||||
/// <summary>The set of countries to seed, identified by name and ISO 3166-1 code.</summary>
|
||||
private static readonly IReadOnlyList<(
|
||||
string CountryName,
|
||||
string CountryCode
|
||||
@@ -15,6 +25,10 @@ internal class LocationSeeder : ISeeder
|
||||
("United States", "US"),
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// The set of states/provinces to seed, each identified by name, ISO 3166-2 code,
|
||||
/// and the ISO 3166-1 code of its parent country.
|
||||
/// </summary>
|
||||
private static IReadOnlyList<(string StateProvinceName, string StateProvinceCode, string CountryCode)> States
|
||||
{
|
||||
get;
|
||||
@@ -123,6 +137,10 @@ internal class LocationSeeder : ISeeder
|
||||
("Ciudad de México", "MX-CMX", "MX"),
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// The set of cities to seed, each identified by name and the ISO 3166-2 code of its
|
||||
/// parent state/province.
|
||||
/// </summary>
|
||||
private static IReadOnlyList<(string StateProvinceCode, string CityName)> Cities { get; } =
|
||||
[
|
||||
("US-CA", "Los Angeles"),
|
||||
@@ -240,6 +258,12 @@ internal class LocationSeeder : ISeeder
|
||||
("MX-ZAC", "Zacatecas"),
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Seeds all countries, then states/provinces, then cities, in that order, so that
|
||||
/// each level's parent reference already exists by the time it is created.
|
||||
/// </summary>
|
||||
/// <param name="connection">An open connection to the target database.</param>
|
||||
/// <returns>A task that completes when all locations have been seeded.</returns>
|
||||
public async Task SeedAsync(SqlConnection connection)
|
||||
{
|
||||
foreach (var (countryName, countryCode) in Countries)
|
||||
@@ -265,6 +289,11 @@ internal class LocationSeeder : ISeeder
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Creates a single country by invoking the <c>dbo.USP_CreateCountry</c> stored procedure.</summary>
|
||||
/// <param name="connection">An open connection to the target database.</param>
|
||||
/// <param name="countryName">The display name of the country.</param>
|
||||
/// <param name="countryCode">The ISO 3166-1 code of the country.</param>
|
||||
/// <returns>A task that completes when the country has been created.</returns>
|
||||
private static async Task CreateCountryAsync(
|
||||
SqlConnection connection,
|
||||
string countryName,
|
||||
@@ -282,6 +311,12 @@ internal class LocationSeeder : ISeeder
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
/// <summary>Creates a single state/province by invoking the <c>dbo.USP_CreateStateProvince</c> stored procedure.</summary>
|
||||
/// <param name="connection">An open connection to the target database.</param>
|
||||
/// <param name="stateProvinceName">The display name of the state/province.</param>
|
||||
/// <param name="stateProvinceCode">The ISO 3166-2 code of the state/province.</param>
|
||||
/// <param name="countryCode">The ISO 3166-1 code of the parent country, which must already exist.</param>
|
||||
/// <returns>A task that completes when the state/province has been created.</returns>
|
||||
private static async Task CreateStateProvinceAsync(
|
||||
SqlConnection connection,
|
||||
string stateProvinceName,
|
||||
@@ -304,6 +339,11 @@ internal class LocationSeeder : ISeeder
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
/// <summary>Creates a single city by invoking the <c>dbo.USP_CreateCity</c> stored procedure.</summary>
|
||||
/// <param name="connection">An open connection to the target database.</param>
|
||||
/// <param name="cityName">The display name of the city.</param>
|
||||
/// <param name="stateProvinceCode">The ISO 3166-2 code of the parent state/province, which must already exist.</param>
|
||||
/// <returns>A task that completes when the city has been created.</returns>
|
||||
private static async Task CreateCityAsync(
|
||||
SqlConnection connection,
|
||||
string cityName,
|
||||
|
||||
@@ -3,6 +3,20 @@ using DbUp;
|
||||
using System.Reflection;
|
||||
using Database.Seed;
|
||||
|
||||
// Entry point for the database seeding utility. Connects to the target database
|
||||
// (retrying on transient failures), then runs each registered ISeeder in order to
|
||||
// populate location and user data.
|
||||
|
||||
/// <summary>
|
||||
/// Builds a SQL Server connection string from the <c>DB_SERVER</c>, <c>DB_NAME</c>,
|
||||
/// <c>DB_USER</c>, <c>DB_PASSWORD</c>, and <c>DB_TRUST_SERVER_CERTIFICATE</c>
|
||||
/// environment variables.
|
||||
/// </summary>
|
||||
/// <returns>A fully built SQL Server connection string.</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown when <c>DB_SERVER</c>, <c>DB_NAME</c>, <c>DB_USER</c>, or <c>DB_PASSWORD</c>
|
||||
/// is not set.
|
||||
/// </exception>
|
||||
string BuildConnectionString()
|
||||
{
|
||||
var server = Environment.GetEnvironmentVariable("DB_SERVER")
|
||||
|
||||
@@ -7,8 +7,18 @@ using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace Database.Seed;
|
||||
|
||||
/// <summary>
|
||||
/// Seeds user accounts, credentials, and verification records. Creates one fixed
|
||||
/// "Test User" account (<c>test.user@thebiergarten.app</c> / password <c>"password"</c>)
|
||||
/// for testing, followed by a randomly-generated account for each name in
|
||||
/// <see cref="SeedNames"/>, each with a random password and date of birth. Skips
|
||||
/// adding a verification record for a user that already has one, so this seeder is
|
||||
/// safe to re-run, though re-running will still attempt to register (and may fail on)
|
||||
/// users that already exist.
|
||||
/// </summary>
|
||||
internal class UserSeeder : ISeeder
|
||||
{
|
||||
/// <summary>The first/last name pairs used to generate seed user accounts.</summary>
|
||||
private static readonly IReadOnlyList<(
|
||||
string FirstName,
|
||||
string LastName
|
||||
@@ -116,6 +126,14 @@ internal class UserSeeder : ISeeder
|
||||
("Zoie", "Armstrong"),
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Registers a fixed test user account followed by one randomly-generated account
|
||||
/// per entry in <see cref="SeedNames"/>, adding a user verification record for each
|
||||
/// newly created account that does not already have one. Progress counts are
|
||||
/// written to the console on completion.
|
||||
/// </summary>
|
||||
/// <param name="connection">An open connection to the target database.</param>
|
||||
/// <returns>A task that completes when all users have been seeded.</returns>
|
||||
public async Task SeedAsync(SqlConnection connection)
|
||||
{
|
||||
var generator = new PasswordGenerator();
|
||||
@@ -184,6 +202,18 @@ internal class UserSeeder : ISeeder
|
||||
Console.WriteLine($"Added {createdVerifications} user verifications.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a new user account and its credential by invoking the
|
||||
/// <c>dbo.USP_RegisterUser</c> stored procedure.
|
||||
/// </summary>
|
||||
/// <param name="connection">An open connection to the target database.</param>
|
||||
/// <param name="username">The unique username for the account.</param>
|
||||
/// <param name="firstName">The user's first name.</param>
|
||||
/// <param name="lastName">The user's last name.</param>
|
||||
/// <param name="dateOfBirth">The user's date of birth.</param>
|
||||
/// <param name="email">The user's email address.</param>
|
||||
/// <param name="hash">The salted password hash, as produced by <see cref="GeneratePasswordHash"/>.</param>
|
||||
/// <returns>The newly created user account's <see cref="Guid"/> identifier.</returns>
|
||||
private static async Task<Guid> RegisterUserAsync(
|
||||
SqlConnection connection,
|
||||
string username,
|
||||
@@ -211,6 +241,13 @@ internal class UserSeeder : ISeeder
|
||||
return (Guid)result!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hashes a plaintext password using Argon2id with a randomly generated 16-byte
|
||||
/// salt, a degree of parallelism matching the available processor count, 64 MB of
|
||||
/// memory, and 4 iterations.
|
||||
/// </summary>
|
||||
/// <param name="pwd">The plaintext password to hash.</param>
|
||||
/// <returns>A string containing the base64-encoded salt and hash, separated by a colon.</returns>
|
||||
private static string GeneratePasswordHash(string pwd)
|
||||
{
|
||||
byte[] salt = RandomNumberGenerator.GetBytes(16);
|
||||
@@ -227,6 +264,10 @@ internal class UserSeeder : ISeeder
|
||||
return $"{Convert.ToBase64String(salt)}:{Convert.ToBase64String(hash)}";
|
||||
}
|
||||
|
||||
/// <summary>Checks whether a user verification record already exists for the given user account.</summary>
|
||||
/// <param name="connection">An open connection to the target database.</param>
|
||||
/// <param name="userAccountId">The user account identifier to check.</param>
|
||||
/// <returns><c>true</c> if a verification record already exists; otherwise <c>false</c>.</returns>
|
||||
private static async Task<bool> HasUserVerificationAsync(
|
||||
SqlConnection connection,
|
||||
Guid userAccountId
|
||||
@@ -243,6 +284,10 @@ internal class UserSeeder : ISeeder
|
||||
return result is not null;
|
||||
}
|
||||
|
||||
/// <summary>Creates a user verification record by invoking the <c>dbo.USP_CreateUserVerification</c> stored procedure.</summary>
|
||||
/// <param name="connection">An open connection to the target database.</param>
|
||||
/// <param name="userAccountId">The identifier of the user account to verify.</param>
|
||||
/// <returns>A task that completes when the verification record has been created.</returns>
|
||||
private static async Task AddUserVerificationAsync(
|
||||
SqlConnection connection,
|
||||
Guid userAccountId
|
||||
@@ -258,6 +303,12 @@ internal class UserSeeder : ISeeder
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a random date of birth corresponding to an age between 19 and 48
|
||||
/// years (inclusive), with a random day offset within that birth year.
|
||||
/// </summary>
|
||||
/// <param name="random">The random number source to use.</param>
|
||||
/// <returns>A randomly generated date of birth.</returns>
|
||||
private static DateTime GenerateDateOfBirth(Random random)
|
||||
{
|
||||
int age = 19 + random.Next(0, 30);
|
||||
|
||||
Reference in New Issue
Block a user