diff --git a/CHANGELOG.md b/CHANGELOG.md index 57c90e7..5d8b9fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,3 +11,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Implemented Pokémon species import. - CI/CD build pipeline. +- Implemented Region, Generation & Type synchronization. diff --git a/PokeData.sln b/PokeData.sln index 17aa44d..3190480 100644 --- a/PokeData.sln +++ b/PokeData.sln @@ -30,7 +30,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PokeData.EntityFrameworkCor EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PokeData", "src\PokeData\PokeData.csproj", "{C90A7D50-2763-4345-9CFD-5EF3F05B6388}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PokeData.ETL", "src\PokeData.ETL\PokeData.ETL.csproj", "{9155019E-35B2-471D-B227-F1AEA104ECE8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PokeData.ETL", "src\PokeData.ETL\PokeData.ETL.csproj", "{9155019E-35B2-471D-B227-F1AEA104ECE8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{6017B644-B8B7-422A-8D1F-049300B7C42E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PokeData.Tools.Synchronization", "tools\PokeData.Tools.Synchronization\PokeData.Tools.Synchronization.csproj", "{90AF9D86-E895-4B77-B7C5-36760B619EB8}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -74,10 +78,17 @@ Global {9155019E-35B2-471D-B227-F1AEA104ECE8}.Debug|Any CPU.Build.0 = Debug|Any CPU {9155019E-35B2-471D-B227-F1AEA104ECE8}.Release|Any CPU.ActiveCfg = Release|Any CPU {9155019E-35B2-471D-B227-F1AEA104ECE8}.Release|Any CPU.Build.0 = Release|Any CPU + {90AF9D86-E895-4B77-B7C5-36760B619EB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90AF9D86-E895-4B77-B7C5-36760B619EB8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90AF9D86-E895-4B77-B7C5-36760B619EB8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90AF9D86-E895-4B77-B7C5-36760B619EB8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {90AF9D86-E895-4B77-B7C5-36760B619EB8} = {6017B644-B8B7-422A-8D1F-049300B7C42E} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AE600868-6420-4204-8CA6-35B3E1C0CEC9} EndGlobalSection diff --git a/src/PokeData.Domain/Resources/ResourceId.cs b/src/PokeData.Domain/Resources/ResourceId.cs index bd932ed..2c2d049 100644 --- a/src/PokeData.Domain/Resources/ResourceId.cs +++ b/src/PokeData.Domain/Resources/ResourceId.cs @@ -4,18 +4,32 @@ namespace PokeData.Domain.Resources; public record ResourceId { + private const char Separator = ':'; + public AggregateId AggregateId { get; } + public string Type { get; } + public string Identifier { get; } + public ResourceId(AggregateId aggregateId) { AggregateId = aggregateId; + + string[] parts = aggregateId.Value.Split(Separator); + if (parts.Length != 2) + { + throw new ArgumentException($"The value '{aggregateId}' is not a valid resource identifier.", nameof(aggregateId)); + } + Type = parts[0]; + Identifier = parts[1]; } - public static ResourceId Create(string identifier) + private ResourceId(string type, string identifier) { - string value = string.Join(':', new[] { typeof(T).Name, identifier }); - AggregateId aggregateId = new(value); - - return new ResourceId(aggregateId); + AggregateId = new(string.Join(Separator, type, identifier)); + Type = type; + Identifier = identifier; } + + public static ResourceId Create(string identifier) => new(typeof(T).Name, identifier); } diff --git a/src/PokeData.EntityFrameworkCore.PostgreSQL/Migrations/20231229070437_CreateRegionTable.Designer.cs b/src/PokeData.EntityFrameworkCore.PostgreSQL/Migrations/20231229070437_CreateRegionTable.Designer.cs new file mode 100644 index 0000000..a265286 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.PostgreSQL/Migrations/20231229070437_CreateRegionTable.Designer.cs @@ -0,0 +1,126 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using PokeData.EntityFrameworkCore.Relational; + +#nullable disable + +namespace PokeData.EntityFrameworkCore.PostgreSQL.Migrations +{ + [DbContext(typeof(PokemonContext))] + [Migration("20231229070437_CreateRegionTable")] + partial class CreateRegionTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.RegionEntity", b => + { + b.Property("RegionId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RegionId")); + + b.Property("DisplayName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UniqueName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("RegionId"); + + b.HasIndex("DisplayName"); + + b.HasIndex("UniqueName") + .IsUnique(); + + b.ToTable("Regions", (string)null); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.ResourceEntity", b => + { + b.Property("ResourceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ResourceId")); + + b.Property("AggregateId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Json") + .IsRequired() + .HasColumnType("text"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("SourceNormalized") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("ResourceId"); + + b.HasIndex("AggregateId") + .IsUnique(); + + b.HasIndex("CreatedBy"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Source"); + + b.HasIndex("SourceNormalized") + .IsUnique(); + + b.HasIndex("UpdatedBy"); + + b.HasIndex("UpdatedOn"); + + b.HasIndex("Version"); + + b.ToTable("Resources", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/PokeData.EntityFrameworkCore.PostgreSQL/Migrations/20231229070437_CreateRegionTable.cs b/src/PokeData.EntityFrameworkCore.PostgreSQL/Migrations/20231229070437_CreateRegionTable.cs new file mode 100644 index 0000000..3ecf0d3 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.PostgreSQL/Migrations/20231229070437_CreateRegionTable.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace PokeData.EntityFrameworkCore.PostgreSQL.Migrations +{ + /// + public partial class CreateRegionTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Regions", + columns: table => new + { + RegionId = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UniqueName = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + DisplayName = table.Column(type: "character varying(255)", maxLength: 255, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Regions", x => x.RegionId); + }); + + migrationBuilder.CreateIndex( + name: "IX_Regions_DisplayName", + table: "Regions", + column: "DisplayName"); + + migrationBuilder.CreateIndex( + name: "IX_Regions_UniqueName", + table: "Regions", + column: "UniqueName", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Regions"); + } + } +} diff --git a/src/PokeData.EntityFrameworkCore.PostgreSQL/Migrations/20231229073054_CreateGenerationTable.Designer.cs b/src/PokeData.EntityFrameworkCore.PostgreSQL/Migrations/20231229073054_CreateGenerationTable.Designer.cs new file mode 100644 index 0000000..d86e1f1 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.PostgreSQL/Migrations/20231229073054_CreateGenerationTable.Designer.cs @@ -0,0 +1,153 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using PokeData.EntityFrameworkCore.Relational; + +#nullable disable + +namespace PokeData.EntityFrameworkCore.PostgreSQL.Migrations +{ + [DbContext(typeof(PokemonContext))] + [Migration("20231229073054_CreateGenerationTable")] + partial class CreateGenerationTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.GenerationEntity", b => + { + b.Property("GenerationId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("GenerationId")); + + b.Property("DisplayName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UniqueName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("GenerationId"); + + b.HasIndex("DisplayName"); + + b.HasIndex("UniqueName") + .IsUnique(); + + b.ToTable("Generations", (string)null); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.RegionEntity", b => + { + b.Property("RegionId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RegionId")); + + b.Property("DisplayName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UniqueName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("RegionId"); + + b.HasIndex("DisplayName"); + + b.HasIndex("UniqueName") + .IsUnique(); + + b.ToTable("Regions", (string)null); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.ResourceEntity", b => + { + b.Property("ResourceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ResourceId")); + + b.Property("AggregateId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Json") + .IsRequired() + .HasColumnType("text"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("SourceNormalized") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("ResourceId"); + + b.HasIndex("AggregateId") + .IsUnique(); + + b.HasIndex("CreatedBy"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Source"); + + b.HasIndex("SourceNormalized") + .IsUnique(); + + b.HasIndex("UpdatedBy"); + + b.HasIndex("UpdatedOn"); + + b.HasIndex("Version"); + + b.ToTable("Resources", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/PokeData.EntityFrameworkCore.PostgreSQL/Migrations/20231229073054_CreateGenerationTable.cs b/src/PokeData.EntityFrameworkCore.PostgreSQL/Migrations/20231229073054_CreateGenerationTable.cs new file mode 100644 index 0000000..f6991af --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.PostgreSQL/Migrations/20231229073054_CreateGenerationTable.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace PokeData.EntityFrameworkCore.PostgreSQL.Migrations +{ + /// + public partial class CreateGenerationTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Generations", + columns: table => new + { + GenerationId = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UniqueName = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + DisplayName = table.Column(type: "character varying(255)", maxLength: 255, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Generations", x => x.GenerationId); + }); + + migrationBuilder.CreateIndex( + name: "IX_Generations_DisplayName", + table: "Generations", + column: "DisplayName"); + + migrationBuilder.CreateIndex( + name: "IX_Generations_UniqueName", + table: "Generations", + column: "UniqueName", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Generations"); + } + } +} diff --git a/src/PokeData.EntityFrameworkCore.PostgreSQL/Migrations/20231229073725_CreateTypeTable.Designer.cs b/src/PokeData.EntityFrameworkCore.PostgreSQL/Migrations/20231229073725_CreateTypeTable.Designer.cs new file mode 100644 index 0000000..3757521 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.PostgreSQL/Migrations/20231229073725_CreateTypeTable.Designer.cs @@ -0,0 +1,180 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using PokeData.EntityFrameworkCore.Relational; + +#nullable disable + +namespace PokeData.EntityFrameworkCore.PostgreSQL.Migrations +{ + [DbContext(typeof(PokemonContext))] + [Migration("20231229073725_CreateTypeTable")] + partial class CreateTypeTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.GenerationEntity", b => + { + b.Property("GenerationId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("GenerationId")); + + b.Property("DisplayName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UniqueName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("GenerationId"); + + b.HasIndex("DisplayName"); + + b.HasIndex("UniqueName") + .IsUnique(); + + b.ToTable("Generations", (string)null); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.RegionEntity", b => + { + b.Property("RegionId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RegionId")); + + b.Property("DisplayName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UniqueName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("RegionId"); + + b.HasIndex("DisplayName"); + + b.HasIndex("UniqueName") + .IsUnique(); + + b.ToTable("Regions", (string)null); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.ResourceEntity", b => + { + b.Property("ResourceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("ResourceId")); + + b.Property("AggregateId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Json") + .IsRequired() + .HasColumnType("text"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("SourceNormalized") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("UpdatedBy") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .HasColumnType("bigint"); + + b.HasKey("ResourceId"); + + b.HasIndex("AggregateId") + .IsUnique(); + + b.HasIndex("CreatedBy"); + + b.HasIndex("CreatedOn"); + + b.HasIndex("Source"); + + b.HasIndex("SourceNormalized") + .IsUnique(); + + b.HasIndex("UpdatedBy"); + + b.HasIndex("UpdatedOn"); + + b.HasIndex("Version"); + + b.ToTable("Resources", (string)null); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.TypeEntity", b => + { + b.Property("TypeId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("TypeId")); + + b.Property("DisplayName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UniqueName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("TypeId"); + + b.HasIndex("DisplayName"); + + b.HasIndex("UniqueName") + .IsUnique(); + + b.ToTable("Types", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/PokeData.EntityFrameworkCore.PostgreSQL/Migrations/20231229073725_CreateTypeTable.cs b/src/PokeData.EntityFrameworkCore.PostgreSQL/Migrations/20231229073725_CreateTypeTable.cs new file mode 100644 index 0000000..25e4caa --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.PostgreSQL/Migrations/20231229073725_CreateTypeTable.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace PokeData.EntityFrameworkCore.PostgreSQL.Migrations +{ + /// + public partial class CreateTypeTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Types", + columns: table => new + { + TypeId = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + UniqueName = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), + DisplayName = table.Column(type: "character varying(255)", maxLength: 255, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Types", x => x.TypeId); + }); + + migrationBuilder.CreateIndex( + name: "IX_Types_DisplayName", + table: "Types", + column: "DisplayName"); + + migrationBuilder.CreateIndex( + name: "IX_Types_UniqueName", + table: "Types", + column: "UniqueName", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Types"); + } + } +} diff --git a/src/PokeData.EntityFrameworkCore.PostgreSQL/Migrations/PokemonContextModelSnapshot.cs b/src/PokeData.EntityFrameworkCore.PostgreSQL/Migrations/PokemonContextModelSnapshot.cs index 644948f..b0535df 100644 --- a/src/PokeData.EntityFrameworkCore.PostgreSQL/Migrations/PokemonContextModelSnapshot.cs +++ b/src/PokeData.EntityFrameworkCore.PostgreSQL/Migrations/PokemonContextModelSnapshot.cs @@ -22,6 +22,60 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.GenerationEntity", b => + { + b.Property("GenerationId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("GenerationId")); + + b.Property("DisplayName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UniqueName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("GenerationId"); + + b.HasIndex("DisplayName"); + + b.HasIndex("UniqueName") + .IsUnique(); + + b.ToTable("Generations", (string)null); + }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.RegionEntity", b => + { + b.Property("RegionId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("RegionId")); + + b.Property("DisplayName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UniqueName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("RegionId"); + + b.HasIndex("DisplayName"); + + b.HasIndex("UniqueName") + .IsUnique(); + + b.ToTable("Regions", (string)null); + }); + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.ResourceEntity", b => { b.Property("ResourceId") @@ -90,6 +144,33 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Resources", (string)null); }); + + modelBuilder.Entity("PokeData.EntityFrameworkCore.Relational.Entities.TypeEntity", b => + { + b.Property("TypeId") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("TypeId")); + + b.Property("DisplayName") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UniqueName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("TypeId"); + + b.HasIndex("DisplayName"); + + b.HasIndex("UniqueName") + .IsUnique(); + + b.ToTable("Types", (string)null); + }); #pragma warning restore 612, 618 } } diff --git a/src/PokeData.EntityFrameworkCore.Relational/Configurations/GenerationConfiguration.cs b/src/PokeData.EntityFrameworkCore.Relational/Configurations/GenerationConfiguration.cs new file mode 100644 index 0000000..e1b77be --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Configurations/GenerationConfiguration.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using PokeData.EntityFrameworkCore.Relational.Entities; + +namespace PokeData.EntityFrameworkCore.Relational.Configurations; + +internal class GenerationConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(PokemonContext.Generations)); + builder.HasKey(x => x.GenerationId); + + builder.HasIndex(x => x.UniqueName).IsUnique(); + builder.HasIndex(x => x.DisplayName); + + builder.Property(x => x.UniqueName).HasMaxLength(byte.MaxValue); + builder.Property(x => x.DisplayName).HasMaxLength(byte.MaxValue); + } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Configurations/RegionConfiguration.cs b/src/PokeData.EntityFrameworkCore.Relational/Configurations/RegionConfiguration.cs new file mode 100644 index 0000000..f0d7df5 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Configurations/RegionConfiguration.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using PokeData.EntityFrameworkCore.Relational.Entities; + +namespace PokeData.EntityFrameworkCore.Relational.Configurations; + +internal class RegionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(PokemonContext.Regions)); + builder.HasKey(x => x.RegionId); + + builder.HasIndex(x => x.UniqueName).IsUnique(); + builder.HasIndex(x => x.DisplayName); + + builder.Property(x => x.UniqueName).HasMaxLength(byte.MaxValue); + builder.Property(x => x.DisplayName).HasMaxLength(byte.MaxValue); + } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Configurations/TypeConfiguration.cs b/src/PokeData.EntityFrameworkCore.Relational/Configurations/TypeConfiguration.cs new file mode 100644 index 0000000..f7bbdcc --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Configurations/TypeConfiguration.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using PokeData.EntityFrameworkCore.Relational.Entities; + +namespace PokeData.EntityFrameworkCore.Relational.Configurations; + +internal class TypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable(nameof(PokemonContext.Types)); + builder.HasKey(x => x.TypeId); + + builder.HasIndex(x => x.UniqueName).IsUnique(); + builder.HasIndex(x => x.DisplayName); + + builder.Property(x => x.UniqueName).HasMaxLength(byte.MaxValue); + builder.Property(x => x.DisplayName).HasMaxLength(byte.MaxValue); + } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Entities/GenerationEntity.cs b/src/PokeData.EntityFrameworkCore.Relational/Entities/GenerationEntity.cs new file mode 100644 index 0000000..3cf5a3d --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Entities/GenerationEntity.cs @@ -0,0 +1,9 @@ +namespace PokeData.EntityFrameworkCore.Relational.Entities; + +internal class GenerationEntity // TODO(fpion): private access +{ + public int GenerationId { get; set; } + + public string UniqueName { get; set; } = string.Empty; + public string? DisplayName { get; set; } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Entities/RegionEntity.cs b/src/PokeData.EntityFrameworkCore.Relational/Entities/RegionEntity.cs new file mode 100644 index 0000000..83a17e4 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Entities/RegionEntity.cs @@ -0,0 +1,9 @@ +namespace PokeData.EntityFrameworkCore.Relational.Entities; + +internal class RegionEntity // TODO(fpion): private access +{ + public int RegionId { get; set; } + + public string UniqueName { get; set; } = string.Empty; + public string? DisplayName { get; set; } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Entities/TypeEntity.cs b/src/PokeData.EntityFrameworkCore.Relational/Entities/TypeEntity.cs new file mode 100644 index 0000000..03ea68c --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Entities/TypeEntity.cs @@ -0,0 +1,9 @@ +namespace PokeData.EntityFrameworkCore.Relational.Entities; + +internal class TypeEntity // TODO(fpion): private access +{ + public int TypeId { get; set; } + + public string UniqueName { get; set; } = string.Empty; + public string? DisplayName { get; set; } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Handlers/Synchronization/GenerationSynchronizationEventHandler.cs b/src/PokeData.EntityFrameworkCore.Relational/Handlers/Synchronization/GenerationSynchronizationEventHandler.cs new file mode 100644 index 0000000..ff3baa9 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Handlers/Synchronization/GenerationSynchronizationEventHandler.cs @@ -0,0 +1,47 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using PokeData.Domain.Resources; +using PokeData.Domain.Resources.Events; +using PokeData.EntityFrameworkCore.Relational.Entities; +using PokeData.EntityFrameworkCore.Relational.Models; +using System.Text.Json; + +namespace PokeData.EntityFrameworkCore.Relational.Handlers.Synchronization; + +internal class GenerationSynchronizationEventHandler : INotificationHandler +{ + + private readonly PokemonContext _context; + + public GenerationSynchronizationEventHandler(PokemonContext context) + { + _context = context; + } + + public async Task Handle(ResourceSavedEvent @event, CancellationToken cancellationToken) + { + ResourceId resourceId = new(@event.AggregateId); + if (resourceId.Type == typeof(Generation).Name) + { + Generation? generation = JsonSerializer.Deserialize(@event.Json.Value); + if (generation != null) + { + GenerationEntity? entity = await _context.Generations + .SingleOrDefaultAsync(x => x.GenerationId == generation.Id, cancellationToken); + if (entity == null) + { + entity = new() + { + GenerationId = generation.Id + }; + _context.Generations.Add(entity); + } + + entity.UniqueName = generation.UniqueName; + entity.DisplayName = generation.DisplayNames.SingleOrDefault(name => name.Language?.Name == "en")?.Value; // TODO(fpion): configuration + + await _context.SaveChangesAsync(cancellationToken); + } + } + } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Handlers/Synchronization/RegionSynchronizationEventHandler.cs b/src/PokeData.EntityFrameworkCore.Relational/Handlers/Synchronization/RegionSynchronizationEventHandler.cs new file mode 100644 index 0000000..e243d11 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Handlers/Synchronization/RegionSynchronizationEventHandler.cs @@ -0,0 +1,47 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using PokeData.Domain.Resources; +using PokeData.Domain.Resources.Events; +using PokeData.EntityFrameworkCore.Relational.Entities; +using PokeData.EntityFrameworkCore.Relational.Models; +using System.Text.Json; + +namespace PokeData.EntityFrameworkCore.Relational.Handlers.Synchronization; + +internal class RegionSynchronizationEventHandler : INotificationHandler +{ + + private readonly PokemonContext _context; + + public RegionSynchronizationEventHandler(PokemonContext context) + { + _context = context; + } + + public async Task Handle(ResourceSavedEvent @event, CancellationToken cancellationToken) + { + ResourceId resourceId = new(@event.AggregateId); + if (resourceId.Type == typeof(Region).Name) + { + Region? region = JsonSerializer.Deserialize(@event.Json.Value); + if (region != null) + { + RegionEntity? entity = await _context.Regions + .SingleOrDefaultAsync(x => x.RegionId == region.Id, cancellationToken); + if (entity == null) + { + entity = new() + { + RegionId = region.Id + }; + _context.Regions.Add(entity); + } + + entity.UniqueName = region.UniqueName; + entity.DisplayName = region.DisplayNames.SingleOrDefault(name => name.Language?.Name == "en")?.Value; // TODO(fpion): configuration + + await _context.SaveChangesAsync(cancellationToken); + } + } + } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Handlers/Synchronization/TypeSynchronizationEventHandler.cs b/src/PokeData.EntityFrameworkCore.Relational/Handlers/Synchronization/TypeSynchronizationEventHandler.cs new file mode 100644 index 0000000..83c892e --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Handlers/Synchronization/TypeSynchronizationEventHandler.cs @@ -0,0 +1,46 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using PokeData.Domain.Resources; +using PokeData.Domain.Resources.Events; +using PokeData.EntityFrameworkCore.Relational.Entities; +using System.Text.Json; + +namespace PokeData.EntityFrameworkCore.Relational.Handlers.Synchronization; + +internal class TypeSynchronizationEventHandler : INotificationHandler +{ + + private readonly PokemonContext _context; + + public TypeSynchronizationEventHandler(PokemonContext context) + { + _context = context; + } + + public async Task Handle(ResourceSavedEvent @event, CancellationToken cancellationToken) + { + ResourceId resourceId = new(@event.AggregateId); + if (resourceId.Type == typeof(Models.Type).Name) + { + Models.Type? type = JsonSerializer.Deserialize(@event.Json.Value); + if (type != null) + { + TypeEntity? entity = await _context.Types + .SingleOrDefaultAsync(x => x.TypeId == type.Id, cancellationToken); + if (entity == null) + { + entity = new() + { + TypeId = type.Id + }; + _context.Types.Add(entity); + } + + entity.UniqueName = type.UniqueName; + entity.DisplayName = type.DisplayNames.SingleOrDefault(name => name.Language?.Name == "en")?.Value; // TODO(fpion): configuration + + await _context.SaveChangesAsync(cancellationToken); + } + } + } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Models/APIResource.cs b/src/PokeData.EntityFrameworkCore.Relational/Models/APIResource.cs new file mode 100644 index 0000000..5df250b --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Models/APIResource.cs @@ -0,0 +1,10 @@ +namespace PokeData.EntityFrameworkCore.Relational.Models; + +internal record APIResource // TODO(fpion): code duplication +{ + /// + /// The URL of the referenced resource. + /// + [JsonPropertyName("url")] + public string Url { get; set; } = string.Empty; +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Models/Generation.cs b/src/PokeData.EntityFrameworkCore.Relational/Models/Generation.cs new file mode 100644 index 0000000..5b6e040 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Models/Generation.cs @@ -0,0 +1,22 @@ +namespace PokeData.EntityFrameworkCore.Relational.Models; + +internal class Generation // TODO(fpion): code duplication +{ + /// + /// The identifier for this resource. + /// + [JsonPropertyName("id")] + public int Id { get; set; } + + /// + /// The name for this resource. + /// + [JsonPropertyName("name")] + public string UniqueName { get; set; } = string.Empty; + + /// + /// The name of this resource listed in different languages. + /// + [JsonPropertyName("names")] + public List DisplayNames { get; set; } = []; +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Models/Name.cs b/src/PokeData.EntityFrameworkCore.Relational/Models/Name.cs new file mode 100644 index 0000000..10e6746 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Models/Name.cs @@ -0,0 +1,16 @@ +namespace PokeData.EntityFrameworkCore.Relational.Models; + +internal record Name // TODO(fpion): code duplication +{ + /// + /// The localized name for an API resource in a specific language. + /// + [JsonPropertyName("name")] + public string Value { get; set; } = string.Empty; + + /// + /// The language this name is in. + /// + [JsonPropertyName("language")] + public NamedAPIResource? Language { get; set; } +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Models/NamedAPIResource.cs b/src/PokeData.EntityFrameworkCore.Relational/Models/NamedAPIResource.cs new file mode 100644 index 0000000..d2ecf20 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Models/NamedAPIResource.cs @@ -0,0 +1,10 @@ +namespace PokeData.EntityFrameworkCore.Relational.Models; + +internal record NamedAPIResource : APIResource // TODO(fpion): code duplication +{ + /// + /// The name of the referenced resource. + /// + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Models/Region.cs b/src/PokeData.EntityFrameworkCore.Relational/Models/Region.cs new file mode 100644 index 0000000..ddbbe0d --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Models/Region.cs @@ -0,0 +1,22 @@ +namespace PokeData.EntityFrameworkCore.Relational.Models; + +internal class Region // TODO(fpion): code duplication +{ + /// + /// The identifier for this resource. + /// + [JsonPropertyName("id")] + public int Id { get; set; } + + /// + /// The name for this resource. + /// + [JsonPropertyName("name")] + public string UniqueName { get; set; } = string.Empty; + + /// + /// The name of this resource listed in different languages. + /// + [JsonPropertyName("names")] + public List DisplayNames { get; set; } = []; +} diff --git a/src/PokeData.EntityFrameworkCore.Relational/Models/Type.cs b/src/PokeData.EntityFrameworkCore.Relational/Models/Type.cs new file mode 100644 index 0000000..4cacbf8 --- /dev/null +++ b/src/PokeData.EntityFrameworkCore.Relational/Models/Type.cs @@ -0,0 +1,23 @@ +namespace PokeData.EntityFrameworkCore.Relational.Models; + +internal record Type // TODO(fpion): code duplication +{ + /// + /// The identifier for this resource. + /// + [JsonPropertyName("id")] + public int Id { get; set; } + + /// + /// The name for this resource. + /// + [JsonPropertyName("name")] + public string UniqueName { get; set; } = string.Empty; + + /// + /// The name of this resource listed in different languages. + /// + [JsonPropertyName("names")] + public List DisplayNames { get; set; } = []; +} + diff --git a/src/PokeData.EntityFrameworkCore.Relational/PokeData.EntityFrameworkCore.Relational.csproj b/src/PokeData.EntityFrameworkCore.Relational/PokeData.EntityFrameworkCore.Relational.csproj index 2dff201..978f050 100644 --- a/src/PokeData.EntityFrameworkCore.Relational/PokeData.EntityFrameworkCore.Relational.csproj +++ b/src/PokeData.EntityFrameworkCore.Relational/PokeData.EntityFrameworkCore.Relational.csproj @@ -18,6 +18,7 @@ + diff --git a/src/PokeData.EntityFrameworkCore.Relational/PokemonContext.cs b/src/PokeData.EntityFrameworkCore.Relational/PokemonContext.cs index f0b5753..daa1b7d 100644 --- a/src/PokeData.EntityFrameworkCore.Relational/PokemonContext.cs +++ b/src/PokeData.EntityFrameworkCore.Relational/PokemonContext.cs @@ -10,6 +10,9 @@ public PokemonContext(DbContextOptions options) : base(options) } internal DbSet Resources { get; private set; } + internal DbSet Generations { get; private set; } + internal DbSet Regions { get; private set; } + internal DbSet Types { get; private set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/tools/PokeData.Tools.Synchronization/Dockerfile b/tools/PokeData.Tools.Synchronization/Dockerfile new file mode 100644 index 0000000..671bb3c --- /dev/null +++ b/tools/PokeData.Tools.Synchronization/Dockerfile @@ -0,0 +1,23 @@ +#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base +USER app +WORKDIR /app + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["tools/PokeData.Tools.Synchronization/PokeData.Tools.Synchronization.csproj", "tools/PokeData.Tools.Synchronization/"] +RUN dotnet restore "./tools/PokeData.Tools.Synchronization/./PokeData.Tools.Synchronization.csproj" +COPY . . +WORKDIR "/src/tools/PokeData.Tools.Synchronization" +RUN dotnet build "./PokeData.Tools.Synchronization.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./PokeData.Tools.Synchronization.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "PokeData.Tools.Synchronization.dll"] \ No newline at end of file diff --git a/tools/PokeData.Tools.Synchronization/FakeResourceExtractor.cs b/tools/PokeData.Tools.Synchronization/FakeResourceExtractor.cs new file mode 100644 index 0000000..2b081a5 --- /dev/null +++ b/tools/PokeData.Tools.Synchronization/FakeResourceExtractor.cs @@ -0,0 +1,11 @@ +using PokeData.Application.Resources; + +namespace PokeData.Tools.Synchronization; + +internal class FakeResourceExtractor : IResourceExtractor +{ + public Task> GetSpeciesAsync(string id, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } +} diff --git a/tools/PokeData.Tools.Synchronization/PokeData.Tools.Synchronization.csproj b/tools/PokeData.Tools.Synchronization/PokeData.Tools.Synchronization.csproj new file mode 100644 index 0000000..314a301 --- /dev/null +++ b/tools/PokeData.Tools.Synchronization/PokeData.Tools.Synchronization.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + dotnet-PokeData.Tools.Synchronization-09fd4965-45f7-427d-8628-d3ab7b441b89 + Linux + ..\.. + + + + + + + + + + + + + diff --git a/tools/PokeData.Tools.Synchronization/Program.cs b/tools/PokeData.Tools.Synchronization/Program.cs new file mode 100644 index 0000000..157ef0d --- /dev/null +++ b/tools/PokeData.Tools.Synchronization/Program.cs @@ -0,0 +1,23 @@ +using PokeData.Application; +using PokeData.Application.Resources; +using PokeData.EntityFrameworkCore.PostgreSQL; + +namespace PokeData.Tools.Synchronization; + +public class Program +{ + public static void Main(string[] args) + { + HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); + builder.Services.AddHostedService(); + + // TODO(fpion): BEGIN REFACTOR + builder.Services.AddPokeDataWithEntityFrameworkCorePostgreSQL(builder.Configuration); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + // TODO(fpion): END REFACTOR + + IHost host = builder.Build(); + host.Run(); + } +} diff --git a/tools/PokeData.Tools.Synchronization/Properties/launchSettings.json b/tools/PokeData.Tools.Synchronization/Properties/launchSettings.json new file mode 100644 index 0000000..56ca44f --- /dev/null +++ b/tools/PokeData.Tools.Synchronization/Properties/launchSettings.json @@ -0,0 +1,15 @@ +{ + "profiles": { + "PokeData.Tools.Synchronization": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true + }, + "Docker": { + "commandName": "Docker" + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json" +} \ No newline at end of file diff --git a/tools/PokeData.Tools.Synchronization/SynchronizationApplicationContext.cs b/tools/PokeData.Tools.Synchronization/SynchronizationApplicationContext.cs new file mode 100644 index 0000000..09f16b5 --- /dev/null +++ b/tools/PokeData.Tools.Synchronization/SynchronizationApplicationContext.cs @@ -0,0 +1,9 @@ +using Logitar.EventSourcing; +using PokeData.Application; + +namespace PokeData.Tools.Synchronization; + +internal class SynchronizationApplicationContext : IApplicationContext +{ + public ActorId ActorId { get; } +} diff --git a/tools/PokeData.Tools.Synchronization/Worker.cs b/tools/PokeData.Tools.Synchronization/Worker.cs new file mode 100644 index 0000000..11b58d6 --- /dev/null +++ b/tools/PokeData.Tools.Synchronization/Worker.cs @@ -0,0 +1,55 @@ +using Logitar; +using Logitar.EventSourcing; +using Logitar.EventSourcing.EntityFrameworkCore.Relational; +using Logitar.EventSourcing.Infrastructure; +using MediatR; +using Microsoft.EntityFrameworkCore; +using PokeData.Domain.Resources; +using PokeData.Infrastructure.Commands; +using System.Diagnostics; + +namespace PokeData.Tools.Synchronization; + +public class Worker : BackgroundService +{ + private readonly ILogger _logger; + private readonly IServiceProvider _serviceProvider; + + public Worker(ILogger logger, IServiceProvider serviceProvider) + { + _logger = logger; + _serviceProvider = serviceProvider; + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + Stopwatch chrono = Stopwatch.StartNew(); + + using IServiceScope scope = _serviceProvider.CreateScope(); + IPublisher publisher = scope.ServiceProvider.GetRequiredService(); + using EventContext context = scope.ServiceProvider.GetRequiredService(); + IEventSerializer serializer = scope.ServiceProvider.GetRequiredService(); + IEventBus bus = scope.ServiceProvider.GetRequiredService(); + + await publisher.Publish(new InitializeDatabaseCommand(), cancellationToken); + + string aggregateType = typeof(ResourceAggregate).GetNamespaceQualifiedName(); + EventEntity[] events = await context.Events.AsNoTracking() + .Where(e => e.AggregateType == aggregateType) + .OrderBy(e => e.OccurredOn) + .ToArrayAsync(cancellationToken); + _logger.LogInformation("Found {count} events.", events.Length); + + for (int i = 0; i < events.Length; i++) + { + double percentage = (i + 1) / (double)events.Length; + _logger.LogInformation("Handling event {index} of {total} ({percentage:P2}).", i + 1, events.Length, percentage); + + DomainEvent @event = serializer.Deserialize(events[i]); + await bus.PublishAsync(@event, cancellationToken); + } + + chrono.Stop(); + _logger.LogInformation("Operation completed in {elapsed} milliseconds.", chrono.ElapsedMilliseconds); + } +} diff --git a/tools/PokeData.Tools.Synchronization/appsettings.Development.json b/tools/PokeData.Tools.Synchronization/appsettings.Development.json new file mode 100644 index 0000000..a145610 --- /dev/null +++ b/tools/PokeData.Tools.Synchronization/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "POSTGRESQLCONNSTR_Pokemon": "User Id=postgres;Password=h8xSN524CR63aXtV;Host=host.docker.internal;Port=5436;Database=pokemon;", + "SQLCONNSTR_Pokemon": "Server=host.docker.internal,1436;Database=Pokemon;User Id=SA;Password=SBfZCaL8JM5Yq3FK;Persist Security Info=False;Encrypt=False;" +} diff --git a/tools/PokeData.Tools.Synchronization/appsettings.json b/tools/PokeData.Tools.Synchronization/appsettings.json new file mode 100644 index 0000000..b2dcdb6 --- /dev/null +++ b/tools/PokeData.Tools.Synchronization/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +}