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