PI06 i PI06-1. Docker definitions for MSSQL and Postgres. Data seeder/generator for countries and people. Entity Framework example with variants for Postgres and MSSQL
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Bogus" Version="35.6.5" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.14.37027.9 d17.14
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PeopleDataGenerator", "PeopleDataGenerator.csproj", "{08786A49-9D47-4F1D-AE9D-47AB02CE8954}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{08786A49-9D47-4F1D-AE9D-47AB02CE8954}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{08786A49-9D47-4F1D-AE9D-47AB02CE8954}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{08786A49-9D47-4F1D-AE9D-47AB02CE8954}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{08786A49-9D47-4F1D-AE9D-47AB02CE8954}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {EC9B8434-351C-4169-A527-51408CB7ABED}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
230
docker-definitions/PeopleDataGenerator/Program.cs
Normal file
230
docker-definitions/PeopleDataGenerator/Program.cs
Normal file
@@ -0,0 +1,230 @@
|
||||
using System.Text;
|
||||
using Bogus;
|
||||
using Bogus.Extensions;
|
||||
|
||||
const int peoplePerCountry = 50;
|
||||
var generatorDirectory = GetGeneratorDirectory();
|
||||
|
||||
Console.OutputEncoding = Encoding.UTF8;
|
||||
|
||||
var targetDatabase = PromptForTargetDatabase(args);
|
||||
var defaultOutputPath = GetDefaultPeopleOutputPath(generatorDirectory, targetDatabase);
|
||||
var outputPath = PromptForPath("Enter people SQL output", defaultOutputPath);
|
||||
var fullOutputPath = Path.GetFullPath(outputPath, Environment.CurrentDirectory);
|
||||
|
||||
var insertStatements = BuildPersonInsertStatements(targetDatabase);
|
||||
var fileContent = BuildOutputContent(targetDatabase, insertStatements);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(fullOutputPath)!);
|
||||
await File.WriteAllTextAsync(fullOutputPath, fileContent, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
|
||||
|
||||
Console.WriteLine($"Target database: {targetDatabase.DisplayName()}");
|
||||
Console.WriteLine($"SQL script saved to: {fullOutputPath}");
|
||||
|
||||
static DatabaseTarget PromptForTargetDatabase(string[] args)
|
||||
{
|
||||
if (args.Length > 0)
|
||||
{
|
||||
return ParseTargetDatabase(args[0]);
|
||||
}
|
||||
|
||||
Console.Write("Generate schema for [postgres/mssql] (default: mssql): ");
|
||||
var input = Console.ReadLine();
|
||||
|
||||
return string.IsNullOrWhiteSpace(input) ? DatabaseTarget.MsSql : ParseTargetDatabase(input.Trim());
|
||||
}
|
||||
|
||||
static DatabaseTarget ParseTargetDatabase(string value)
|
||||
{
|
||||
return value.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"postgres" or "postgresql" or "pg" => DatabaseTarget.Postgres,
|
||||
"mssql" or "sqlserver" or "sql-server" or "sql" => DatabaseTarget.MsSql,
|
||||
_ => throw new ArgumentException($"Unsupported target database '{value}'. Use 'postgres' or 'mssql'.")
|
||||
};
|
||||
}
|
||||
|
||||
static string GetDefaultPeopleOutputPath(string generatorDirectory, DatabaseTarget targetDatabase) =>
|
||||
targetDatabase == DatabaseTarget.Postgres
|
||||
? Path.GetFullPath(@"..\postgres-eventsdb\init\06-people.sql", generatorDirectory)
|
||||
: Path.GetFullPath(@"..\mssql-eventsdb\init\06-people.sql", generatorDirectory);
|
||||
|
||||
static string GetGeneratorDirectory()
|
||||
{
|
||||
var currentDirectory = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
|
||||
while (currentDirectory is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(currentDirectory.FullName, "PeopleDataGenerator.csproj")))
|
||||
{
|
||||
return currentDirectory.FullName;
|
||||
}
|
||||
|
||||
currentDirectory = currentDirectory.Parent;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Could not locate the PeopleDataGenerator project directory.");
|
||||
}
|
||||
|
||||
static string PromptForPath(string prompt, string defaultPath)
|
||||
{
|
||||
Console.Write($"{prompt} [{defaultPath}]: ");
|
||||
var input = Console.ReadLine();
|
||||
|
||||
return string.IsNullOrWhiteSpace(input) ? defaultPath : input.Trim();
|
||||
}
|
||||
|
||||
static string BuildOutputContent(DatabaseTarget targetDatabase, IReadOnlyList<string> insertStatements)
|
||||
{
|
||||
var content = string.Join("\n", insertStatements) + "\n";
|
||||
|
||||
if (targetDatabase == DatabaseTarget.MsSql)
|
||||
{
|
||||
return $"USE [$(MSSQL_DB)];\nGO\n\n{content}";
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
static List<string> BuildPersonInsertStatements(DatabaseTarget targetDatabase)
|
||||
{
|
||||
var statements = new List<string>();
|
||||
var nonLatinLocales = new HashSet<string>
|
||||
{
|
||||
"ar",
|
||||
"el",
|
||||
"fa",
|
||||
"ge",
|
||||
"ne",
|
||||
"ru",
|
||||
"uk"
|
||||
};
|
||||
|
||||
var localeToCountryCode = new (string Locale, string CountryCode)[]
|
||||
{
|
||||
("af_ZA", "ZA"),
|
||||
("ar", "SA"),
|
||||
("az", "AZ"),
|
||||
("cz", "CZ"),
|
||||
("de", "DE"),
|
||||
("de_AT", "AT"),
|
||||
("de_CH", "CH"),
|
||||
("el", "GR"),
|
||||
("en_AU", "AU"),
|
||||
("en_CA", "CA"),
|
||||
("en_GB", "GB"),
|
||||
("en_IE", "IE"),
|
||||
("en_IND", "IN"),
|
||||
("en_NG", "NG"),
|
||||
("en_US", "US"),
|
||||
("es", "ES"),
|
||||
("es_MX", "MX"),
|
||||
("fa", "IR"),
|
||||
("fi", "FI"),
|
||||
("fr", "FR"),
|
||||
("ge", "GE"),
|
||||
("hr", "HR"),
|
||||
("id_ID", "ID"),
|
||||
("it", "IT"),
|
||||
("lv", "LV"),
|
||||
("nb_NO", "NO"),
|
||||
("ne", "NP"),
|
||||
("nl", "NL"),
|
||||
("nl_BE", "BE"),
|
||||
("pl", "PL"),
|
||||
("pt_BR", "BR"),
|
||||
("pt_PT", "PT"),
|
||||
("ro", "RO"),
|
||||
("ru", "RU"),
|
||||
("sk", "SK"),
|
||||
("sv", "SE"),
|
||||
("tr", "TR"),
|
||||
("uk", "UA"),
|
||||
("vi", "VN")
|
||||
};
|
||||
|
||||
foreach (var (locale, countryCode) in localeToCountryCode)
|
||||
{
|
||||
var faker = new Faker(locale);
|
||||
var transliterationLanguage = locale.Split('_')[0];
|
||||
var useTranscriptionForAddress = nonLatinLocales.Contains(transliterationLanguage);
|
||||
|
||||
for (var i = 0; i < peoplePerCountry; i++)
|
||||
{
|
||||
var firstName = faker.Name.FirstName();
|
||||
var lastName = faker.Name.LastName();
|
||||
var firstNameTranscription = firstName.Transliterate(transliterationLanguage);
|
||||
var lastNameTranscription = lastName.Transliterate(transliterationLanguage);
|
||||
var city = NormalizeForStorage(faker.Address.City(), transliterationLanguage, useTranscriptionForAddress);
|
||||
var addressLine = NormalizeForStorage(faker.Address.StreetAddress(), transliterationLanguage, useTranscriptionForAddress);
|
||||
var postalCode = NormalizeForStorage(faker.Address.ZipCode(), transliterationLanguage, useTranscriptionForAddress);
|
||||
var addressCountry = NormalizeForStorage(faker.Address.Country(), transliterationLanguage, useTranscriptionForAddress);
|
||||
var email = faker.Internet.Email(firstNameTranscription, lastNameTranscription);
|
||||
var contactPhone = NormalizePhoneNumber(faker.Phone.PhoneNumber());
|
||||
var birthDate = faker.Date.BetweenDateOnly(new DateOnly(1950, 1, 1), new DateOnly(2010, 12, 31));
|
||||
var documentNumber = $"{countryCode}-{faker.Random.Replace("########")}";
|
||||
|
||||
statements.Add(CreateInsertStatement(
|
||||
targetDatabase,
|
||||
firstName,
|
||||
lastName,
|
||||
firstNameTranscription,
|
||||
lastNameTranscription,
|
||||
addressLine,
|
||||
postalCode,
|
||||
city,
|
||||
addressCountry,
|
||||
email,
|
||||
contactPhone,
|
||||
birthDate,
|
||||
documentNumber,
|
||||
countryCode));
|
||||
}
|
||||
}
|
||||
|
||||
return statements;
|
||||
}
|
||||
|
||||
static string CreateInsertStatement(
|
||||
DatabaseTarget targetDatabase,
|
||||
string firstName,
|
||||
string lastName,
|
||||
string firstNameTranscription,
|
||||
string lastNameTranscription,
|
||||
string addressLine,
|
||||
string postalCode,
|
||||
string city,
|
||||
string addressCountry,
|
||||
string email,
|
||||
string contactPhone,
|
||||
DateOnly birthDate,
|
||||
string documentNumber,
|
||||
string countryCode)
|
||||
{
|
||||
var tableName = targetDatabase == DatabaseTarget.Postgres ? "person" : "dbo.Person";
|
||||
var columnList = targetDatabase == DatabaseTarget.Postgres
|
||||
? "(first_name, last_name, first_name_transcription, last_name_transcription, address_line, postal_code, city, address_country, email, contact_phone, birth_date, document_number, country_code)"
|
||||
: "(FirstName, LastName, FirstNameTranscription, LastNameTranscription, AddressLine, PostalCode, City, AddressCountry, Email, ContactPhone, BirthDate, DocumentNumber, CountryCode)";
|
||||
|
||||
return $"INSERT INTO {tableName} {columnList} VALUES ('{EscapeSql(firstName)}', '{EscapeSql(lastName)}', '{EscapeSql(firstNameTranscription)}', '{EscapeSql(lastNameTranscription)}', '{EscapeSql(addressLine)}', '{EscapeSql(postalCode)}', '{EscapeSql(city)}', '{EscapeSql(addressCountry)}', '{EscapeSql(email)}', '{EscapeSql(contactPhone)}', '{birthDate:yyyy-MM-dd}', '{EscapeSql(documentNumber)}', '{countryCode}');";
|
||||
}
|
||||
|
||||
static string EscapeSql(string value) => value.Replace("'", "''");
|
||||
|
||||
static string NormalizeForStorage(string value, string transliterationLanguage, bool useTranscriptionForAddress) =>
|
||||
useTranscriptionForAddress ? value.Transliterate(transliterationLanguage) : value;
|
||||
|
||||
static string NormalizePhoneNumber(string value) =>
|
||||
value.Replace("\r", string.Empty).Replace("\n", " ").Trim();
|
||||
|
||||
enum DatabaseTarget
|
||||
{
|
||||
Postgres,
|
||||
MsSql
|
||||
}
|
||||
|
||||
static class DatabaseTargetExtensions
|
||||
{
|
||||
public static string DisplayName(this DatabaseTarget targetDatabase) =>
|
||||
targetDatabase == DatabaseTarget.Postgres ? "PostgreSQL" : "Microsoft SQL Server";
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"profiles": {
|
||||
"PeopleDataGenerator": {
|
||||
"commandName": "Project",
|
||||
"environmentVariables": {
|
||||
"DOTNET_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
43
docker-definitions/PeopleDataGenerator/README.md
Normal file
43
docker-definitions/PeopleDataGenerator/README.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# PeopleDataGenerator
|
||||
|
||||
`PeopleDataGenerator` contains the .NET helper for generating person seed SQL for both PostgreSQL and Microsoft SQL Server.
|
||||
|
||||
It is used as supporting material for the teaching examples in this repository and helps produce realistic multilingual seed data for the sports-events domain.
|
||||
|
||||
The commands below assume you are running them from inside the `PeopleDataGenerator` folder.
|
||||
|
||||
## What It Does
|
||||
|
||||
- generates fake participant data with locale-specific names and transliterations
|
||||
- asks whether the target should be PostgreSQL or Microsoft SQL Server
|
||||
- writes SQL insert statements to the matching `init` folder
|
||||
|
||||
By default, the people generator writes to:
|
||||
|
||||
```text
|
||||
docker-definitions\postgres-eventsdb\init\06-people.sql
|
||||
docker-definitions\mssql-eventsdb\init\06-people.sql
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- .NET SDK 10.0
|
||||
|
||||
## Running
|
||||
|
||||
```powershell
|
||||
dotnet run --project PeopleDataGenerator.csproj
|
||||
```
|
||||
|
||||
You can also pass the target as the first argument:
|
||||
|
||||
```powershell
|
||||
dotnet run --project PeopleDataGenerator.csproj -- postgres
|
||||
dotnet run --project PeopleDataGenerator.csproj -- mssql
|
||||
```
|
||||
|
||||
The tool asks for the target database when no argument is provided, then asks for the output path. Press `Enter` to accept the default path derived from the `PeopleDataGenerator` folder.
|
||||
|
||||
## Files
|
||||
|
||||
- [Program.cs](Program.cs): .NET generator for `06-people.sql` targeting PostgreSQL or Microsoft SQL Server
|
||||
Reference in New Issue
Block a user