diff --git a/PostalRegistry.sln b/PostalRegistry.sln
index bdf70c98..e7049567 100755
--- a/PostalRegistry.sln
+++ b/PostalRegistry.sln
@@ -63,6 +63,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PostalRegistry.Producer", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PostalRegistry.Producer.Snapshot.Oslo", "src\PostalRegistry.Producer.Snapshot.Oslo\PostalRegistry.Producer.Snapshot.Oslo.csproj", "{B6B9729E-0EF7-48C8-88B1-BB4C437D20E6}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PostalRegistry.Projections.Integration", "src\PostalRegistry.Projections.Integration\PostalRegistry.Projections.Integration.csproj", "{8049759B-20E1-4AEE-9086-5422C437D742}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -137,6 +139,10 @@ Global
{B6B9729E-0EF7-48C8-88B1-BB4C437D20E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B6B9729E-0EF7-48C8-88B1-BB4C437D20E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B6B9729E-0EF7-48C8-88B1-BB4C437D20E6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8049759B-20E1-4AEE-9086-5422C437D742}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8049759B-20E1-4AEE-9086-5422C437D742}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8049759B-20E1-4AEE-9086-5422C437D742}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8049759B-20E1-4AEE-9086-5422C437D742}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -159,6 +165,7 @@ Global
{D13F985B-F16B-4F38-B170-F86E5D447926} = {81BE6CDF-434E-42AB-9CB1-B5E7A26DC317}
{DAC34E64-9C52-495E-9BF6-868E3DFE99EE} = {81BE6CDF-434E-42AB-9CB1-B5E7A26DC317}
{B6B9729E-0EF7-48C8-88B1-BB4C437D20E6} = {81BE6CDF-434E-42AB-9CB1-B5E7A26DC317}
+ {8049759B-20E1-4AEE-9086-5422C437D742} = {81BE6CDF-434E-42AB-9CB1-B5E7A26DC317}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {14E5CC1E-7FCC-4086-A7DD-CEFDAD492801}
diff --git a/paket.dependencies b/paket.dependencies
index 69d1fd1d..fbae9796 100755
--- a/paket.dependencies
+++ b/paket.dependencies
@@ -12,7 +12,7 @@ nuget CsvHelper 27.2.1
nuget NetTopologySuite 2.4.0
nuget Dapper 2.0.123
-
+nuget NodaTime 3.1.6
// For more healtchecks, look at https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks
nuget AspNetCore.HealthChecks.SqlServer 6.0.2
@@ -54,6 +54,7 @@ nuget Be.Vlaanderen.Basisregisters.ProjectionHandling.Runner 12.0.1
nuget Be.Vlaanderen.Basisregisters.ProjectionHandling.Runner.SqlServer 12.0.1
nuget Be.Vlaanderen.Basisregisters.ProjectionHandling.Runner.Npgsql 12.0.1
nuget Be.Vlaanderen.Basisregisters.ProjectionHandling.Connector.Testing 12.0.1
+nuget Be.Vlaanderen.Basisregisters.ProjectionHandling.Testing.Xunit 12.0.1
nuget Be.Vlaanderen.Basisregisters.ProjectionHandling.Syndication 12.0.1
nuget Be.Vlaanderen.Basisregisters.Projector 13.1.0
diff --git a/paket.lock b/paket.lock
index bcd5b835..4a84d06b 100644
--- a/paket.lock
+++ b/paket.lock
@@ -357,6 +357,12 @@ NUGET
Microsoft.Extensions.Http (>= 6.0)
Microsoft.Extensions.Logging (>= 6.0)
Microsoft.SyndicationFeed.ReaderWriter (>= 1.0.2)
+ Be.Vlaanderen.Basisregisters.ProjectionHandling.Testing.Xunit (12.0.1)
+ Be.Vlaanderen.Basisregisters.ProjectionHandling.Connector.Testing (12.0.1)
+ Be.Vlaanderen.Basisregisters.ProjectionHandling.SqlStreamStore (12.0.1)
+ Microsoft.EntityFrameworkCore (>= 6.0.3)
+ Microsoft.Extensions.Logging (>= 6.0)
+ xunit (>= 2.4.1)
Be.Vlaanderen.Basisregisters.Projector (13.1)
Autofac (>= 6.3)
Autofac.Extensions.DependencyInjection (>= 7.2)
@@ -814,7 +820,7 @@ NUGET
Newtonsoft.Json (>= 9.0.1)
NJsonSchema (>= 10.6.10)
NJsonSchema.CodeGeneration (>= 10.6.10)
- NodaTime (3.0.10)
+ NodaTime (3.1.6)
System.Runtime.CompilerServices.Unsafe (>= 4.7.1)
NodaTime.Serialization.JsonNet (3.0)
Newtonsoft.Json (>= 12.0.1)
diff --git a/src/EF.MigrationsHelper/EF.MigrationsHelper.csproj b/src/EF.MigrationsHelper/EF.MigrationsHelper.csproj
index e872f51f..59d0b5b6 100644
--- a/src/EF.MigrationsHelper/EF.MigrationsHelper.csproj
+++ b/src/EF.MigrationsHelper/EF.MigrationsHelper.csproj
@@ -29,6 +29,7 @@
+
diff --git a/src/EF.MigrationsHelper/appsettings.json b/src/EF.MigrationsHelper/appsettings.json
index 768f9851..0b53402d 100644
--- a/src/EF.MigrationsHelper/appsettings.json
+++ b/src/EF.MigrationsHelper/appsettings.json
@@ -8,7 +8,9 @@
"SyndicationProjections": "Server=(localdb)\\mssqllocaldb;Database=EFProviders.InMemory.PostalRegistry;Trusted_Connection=True;",
"SyndicationProjectionsAdmin": "Server=(localdb)\\mssqllocaldb;Database=EFProviders.InMemory.PostalRegistry;Trusted_Connection=True;",
"LastChangedListAdmin": "Server=(localdb)\\mssqllocaldb;Database=EFProviders.InMemory.MunicipalityRegistry;Trusted_Connection=True;",
- "ProducerProjectionsAdmin": "Server=(localdb)\\mssqllocaldb;Database=EFProviders.InMemory.MunicipalityRegistry;Trusted_Connection=True;"
+ "ProducerProjectionsAdmin": "Server=(localdb)\\mssqllocaldb;Database=EFProviders.InMemory.MunicipalityRegistry;Trusted_Connection=True;",
+ "IntegrationProjections": ".",
+ "IntegrationProjectionsAdmin": "."
},
"Serilog": {
diff --git a/src/PostalRegistry.Infrastructure/Schema.cs b/src/PostalRegistry.Infrastructure/Schema.cs
index 01cb7b27..08c215fb 100755
--- a/src/PostalRegistry.Infrastructure/Schema.cs
+++ b/src/PostalRegistry.Infrastructure/Schema.cs
@@ -11,6 +11,8 @@ public static class Schema
public const string Producer = "PostalRegistryProducer";
public const string ProducerSnapshotOslo = "PostalRegistryProducerSnapshotOslo";
+
+ public const string Integration = "integration_postal";
}
public static class MigrationTables
@@ -22,5 +24,7 @@ public static class MigrationTables
public const string Producer = "__EFMigrationsHistoryProducer";
public const string ProducerSnapshotOslo = "__EFMigrationsHistoryProducerSnapshotOslo";
+
+ public const string Integration = "__EFMigrationsHistory";
}
}
diff --git a/src/PostalRegistry.Projections.Integration/Infrastructure/IntegrationModule.cs b/src/PostalRegistry.Projections.Integration/Infrastructure/IntegrationModule.cs
new file mode 100644
index 00000000..52190918
--- /dev/null
+++ b/src/PostalRegistry.Projections.Integration/Infrastructure/IntegrationModule.cs
@@ -0,0 +1,61 @@
+namespace PostalRegistry.Projections.Integration.Infrastructure
+{
+ using System;
+ using Autofac;
+ using Microsoft.EntityFrameworkCore;
+ using Microsoft.Extensions.Configuration;
+ using Microsoft.Extensions.DependencyInjection;
+ using Microsoft.Extensions.Logging;
+ using PostalRegistry.Infrastructure;
+
+ public class IntegrationModule : Module
+ {
+ public IntegrationModule(
+ IConfiguration configuration,
+ IServiceCollection services,
+ ILoggerFactory loggerFactory)
+ {
+ var logger = loggerFactory.CreateLogger();
+ var connectionString = configuration.GetConnectionString("IntegrationProjections");
+
+ var hasConnectionString = !string.IsNullOrWhiteSpace(connectionString);
+ if (hasConnectionString)
+ RunOnNpgSqlServer(services, connectionString);
+ else
+ RunInMemoryDb(services, loggerFactory, logger);
+
+ logger.LogInformation(
+ "Added {Context} to services:" +
+ Environment.NewLine +
+ "\tSchema: {Schema}" +
+ Environment.NewLine +
+ "\tTableName: {TableName}",
+ nameof(IntegrationContext), Schema.Integration, MigrationTables.Integration);
+ }
+
+ private static void RunOnNpgSqlServer(
+ IServiceCollection services,
+ string backofficeProjectionsConnectionString)
+ {
+ services
+ .AddNpgsql(backofficeProjectionsConnectionString, sqlServerOptions =>
+ {
+ sqlServerOptions.EnableRetryOnFailure();
+ sqlServerOptions.MigrationsHistoryTable(MigrationTables.Integration, Schema.Integration);
+ });
+ }
+
+ private static void RunInMemoryDb(
+ IServiceCollection services,
+ ILoggerFactory loggerFactory,
+ ILogger logger)
+ {
+ services
+ .AddDbContext(options => options
+ .UseLoggerFactory(loggerFactory)
+ .UseInMemoryDatabase(Guid.NewGuid().ToString(), _ => { }));
+
+ logger.LogWarning("Running InMemory for {Context}!", nameof(IntegrationContext));
+ }
+ }
+}
diff --git a/src/PostalRegistry.Projections.Integration/Infrastructure/IntegrationOptions.cs b/src/PostalRegistry.Projections.Integration/Infrastructure/IntegrationOptions.cs
new file mode 100644
index 00000000..7ff40459
--- /dev/null
+++ b/src/PostalRegistry.Projections.Integration/Infrastructure/IntegrationOptions.cs
@@ -0,0 +1,8 @@
+namespace PostalRegistry.Projections.Integration.Infrastructure
+{
+ public class IntegrationOptions
+ {
+ public string Namespace { get; set; }
+ public bool Enabled { get; set; }
+ }
+}
diff --git a/src/PostalRegistry.Projections.Integration/IntegrationContext.cs b/src/PostalRegistry.Projections.Integration/IntegrationContext.cs
new file mode 100644
index 00000000..d2d175bc
--- /dev/null
+++ b/src/PostalRegistry.Projections.Integration/IntegrationContext.cs
@@ -0,0 +1,20 @@
+namespace PostalRegistry.Projections.Integration
+{
+ using Be.Vlaanderen.Basisregisters.ProjectionHandling.Runner;
+ using Microsoft.EntityFrameworkCore;
+ using PostalRegistry.Infrastructure;
+
+ public class IntegrationContext : RunnerDbContext
+ {
+ public override string ProjectionStateSchema => Schema.Integration;
+
+ public DbSet PostalLatestItems => Set();
+
+ // This needs to be here to please EF
+ public IntegrationContext() { }
+
+ // This needs to be DbContextOptions for Autofac!
+ public IntegrationContext(DbContextOptions options)
+ : base(options) { }
+ }
+}
diff --git a/src/PostalRegistry.Projections.Integration/IntegrationContextMigrationFactory.cs b/src/PostalRegistry.Projections.Integration/IntegrationContextMigrationFactory.cs
new file mode 100644
index 00000000..cae91996
--- /dev/null
+++ b/src/PostalRegistry.Projections.Integration/IntegrationContextMigrationFactory.cs
@@ -0,0 +1,23 @@
+namespace PostalRegistry.Projections.Integration
+{
+ using Be.Vlaanderen.Basisregisters.ProjectionHandling.Runner.Npgsql;
+ using Microsoft.EntityFrameworkCore;
+ using PostalRegistry.Infrastructure;
+
+ public class IntegrationContextMigrationFactory : NpgsqlRunnerDbContextMigrationFactory
+ {
+ public IntegrationContextMigrationFactory()
+ : base("IntegrationProjectionsAdmin", HistoryConfiguration) { }
+
+ private static MigrationHistoryConfiguration HistoryConfiguration =>
+ new MigrationHistoryConfiguration
+ {
+ Schema = Schema.Integration,
+ Table = MigrationTables.Integration
+ };
+
+ protected override IntegrationContext CreateContext(
+ DbContextOptions migrationContextOptions) =>
+ new IntegrationContext(migrationContextOptions);
+ }
+}
diff --git a/src/PostalRegistry.Projections.Integration/Migrations/20240131111518_Initial.Designer.cs b/src/PostalRegistry.Projections.Integration/Migrations/20240131111518_Initial.Designer.cs
new file mode 100644
index 00000000..c17ce83a
--- /dev/null
+++ b/src/PostalRegistry.Projections.Integration/Migrations/20240131111518_Initial.Designer.cs
@@ -0,0 +1,130 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using PostalRegistry.Projections.Integration;
+
+#nullable disable
+
+namespace PostalRegistry.Projections.Integration.Migrations
+{
+ [DbContext(typeof(IntegrationContext))]
+ [Migration("20240131111518_Initial")]
+ partial class Initial
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.3")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Be.Vlaanderen.Basisregisters.ProjectionHandling.Runner.ProjectionStates.ProjectionStateItem", b =>
+ {
+ b.Property("Name")
+ .HasColumnType("text");
+
+ b.Property("DesiredState")
+ .HasColumnType("text");
+
+ b.Property("DesiredStateChangedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ErrorMessage")
+ .HasColumnType("text");
+
+ b.Property("Position")
+ .HasColumnType("bigint");
+
+ b.HasKey("Name");
+
+ b.ToTable("ProjectionStates", "integration_postal");
+ });
+
+ modelBuilder.Entity("PostalRegistry.Projections.Integration.PostalInformationName", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("Language")
+ .HasColumnType("integer")
+ .HasColumnName("language");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property("PostalCode")
+ .HasColumnType("text")
+ .HasColumnName("postal_code");
+
+ b.Property("SearchName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("search_name");
+
+ b.HasKey("Id");
+
+ b.HasIndex("PostalCode");
+
+ b.HasIndex("SearchName");
+
+ b.ToTable("postal_information_name", "integration_postal");
+ });
+
+ modelBuilder.Entity("PostalRegistry.Projections.Integration.PostalLatestItem", b =>
+ {
+ b.Property("PostalCode")
+ .HasColumnType("text")
+ .HasColumnName("postal_code");
+
+ b.Property("IsRetired")
+ .HasColumnType("boolean")
+ .HasColumnName("is_retired");
+
+ b.Property("Namespace")
+ .HasColumnType("text")
+ .HasColumnName("namespace");
+
+ b.Property("NisCode")
+ .HasColumnType("text")
+ .HasColumnName("nis_code");
+
+ b.Property("PuriId")
+ .HasColumnType("text")
+ .HasColumnName("puri_id");
+
+ b.Property("VersionTimestampAsDateTimeOffset")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("version_timestamp");
+
+ b.HasKey("PostalCode");
+
+ b.HasIndex("NisCode");
+
+ b.ToTable("postal_latest_items", "integration_postal");
+ });
+
+ modelBuilder.Entity("PostalRegistry.Projections.Integration.PostalInformationName", b =>
+ {
+ b.HasOne("PostalRegistry.Projections.Integration.PostalLatestItem", null)
+ .WithMany("PostalNames")
+ .HasForeignKey("PostalCode");
+ });
+
+ modelBuilder.Entity("PostalRegistry.Projections.Integration.PostalLatestItem", b =>
+ {
+ b.Navigation("PostalNames");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/PostalRegistry.Projections.Integration/Migrations/20240131111518_Initial.cs b/src/PostalRegistry.Projections.Integration/Migrations/20240131111518_Initial.cs
new file mode 100644
index 00000000..a727ad4b
--- /dev/null
+++ b/src/PostalRegistry.Projections.Integration/Migrations/20240131111518_Initial.cs
@@ -0,0 +1,104 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace PostalRegistry.Projections.Integration.Migrations
+{
+ public partial class Initial : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.EnsureSchema(
+ name: "integration_postal");
+
+ migrationBuilder.CreateTable(
+ name: "postal_latest_items",
+ schema: "integration_postal",
+ columns: table => new
+ {
+ postal_code = table.Column(type: "text", nullable: false),
+ nis_code = table.Column(type: "text", nullable: true),
+ is_retired = table.Column(type: "boolean", nullable: false),
+ puri_id = table.Column(type: "text", nullable: true),
+ @namespace = table.Column(name: "namespace", type: "text", nullable: true),
+ version_timestamp = table.Column(type: "timestamp with time zone", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_postal_latest_items", x => x.postal_code);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "ProjectionStates",
+ schema: "integration_postal",
+ columns: table => new
+ {
+ Name = table.Column(type: "text", nullable: false),
+ Position = table.Column(type: "bigint", nullable: false),
+ DesiredState = table.Column(type: "text", nullable: true),
+ DesiredStateChangedAt = table.Column(type: "timestamp with time zone", nullable: true),
+ ErrorMessage = table.Column(type: "text", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_ProjectionStates", x => x.Name);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "postal_information_name",
+ schema: "integration_postal",
+ columns: table => new
+ {
+ id = table.Column(type: "uuid", nullable: false),
+ name = table.Column(type: "text", nullable: false),
+ search_name = table.Column(type: "text", nullable: false),
+ language = table.Column(type: "integer", nullable: false),
+ postal_code = table.Column(type: "text", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_postal_information_name", x => x.id);
+ table.ForeignKey(
+ name: "FK_postal_information_name_postal_latest_items_postal_code",
+ column: x => x.postal_code,
+ principalSchema: "integration_postal",
+ principalTable: "postal_latest_items",
+ principalColumn: "postal_code");
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_postal_information_name_postal_code",
+ schema: "integration_postal",
+ table: "postal_information_name",
+ column: "postal_code");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_postal_information_name_search_name",
+ schema: "integration_postal",
+ table: "postal_information_name",
+ column: "search_name");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_postal_latest_items_nis_code",
+ schema: "integration_postal",
+ table: "postal_latest_items",
+ column: "nis_code");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "postal_information_name",
+ schema: "integration_postal");
+
+ migrationBuilder.DropTable(
+ name: "ProjectionStates",
+ schema: "integration_postal");
+
+ migrationBuilder.DropTable(
+ name: "postal_latest_items",
+ schema: "integration_postal");
+ }
+ }
+}
diff --git a/src/PostalRegistry.Projections.Integration/Migrations/IntegrationContextModelSnapshot.cs b/src/PostalRegistry.Projections.Integration/Migrations/IntegrationContextModelSnapshot.cs
new file mode 100644
index 00000000..b61f94f0
--- /dev/null
+++ b/src/PostalRegistry.Projections.Integration/Migrations/IntegrationContextModelSnapshot.cs
@@ -0,0 +1,128 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using PostalRegistry.Projections.Integration;
+
+#nullable disable
+
+namespace PostalRegistry.Projections.Integration.Migrations
+{
+ [DbContext(typeof(IntegrationContext))]
+ partial class IntegrationContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.3")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Be.Vlaanderen.Basisregisters.ProjectionHandling.Runner.ProjectionStates.ProjectionStateItem", b =>
+ {
+ b.Property("Name")
+ .HasColumnType("text");
+
+ b.Property("DesiredState")
+ .HasColumnType("text");
+
+ b.Property("DesiredStateChangedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("ErrorMessage")
+ .HasColumnType("text");
+
+ b.Property("Position")
+ .HasColumnType("bigint");
+
+ b.HasKey("Name");
+
+ b.ToTable("ProjectionStates", "integration_postal");
+ });
+
+ modelBuilder.Entity("PostalRegistry.Projections.Integration.PostalInformationName", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("Language")
+ .HasColumnType("integer")
+ .HasColumnName("language");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.Property("PostalCode")
+ .HasColumnType("text")
+ .HasColumnName("postal_code");
+
+ b.Property("SearchName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("search_name");
+
+ b.HasKey("Id");
+
+ b.HasIndex("PostalCode");
+
+ b.HasIndex("SearchName");
+
+ b.ToTable("postal_information_name", "integration_postal");
+ });
+
+ modelBuilder.Entity("PostalRegistry.Projections.Integration.PostalLatestItem", b =>
+ {
+ b.Property("PostalCode")
+ .HasColumnType("text")
+ .HasColumnName("postal_code");
+
+ b.Property("IsRetired")
+ .HasColumnType("boolean")
+ .HasColumnName("is_retired");
+
+ b.Property("Namespace")
+ .HasColumnType("text")
+ .HasColumnName("namespace");
+
+ b.Property("NisCode")
+ .HasColumnType("text")
+ .HasColumnName("nis_code");
+
+ b.Property("PuriId")
+ .HasColumnType("text")
+ .HasColumnName("puri_id");
+
+ b.Property("VersionTimestampAsDateTimeOffset")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("version_timestamp");
+
+ b.HasKey("PostalCode");
+
+ b.HasIndex("NisCode");
+
+ b.ToTable("postal_latest_items", "integration_postal");
+ });
+
+ modelBuilder.Entity("PostalRegistry.Projections.Integration.PostalInformationName", b =>
+ {
+ b.HasOne("PostalRegistry.Projections.Integration.PostalLatestItem", null)
+ .WithMany("PostalNames")
+ .HasForeignKey("PostalCode");
+ });
+
+ modelBuilder.Entity("PostalRegistry.Projections.Integration.PostalLatestItem", b =>
+ {
+ b.Navigation("PostalNames");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/PostalRegistry.Projections.Integration/PostalInformationName.cs b/src/PostalRegistry.Projections.Integration/PostalInformationName.cs
new file mode 100644
index 00000000..30aa1fe3
--- /dev/null
+++ b/src/PostalRegistry.Projections.Integration/PostalInformationName.cs
@@ -0,0 +1,53 @@
+namespace PostalRegistry.Projections.Integration
+{
+ using System;
+ using Microsoft.EntityFrameworkCore;
+ using Microsoft.EntityFrameworkCore.Metadata.Builders;
+ using PostalRegistry.Infrastructure;
+
+ public class PostalInformationName
+ {
+ public Guid Id { get; set; }
+ public string Name { get; set; }
+ public string SearchName { get; set; }
+ public Language Language { get; set; }
+ public string? PostalCode { get; set; }
+
+ public PostalInformationName() { }
+
+ public PostalInformationName(string name, string searchName, string postalCode, Language language)
+ {
+ Name = name;
+ SearchName = searchName;
+ PostalCode = postalCode;
+ Language = language;
+ }
+ }
+
+ public class PostalInformationNameConfiguration : IEntityTypeConfiguration
+ {
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder.ToTable("postal_information_name", Schema.Integration)
+ .HasKey(p => p.Id);
+
+ builder.Property(p => p.Id)
+ .ValueGeneratedOnAdd()
+ .HasColumnName("id");;
+
+ builder.Property(p => p.Name)
+ .IsRequired()
+ .HasColumnName("name");;
+ builder.Property(p => p.SearchName)
+ .IsRequired()
+ .HasColumnName("search_name");;
+ builder.Property(p => p.PostalCode)
+ .HasColumnName("postal_code");;
+ builder.Property(p => p.Language)
+ .HasColumnName("language");;
+
+ builder.HasIndex(x => x.PostalCode);
+ builder.HasIndex(x => x.SearchName);
+ }
+ }
+}
diff --git a/src/PostalRegistry.Projections.Integration/PostalLatestItem.cs b/src/PostalRegistry.Projections.Integration/PostalLatestItem.cs
new file mode 100644
index 00000000..34b883f3
--- /dev/null
+++ b/src/PostalRegistry.Projections.Integration/PostalLatestItem.cs
@@ -0,0 +1,66 @@
+namespace PostalRegistry.Projections.Integration
+{
+ using System;
+ using System.Collections.Generic;
+ using Be.Vlaanderen.Basisregisters.GrAr.Common;
+ using Be.Vlaanderen.Basisregisters.Utilities;
+ using Microsoft.EntityFrameworkCore;
+ using Microsoft.EntityFrameworkCore.Metadata.Builders;
+ using NodaTime;
+ using PostalRegistry.Infrastructure;
+
+ public sealed class PostalLatestItem
+ {
+ public const string VersionTimestampBackingPropertyName = nameof(VersionTimestampAsDateTimeOffset);
+
+ public string PostalCode { get; set; }
+ public string? NisCode { get; set; }
+ public bool IsRetired { get; set; }
+ public List PostalNames { get; set; }
+ public string? PuriId { get; set; }
+ public string? Namespace { get; set; }
+
+ private DateTimeOffset VersionTimestampAsDateTimeOffset { get; set; }
+
+ public Instant VersionTimestamp
+ {
+ get => Instant.FromDateTimeOffset(VersionTimestampAsDateTimeOffset);
+ set => VersionTimestampAsDateTimeOffset = value.ToDateTimeOffset();
+ }
+ public PostalLatestItem()
+ { }
+ }
+
+ public sealed class MunicipalityLatestItemConfiguration : IEntityTypeConfiguration
+ {
+ public void Configure(EntityTypeBuilder builder)
+ {
+ builder
+ .ToTable("postal_latest_items", Schema.Integration)
+ .HasKey(x => x.PostalCode);
+
+ builder.Property(p => p.PostalCode)
+ .HasColumnName("postal_code");
+
+ builder.Property(p => p.NisCode)
+ .HasColumnName("nis_code");
+
+ builder.Property(p => p.IsRetired)
+ .HasColumnName("is_retired");
+
+ builder.HasMany(p => p.PostalNames)
+ .WithOne()
+ .HasForeignKey(p => p.PostalCode);
+
+ builder.Property(PostalLatestItem.VersionTimestampBackingPropertyName)
+ .HasColumnName("version_timestamp");
+
+ builder.Property(x => x.PuriId).HasColumnName("puri_id");
+ builder.Property(x => x.Namespace).HasColumnName("namespace");
+
+ builder.Ignore(p => p.VersionTimestamp);
+
+ builder.HasIndex(x => x.NisCode);
+ }
+ }
+}
diff --git a/src/PostalRegistry.Projections.Integration/PostalLatestItemExtensions.cs b/src/PostalRegistry.Projections.Integration/PostalLatestItemExtensions.cs
new file mode 100644
index 00000000..848fad83
--- /dev/null
+++ b/src/PostalRegistry.Projections.Integration/PostalLatestItemExtensions.cs
@@ -0,0 +1,30 @@
+namespace PostalRegistry.Projections.Integration
+{
+ using System;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Be.Vlaanderen.Basisregisters.ProjectionHandling.Connector;
+
+ public static class PostalLatestItemExtensions
+ {
+ public static async Task FindAndUpdatePostal(this IntegrationContext context,
+ string postalCode,
+ Action updateFunc,
+ CancellationToken ct)
+ {
+ var postalItem = await context
+ .PostalLatestItems
+ .FindAsync(postalCode, cancellationToken: ct);
+
+ if (postalItem == null)
+ throw DatabaseItemNotFound(postalCode);
+
+ updateFunc(postalItem);
+
+ return postalItem;
+ }
+
+ private static ProjectionItemNotFoundException DatabaseItemNotFound(string postalCode)
+ => new ProjectionItemNotFoundException(postalCode);
+ }
+}
diff --git a/src/PostalRegistry.Projections.Integration/PostalLatestItemProjections.cs b/src/PostalRegistry.Projections.Integration/PostalLatestItemProjections.cs
new file mode 100644
index 00000000..aa4f6156
--- /dev/null
+++ b/src/PostalRegistry.Projections.Integration/PostalLatestItemProjections.cs
@@ -0,0 +1,119 @@
+namespace PostalRegistry.Projections.Integration
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using Be.Vlaanderen.Basisregisters.GrAr.Common;
+ using Be.Vlaanderen.Basisregisters.ProjectionHandling.Connector;
+ using Be.Vlaanderen.Basisregisters.ProjectionHandling.SqlStreamStore;
+ using Infrastructure;
+ using Microsoft.Extensions.Options;
+ using NodaTime;
+ using PostalInformation.Events;
+
+ [ConnectedProjectionName("Integratie postinfo latest item")]
+ [ConnectedProjectionDescription("Projectie die de laatste postinfo data voor de integratie database bijhoudt.")]
+ public sealed class PostalLatestItemProjections : ConnectedProjection
+ {
+ public PostalLatestItemProjections(IOptions options)
+ {
+ When>(async (context, message, ct) =>
+ {
+ await context
+ .PostalLatestItems
+ .AddAsync(
+ new PostalLatestItem
+ {
+ PostalCode = message.Message.PostalCode,
+ Namespace = options.Value.Namespace,
+ PuriId = $"{options.Value.Namespace}/{message.Message.PostalCode}",
+ VersionTimestamp = message.Message.Provenance.Timestamp
+ }, ct);
+ });
+
+ When>(async (context, message, ct) =>
+ {
+ await context.FindAndUpdatePostal(
+ message.Message.PostalCode,
+ postalInformation =>
+ {
+ postalInformation.NisCode = message.Message.NisCode;
+ UpdateVersionTimestamp(postalInformation, message.Message.Provenance.Timestamp);
+ },
+ ct);
+ });
+
+ When>(async (context, message, ct) =>
+ {
+ await context.FindAndUpdatePostal(
+ message.Message.PostalCode,
+ postalInformation =>
+ {
+ postalInformation.IsRetired = false;
+ UpdateVersionTimestamp(postalInformation, message.Message.Provenance.Timestamp);
+ },
+ ct);
+ });
+
+ When>(async (context, message, ct) =>
+ {
+ await context.FindAndUpdatePostal(
+ message.Message.PostalCode,
+ postalInformation =>
+ {
+ if (postalInformation.PostalNames != null
+ && postalInformation.PostalNames.Any(p =>
+ p.Name.Equals(message.Message.Name, StringComparison.OrdinalIgnoreCase)))
+ {
+ return;
+ }
+
+ var postalInformationName = new PostalInformationName(
+ message.Message.Name,
+ message.Message.Name.RemoveDiacritics(),
+ postalInformation.PostalCode,
+ message.Message.Language);
+
+ if (postalInformation.PostalNames == null)
+ {
+ postalInformation.PostalNames = new List { postalInformationName };
+ }
+ else
+ {
+ postalInformation.PostalNames.Add(postalInformationName);
+ }
+
+ UpdateVersionTimestamp(postalInformation, message.Message.Provenance.Timestamp);
+ },
+ ct);
+ });
+
+ When>(async (context, message, ct) =>
+ {
+ await context.FindAndUpdatePostal(
+ message.Message.PostalCode,
+ postalInformation =>
+ {
+ postalInformation.PostalNames.RemoveAll(r => r.Name == message.Message.Name);
+ UpdateVersionTimestamp(postalInformation, message.Message.Provenance.Timestamp);
+ },
+ ct);
+ });
+
+ When>(async (context, message, ct) =>
+ {
+ await context.FindAndUpdatePostal(
+ message.Message.PostalCode,
+ postalInformation =>
+ {
+ postalInformation.IsRetired = true;
+ UpdateVersionTimestamp(postalInformation, message.Message.Provenance.Timestamp);
+ },
+ ct);
+ });
+ }
+
+ private static void UpdateVersionTimestamp(PostalLatestItem postal, Instant versionTimestamp)
+ => postal.VersionTimestamp = versionTimestamp;
+ }
+}
diff --git a/src/PostalRegistry.Projections.Integration/PostalRegistry.Projections.Integration.csproj b/src/PostalRegistry.Projections.Integration/PostalRegistry.Projections.Integration.csproj
new file mode 100644
index 00000000..75b7bca2
--- /dev/null
+++ b/src/PostalRegistry.Projections.Integration/PostalRegistry.Projections.Integration.csproj
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/PostalRegistry.Projections.Integration/Properties/AssemblyInfo.cs b/src/PostalRegistry.Projections.Integration/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..91febbca
--- /dev/null
+++ b/src/PostalRegistry.Projections.Integration/Properties/AssemblyInfo.cs
@@ -0,0 +1,7 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+[assembly: AssemblyDescription("PostalRegistry Integration Projections")]
+
+[assembly: ComVisible(false)]
+[assembly: Guid("58594a67-dce5-4fcf-be3d-fa00b8ed9f13")]
diff --git a/src/PostalRegistry.Projections.Integration/paket.references b/src/PostalRegistry.Projections.Integration/paket.references
new file mode 100644
index 00000000..0b4c6c3d
--- /dev/null
+++ b/src/PostalRegistry.Projections.Integration/paket.references
@@ -0,0 +1,13 @@
+Be.Vlaanderen.Basisregisters.AggregateSource
+Be.Vlaanderen.Basisregisters.EventHandling.Autofac
+Be.Vlaanderen.Basisregisters.DataDog.Tracing.Sql
+Be.Vlaanderen.Basisregisters.ProjectionHandling.Runner.Npgsql
+Be.Vlaanderen.Basisregisters.ProjectionHandling.SqlStreamStore.Autofac
+
+Npgsql.EntityFrameworkCore.PostgreSQL.Design
+
+Be.Vlaanderen.Basisregisters.GrAr.Common
+Be.Vlaanderen.Basisregisters.GrAr.Legacy
+
+SourceLink.Embed.AllSourceFiles
+SourceLink.Copy.PdbFiles
diff --git a/src/PostalRegistry.Projector/Infrastructure/Modules/ApiModule.cs b/src/PostalRegistry.Projector/Infrastructure/Modules/ApiModule.cs
index 33dc29e7..ca1f1975 100755
--- a/src/PostalRegistry.Projector/Infrastructure/Modules/ApiModule.cs
+++ b/src/PostalRegistry.Projector/Infrastructure/Modules/ApiModule.cs
@@ -20,6 +20,8 @@ namespace PostalRegistry.Projector.Infrastructure.Modules
using PostalRegistry.Infrastructure;
using PostalRegistry.Projections.Extract;
using PostalRegistry.Projections.Extract.PostalInformationExtract;
+ using PostalRegistry.Projections.Integration;
+ using PostalRegistry.Projections.Integration.Infrastructure;
using PostalRegistry.Projections.LastChangedList;
using PostalRegistry.Projections.Legacy;
using PostalRegistry.Projections.Legacy.PostalInformation;
@@ -68,6 +70,26 @@ private void RegisterProjectionSetup(ContainerBuilder builder)
RegisterExtractProjections(builder);
RegisterLastChangedProjections(builder);
RegisterLegacyProjections(builder);
+
+ if(_configuration.GetSection("Integration").GetValue("Enabled", false))
+ RegisterIntegrationProjections(builder);
+ }
+
+ private void RegisterIntegrationProjections(ContainerBuilder builder)
+ {
+ builder
+ .RegisterModule(
+ new IntegrationModule(
+ _configuration,
+ _services,
+ _loggerFactory));
+ builder
+ .RegisterProjectionMigrator(
+ _configuration,
+ _loggerFactory)
+ .RegisterProjections(
+ context => new PostalLatestItemProjections(context.Resolve>()),
+ ConnectedProjectionSettings.Default);
}
private void RegisterExtractProjections(ContainerBuilder builder)
diff --git a/src/PostalRegistry.Projector/Infrastructure/Startup.cs b/src/PostalRegistry.Projector/Infrastructure/Startup.cs
index 1af412b4..4e1aea08 100755
--- a/src/PostalRegistry.Projector/Infrastructure/Startup.cs
+++ b/src/PostalRegistry.Projector/Infrastructure/Startup.cs
@@ -23,6 +23,7 @@ namespace PostalRegistry.Projector.Infrastructure
using PostalRegistry.Projections.Legacy;
using Microsoft.OpenApi.Models;
using System.Threading;
+ using PostalRegistry.Projections.Integration.Infrastructure;
/// Represents the startup process for the application.
public class Startup
@@ -92,12 +93,25 @@ public IServiceProvider ConfigureServices(IServiceCollection services)
var connectionStrings = _configuration
.GetSection("ConnectionStrings")
.GetChildren();
+ if (!_configuration.GetSection("Integration").GetValue("Enabled", false))
+ connectionStrings = connectionStrings
+ .Where(x => !x.Key.StartsWith("Integration", StringComparison.OrdinalIgnoreCase))
+ .ToList();
- foreach (var connectionString in connectionStrings)
+
+ foreach (var connectionString in connectionStrings.Where(x => !x.Value.Contains("host", StringComparison.OrdinalIgnoreCase)))
+ {
health.AddSqlServer(
connectionString.Value,
name: $"sqlserver-{connectionString.Key.ToLowerInvariant()}",
tags: new[] {DatabaseTag, "sql", "sqlserver"});
+ }
+
+ foreach (var connectionString in connectionStrings.Where(x => x.Value.Contains("host", StringComparison.OrdinalIgnoreCase)))
+ health.AddNpgSql(
+ connectionString.Value,
+ name: $"npgsql-{connectionString.Key.ToLowerInvariant()}",
+ tags: new[] {DatabaseTag, "sql", "npgsql"});
health.AddDbContextCheck(
$"dbcontext-{nameof(ExtractContext).ToLowerInvariant()}",
@@ -113,7 +127,8 @@ public IServiceProvider ConfigureServices(IServiceCollection services)
}
}
})
- .Configure(_configuration.GetSection("Extract"));
+ .Configure(_configuration.GetSection("Extract"))
+ .Configure(_configuration.GetSection("Integration"));
var containerBuilder = new ContainerBuilder();
containerBuilder.RegisterModule(new LoggingModule(_configuration, services));
diff --git a/src/PostalRegistry.Projector/PostalRegistry.Projector.csproj b/src/PostalRegistry.Projector/PostalRegistry.Projector.csproj
index 184901b2..4b398fad 100644
--- a/src/PostalRegistry.Projector/PostalRegistry.Projector.csproj
+++ b/src/PostalRegistry.Projector/PostalRegistry.Projector.csproj
@@ -26,6 +26,7 @@
+
diff --git a/src/PostalRegistry.Projector/appsettings.json b/src/PostalRegistry.Projector/appsettings.json
index 32b25cc4..4d7d2284 100755
--- a/src/PostalRegistry.Projector/appsettings.json
+++ b/src/PostalRegistry.Projector/appsettings.json
@@ -7,7 +7,9 @@
"ExtractProjectionsAdmin": "Server=(localdb)\\mssqllocaldb;Database=EFProviders.InMemory.PostalRegistry;Trusted_Connection=True;",
"LastChangedList": "Server=(localdb)\\mssqllocaldb;Database=EFProviders.InMemory.PostalRegistry;Trusted_Connection=True;",
"LastChangedListAdmin": "Server=(localdb)\\mssqllocaldb;Database=EFProviders.InMemory.PostalRegistry;Trusted_Connection=True;",
- "Syndication": "Server=(localdb)\\mssqllocaldb;Database=EFProviders.InMemory.PostalRegistry;Trusted_Connection=True;"
+ "Syndication": "Server=(localdb)\\mssqllocaldb;Database=EFProviders.InMemory.PostalRegistry;Trusted_Connection=True;",
+ "IntegrationProjections": ".",
+ "IntegrationProjectionsAdmin": "."
},
"DataDog": {
@@ -16,6 +18,11 @@
"ServiceName": "postal-registry-projector-dev"
},
+ "Integration": {
+ "Namespace": "https://data.vlaanderen.be/id/postinfo",
+ "Enabled": false
+ },
+
"BaseUrl": "https://api.staging-basisregisters.vlaanderen/",
"Extract": {
@@ -23,7 +30,7 @@
},
"DistributedLock": {
- "Region": "eu-west-1",
+ "Region": "eu-west-1",
"TableName": "__DistributedLocks__",
"LeasePeriodInMinutes": 5,
"ThrowOnFailedRenew": true,
diff --git a/src/PostalRegistry.Projector/paket.references b/src/PostalRegistry.Projector/paket.references
index 9b67d2ec..c18e667b 100644
--- a/src/PostalRegistry.Projector/paket.references
+++ b/src/PostalRegistry.Projector/paket.references
@@ -4,7 +4,14 @@ Be.Vlaanderen.Basisregisters.EventHandling.Autofac
Be.Vlaanderen.Basisregisters.DataDog.Tracing.Autofac
Be.Vlaanderen.Basisregisters.Projector
+Npgsql
+Npgsql.EntityFrameworkCore.PostgreSQL
+
AspNetCore.HealthChecks.SqlServer
+AspNetCore.HealthChecks.NpgSql
+
+NodaTime
+
Dapper
SourceLink.Embed.AllSourceFiles
diff --git a/test/PostalRegistry.Tests/AutoFixture/InfrastructureCustomization.cs b/test/PostalRegistry.Tests/AutoFixture/InfrastructureCustomization.cs
new file mode 100644
index 00000000..6bbdb222
--- /dev/null
+++ b/test/PostalRegistry.Tests/AutoFixture/InfrastructureCustomization.cs
@@ -0,0 +1,13 @@
+namespace PostalRegistry.Tests.AutoFixture
+{
+ using global::AutoFixture;
+
+ public class InfrastructureCustomization : ICustomization
+ {
+ public void Customize(IFixture fixture)
+ {
+ fixture.Customize(new NodaTimeCustomization());
+ fixture.Customize(new SetProvenanceImplementationsCallSetProvenance());
+ }
+ }
+}
diff --git a/test/PostalRegistry.Tests/AutoFixture/IntegerNisCode.cs b/test/PostalRegistry.Tests/AutoFixture/IntegerNisCode.cs
new file mode 100644
index 00000000..39ffe072
--- /dev/null
+++ b/test/PostalRegistry.Tests/AutoFixture/IntegerNisCode.cs
@@ -0,0 +1,39 @@
+namespace PostalRegistry.Tests.AutoFixture
+{
+ using global::AutoFixture;
+ using global::AutoFixture.Kernel;
+
+ public class WithIntegerNisCode : ICustomization
+ {
+ public void Customize(IFixture fixture)
+ {
+ var nisCode = new NisCode(fixture.Create().ToString());
+ fixture.Register(() => nisCode);
+
+ fixture.Customizations.Add(
+ new FilteringSpecimenBuilder(
+ new FixedBuilder(nisCode.ToString()),
+ new ParameterSpecification(
+ typeof(string),
+ "nisCode")));
+ }
+ }
+
+ public class WithFixedPostalCode : ICustomization
+ {
+ public void Customize(IFixture fixture)
+ {
+
+ var postalCode = new PostalCode("9000");
+
+ fixture.Register(() => postalCode);
+
+ fixture.Customizations.Add(
+ new FilteringSpecimenBuilder(
+ new FixedBuilder(postalCode),
+ new ParameterSpecification(
+ typeof(string),
+ "postalCode")));
+ }
+ }
+}
diff --git a/test/PostalRegistry.Tests/AutoFixture/NodaTimeCustomization.cs b/test/PostalRegistry.Tests/AutoFixture/NodaTimeCustomization.cs
new file mode 100644
index 00000000..dce9bb50
--- /dev/null
+++ b/test/PostalRegistry.Tests/AutoFixture/NodaTimeCustomization.cs
@@ -0,0 +1,78 @@
+namespace PostalRegistry.Tests.AutoFixture
+{
+ using System;
+ using global::AutoFixture;
+ using global::AutoFixture.Kernel;
+ using NodaTime;
+
+ public class NodaTimeCustomization : ICustomization
+ {
+ public void Customize(IFixture fixture)
+ {
+ fixture.Customizations.Add(new LocalDateGenerator());
+ fixture.Customizations.Add(new LocalTimeGenerator());
+ fixture.Customizations.Add(new InstantGenerator());
+ fixture.Customizations.Add(new LocalDateTimeGenerator());
+ }
+
+ public class LocalDateGenerator : ISpecimenBuilder
+ {
+ public object Create(object request, ISpecimenContext context)
+ {
+ if (context == null)
+ throw new ArgumentNullException(nameof(context));
+
+ if (!typeof(LocalDate).Equals(request))
+ return new NoSpecimen();
+
+ return LocalDate.FromDateTime(DateTime.Today);
+ }
+ }
+
+ public class LocalTimeGenerator : ISpecimenBuilder
+ {
+ public object Create(object request, ISpecimenContext context)
+ {
+ if (context == null)
+ throw new ArgumentNullException(nameof(context));
+
+ if (!typeof(LocalTime).Equals(request))
+ return new NoSpecimen();
+
+ return LocalTime.FromTicksSinceMidnight(DateTime.Now.TimeOfDay.Ticks);
+ }
+ }
+
+ public class InstantGenerator : ISpecimenBuilder
+ {
+ public object Create(object request, ISpecimenContext context)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (!typeof(Instant).Equals(request))
+ {
+ return new NoSpecimen();
+ }
+
+ return Instant.FromDateTimeOffset(DateTimeOffset.Now);
+ }
+ }
+
+ public class LocalDateTimeGenerator : ISpecimenBuilder
+ {
+ public object Create(object request, ISpecimenContext context)
+ {
+ if (context == null)
+ throw new ArgumentNullException(nameof(context));
+
+ if (!typeof(LocalDateTime).Equals(request))
+ return new NoSpecimen();
+
+ return LocalDateTime.FromDateTime(DateTime.Now);
+ }
+ }
+ }
+}
diff --git a/test/PostalRegistry.Tests/AutoFixture/PrivateGreedyConstructorQuery.cs b/test/PostalRegistry.Tests/AutoFixture/PrivateGreedyConstructorQuery.cs
new file mode 100644
index 00000000..7c223152
--- /dev/null
+++ b/test/PostalRegistry.Tests/AutoFixture/PrivateGreedyConstructorQuery.cs
@@ -0,0 +1,46 @@
+namespace PostalRegistry.Tests.AutoFixture
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Reflection;
+ using global::AutoFixture.Kernel;
+
+ ///
+ /// Selects public constructors ordered by the most modest constructor first.
+ ///
+ public class PrivateGreedyConstructorQuery : IMethodQuery
+ {
+ ///
+ /// Selects the constructors for the supplied type.
+ ///
+ /// The type.
+ ///
+ /// All public constructors for , ordered by the most modest
+ /// constructor first.
+ ///
+ ///
+ ///
+ /// The ordering of the returned constructors is based on the number of parameters of the
+ /// constructor. Constructors with fewer parameters are returned before constructors with
+ /// more parameters. This means that if a default constructor exists, it will be the first
+ /// one returned.
+ ///
+ ///
+ /// In case of two constructors with an equal number of parameters, the ordering is
+ /// unspecified.
+ ///
+ ///
+ public IEnumerable SelectMethods(Type type)
+ {
+ if (type == null)
+ throw new ArgumentNullException(nameof(type));
+
+ return from ci in type.GetTypeInfo().GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
+ let parameters = ci.GetParameters()
+ where parameters.All(p => p.ParameterType != type)
+ orderby parameters.Length descending
+ select new ConstructorMethod(ci) as IMethod;
+ }
+ }
+}
diff --git a/test/PostalRegistry.Tests/AutoFixture/SetProvenanceImplementationsCallSetProvenance.cs b/test/PostalRegistry.Tests/AutoFixture/SetProvenanceImplementationsCallSetProvenance.cs
new file mode 100644
index 00000000..26c5af4c
--- /dev/null
+++ b/test/PostalRegistry.Tests/AutoFixture/SetProvenanceImplementationsCallSetProvenance.cs
@@ -0,0 +1,41 @@
+namespace PostalRegistry.Tests.AutoFixture
+{
+ using System;
+ using System.Linq;
+ using System.Reflection;
+ using System.Runtime.CompilerServices;
+ using Be.Vlaanderen.Basisregisters.GrAr.Provenance;
+ using global::AutoFixture;
+ using global::AutoFixture.Dsl;
+ using global::AutoFixture.Kernel;
+
+ public class SetProvenanceImplementationsCallSetProvenance : ICustomization
+ {
+ public void Customize(IFixture fixture)
+ {
+ bool IsEventNamespace(Type t) => t.Namespace.EndsWith("Events");
+ bool IsNotCompilerGenerated(MemberInfo t) => Attribute.GetCustomAttribute(t, typeof(CompilerGeneratedAttribute)) == null;
+
+ var provenanceEventTypes = typeof(DomainAssemblyMarker).Assembly
+ .GetTypes()
+ .Where(t => t.IsClass && t.Namespace != null && IsEventNamespace(t) && IsNotCompilerGenerated(t) && t.GetInterfaces().Any(i => i == typeof(ISetProvenance)))
+ .ToList();
+
+ foreach (var allEventType in provenanceEventTypes)
+ {
+ var getSetProvenanceMethod = GetType()
+ .GetMethod(nameof(GetSetProvenance), BindingFlags.NonPublic | BindingFlags.Instance)
+ .MakeGenericMethod(allEventType);
+ var setProvenanceDelegate = getSetProvenanceMethod.Invoke(this, new object[] { fixture.Create() });
+
+ var customizeMethod = typeof(Fixture).GetMethods().Single(m => m.Name == "Customize" && m.IsGenericMethod);
+ var genericCustomizeMethod = customizeMethod.MakeGenericMethod(allEventType);
+ genericCustomizeMethod.Invoke(fixture, new object[] { setProvenanceDelegate });
+ }
+ }
+
+ private Func, ISpecimenBuilder> GetSetProvenance(Provenance provenance)
+ where T : ISetProvenance =>
+ c => c.Do(@event => (@event as ISetProvenance).SetProvenance(provenance));
+ }
+}
diff --git a/test/PostalRegistry.Tests/Builders/PostalInformationPostalNameWasAddedBuilder.cs b/test/PostalRegistry.Tests/Builders/PostalInformationPostalNameWasAddedBuilder.cs
new file mode 100644
index 00000000..9ee5ee94
--- /dev/null
+++ b/test/PostalRegistry.Tests/Builders/PostalInformationPostalNameWasAddedBuilder.cs
@@ -0,0 +1,34 @@
+namespace PostalRegistry.Tests.Builders
+{
+ using Be.Vlaanderen.Basisregisters.GrAr.Provenance;
+ using global::AutoFixture;
+ using PostalInformation.Events;
+
+ public class PostalInformationPostalNameWasAddedBuilder
+ {
+ private PostalName? _name;
+ private readonly Fixture _fixture;
+
+ public PostalInformationPostalNameWasAddedBuilder(Fixture fixture)
+ {
+ _fixture = fixture;
+ }
+
+ public PostalInformationPostalNameWasAddedBuilder WithName(string name, Language language)
+ {
+ _name = new PostalName(name, language);
+
+ return this;
+ }
+
+ public PostalInformationPostalNameWasAdded Build()
+ {
+ var @event = new PostalInformationPostalNameWasAdded(
+ _fixture.Create(),
+ _name ?? _fixture.Create());
+ ((ISetProvenance)@event).SetProvenance(_fixture.Create());
+
+ return @event;
+ }
+ }
+}
diff --git a/test/PostalRegistry.Tests/Builders/PostalInformationPostalNameWasRemovedBuilder.cs b/test/PostalRegistry.Tests/Builders/PostalInformationPostalNameWasRemovedBuilder.cs
new file mode 100644
index 00000000..8312c230
--- /dev/null
+++ b/test/PostalRegistry.Tests/Builders/PostalInformationPostalNameWasRemovedBuilder.cs
@@ -0,0 +1,34 @@
+namespace PostalRegistry.Tests.Builders
+{
+ using Be.Vlaanderen.Basisregisters.GrAr.Provenance;
+ using global::AutoFixture;
+ using PostalInformation.Events;
+
+ public class PostalInformationPostalNameWasRemovedBuilder
+ {
+ private PostalName? _name;
+ private readonly Fixture _fixture;
+
+ public PostalInformationPostalNameWasRemovedBuilder(Fixture fixture)
+ {
+ _fixture = fixture;
+ }
+
+ public PostalInformationPostalNameWasRemovedBuilder WithName(string name, Language language)
+ {
+ _name = new PostalName(name, language);
+
+ return this;
+ }
+
+ public PostalInformationPostalNameWasRemoved Build()
+ {
+ var @event = new PostalInformationPostalNameWasRemoved(
+ _fixture.Create(),
+ _name ?? _fixture.Create());
+ ((ISetProvenance)@event).SetProvenance(_fixture.Create());
+
+ return @event;
+ }
+ }
+}
diff --git a/test/PostalRegistry.Tests/Integration/IntegrationProjectionTest.cs b/test/PostalRegistry.Tests/Integration/IntegrationProjectionTest.cs
new file mode 100644
index 00000000..8f58d668
--- /dev/null
+++ b/test/PostalRegistry.Tests/Integration/IntegrationProjectionTest.cs
@@ -0,0 +1,30 @@
+namespace PostalRegistry.Tests.Integration
+{
+ using System;
+ using Be.Vlaanderen.Basisregisters.ProjectionHandling.Connector;
+ using Be.Vlaanderen.Basisregisters.ProjectionHandling.Testing;
+ using Microsoft.EntityFrameworkCore;
+ using Projections.Integration;
+
+ public abstract class IntegrationProjectionTest
+ where TProjection : ConnectedProjection
+ {
+ protected ConnectedProjectionTest Sut { get; }
+
+ protected IntegrationProjectionTest()
+ {
+ Sut = new ConnectedProjectionTest(CreateContext, CreateProjection);
+ }
+
+ protected virtual IntegrationContext CreateContext()
+ {
+ var options = new DbContextOptionsBuilder()
+ .UseInMemoryDatabase(Guid.NewGuid().ToString())
+ .Options;
+
+ return new IntegrationContext(options);
+ }
+
+ protected abstract TProjection CreateProjection();
+ }
+}
diff --git a/test/PostalRegistry.Tests/Integration/PostalLatestItemTest.cs b/test/PostalRegistry.Tests/Integration/PostalLatestItemTest.cs
new file mode 100644
index 00000000..71dc6f5d
--- /dev/null
+++ b/test/PostalRegistry.Tests/Integration/PostalLatestItemTest.cs
@@ -0,0 +1,164 @@
+namespace PostalRegistry.Tests.Integration
+{
+ using System.Threading.Tasks;
+ using AutoFixture;
+ using Builders;
+ using FluentAssertions;
+ using global::AutoFixture;
+ using Microsoft.Extensions.Options;
+ using PostalInformation.Events;
+ using Projections.Integration;
+ using Projections.Integration.Infrastructure;
+ using Xunit;
+
+ public class PostalLatestItemTest : IntegrationProjectionTest
+ {
+ private const string Namespace = "https://data.vlaanderen.be/id/postinfo";
+ private readonly Fixture _fixture;
+
+ public PostalLatestItemTest()
+ {
+ _fixture = new Fixture();
+ _fixture.Customize(new InfrastructureCustomization());
+ _fixture.Customize(new WithIntegerNisCode());
+ _fixture.Customize(new WithFixedPostalCode());
+ }
+
+ [Fact]
+ public async Task WhenPostalInformationWasRegistered()
+ {
+ var postalInformationWasRegistered = _fixture.Create();
+
+ await Sut
+ .Given(postalInformationWasRegistered)
+ .Then(async ct =>
+ {
+ var expectedLatestItem =
+ await ct.PostalLatestItems.FindAsync(postalInformationWasRegistered.PostalCode);
+ expectedLatestItem.Should().NotBeNull();
+ expectedLatestItem!.Namespace.Should().Be(Namespace);
+ expectedLatestItem.PuriId.Should().Be($"{Namespace}/{postalInformationWasRegistered.PostalCode}");
+ expectedLatestItem.VersionTimestamp.Should()
+ .Be(postalInformationWasRegistered.Provenance.Timestamp);
+ });
+ }
+
+ [Fact]
+ public async Task WhenMunicipalityWasAttached()
+ {
+ var municipalityWasAttached = _fixture.Create();
+
+ await Sut
+ .Given(
+ _fixture.Create(),
+ municipalityWasAttached)
+ .Then(async ct =>
+ {
+ var expectedLatestItem =
+ await ct.PostalLatestItems.FindAsync(municipalityWasAttached.PostalCode);
+ expectedLatestItem.Should().NotBeNull();
+ expectedLatestItem!.NisCode.Should().Be(municipalityWasAttached.NisCode);
+ });
+ }
+
+ [Fact]
+ public async Task WhenPostalInformationWasRealized()
+ {
+ var postalInformationWasRealized = _fixture.Create();
+
+ await Sut
+ .Given(
+ _fixture.Create(),
+ postalInformationWasRealized)
+ .Then(async ct =>
+ {
+ var expectedLatestItem =
+ await ct.PostalLatestItems.FindAsync(postalInformationWasRealized.PostalCode);
+ expectedLatestItem.Should().NotBeNull();
+ expectedLatestItem!.IsRetired.Should().BeFalse();
+ });
+ }
+
+ [Fact]
+ public async Task WhenPostalInformationPostalNameWasAdded()
+ {
+ var postalInformationPostalNameWasAdded = new PostalInformationPostalNameWasAddedBuilder(_fixture)
+ .WithName("dutchPostal", Language.Dutch)
+ .Build();
+ var secondPostalNameWasAdded = new PostalInformationPostalNameWasAddedBuilder(_fixture)
+ .WithName("dutchFrench", Language.French)
+ .Build();
+
+ var thirdPostalNameThatShouldNotBeAdded = new PostalInformationPostalNameWasAddedBuilder(_fixture)
+ .WithName(secondPostalNameWasAdded.Name, secondPostalNameWasAdded.Language)
+ .Build();
+
+ await Sut
+ .Given(
+ _fixture.Create(),
+ postalInformationPostalNameWasAdded,
+ secondPostalNameWasAdded,
+ thirdPostalNameThatShouldNotBeAdded)
+ .Then(async ct =>
+ {
+ var expectedLatestItem =
+ await ct.PostalLatestItems.FindAsync(postalInformationPostalNameWasAdded.PostalCode);
+ expectedLatestItem.Should().NotBeNull();
+ expectedLatestItem!.Namespace.Should().Be(Namespace);
+
+ expectedLatestItem.PostalNames.Count.Should().Be(2);
+
+ expectedLatestItem.IsRetired.Should().BeFalse();
+ expectedLatestItem.PuriId.Should()
+ .Be($"{Namespace}/{postalInformationPostalNameWasAdded.PostalCode}");
+ });
+ }
+
+ [Fact]
+ public async Task WhenPostalInformationPostalNameWasRemoved()
+ {
+ var postalInformationPostalNameWasAdded = new PostalInformationPostalNameWasAddedBuilder(_fixture)
+ .WithName("dutchPostal", Language.Dutch)
+ .Build();
+
+ var postalInformationPostalNameWasRemoved = new PostalInformationPostalNameWasRemovedBuilder(_fixture)
+ .WithName(postalInformationPostalNameWasAdded.Name, postalInformationPostalNameWasAdded.Language)
+ .Build();
+
+ await Sut
+ .Given(
+ _fixture.Create(),
+ postalInformationPostalNameWasAdded,
+ postalInformationPostalNameWasRemoved)
+ .Then(async ct =>
+ {
+ var expectedLatestItem =
+ await ct.PostalLatestItems.FindAsync(postalInformationPostalNameWasAdded.PostalCode);
+ expectedLatestItem.Should().NotBeNull();
+ expectedLatestItem!.PostalNames.Count.Should().Be(0);
+ });
+ }
+
+ [Fact]
+ public async Task WhenPostalInformationWasRetired()
+ {
+ var postalInformationWasRetired = _fixture.Create();
+
+ await Sut
+ .Given(
+ _fixture.Create(),
+ postalInformationWasRetired)
+ .Then(async ct =>
+ {
+ var expectedLatestItem =
+ await ct.PostalLatestItems.FindAsync(postalInformationWasRetired.PostalCode);
+ expectedLatestItem.Should().NotBeNull();
+ expectedLatestItem!.IsRetired.Should().BeTrue();
+ });
+ }
+
+ protected override PostalLatestItemProjections CreateProjection()
+ => new PostalLatestItemProjections(
+ new OptionsWrapper(new IntegrationOptions { Namespace = Namespace }));
+ }
+}
diff --git a/test/PostalRegistry.Tests/PostalRegistry.Tests.csproj b/test/PostalRegistry.Tests/PostalRegistry.Tests.csproj
index 839560a2..30b7279b 100644
--- a/test/PostalRegistry.Tests/PostalRegistry.Tests.csproj
+++ b/test/PostalRegistry.Tests/PostalRegistry.Tests.csproj
@@ -3,6 +3,7 @@
+
diff --git a/test/PostalRegistry.Tests/paket.references b/test/PostalRegistry.Tests/paket.references
index b347f920..551aa96d 100755
--- a/test/PostalRegistry.Tests/paket.references
+++ b/test/PostalRegistry.Tests/paket.references
@@ -9,4 +9,7 @@ Be.Vlaanderen.Basisregisters.AggregateSource.Testing
Be.Vlaanderen.Basisregisters.AggregateSource.Testing.SqlStreamStore.Autofac
Be.Vlaanderen.Basisregisters.AggregateSource.Testing.Xunit
+Be.Vlaanderen.Basisregisters.ProjectionHandling.Connector.Testing
+Be.Vlaanderen.Basisregisters.ProjectionHandling.Testing.Xunit
+
Be.Vlaanderen.Basisregisters.EventHandling.Autofac