From 5d7879dde1337ffa31037563c69219c8ff39636b Mon Sep 17 00:00:00 2001 From: Jakub Zalas Date: Fri, 20 Sep 2024 13:19:00 +0100 Subject: [PATCH 01/10] Show our own debug logs locally --- Services/CO.CDP.Organisation.WebApi/appsettings.Development.json | 1 + 1 file changed, 1 insertion(+) diff --git a/Services/CO.CDP.Organisation.WebApi/appsettings.Development.json b/Services/CO.CDP.Organisation.WebApi/appsettings.Development.json index 048be91dd..356bef1bb 100644 --- a/Services/CO.CDP.Organisation.WebApi/appsettings.Development.json +++ b/Services/CO.CDP.Organisation.WebApi/appsettings.Development.json @@ -4,6 +4,7 @@ "MinimumLevel": { "Default": "Information", "Override": { + "CO.CDP": "Debug", "Microsoft.AspNetCore.Mvc": "Warning", "Microsoft.AspNetCore.Routing": "Warning", "Microsoft.AspNetCore.Hosting": "Information" From 6e1321f500f17faa5c5b2132a6b9d7fd57d2880b Mon Sep 17 00:00:00 2001 From: Jakub Zalas Date: Fri, 20 Sep 2024 13:23:05 +0100 Subject: [PATCH 02/10] Define indexes on properties that are used in queries --- Libraries/CO.CDP.MQ/Outbox/ModelBuilderExtensions.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Libraries/CO.CDP.MQ/Outbox/ModelBuilderExtensions.cs b/Libraries/CO.CDP.MQ/Outbox/ModelBuilderExtensions.cs index 4110b7270..e0e58ef36 100644 --- a/Libraries/CO.CDP.MQ/Outbox/ModelBuilderExtensions.cs +++ b/Libraries/CO.CDP.MQ/Outbox/ModelBuilderExtensions.cs @@ -8,7 +8,11 @@ public static class ModelBuilderExtensions public static ModelBuilder OnOutboxMessageCreating(this ModelBuilder modelBuilder) { modelBuilder.Entity(om => - om.Property(m => m.CreatedOn).HasTimestampDefault() + { + om.Property(m => m.CreatedOn).HasTimestampDefault(); + om.HasIndex(m => m.CreatedOn); + om.HasIndex(m => m.Published); + } ); return modelBuilder; } From 5b8cef4190ffcb572cf9923e723f975835bba352 Mon Sep 17 00:00:00 2001 From: Jakub Zalas Date: Fri, 20 Sep 2024 13:24:16 +0100 Subject: [PATCH 03/10] Configure the outbox publisher in the organisation service --- .../CO.CDP.AwsServices/AwsConfiguration.cs | 3 + Libraries/CO.CDP.AwsServices/Extensions.cs | 26 + .../CO.CDP.Organisation.WebApi/Program.cs | 3 +- .../appsettings.json | 6 +- ...OrganisationInformation.Persistence.csproj | 1 + .../20240920113939_OutboxMessage.Designer.cs | 1878 +++++++++++++++++ .../20240920113939_OutboxMessage.cs | 50 + ...nisationInformationContextModelSnapshot.cs | 47 + .../OrganisationInformationContext.cs | 6 +- 9 files changed, 2017 insertions(+), 3 deletions(-) create mode 100644 Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240920113939_OutboxMessage.Designer.cs create mode 100644 Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240920113939_OutboxMessage.cs diff --git a/Libraries/CO.CDP.AwsServices/AwsConfiguration.cs b/Libraries/CO.CDP.AwsServices/AwsConfiguration.cs index 95a47371b..b1cad5ae0 100644 --- a/Libraries/CO.CDP.AwsServices/AwsConfiguration.cs +++ b/Libraries/CO.CDP.AwsServices/AwsConfiguration.cs @@ -1,3 +1,5 @@ +using static CO.CDP.MQ.Hosting.OutboxProcessorBackgroundService; + namespace CO.CDP.AwsServices; public record AwsConfiguration @@ -40,4 +42,5 @@ public record SqsPublisherConfiguration { public required string QueueUrl { get; init; } public required string? MessageGroupId { get; init; } + public OutboxProcessorConfiguration? Outbox { get; init; } } \ No newline at end of file diff --git a/Libraries/CO.CDP.AwsServices/Extensions.cs b/Libraries/CO.CDP.AwsServices/Extensions.cs index ceb1a17b7..5bea62cb5 100644 --- a/Libraries/CO.CDP.AwsServices/Extensions.cs +++ b/Libraries/CO.CDP.AwsServices/Extensions.cs @@ -5,6 +5,9 @@ using CO.CDP.AwsServices.S3; using CO.CDP.AwsServices.Sqs; using CO.CDP.MQ; +using CO.CDP.MQ.Hosting; +using CO.CDP.MQ.Outbox; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -75,6 +78,29 @@ public static IServiceCollection AddSqsPublisher(this IServiceCollection service return services.AddScoped(); } + public static IServiceCollection AddOutboxSqsPublisher(this IServiceCollection services) + where TDbContext : DbContext, IOutboxMessageDbContext + { + services.AddScoped>(); + + services.AddKeyedScoped("SqsPublisher"); + services.AddScoped(); + + services.AddSingleton(s => + s.GetRequiredService>().Value.SqsPublisher?.Outbox ?? + new OutboxProcessorBackgroundService.OutboxProcessorConfiguration() + ); + + services.AddScoped(s => + new OutboxProcessor( + s.GetRequiredKeyedService("SqsPublisher"), + s.GetRequiredService(), + s.GetRequiredService>() + ) + ); + return services; + } + public static IServiceCollection AddSqsDispatcher(this IServiceCollection services, Deserializer deserializer, Action addSubscribers, diff --git a/Services/CO.CDP.Organisation.WebApi/Program.cs b/Services/CO.CDP.Organisation.WebApi/Program.cs index 567c499b0..9fe1c7e5b 100644 --- a/Services/CO.CDP.Organisation.WebApi/Program.cs +++ b/Services/CO.CDP.Organisation.WebApi/Program.cs @@ -34,7 +34,7 @@ builder.Services .AddAwsConfiguration(builder.Configuration) .AddAwsSqsService() - .AddSqsPublisher() + .AddOutboxSqsPublisher() .AddSqsDispatcher( EventDeserializer.Deserializer, (services) => @@ -49,6 +49,7 @@ if (Assembly.GetEntryAssembly().IsRunAs("CO.CDP.Organisation.WebApi")) { builder.Services.AddHostedService(); + builder.Services.AddHostedService(); } builder.Services.AddDbContext(o => o.UseNpgsql(ConnectionStringHelper.GetConnectionString(builder.Configuration, "OrganisationInformationDatabase"))); diff --git a/Services/CO.CDP.Organisation.WebApi/appsettings.json b/Services/CO.CDP.Organisation.WebApi/appsettings.json index af2854811..7f40debec 100644 --- a/Services/CO.CDP.Organisation.WebApi/appsettings.json +++ b/Services/CO.CDP.Organisation.WebApi/appsettings.json @@ -33,7 +33,11 @@ }, "SqsPublisher": { "QueueUrl": "", - "MessageGroupId": "Organisation" + "MessageGroupId": "Organisation", + "Outbox": { + "BatchSize": 10, + "ExecutionInterval": "00:00:30" + } }, "CloudWatch": { "LogGroup": "/ecs/organisation", diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/CO.CDP.OrganisationInformation.Persistence.csproj b/Services/CO.CDP.OrganisationInformation.Persistence/CO.CDP.OrganisationInformation.Persistence.csproj index d1b64fe30..7ce0ef5d4 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence/CO.CDP.OrganisationInformation.Persistence.csproj +++ b/Services/CO.CDP.OrganisationInformation.Persistence/CO.CDP.OrganisationInformation.Persistence.csproj @@ -12,6 +12,7 @@ + diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240920113939_OutboxMessage.Designer.cs b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240920113939_OutboxMessage.Designer.cs new file mode 100644 index 000000000..a05431c10 --- /dev/null +++ b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240920113939_OutboxMessage.Designer.cs @@ -0,0 +1,1878 @@ +// +using System; +using System.Collections.Generic; +using CO.CDP.OrganisationInformation.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CO.CDP.OrganisationInformation.Persistence.Migrations +{ + [DbContext(typeof(OrganisationInformationContext))] + [Migration("20240920113939_OutboxMessage")] + partial class OutboxMessage + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "connected_entity_type", new[] { "organisation", "individual", "trust_or_trustee" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "connected_organisation_category", new[] { "registered_company", "director_or_the_same_responsibilities", "parent_or_subsidiary_company", "a_company_your_organisation_has_taken_over", "any_other_organisation_with_significant_influence_or_control" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "connected_person_category", new[] { "person_with_significant_control", "director_or_individual_with_the_same_responsibilities", "any_other_individual_with_significant_influence_or_control" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "connected_person_type", new[] { "individual", "trust_or_trustee" }); + NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "control_condition", new[] { "none", "owns_shares", "has_voting_rights", "can_appoint_or_remove_directors", "has_other_significant_influence_or_control" }); + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CO.CDP.MQ.Outbox.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text") + .HasColumnName("message"); + + b.Property("Published") + .HasColumnType("boolean") + .HasColumnName("published"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text") + .HasColumnName("type"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_outbox_messages"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_outbox_messages_created_on"); + + b.HasIndex("Published") + .HasDatabaseName("ix_outbox_messages_published"); + + b.ToTable("outbox_messages", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Address", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Country") + .IsRequired() + .HasColumnType("text") + .HasColumnName("country"); + + b.Property("CountryName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("country_name"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Locality") + .IsRequired() + .HasColumnType("text") + .HasColumnName("locality"); + + b.Property("PostalCode") + .IsRequired() + .HasColumnType("text") + .HasColumnName("postal_code"); + + b.Property("Region") + .HasColumnType("text") + .HasColumnName("region"); + + b.Property("StreetAddress") + .IsRequired() + .HasColumnType("text") + .HasColumnName("street_address"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_addresses"); + + b.ToTable("addresses", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.AuthenticationKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text") + .HasColumnName("key"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OrganisationId") + .HasColumnType("integer") + .HasColumnName("organisation_id"); + + b.Property("Scopes") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("scopes"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_authentication_keys"); + + b.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_authentication_keys_key"); + + b.HasIndex("OrganisationId") + .HasDatabaseName("ix_authentication_keys_organisation_id"); + + b.ToTable("authentication_keys", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.ConnectedEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CompanyHouseNumber") + .HasColumnType("text") + .HasColumnName("company_house_number"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_date"); + + b.Property("EntityType") + .HasColumnType("integer") + .HasColumnName("entity_type"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b.Property("HasCompnayHouseNumber") + .HasColumnType("boolean") + .HasColumnName("has_compnay_house_number"); + + b.Property("OverseasCompanyNumber") + .HasColumnType("text") + .HasColumnName("overseas_company_number"); + + b.Property("RegisterName") + .HasColumnType("text") + .HasColumnName("register_name"); + + b.Property("RegisteredDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("registered_date"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_date"); + + b.Property("SupplierOrganisationId") + .HasColumnType("integer") + .HasColumnName("supplier_organisation_id"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_connected_entities"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_connected_entities_guid"); + + b.HasIndex("SupplierOrganisationId") + .HasDatabaseName("ix_connected_entities_supplier_organisation_id"); + + b.ToTable("connected_entities", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.Form", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b.Property("IsRequired") + .HasColumnType("boolean") + .HasColumnName("is_required"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Scope") + .HasColumnType("integer") + .HasColumnName("scope"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Version") + .IsRequired() + .HasColumnType("text") + .HasColumnName("version"); + + b.HasKey("Id") + .HasName("pk_forms"); + + b.ToTable("forms", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.FormAnswer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddressValue") + .HasColumnType("jsonb") + .HasColumnName("address_value"); + + b.Property("BoolValue") + .HasColumnType("boolean") + .HasColumnName("bool_value"); + + b.Property("CreatedFrom") + .HasColumnType("uuid") + .HasColumnName("created_from"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("DateValue") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_value"); + + b.Property("EndValue") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_value"); + + b.Property("FormAnswerSetId") + .HasColumnType("integer") + .HasColumnName("form_answer_set_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b.Property("NumericValue") + .HasColumnType("double precision") + .HasColumnName("numeric_value"); + + b.Property("OptionValue") + .HasColumnType("text") + .HasColumnName("option_value"); + + b.Property("QuestionId") + .HasColumnType("integer") + .HasColumnName("question_id"); + + b.Property("StartValue") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_value"); + + b.Property("TextValue") + .HasColumnType("text") + .HasColumnName("text_value"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_form_answers"); + + b.HasIndex("FormAnswerSetId") + .HasDatabaseName("ix_form_answers_form_answer_set_id"); + + b.HasIndex("QuestionId") + .HasDatabaseName("ix_form_answers_question_id"); + + b.ToTable("form_answers", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.FormAnswerSet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedFrom") + .HasColumnType("uuid") + .HasColumnName("created_from"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Deleted") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("deleted"); + + b.Property("FurtherQuestionsExempted") + .HasColumnType("boolean") + .HasColumnName("further_questions_exempted"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b.Property("SectionId") + .HasColumnType("integer") + .HasColumnName("section_id"); + + b.Property("SharedConsentId") + .HasColumnType("integer") + .HasColumnName("shared_consent_id"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_form_answer_sets"); + + b.HasIndex("SectionId") + .HasDatabaseName("ix_form_answer_sets_section_id"); + + b.HasIndex("SharedConsentId") + .HasDatabaseName("ix_form_answer_sets_shared_consent_id"); + + b.ToTable("form_answer_sets", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.FormQuestion", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Caption") + .HasColumnType("text") + .HasColumnName("caption"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b.Property("IsRequired") + .HasColumnType("boolean") + .HasColumnName("is_required"); + + b.Property("NextQuestionAlternativeId") + .HasColumnType("integer") + .HasColumnName("next_question_alternative_id"); + + b.Property("NextQuestionId") + .HasColumnType("integer") + .HasColumnName("next_question_id"); + + b.Property("Options") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("options"); + + b.Property("SectionId") + .HasColumnType("integer") + .HasColumnName("section_id"); + + b.Property("SummaryTitle") + .HasColumnType("text") + .HasColumnName("summary_title"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_form_questions"); + + b.HasIndex("NextQuestionAlternativeId") + .HasDatabaseName("ix_form_questions_next_question_alternative_id"); + + b.HasIndex("NextQuestionId") + .HasDatabaseName("ix_form_questions_next_question_id"); + + b.HasIndex("SectionId") + .HasDatabaseName("ix_form_questions_section_id"); + + b.ToTable("form_questions", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.FormSection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowsMultipleAnswerSets") + .HasColumnType("boolean") + .HasColumnName("allows_multiple_answer_sets"); + + b.Property("Configuration") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("configuration"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("FormId") + .HasColumnType("integer") + .HasColumnName("form_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("Type") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("type"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_form_sections"); + + b.HasIndex("FormId") + .HasDatabaseName("ix_form_sections_form_id"); + + b.ToTable("form_sections", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.SharedConsent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedFrom") + .HasColumnType("uuid") + .HasColumnName("created_from"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("FormId") + .HasColumnType("integer") + .HasColumnName("form_id"); + + b.Property("FormVersionId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("form_version_id"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b.Property("OrganisationId") + .HasColumnType("integer") + .HasColumnName("organisation_id"); + + b.Property("ShareCode") + .HasColumnType("text") + .HasColumnName("share_code"); + + b.Property("SubmissionState") + .HasColumnType("integer") + .HasColumnName("submission_state"); + + b.Property("SubmittedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("submitted_at"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_shared_consents"); + + b.HasIndex("FormId") + .HasDatabaseName("ix_shared_consents_form_id"); + + b.HasIndex("OrganisationId") + .HasDatabaseName("ix_shared_consents_organisation_id"); + + b.ToTable("shared_consents", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Organisation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApprovedById") + .HasColumnType("integer") + .HasColumnName("approved_by_id"); + + b.Property("ApprovedComment") + .HasColumnType("text") + .HasColumnName("approved_comment"); + + b.Property("ApprovedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("approved_on"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Roles") + .IsRequired() + .HasColumnType("integer[]") + .HasColumnName("roles"); + + b.Property("TenantId") + .HasColumnType("integer") + .HasColumnName("tenant_id"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_organisations"); + + b.HasIndex("ApprovedById") + .HasDatabaseName("ix_organisations_approved_by_id"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_organisations_guid"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_organisations_name"); + + b.HasIndex("TenantId") + .HasDatabaseName("ix_organisations_tenant_id"); + + b.ToTable("organisations", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.OrganisationPerson", b => + { + b.Property("OrganisationId") + .HasColumnType("integer") + .HasColumnName("organisation_id"); + + b.Property("PersonId") + .HasColumnType("integer") + .HasColumnName("person_id"); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Scopes") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("scopes"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("OrganisationId", "PersonId") + .HasName("pk_organisation_person"); + + b.HasIndex("PersonId") + .HasDatabaseName("ix_organisation_person_person_id"); + + b.ToTable("organisation_person", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("last_name"); + + b.Property("Phone") + .HasColumnType("text") + .HasColumnName("phone"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UserUrn") + .HasColumnType("text") + .HasColumnName("user_urn"); + + b.HasKey("Id") + .HasName("pk_persons"); + + b.HasIndex("Email") + .IsUnique() + .HasDatabaseName("ix_persons_email"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_persons_guid"); + + b.ToTable("persons", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.PersonInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("first_name"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b.Property("InviteSentOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("invite_sent_on"); + + b.Property("LastName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("last_name"); + + b.Property("OrganisationId") + .HasColumnType("integer") + .HasColumnName("organisation_id"); + + b.Property("PersonId") + .HasColumnType("integer") + .HasColumnName("person_id"); + + b.Property>("Scopes") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("scopes"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_person_invites"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_person_invites_guid"); + + b.HasIndex("OrganisationId") + .HasDatabaseName("ix_person_invites_organisation_id"); + + b.HasIndex("PersonId") + .HasDatabaseName("ix_person_invites_person_id"); + + b.ToTable("person_invites", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiry_date"); + + b.Property("Revoked") + .HasColumnType("boolean") + .HasColumnName("revoked"); + + b.Property("TokenHash") + .IsRequired() + .HasColumnType("text") + .HasColumnName("token_hash"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_refresh_tokens"); + + b.HasIndex("TokenHash") + .IsUnique() + .HasDatabaseName("ix_refresh_tokens_token_hash"); + + b.ToTable("refresh_tokens", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_tenants"); + + b.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_tenants_guid"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_tenants_name"); + + b.ToTable("tenants", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.TenantPerson", b => + { + b.Property("PersonId") + .HasColumnType("integer") + .HasColumnName("person_id"); + + b.Property("TenantId") + .HasColumnType("integer") + .HasColumnName("tenant_id"); + + b.HasKey("PersonId", "TenantId") + .HasName("pk_tenant_person"); + + b.HasIndex("TenantId") + .HasDatabaseName("ix_tenant_person_tenant_id"); + + b.ToTable("tenant_person", (string)null); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.AuthenticationKey", b => + { + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Organisation", "Organisation") + .WithMany() + .HasForeignKey("OrganisationId") + .HasConstraintName("fk_authentication_keys_organisations_organisation_id"); + + b.Navigation("Organisation"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.ConnectedEntity", b => + { + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Organisation", "SupplierOrganisation") + .WithMany() + .HasForeignKey("SupplierOrganisationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_connected_entities_organisations_supplier_organisation_id"); + + b.OwnsMany("CO.CDP.OrganisationInformation.Persistence.ConnectedEntity+ConnectedEntityAddress", "Addresses", b1 => + { + b1.Property("ConnectedEntityId") + .HasColumnType("integer") + .HasColumnName("connected_entity_id"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("AddressId") + .HasColumnType("integer") + .HasColumnName("address_id"); + + b1.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b1.HasKey("ConnectedEntityId", "Id") + .HasName("pk_connected_entity_address"); + + b1.HasIndex("AddressId") + .HasDatabaseName("ix_connected_entity_address_address_id"); + + b1.ToTable("connected_entity_address", (string)null); + + b1.HasOne("CO.CDP.OrganisationInformation.Persistence.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_connected_entity_address_address_address_id"); + + b1.WithOwner() + .HasForeignKey("ConnectedEntityId") + .HasConstraintName("fk_connected_entity_address_connected_entities_connected_entit"); + + b1.Navigation("Address"); + }); + + b.OwnsOne("CO.CDP.OrganisationInformation.Persistence.ConnectedEntity+ConnectedIndividualTrust", "IndividualOrTrust", b1 => + { + b1.Property("Id") + .HasColumnType("integer") + .HasColumnName("connected_individual_trust_id"); + + b1.Property("Category") + .HasColumnType("integer") + .HasColumnName("category"); + + b1.Property("ConnectedType") + .HasColumnType("integer") + .HasColumnName("connected_type"); + + b1.Property("ControlCondition") + .IsRequired() + .HasColumnType("integer[]") + .HasColumnName("control_condition"); + + b1.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b1.Property("DateOfBirth") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_of_birth"); + + b1.Property("FirstName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("first_name"); + + b1.Property("LastName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("last_name"); + + b1.Property("Nationality") + .HasColumnType("text") + .HasColumnName("nationality"); + + b1.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b1.Property("ResidentCountry") + .HasColumnType("text") + .HasColumnName("resident_country"); + + b1.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b1.HasKey("Id") + .HasName("pk_connected_individual_trust"); + + b1.ToTable("connected_individual_trust", (string)null); + + b1.WithOwner() + .HasForeignKey("Id") + .HasConstraintName("fk_connected_individual_trust_connected_entities_connected_ind"); + }); + + b.OwnsOne("CO.CDP.OrganisationInformation.Persistence.ConnectedEntity+ConnectedOrganisation", "Organisation", b1 => + { + b1.Property("Id") + .HasColumnType("integer") + .HasColumnName("connected_organisation_id"); + + b1.Property("Category") + .HasColumnType("integer") + .HasColumnName("category"); + + b1.Property("ControlCondition") + .IsRequired() + .HasColumnType("integer[]") + .HasColumnName("control_condition"); + + b1.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b1.Property("InsolvencyDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("insolvency_date"); + + b1.Property("LawRegistered") + .HasColumnType("text") + .HasColumnName("law_registered"); + + b1.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b1.Property("OrganisationId") + .HasColumnType("uuid") + .HasColumnName("organisation_id"); + + b1.Property("RegisteredLegalForm") + .HasColumnType("text") + .HasColumnName("registered_legal_form"); + + b1.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b1.HasKey("Id") + .HasName("pk_connected_organisation"); + + b1.ToTable("connected_organisation", (string)null); + + b1.WithOwner() + .HasForeignKey("Id") + .HasConstraintName("fk_connected_organisation_connected_entities_connected_organis"); + }); + + b.Navigation("Addresses"); + + b.Navigation("IndividualOrTrust"); + + b.Navigation("Organisation"); + + b.Navigation("SupplierOrganisation"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.FormAnswer", b => + { + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Forms.FormAnswerSet", "FormAnswerSet") + .WithMany("Answers") + .HasForeignKey("FormAnswerSetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_form_answers_form_answer_sets_form_answer_set_id"); + + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Forms.FormQuestion", "Question") + .WithMany() + .HasForeignKey("QuestionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_form_answers_form_questions_question_id"); + + b.Navigation("FormAnswerSet"); + + b.Navigation("Question"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.FormAnswerSet", b => + { + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Forms.FormSection", "Section") + .WithMany() + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_form_answer_sets_form_section_section_id"); + + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Forms.SharedConsent", "SharedConsent") + .WithMany("AnswerSets") + .HasForeignKey("SharedConsentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_form_answer_sets_shared_consents_shared_consent_id"); + + b.Navigation("Section"); + + b.Navigation("SharedConsent"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.FormQuestion", b => + { + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Forms.FormQuestion", "NextQuestionAlternative") + .WithMany() + .HasForeignKey("NextQuestionAlternativeId") + .HasConstraintName("fk_form_questions_form_questions_next_question_alternative_id"); + + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Forms.FormQuestion", "NextQuestion") + .WithMany() + .HasForeignKey("NextQuestionId") + .HasConstraintName("fk_form_questions_form_questions_next_question_id"); + + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Forms.FormSection", "Section") + .WithMany("Questions") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_form_questions_form_sections_section_id"); + + b.Navigation("NextQuestion"); + + b.Navigation("NextQuestionAlternative"); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.FormSection", b => + { + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Forms.Form", "Form") + .WithMany("Sections") + .HasForeignKey("FormId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_form_sections_forms_form_id"); + + b.Navigation("Form"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.SharedConsent", b => + { + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Forms.Form", "Form") + .WithMany() + .HasForeignKey("FormId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_shared_consents_forms_form_id"); + + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Organisation", "Organisation") + .WithMany() + .HasForeignKey("OrganisationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_shared_consents_organisations_organisation_id"); + + b.Navigation("Form"); + + b.Navigation("Organisation"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Organisation", b => + { + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Person", "ApprovedBy") + .WithMany() + .HasForeignKey("ApprovedById") + .HasConstraintName("fk_organisations_persons_approved_by_id"); + + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Tenant", "Tenant") + .WithMany("Organisations") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_organisations_tenants_tenant_id"); + + b.OwnsOne("CO.CDP.OrganisationInformation.Persistence.Organisation+BuyerInformation", "BuyerInfo", b1 => + { + b1.Property("OrganisationId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("BuyerType") + .HasColumnType("text") + .HasColumnName("buyer_type"); + + b1.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b1.Property("DevolvedRegulations") + .IsRequired() + .HasColumnType("integer[]") + .HasColumnName("devolved_regulations"); + + b1.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b1.HasKey("OrganisationId") + .HasName("pk_buyer_information"); + + b1.ToTable("buyer_information", (string)null); + + b1.WithOwner() + .HasForeignKey("OrganisationId") + .HasConstraintName("fk_buyer_information_organisations_id"); + }); + + b.OwnsMany("CO.CDP.OrganisationInformation.Persistence.Organisation+ContactPoint", "ContactPoints", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b1.Property("Email") + .HasColumnType("text") + .HasColumnName("email"); + + b1.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b1.Property("OrganisationId") + .HasColumnType("integer") + .HasColumnName("organisation_id"); + + b1.Property("Telephone") + .HasColumnType("text") + .HasColumnName("telephone"); + + b1.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b1.Property("Url") + .HasColumnType("text") + .HasColumnName("url"); + + b1.HasKey("Id") + .HasName("pk_contact_points"); + + b1.HasIndex("OrganisationId") + .HasDatabaseName("ix_contact_points_organisation_id"); + + b1.ToTable("contact_points", (string)null); + + b1.WithOwner() + .HasForeignKey("OrganisationId") + .HasConstraintName("fk_contact_points_organisations_organisation_id"); + }); + + b.OwnsMany("CO.CDP.OrganisationInformation.Persistence.Organisation+Identifier", "Identifiers", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b1.Property("IdentifierId") + .HasColumnType("text") + .HasColumnName("identifier_id"); + + b1.Property("LegalName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("legal_name"); + + b1.Property("OrganisationId") + .HasColumnType("integer") + .HasColumnName("organisation_id"); + + b1.Property("Primary") + .HasColumnType("boolean") + .HasColumnName("primary"); + + b1.Property("Scheme") + .IsRequired() + .HasColumnType("text") + .HasColumnName("scheme"); + + b1.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b1.Property("Uri") + .HasColumnType("text") + .HasColumnName("uri"); + + b1.HasKey("Id") + .HasName("pk_identifiers"); + + b1.HasIndex("OrganisationId") + .HasDatabaseName("ix_identifiers_organisation_id"); + + b1.HasIndex("IdentifierId", "Scheme") + .IsUnique() + .HasDatabaseName("ix_identifiers_identifier_id_scheme"); + + b1.ToTable("identifiers", (string)null); + + b1.WithOwner() + .HasForeignKey("OrganisationId") + .HasConstraintName("fk_identifiers_organisations_organisation_id"); + }); + + b.OwnsMany("CO.CDP.OrganisationInformation.Persistence.Organisation+OrganisationAddress", "Addresses", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b1.Property("Id")); + + b1.Property("AddressId") + .HasColumnType("integer") + .HasColumnName("address_id"); + + b1.Property("OrganisationId") + .HasColumnType("integer") + .HasColumnName("organisation_id"); + + b1.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b1.HasKey("Id") + .HasName("pk_organisation_address"); + + b1.HasIndex("AddressId") + .HasDatabaseName("ix_organisation_address_address_id"); + + b1.HasIndex("OrganisationId") + .HasDatabaseName("ix_organisation_address_organisation_id"); + + b1.ToTable("organisation_address", (string)null); + + b1.HasOne("CO.CDP.OrganisationInformation.Persistence.Address", "Address") + .WithMany() + .HasForeignKey("AddressId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_organisation_address_address_address_id"); + + b1.WithOwner() + .HasForeignKey("OrganisationId") + .HasConstraintName("fk_organisation_address_organisations_organisation_id"); + + b1.Navigation("Address"); + }); + + b.OwnsOne("CO.CDP.OrganisationInformation.Persistence.Organisation+SupplierInformation", "SupplierInfo", b1 => + { + b1.Property("OrganisationId") + .HasColumnType("integer") + .HasColumnName("id"); + + b1.Property("CompletedConnectedPerson") + .HasColumnType("boolean") + .HasColumnName("completed_connected_person"); + + b1.Property("CompletedEmailAddress") + .HasColumnType("boolean") + .HasColumnName("completed_email_address"); + + b1.Property("CompletedLegalForm") + .HasColumnType("boolean") + .HasColumnName("completed_legal_form"); + + b1.Property("CompletedOperationType") + .HasColumnType("boolean") + .HasColumnName("completed_operation_type"); + + b1.Property("CompletedPostalAddress") + .HasColumnType("boolean") + .HasColumnName("completed_postal_address"); + + b1.Property("CompletedQualification") + .HasColumnType("boolean") + .HasColumnName("completed_qualification"); + + b1.Property("CompletedRegAddress") + .HasColumnType("boolean") + .HasColumnName("completed_reg_address"); + + b1.Property("CompletedTradeAssurance") + .HasColumnType("boolean") + .HasColumnName("completed_trade_assurance"); + + b1.Property("CompletedVat") + .HasColumnType("boolean") + .HasColumnName("completed_vat"); + + b1.Property("CompletedWebsiteAddress") + .HasColumnType("boolean") + .HasColumnName("completed_website_address"); + + b1.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b1.Property("OperationTypes") + .IsRequired() + .HasColumnType("integer[]") + .HasColumnName("operation_types"); + + b1.Property("SupplierType") + .HasColumnType("integer") + .HasColumnName("supplier_type"); + + b1.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b1.HasKey("OrganisationId") + .HasName("pk_supplier_information"); + + b1.ToTable("supplier_information", (string)null); + + b1.WithOwner() + .HasForeignKey("OrganisationId") + .HasConstraintName("fk_supplier_information_organisations_id"); + + b1.OwnsOne("CO.CDP.OrganisationInformation.Persistence.Organisation+LegalForm", "LegalForm", b2 => + { + b2.Property("SupplierInformationOrganisationId") + .HasColumnType("integer") + .HasColumnName("id"); + + b2.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b2.Property("LawRegistered") + .IsRequired() + .HasColumnType("text") + .HasColumnName("law_registered"); + + b2.Property("RegisteredLegalForm") + .IsRequired() + .HasColumnType("text") + .HasColumnName("registered_legal_form"); + + b2.Property("RegisteredUnderAct2006") + .HasColumnType("boolean") + .HasColumnName("registered_under_act2006"); + + b2.Property("RegistrationDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("registration_date"); + + b2.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b2.HasKey("SupplierInformationOrganisationId") + .HasName("pk_legal_forms"); + + b2.ToTable("legal_forms", (string)null); + + b2.WithOwner() + .HasForeignKey("SupplierInformationOrganisationId") + .HasConstraintName("fk_legal_forms_supplier_information_id"); + }); + + b1.OwnsMany("CO.CDP.OrganisationInformation.Persistence.Organisation+Qualification", "Qualifications", b2 => + { + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b2.Property("Id")); + + b2.Property("AwardedByPersonOrBodyName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("awarded_by_person_or_body_name"); + + b2.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b2.Property("DateAwarded") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_awarded"); + + b2.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b2.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b2.Property("SupplierInformationOrganisationId") + .HasColumnType("integer") + .HasColumnName("supplier_information_organisation_id"); + + b2.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b2.HasKey("Id") + .HasName("pk_qualifications"); + + b2.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_qualifications_guid"); + + b2.HasIndex("SupplierInformationOrganisationId") + .HasDatabaseName("ix_qualifications_supplier_information_organisation_id"); + + b2.ToTable("qualifications", (string)null); + + b2.WithOwner() + .HasForeignKey("SupplierInformationOrganisationId") + .HasConstraintName("fk_qualifications_supplier_information_supplier_information_or"); + }); + + b1.OwnsMany("CO.CDP.OrganisationInformation.Persistence.Organisation+TradeAssurance", "TradeAssurances", b2 => + { + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b2.Property("Id")); + + b2.Property("AwardedByPersonOrBodyName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("awarded_by_person_or_body_name"); + + b2.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b2.Property("DateAwarded") + .HasColumnType("timestamp with time zone") + .HasColumnName("date_awarded"); + + b2.Property("Guid") + .HasColumnType("uuid") + .HasColumnName("guid"); + + b2.Property("ReferenceNumber") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reference_number"); + + b2.Property("SupplierInformationOrganisationId") + .HasColumnType("integer") + .HasColumnName("supplier_information_organisation_id"); + + b2.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b2.HasKey("Id") + .HasName("pk_trade_assurances"); + + b2.HasIndex("Guid") + .IsUnique() + .HasDatabaseName("ix_trade_assurances_guid"); + + b2.HasIndex("SupplierInformationOrganisationId") + .HasDatabaseName("ix_trade_assurances_supplier_information_organisation_id"); + + b2.ToTable("trade_assurances", (string)null); + + b2.WithOwner() + .HasForeignKey("SupplierInformationOrganisationId") + .HasConstraintName("fk_trade_assurances_supplier_information_supplier_information_"); + }); + + b1.Navigation("LegalForm"); + + b1.Navigation("Qualifications"); + + b1.Navigation("TradeAssurances"); + }); + + b.Navigation("Addresses"); + + b.Navigation("ApprovedBy"); + + b.Navigation("BuyerInfo"); + + b.Navigation("ContactPoints"); + + b.Navigation("Identifiers"); + + b.Navigation("SupplierInfo"); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.OrganisationPerson", b => + { + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Organisation", "Organisation") + .WithMany("OrganisationPersons") + .HasForeignKey("OrganisationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_organisation_person_organisations_organisation_id"); + + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Person", "Person") + .WithMany("PersonOrganisations") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_organisation_person_persons_person_id"); + + b.Navigation("Organisation"); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.PersonInvite", b => + { + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Organisation", "Organisation") + .WithMany() + .HasForeignKey("OrganisationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_person_invites_organisations_organisation_id"); + + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Person", "Person") + .WithMany() + .HasForeignKey("PersonId") + .HasConstraintName("fk_person_invites_persons_person_id"); + + b.Navigation("Organisation"); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.TenantPerson", b => + { + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Person", null) + .WithMany() + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_person_persons_person_id"); + + b.HasOne("CO.CDP.OrganisationInformation.Persistence.Tenant", null) + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tenant_person_tenants_tenant_id"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.Form", b => + { + b.Navigation("Sections"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.FormAnswerSet", b => + { + b.Navigation("Answers"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.FormSection", b => + { + b.Navigation("Questions"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Forms.SharedConsent", b => + { + b.Navigation("AnswerSets"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Organisation", b => + { + b.Navigation("OrganisationPersons"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Person", b => + { + b.Navigation("PersonOrganisations"); + }); + + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Tenant", b => + { + b.Navigation("Organisations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240920113939_OutboxMessage.cs b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240920113939_OutboxMessage.cs new file mode 100644 index 000000000..eabebfff1 --- /dev/null +++ b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/20240920113939_OutboxMessage.cs @@ -0,0 +1,50 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CO.CDP.OrganisationInformation.Persistence.Migrations +{ + /// + public partial class OutboxMessage : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "outbox_messages", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + type = table.Column(type: "text", nullable: false), + message = table.Column(type: "text", nullable: false), + published = table.Column(type: "boolean", nullable: false), + created_on = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"), + updated_on = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP") + }, + constraints: table => + { + table.PrimaryKey("pk_outbox_messages", x => x.id); + }); + + migrationBuilder.CreateIndex( + name: "ix_outbox_messages_created_on", + table: "outbox_messages", + column: "created_on"); + + migrationBuilder.CreateIndex( + name: "ix_outbox_messages_published", + table: "outbox_messages", + column: "published"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "outbox_messages"); + } + } +} diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/OrganisationInformationContextModelSnapshot.cs b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/OrganisationInformationContextModelSnapshot.cs index e0a382649..81c1d7ff8 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/OrganisationInformationContextModelSnapshot.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence/Migrations/OrganisationInformationContextModelSnapshot.cs @@ -28,6 +28,53 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "control_condition", new[] { "none", "owns_shares", "has_voting_rights", "can_appoint_or_remove_directors", "has_other_significant_influence_or_control" }); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("CO.CDP.MQ.Outbox.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text") + .HasColumnName("message"); + + b.Property("Published") + .HasColumnType("boolean") + .HasColumnName("published"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text") + .HasColumnName("type"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_outbox_messages"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_outbox_messages_created_on"); + + b.HasIndex("Published") + .HasDatabaseName("ix_outbox_messages_published"); + + b.ToTable("outbox_messages", (string)null); + }); + modelBuilder.Entity("CO.CDP.OrganisationInformation.Persistence.Address", b => { b.Property("Id") diff --git a/Services/CO.CDP.OrganisationInformation.Persistence/OrganisationInformationContext.cs b/Services/CO.CDP.OrganisationInformation.Persistence/OrganisationInformationContext.cs index ceb611918..4130b337d 100644 --- a/Services/CO.CDP.OrganisationInformation.Persistence/OrganisationInformationContext.cs +++ b/Services/CO.CDP.OrganisationInformation.Persistence/OrganisationInformationContext.cs @@ -7,12 +7,13 @@ using Microsoft.EntityFrameworkCore.Migrations; using System.Text.Json; using System.Text.Json.Serialization; +using CO.CDP.MQ.Outbox; using static CO.CDP.OrganisationInformation.Persistence.ConnectedEntity; namespace CO.CDP.OrganisationInformation.Persistence; public class OrganisationInformationContext(DbContextOptions options) - : DbContext(options) + : DbContext(options), IOutboxMessageDbContext { public DbSet Tenants { get; set; } = null!; public DbSet Organisations { get; set; } = null!; @@ -24,6 +25,7 @@ public class OrganisationInformationContext(DbContextOptions FormAnswerSets { get; set; } = null!; public DbSet RefreshTokens { get; set; } = null!; public DbSet PersonInvites { get; set; } = null!; + public DbSet OutboxMessages { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -163,6 +165,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) OnFormModelCreating(modelBuilder); + modelBuilder.OnOutboxMessageCreating(); + base.OnModelCreating(modelBuilder); } From 76bf104f1e706c70d08b5b6cd41f8376640c39ad Mon Sep 17 00:00:00 2001 From: Jakub Zalas Date: Fri, 20 Sep 2024 13:29:03 +0100 Subject: [PATCH 04/10] Add more debug logging --- .../Hosting/OutboxProcessorBackgroundServiceTest.cs | 11 ++++++++--- .../Hosting/OutboxProcessorBackgroundService.cs | 12 +++++++++--- Libraries/CO.CDP.MQ/Outbox/OutboxProcessor.cs | 2 +- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/Libraries/CO.CDP.MQ.Tests/Hosting/OutboxProcessorBackgroundServiceTest.cs b/Libraries/CO.CDP.MQ.Tests/Hosting/OutboxProcessorBackgroundServiceTest.cs index 5b28d4218..932f8560e 100644 --- a/Libraries/CO.CDP.MQ.Tests/Hosting/OutboxProcessorBackgroundServiceTest.cs +++ b/Libraries/CO.CDP.MQ.Tests/Hosting/OutboxProcessorBackgroundServiceTest.cs @@ -2,8 +2,8 @@ using CO.CDP.MQ.Outbox; using CO.CDP.MQ.Tests.Hosting.TestKit; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Moq; -using Range = Moq.Range; namespace CO.CDP.MQ.Tests.Hosting; @@ -12,6 +12,9 @@ public class OutboxProcessorBackgroundServiceTest private readonly Mock _outboxProcessor = new(); private readonly TestServiceProvider _serviceProvider = new(); + private readonly ILogger _logger = + LoggerFactory.Create(_ => { }).CreateLogger(); + [Fact] public async Task ItExecutesTheOutboxProcessor() { @@ -23,7 +26,8 @@ public async Task ItExecutesTheOutboxProcessor() { BatchSize = 2, ExecutionInterval = TimeSpan.FromSeconds(30) - }); + }, + _logger); await backgroundService.StartAsync(CancellationToken.None); await Task.Delay(TimeSpan.FromMilliseconds(100)); await backgroundService.StopAsync(CancellationToken.None); @@ -42,7 +46,8 @@ public async Task ItContinuesExecutingTheOutboxProcessorInRegularIntervals() { BatchSize = 3, ExecutionInterval = TimeSpan.FromMilliseconds(4) - }); + }, + _logger); await backgroundService.StartAsync(CancellationToken.None); await Task.Delay(TimeSpan.FromMilliseconds(8)); await backgroundService.StopAsync(CancellationToken.None); diff --git a/Libraries/CO.CDP.MQ/Hosting/OutboxProcessorBackgroundService.cs b/Libraries/CO.CDP.MQ/Hosting/OutboxProcessorBackgroundService.cs index 40ab11c3d..d4e8fb787 100644 --- a/Libraries/CO.CDP.MQ/Hosting/OutboxProcessorBackgroundService.cs +++ b/Libraries/CO.CDP.MQ/Hosting/OutboxProcessorBackgroundService.cs @@ -1,12 +1,14 @@ using CO.CDP.MQ.Outbox; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; namespace CO.CDP.MQ.Hosting; public class OutboxProcessorBackgroundService( IServiceProvider services, - OutboxProcessorBackgroundService.OutboxProcessorConfiguration configuration + OutboxProcessorBackgroundService.OutboxProcessorConfiguration configuration, + ILogger logger ) : IHostedService, IDisposable { public record OutboxProcessorConfiguration @@ -15,16 +17,20 @@ public record OutboxProcessorConfiguration public TimeSpan ExecutionInterval { get; init; } = TimeSpan.FromSeconds(60); } - private Timer? _timer = null; + private Timer? _timer; public Task StartAsync(CancellationToken cancellationToken) { + logger.LogDebug( + "Staring the outbox processor background service Interval={INTERVAL} Batch={BATCH}", + configuration.ExecutionInterval, configuration.BatchSize); _timer = new Timer(ExecuteOutboxProcessorAsync, null, TimeSpan.Zero, configuration.ExecutionInterval); return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { + logger.LogDebug("Stopping the outbox processor background service"); _timer?.Change(Timeout.Infinite, 0); return Task.CompletedTask; } diff --git a/Libraries/CO.CDP.MQ/Outbox/OutboxProcessor.cs b/Libraries/CO.CDP.MQ/Outbox/OutboxProcessor.cs index 33b9eed62..abdcedc79 100644 --- a/Libraries/CO.CDP.MQ/Outbox/OutboxProcessor.cs +++ b/Libraries/CO.CDP.MQ/Outbox/OutboxProcessor.cs @@ -20,7 +20,7 @@ public async Task ExecuteAsync(int count) private async Task> FetchMessages(int count) { var messages = await outbox.FindOldest(count); - logger.LogDebug("Fetched `{COUNT}` messages", messages.Count); + logger.LogDebug("Fetched {COUNT} message(s)", messages.Count); return messages; } From b65c45004b81052a157eabd3effa04851671cc1a Mon Sep 17 00:00:00 2001 From: Jakub Zalas Date: Fri, 20 Sep 2024 13:41:54 +0100 Subject: [PATCH 05/10] Configure the outbox publisher in the entity verification service --- Libraries/CO.CDP.AwsServices/Extensions.cs | 7 +- .../20240920123556_OutboxMessage.Designer.cs | 220 ++++++++++++++++++ .../20240920123556_OutboxMessage.cs | 94 ++++++++ .../EntityVerificationContextModelSnapshot.cs | 51 +++- .../Persistence/EntityVerificationContext.cs | 6 +- Services/CO.CDP.EntityVerification/Program.cs | 3 +- .../appsettings.json | 6 +- 7 files changed, 375 insertions(+), 12 deletions(-) create mode 100644 Services/CO.CDP.EntityVerification/Migrations/20240920123556_OutboxMessage.Designer.cs create mode 100644 Services/CO.CDP.EntityVerification/Migrations/20240920123556_OutboxMessage.cs diff --git a/Libraries/CO.CDP.AwsServices/Extensions.cs b/Libraries/CO.CDP.AwsServices/Extensions.cs index 5bea62cb5..f0a6d1b48 100644 --- a/Libraries/CO.CDP.AwsServices/Extensions.cs +++ b/Libraries/CO.CDP.AwsServices/Extensions.cs @@ -73,11 +73,6 @@ public static IServiceCollection AddAwsSqsService(this IServiceCollection servic .AddAWSService(); } - public static IServiceCollection AddSqsPublisher(this IServiceCollection services) - { - return services.AddScoped(); - } - public static IServiceCollection AddOutboxSqsPublisher(this IServiceCollection services) where TDbContext : DbContext, IOutboxMessageDbContext { @@ -96,7 +91,7 @@ public static IServiceCollection AddOutboxSqsPublisher(this IService s.GetRequiredKeyedService("SqsPublisher"), s.GetRequiredService(), s.GetRequiredService>() - ) + ) ); return services; } diff --git a/Services/CO.CDP.EntityVerification/Migrations/20240920123556_OutboxMessage.Designer.cs b/Services/CO.CDP.EntityVerification/Migrations/20240920123556_OutboxMessage.Designer.cs new file mode 100644 index 000000000..a43ec188f --- /dev/null +++ b/Services/CO.CDP.EntityVerification/Migrations/20240920123556_OutboxMessage.Designer.cs @@ -0,0 +1,220 @@ +// +using System; +using CO.CDP.EntityVerification.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CO.CDP.EntityVerification.Migrations +{ + [DbContext(typeof(EntityVerificationContext))] + [Migration("20240920123556_OutboxMessage")] + partial class OutboxMessage + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("entity_verification") + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("CO.CDP.EntityVerification.Persistence.Identifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("IdentifierId") + .HasColumnType("text") + .HasColumnName("identifier_id"); + + b.Property("LegalName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("legal_name"); + + b.Property("PponId") + .HasColumnType("integer") + .HasColumnName("ppon_id"); + + b.Property("Scheme") + .IsRequired() + .HasColumnType("text") + .HasColumnName("scheme"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Uri") + .HasColumnType("text") + .HasColumnName("uri"); + + b.Property("endsOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("ends_on"); + + b.Property("startsOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("starts_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_identifiers"); + + b.HasIndex("IdentifierId") + .HasDatabaseName("ix_identifiers_identifier_id"); + + b.HasIndex("PponId") + .HasDatabaseName("ix_identifiers_ppon_id"); + + b.HasIndex("Scheme") + .HasDatabaseName("ix_identifiers_scheme"); + + b.ToTable("identifiers", "entity_verification"); + }); + + modelBuilder.Entity("CO.CDP.EntityVerification.Persistence.Ppon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("IdentifierId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("identifier_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("OrganisationId") + .HasColumnType("uuid") + .HasColumnName("organisation_id"); + + b.Property("UpdatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("endsOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("ends_on"); + + b.Property("startsOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("starts_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.HasKey("Id") + .HasName("pk_ppons"); + + b.HasIndex("IdentifierId") + .IsUnique() + .HasDatabaseName("ix_ppons_identifier_id"); + + b.HasIndex("Name") + .HasDatabaseName("ix_ppons_name"); + + b.HasIndex("OrganisationId") + .HasDatabaseName("ix_ppons_organisation_id"); + + b.ToTable("ppons", "entity_verification"); + }); + + modelBuilder.Entity("CO.CDP.MQ.Outbox.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text") + .HasColumnName("message"); + + b.Property("Published") + .HasColumnType("boolean") + .HasColumnName("published"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text") + .HasColumnName("type"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.HasKey("Id") + .HasName("pk_outbox_messages"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_outbox_messages_created_on"); + + b.HasIndex("Published") + .HasDatabaseName("ix_outbox_messages_published"); + + b.ToTable("outbox_messages", "entity_verification"); + }); + + modelBuilder.Entity("CO.CDP.EntityVerification.Persistence.Identifier", b => + { + b.HasOne("CO.CDP.EntityVerification.Persistence.Ppon", null) + .WithMany("Identifiers") + .HasForeignKey("PponId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_identifiers_ppons_ppon_id"); + }); + + modelBuilder.Entity("CO.CDP.EntityVerification.Persistence.Ppon", b => + { + b.Navigation("Identifiers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Services/CO.CDP.EntityVerification/Migrations/20240920123556_OutboxMessage.cs b/Services/CO.CDP.EntityVerification/Migrations/20240920123556_OutboxMessage.cs new file mode 100644 index 000000000..69347436b --- /dev/null +++ b/Services/CO.CDP.EntityVerification/Migrations/20240920123556_OutboxMessage.cs @@ -0,0 +1,94 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace CO.CDP.EntityVerification.Migrations +{ + /// + public partial class OutboxMessage : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "ends_on", + schema: "entity_verification", + table: "ppons", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTimeOffset), + oldType: "timestamp with time zone"); + + migrationBuilder.AlterColumn( + name: "ends_on", + schema: "entity_verification", + table: "identifiers", + type: "timestamp with time zone", + nullable: true, + oldClrType: typeof(DateTimeOffset), + oldType: "timestamp with time zone"); + + migrationBuilder.CreateTable( + name: "outbox_messages", + schema: "entity_verification", + columns: table => new + { + id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + type = table.Column(type: "text", nullable: false), + message = table.Column(type: "text", nullable: false), + published = table.Column(type: "boolean", nullable: false), + created_on = table.Column(type: "timestamp with time zone", nullable: false, defaultValueSql: "CURRENT_TIMESTAMP"), + updated_on = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("pk_outbox_messages", x => x.id); + }); + + migrationBuilder.CreateIndex( + name: "ix_outbox_messages_created_on", + schema: "entity_verification", + table: "outbox_messages", + column: "created_on"); + + migrationBuilder.CreateIndex( + name: "ix_outbox_messages_published", + schema: "entity_verification", + table: "outbox_messages", + column: "published"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "outbox_messages", + schema: "entity_verification"); + + migrationBuilder.AlterColumn( + name: "ends_on", + schema: "entity_verification", + table: "ppons", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + oldClrType: typeof(DateTimeOffset), + oldType: "timestamp with time zone", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "ends_on", + schema: "entity_verification", + table: "identifiers", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), + oldClrType: typeof(DateTimeOffset), + oldType: "timestamp with time zone", + oldNullable: true); + } + } +} diff --git a/Services/CO.CDP.EntityVerification/Migrations/EntityVerificationContextModelSnapshot.cs b/Services/CO.CDP.EntityVerification/Migrations/EntityVerificationContextModelSnapshot.cs index 0c02c6b3a..9854f7fda 100644 --- a/Services/CO.CDP.EntityVerification/Migrations/EntityVerificationContextModelSnapshot.cs +++ b/Services/CO.CDP.EntityVerification/Migrations/EntityVerificationContextModelSnapshot.cs @@ -1,4 +1,4 @@ -// +// using System; using CO.CDP.EntityVerification.Persistence; using Microsoft.EntityFrameworkCore; @@ -66,7 +66,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text") .HasColumnName("uri"); - b.Property("endsOn") + b.Property("endsOn") .HasColumnType("timestamp with time zone") .HasColumnName("ends_on"); @@ -126,7 +126,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnName("updated_on") .HasDefaultValueSql("CURRENT_TIMESTAMP"); - b.Property("endsOn") + b.Property("endsOn") .HasColumnType("timestamp with time zone") .HasColumnName("ends_on"); @@ -152,6 +152,51 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ppons", "entity_verification"); }); + modelBuilder.Entity("CO.CDP.MQ.Outbox.OutboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedOn") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text") + .HasColumnName("message"); + + b.Property("Published") + .HasColumnType("boolean") + .HasColumnName("published"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text") + .HasColumnName("type"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.HasKey("Id") + .HasName("pk_outbox_messages"); + + b.HasIndex("CreatedOn") + .HasDatabaseName("ix_outbox_messages_created_on"); + + b.HasIndex("Published") + .HasDatabaseName("ix_outbox_messages_published"); + + b.ToTable("outbox_messages", "entity_verification"); + }); + modelBuilder.Entity("CO.CDP.EntityVerification.Persistence.Identifier", b => { b.HasOne("CO.CDP.EntityVerification.Persistence.Ppon", null) diff --git a/Services/CO.CDP.EntityVerification/Persistence/EntityVerificationContext.cs b/Services/CO.CDP.EntityVerification/Persistence/EntityVerificationContext.cs index a68375280..3d5b01d5d 100644 --- a/Services/CO.CDP.EntityVerification/Persistence/EntityVerificationContext.cs +++ b/Services/CO.CDP.EntityVerification/Persistence/EntityVerificationContext.cs @@ -1,6 +1,7 @@ using System.Text.Json; using CO.CDP.EntityFrameworkCore.Timestamps; using CO.CDP.EntityVerification.EntityFramework; +using CO.CDP.MQ.Outbox; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -9,10 +10,11 @@ namespace CO.CDP.EntityVerification.Persistence; -public class EntityVerificationContext : DbContext +public class EntityVerificationContext : DbContext, IOutboxMessageDbContext { public DbSet Ppons { get; set; } = null!; public DbSet Identifiers { get; set; } = null!; + public DbSet OutboxMessages { get; set; } public EntityVerificationContext() { @@ -50,6 +52,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .IsRequired(); }); + modelBuilder.OnOutboxMessageCreating(); + base.OnModelCreating(modelBuilder); } diff --git a/Services/CO.CDP.EntityVerification/Program.cs b/Services/CO.CDP.EntityVerification/Program.cs index 48dfe8dcf..6403c8646 100644 --- a/Services/CO.CDP.EntityVerification/Program.cs +++ b/Services/CO.CDP.EntityVerification/Program.cs @@ -37,7 +37,7 @@ builder.Services .AddAwsConfiguration(builder.Configuration) .AddAwsSqsService() - .AddSqsPublisher() + .AddOutboxSqsPublisher() .AddSqsDispatcher( EventDeserializer.Deserializer, services => @@ -52,6 +52,7 @@ } ); builder.Services.AddHostedService(); + builder.Services.AddHostedService(); builder.Services .AddAwsConfiguration(builder.Configuration) diff --git a/Services/CO.CDP.EntityVerification/appsettings.json b/Services/CO.CDP.EntityVerification/appsettings.json index 15f88c85a..8b1eecf87 100644 --- a/Services/CO.CDP.EntityVerification/appsettings.json +++ b/Services/CO.CDP.EntityVerification/appsettings.json @@ -19,7 +19,11 @@ }, "SqsPublisher": { "QueueUrl": "", - "MessageGroupId": "EntityVerification" + "MessageGroupId": "EntityVerification", + "Outbox": { + "BatchSize": 10, + "ExecutionInterval": "00:00:30" + } }, "CloudWatch": { "LogGroup": "/etc/entity-verification", From 6edea2f5d5d78f3758dbbab49339c7669093d70b Mon Sep 17 00:00:00 2001 From: Jakub Zalas Date: Fri, 20 Sep 2024 13:49:53 +0100 Subject: [PATCH 06/10] Enable Debug logs for our namespace --- Services/CO.CDP.EntityVerification/appsettings.Development.json | 1 + 1 file changed, 1 insertion(+) diff --git a/Services/CO.CDP.EntityVerification/appsettings.Development.json b/Services/CO.CDP.EntityVerification/appsettings.Development.json index 13299acc5..99512fea7 100644 --- a/Services/CO.CDP.EntityVerification/appsettings.Development.json +++ b/Services/CO.CDP.EntityVerification/appsettings.Development.json @@ -4,6 +4,7 @@ "MinimumLevel": { "Default": "Information", "Override": { + "CO.CDP": "Debug", "Microsoft.AspNetCore.Mvc": "Warning", "Microsoft.AspNetCore.Routing": "Warning", "Microsoft.AspNetCore.Hosting": "Information" From c9e0949f4935d6c6c1bbef5ad70166fba52f0567 Mon Sep 17 00:00:00 2001 From: Jakub Zalas Date: Fri, 20 Sep 2024 13:53:18 +0100 Subject: [PATCH 07/10] Remove redundant constructors --- .../Persistence/EntityVerificationContext.cs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/Services/CO.CDP.EntityVerification/Persistence/EntityVerificationContext.cs b/Services/CO.CDP.EntityVerification/Persistence/EntityVerificationContext.cs index 3d5b01d5d..1a0c04358 100644 --- a/Services/CO.CDP.EntityVerification/Persistence/EntityVerificationContext.cs +++ b/Services/CO.CDP.EntityVerification/Persistence/EntityVerificationContext.cs @@ -10,20 +10,13 @@ namespace CO.CDP.EntityVerification.Persistence; -public class EntityVerificationContext : DbContext, IOutboxMessageDbContext +public class EntityVerificationContext(DbContextOptions options) + : DbContext(options), IOutboxMessageDbContext { public DbSet Ppons { get; set; } = null!; public DbSet Identifiers { get; set; } = null!; public DbSet OutboxMessages { get; set; } - public EntityVerificationContext() - { - } - - public EntityVerificationContext(DbContextOptions options) : base(options) - { - } - protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.HasDefaultSchema("entity_verification"); From d7d873125141e748d05d13f2fef4a4e38e5b1340 Mon Sep 17 00:00:00 2001 From: Jakub Zalas Date: Fri, 20 Sep 2024 14:32:22 +0100 Subject: [PATCH 08/10] Prevent DbContext from being disposed See https://stackoverflow.com/a/55545421/330267 --- Libraries/CO.CDP.MQ/Outbox/OutboxProcessor.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Libraries/CO.CDP.MQ/Outbox/OutboxProcessor.cs b/Libraries/CO.CDP.MQ/Outbox/OutboxProcessor.cs index abdcedc79..65b510f0b 100644 --- a/Libraries/CO.CDP.MQ/Outbox/OutboxProcessor.cs +++ b/Libraries/CO.CDP.MQ/Outbox/OutboxProcessor.cs @@ -14,7 +14,10 @@ public async Task ExecuteAsync(int count) { logger.LogDebug("Executing the outbox processor"); var messages = await FetchMessages(count); - messages.ForEach(PublishMessage); + foreach (var outboxMessage in messages) + { + await PublishMessage(outboxMessage); + } } private async Task> FetchMessages(int count) @@ -24,7 +27,7 @@ private async Task> FetchMessages(int count) return messages; } - private async void PublishMessage(OutboxMessage m) + private async Task PublishMessage(OutboxMessage m) { logger.LogDebug("Publishing the `{TYPE}` message: `{MESSAGE}`", m.Type, m.Message); await publisher.Publish(m); From 804a83eb770cd4adffde9cfbc400cf37de375e7a Mon Sep 17 00:00:00 2001 From: Jakub Zalas Date: Fri, 20 Sep 2024 14:47:53 +0100 Subject: [PATCH 09/10] Use the PeriodicTimer instead of Timer --- .../OutboxProcessorBackgroundService.cs | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/Libraries/CO.CDP.MQ/Hosting/OutboxProcessorBackgroundService.cs b/Libraries/CO.CDP.MQ/Hosting/OutboxProcessorBackgroundService.cs index d4e8fb787..6b08a834b 100644 --- a/Libraries/CO.CDP.MQ/Hosting/OutboxProcessorBackgroundService.cs +++ b/Libraries/CO.CDP.MQ/Hosting/OutboxProcessorBackgroundService.cs @@ -9,7 +9,7 @@ public class OutboxProcessorBackgroundService( IServiceProvider services, OutboxProcessorBackgroundService.OutboxProcessorConfiguration configuration, ILogger logger -) : IHostedService, IDisposable +) : BackgroundService { public record OutboxProcessorConfiguration { @@ -17,30 +17,31 @@ public record OutboxProcessorConfiguration public TimeSpan ExecutionInterval { get; init; } = TimeSpan.FromSeconds(60); } - private Timer? _timer; - - public Task StartAsync(CancellationToken cancellationToken) + protected override async Task ExecuteAsync(CancellationToken stoppingToken) { logger.LogDebug( "Staring the outbox processor background service Interval={INTERVAL} Batch={BATCH}", configuration.ExecutionInterval, configuration.BatchSize); - _timer = new Timer(ExecuteOutboxProcessorAsync, null, TimeSpan.Zero, configuration.ExecutionInterval); - return Task.CompletedTask; - } - public Task StopAsync(CancellationToken cancellationToken) - { - logger.LogDebug("Stopping the outbox processor background service"); - _timer?.Change(Timeout.Infinite, 0); - return Task.CompletedTask; - } + await ExecuteOutboxProcessorAsync(); + + using PeriodicTimer timer = new(configuration.ExecutionInterval); + + try + { + while (await timer.WaitForNextTickAsync(stoppingToken)) + { + await ExecuteOutboxProcessorAsync(); + } + } + catch (OperationCanceledException) + { + logger.LogDebug("Stopping the outbox processor background service"); + } - public void Dispose() - { - _timer?.Dispose(); } - private async void ExecuteOutboxProcessorAsync(object? state) + private async Task ExecuteOutboxProcessorAsync() { using var scope = services.CreateScope(); var outboxProcessor = scope.ServiceProvider.GetRequiredService(); From da7c1fdcb7f802d7b7d09b0522364f4c9aa6a4d8 Mon Sep 17 00:00:00 2001 From: Jakub Zalas Date: Tue, 24 Sep 2024 14:30:57 +0100 Subject: [PATCH 10/10] Unwrap the OutboxMessage before publishing it --- .../Sqs/SqsPublisherTest.cs | 18 ++++++++++++++++++ .../CO.CDP.AwsServices/Sqs/SqsPublisher.cs | 13 +++++-------- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/Libraries/CO.CDP.AwsServices.Tests/Sqs/SqsPublisherTest.cs b/Libraries/CO.CDP.AwsServices.Tests/Sqs/SqsPublisherTest.cs index 8d2540b97..3a15dfd9a 100644 --- a/Libraries/CO.CDP.AwsServices.Tests/Sqs/SqsPublisherTest.cs +++ b/Libraries/CO.CDP.AwsServices.Tests/Sqs/SqsPublisherTest.cs @@ -4,6 +4,7 @@ using Amazon.SQS.Model; using CO.CDP.AwsServices.Sqs; using CO.CDP.MQ; +using CO.CDP.MQ.Outbox; using CO.CDP.MQ.Tests; using FluentAssertions; using Microsoft.Extensions.Logging; @@ -22,6 +23,22 @@ public SqsPublisherTest(LocalStackFixture localStack) _sqsClient = SqsClient(); } + [Fact] + public async Task ItPublishesOutboxMessageToTheQueue() + { + var publisher = await CreatePublisher(); + + await publisher.Publish(new OutboxMessage + { + Type = "TestMessage", + Message = "{\"Id\":13,\"Name\":\"Hello!\"}" + }); + + var message = await waitForOneMessage(); + + message.Should().Be(new TestMessage(13, "Hello!")); + } + protected override async Task waitForOneMessage() where T : class { var queue = await _sqsClient.GetQueueUrlAsync(TestQueue); @@ -34,6 +51,7 @@ protected override async Task waitForOneMessage() where T : class var message = messages.Messages.First(); var type = message.MessageAttributes.GetValueOrDefault("Type")?.StringValue; type.Should().Be(typeof(T).Name); + await _sqsClient.DeleteMessageAsync(queue.QueueUrl, message.ReceiptHandle); return JsonSerializer.Deserialize(message.Body) ?? throw new Exception($"Unable to deserialize {message.Body} into {typeof(T).FullName}"); } diff --git a/Libraries/CO.CDP.AwsServices/Sqs/SqsPublisher.cs b/Libraries/CO.CDP.AwsServices/Sqs/SqsPublisher.cs index b5d13f037..af9237d73 100644 --- a/Libraries/CO.CDP.AwsServices/Sqs/SqsPublisher.cs +++ b/Libraries/CO.CDP.AwsServices/Sqs/SqsPublisher.cs @@ -2,20 +2,17 @@ using Amazon.SQS; using Amazon.SQS.Model; using CO.CDP.MQ; +using CO.CDP.MQ.Outbox; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace CO.CDP.AwsServices.Sqs; -public delegate string Serializer(object message); - -public delegate string TypeMapper(object message); - public class SqsPublisher( IAmazonSQS sqsClient, SqsPublisherConfiguration configuration, - Serializer serializer, - TypeMapper typeMapper, + Func serializer, + Func typeMapper, ILogger logger ) : IPublisher { @@ -29,8 +26,8 @@ public SqsPublisher(IAmazonSQS sqsClient, IOptions configurati public SqsPublisher(IAmazonSQS sqsClient, SqsPublisherConfiguration configuration, ILogger logger) : this( sqsClient, configuration, - o => JsonSerializer.Serialize(o), - o => o.GetType().Name, + OutboxMessageSerializerFactory.Create(o => JsonSerializer.Serialize(o)), + OutboxMessageTypeMapperFactory.Create(o => o.GetType().Name), logger) { }