From 236a7a0fe1d378066b1b4a393381c795bd8bc8be Mon Sep 17 00:00:00 2001 From: Mateusz Skoczek Date: Thu, 29 Jan 2026 00:21:23 +0100 Subject: [PATCH] outbox pattern implemented --- .../Commands/Register/RegisterHandler.cs | 12 +- .../Configuration/EventConfiguration.cs | 34 ---- .../DatabaseContext.cs | 12 +- ...0804_Outbox_Pattern_From_Nuget.Designer.cs | 145 ++++++++++++++++++ ...0260128230804_Outbox_Pattern_From_Nuget.cs | 70 +++++++++ .../DatabaseContextModelSnapshot.cs | 68 ++++---- .../Model/Event.cs | 12 -- ...nd.Services.Authentication.Database.csproj | 1 + .../Program.cs | 8 + ...ner.Backend.Services.Authentication.csproj | 5 +- 10 files changed, 274 insertions(+), 93 deletions(-) delete mode 100644 TimetableDesigner.Backend.Services.Authentication.Database/Configuration/EventConfiguration.cs create mode 100644 TimetableDesigner.Backend.Services.Authentication.Database/Migrations/20260128230804_Outbox_Pattern_From_Nuget.Designer.cs create mode 100644 TimetableDesigner.Backend.Services.Authentication.Database/Migrations/20260128230804_Outbox_Pattern_From_Nuget.cs delete mode 100644 TimetableDesigner.Backend.Services.Authentication.Database/Model/Event.cs diff --git a/TimetableDesigner.Backend.Services.Authentication.Core/Commands/Register/RegisterHandler.cs b/TimetableDesigner.Backend.Services.Authentication.Core/Commands/Register/RegisterHandler.cs index 55ae3e4..6edc3c1 100644 --- a/TimetableDesigner.Backend.Services.Authentication.Core/Commands/Register/RegisterHandler.cs +++ b/TimetableDesigner.Backend.Services.Authentication.Core/Commands/Register/RegisterHandler.cs @@ -1,7 +1,9 @@ using MediatR; +using TimetableDesigner.Backend.Events.OutboxPattern; using TimetableDesigner.Backend.Services.Authentication.Core.Helpers; using TimetableDesigner.Backend.Services.Authentication.Database; using TimetableDesigner.Backend.Services.Authentication.Database.Model; +using TimetableDesigner.Backend.Services.Authentication.DTO.Events; namespace TimetableDesigner.Backend.Services.Authentication.Core.Commands.Register; @@ -27,12 +29,12 @@ public class RegisterHandler : IRequestHandler PasswordSalt = hash.Salt, }; await _databaseContext.Accounts.AddAsync(account, cancellationToken); - - // Change to outbox pattern - //RegisterEvent eventData = account.ToEvent(); - //await _eventQueuePublisher.PublishAsync(eventData); - await _databaseContext.SaveChangesAsync(cancellationToken); + + Event eventData = Event.Create(new RegisterEvent(account.Id, account.Email)); + await _databaseContext.Events.AddAsync(eventData, cancellationToken); + await _databaseContext.SaveChangesAsync(cancellationToken); + return new RegisterResult(account.Id, account.Email); } diff --git a/TimetableDesigner.Backend.Services.Authentication.Database/Configuration/EventConfiguration.cs b/TimetableDesigner.Backend.Services.Authentication.Database/Configuration/EventConfiguration.cs deleted file mode 100644 index 0b39aef..0000000 --- a/TimetableDesigner.Backend.Services.Authentication.Database/Configuration/EventConfiguration.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using TimetableDesigner.Backend.Services.Authentication.Database.Model; - -namespace TimetableDesigner.Backend.Services.Authentication.Database.Configuration; - -public class EventConfiguration : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.HasKey(x => x.Id); - builder.HasIndex(x => x.Id) - .IsUnique(); - builder.Property(x => x.Id) - .IsRequired(); - - builder.Property(x => x.Payload) - .IsRequired(); - - builder.Property(x => x.PayloadType) - .IsRequired(); - - builder.Property(x => x.OccuredOn) - .IsRequired(); - - builder.Property(x => x.ProcessedOn); - - builder.Property(x => x.RetryCount) - .IsRequired(); - - builder.Property(b => b.Version) - .IsRowVersion(); - } -} \ No newline at end of file diff --git a/TimetableDesigner.Backend.Services.Authentication.Database/DatabaseContext.cs b/TimetableDesigner.Backend.Services.Authentication.Database/DatabaseContext.cs index 2ef02d4..f460aa3 100644 --- a/TimetableDesigner.Backend.Services.Authentication.Database/DatabaseContext.cs +++ b/TimetableDesigner.Backend.Services.Authentication.Database/DatabaseContext.cs @@ -1,14 +1,16 @@ using System.Reflection; using Microsoft.EntityFrameworkCore; +using TimetableDesigner.Backend.Events.OutboxPattern; using TimetableDesigner.Backend.Services.Authentication.Database.Model; namespace TimetableDesigner.Backend.Services.Authentication.Database; -public class DatabaseContext : DbContext +public class DatabaseContext : DbContext, IEventOutboxDbContext { public virtual DbSet Accounts { get; set; } public virtual DbSet RefreshTokens { get; set; } - + public virtual DbSet Events { get; set; } + public DatabaseContext() { } @@ -18,6 +20,10 @@ public class DatabaseContext : DbContext protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseNpgsql("name=Database"); - protected override void OnModelCreating(ModelBuilder modelBuilder) => + protected override void OnModelCreating(ModelBuilder modelBuilder) + { modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetAssembly(typeof(DatabaseContext))!); + modelBuilder.ApplyConfiguration(new EventConfiguration()); + } + } \ No newline at end of file diff --git a/TimetableDesigner.Backend.Services.Authentication.Database/Migrations/20260128230804_Outbox_Pattern_From_Nuget.Designer.cs b/TimetableDesigner.Backend.Services.Authentication.Database/Migrations/20260128230804_Outbox_Pattern_From_Nuget.Designer.cs new file mode 100644 index 0000000..8e9d384 --- /dev/null +++ b/TimetableDesigner.Backend.Services.Authentication.Database/Migrations/20260128230804_Outbox_Pattern_From_Nuget.Designer.cs @@ -0,0 +1,145 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TimetableDesigner.Backend.Services.Authentication.Database; + +#nullable disable + +namespace TimetableDesigner.Backend.Services.Authentication.Database.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20260128230804_Outbox_Pattern_From_Nuget")] + partial class Outbox_Pattern_From_Nuget + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TimetableDesigner.Backend.Events.OutboxPattern.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("LastRetryOn") + .HasColumnType("timestamp with time zone"); + + b.Property("OccuredOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .IsRequired() + .HasColumnType("text"); + + b.Property("PayloadType") + .IsRequired() + .HasColumnType("text"); + + b.Property("RetryCount") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .IsUnique(); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("TimetableDesigner.Backend.Services.Authentication.Database.Model.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityAlwaysColumn(b.Property("Id")); + + b.Property("Email") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("character varying(320)"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("bytea"); + + b.Property("PasswordSalt") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .IsUnique(); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("TimetableDesigner.Backend.Services.Authentication.Database.Model.RefreshToken", b => + { + b.Property("Token") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccountId") + .HasColumnType("bigint"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsExtendable") + .HasColumnType("boolean"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("xid") + .HasColumnName("xmin"); + + b.HasKey("Token"); + + b.HasIndex("AccountId"); + + b.HasIndex("Token") + .IsUnique(); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("TimetableDesigner.Backend.Services.Authentication.Database.Model.RefreshToken", b => + { + b.HasOne("TimetableDesigner.Backend.Services.Authentication.Database.Model.Account", "Account") + .WithMany("RefreshTokens") + .HasForeignKey("AccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("TimetableDesigner.Backend.Services.Authentication.Database.Model.Account", b => + { + b.Navigation("RefreshTokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TimetableDesigner.Backend.Services.Authentication.Database/Migrations/20260128230804_Outbox_Pattern_From_Nuget.cs b/TimetableDesigner.Backend.Services.Authentication.Database/Migrations/20260128230804_Outbox_Pattern_From_Nuget.cs new file mode 100644 index 0000000..b8e488d --- /dev/null +++ b/TimetableDesigner.Backend.Services.Authentication.Database/Migrations/20260128230804_Outbox_Pattern_From_Nuget.cs @@ -0,0 +1,70 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TimetableDesigner.Backend.Services.Authentication.Database.Migrations +{ + /// + public partial class Outbox_Pattern_From_Nuget : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Event"); + + migrationBuilder.CreateTable( + name: "Events", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Payload = table.Column(type: "text", nullable: false), + PayloadType = table.Column(type: "text", nullable: false), + OccuredOn = table.Column(type: "timestamp with time zone", nullable: false), + LastRetryOn = table.Column(type: "timestamp with time zone", nullable: true), + RetryCount = table.Column(type: "bigint", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Events", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_Events_Id", + table: "Events", + column: "Id", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Events"); + + migrationBuilder.CreateTable( + name: "Event", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OccuredOn = table.Column(type: "timestamp with time zone", nullable: false), + Payload = table.Column(type: "text", nullable: false), + PayloadType = table.Column(type: "text", nullable: false), + ProcessedOn = table.Column(type: "timestamp with time zone", nullable: true), + RetryCount = table.Column(type: "bigint", nullable: false), + xmin = table.Column(type: "xid", rowVersion: true, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Event", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_Event_Id", + table: "Event", + column: "Id", + unique: true); + } + } +} diff --git a/TimetableDesigner.Backend.Services.Authentication.Database/Migrations/DatabaseContextModelSnapshot.cs b/TimetableDesigner.Backend.Services.Authentication.Database/Migrations/DatabaseContextModelSnapshot.cs index 17a4968..f258f65 100644 --- a/TimetableDesigner.Backend.Services.Authentication.Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/TimetableDesigner.Backend.Services.Authentication.Database/Migrations/DatabaseContextModelSnapshot.cs @@ -22,6 +22,37 @@ namespace TimetableDesigner.Backend.Services.Authentication.Database.Migrations NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("TimetableDesigner.Backend.Events.OutboxPattern.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("LastRetryOn") + .HasColumnType("timestamp with time zone"); + + b.Property("OccuredOn") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .IsRequired() + .HasColumnType("text"); + + b.Property("PayloadType") + .IsRequired() + .HasColumnType("text"); + + b.Property("RetryCount") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .IsUnique(); + + b.ToTable("Events"); + }); + modelBuilder.Entity("TimetableDesigner.Backend.Services.Authentication.Database.Model.Account", b => { b.Property("Id") @@ -59,43 +90,6 @@ namespace TimetableDesigner.Backend.Services.Authentication.Database.Migrations b.ToTable("Accounts"); }); - modelBuilder.Entity("TimetableDesigner.Backend.Services.Authentication.Database.Model.Event", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("OccuredOn") - .HasColumnType("timestamp with time zone"); - - b.Property("Payload") - .IsRequired() - .HasColumnType("text"); - - b.Property("PayloadType") - .IsRequired() - .HasColumnType("text"); - - b.Property("ProcessedOn") - .HasColumnType("timestamp with time zone"); - - b.Property("RetryCount") - .HasColumnType("bigint"); - - b.Property("Version") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("xid") - .HasColumnName("xmin"); - - b.HasKey("Id"); - - b.HasIndex("Id") - .IsUnique(); - - b.ToTable("Event"); - }); - modelBuilder.Entity("TimetableDesigner.Backend.Services.Authentication.Database.Model.RefreshToken", b => { b.Property("Token") diff --git a/TimetableDesigner.Backend.Services.Authentication.Database/Model/Event.cs b/TimetableDesigner.Backend.Services.Authentication.Database/Model/Event.cs deleted file mode 100644 index 0b701bd..0000000 --- a/TimetableDesigner.Backend.Services.Authentication.Database/Model/Event.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace TimetableDesigner.Backend.Services.Authentication.Database.Model; - -public class Event -{ - public Guid Id { get; set; } - public required string Payload { get; set; } - public required string PayloadType { get; set; } - public DateTimeOffset OccuredOn { get; set; } = DateTimeOffset.UtcNow; - public DateTimeOffset? ProcessedOn { get; set; } - public uint RetryCount { get; set; } - public uint Version { get; set; } -} \ No newline at end of file diff --git a/TimetableDesigner.Backend.Services.Authentication.Database/TimetableDesigner.Backend.Services.Authentication.Database.csproj b/TimetableDesigner.Backend.Services.Authentication.Database/TimetableDesigner.Backend.Services.Authentication.Database.csproj index 8d23535..40af1ca 100644 --- a/TimetableDesigner.Backend.Services.Authentication.Database/TimetableDesigner.Backend.Services.Authentication.Database.csproj +++ b/TimetableDesigner.Backend.Services.Authentication.Database/TimetableDesigner.Backend.Services.Authentication.Database.csproj @@ -8,6 +8,7 @@ + diff --git a/TimetableDesigner.Backend.Services.Authentication/Program.cs b/TimetableDesigner.Backend.Services.Authentication/Program.cs index f972cc9..40b0cd4 100644 --- a/TimetableDesigner.Backend.Services.Authentication/Program.cs +++ b/TimetableDesigner.Backend.Services.Authentication/Program.cs @@ -3,6 +3,7 @@ using FluentValidation; using Microsoft.AspNetCore.Identity.Data; using Microsoft.EntityFrameworkCore; using TimetableDesigner.Backend.Events.Extensions.AspNetCore.OpenApi; +using TimetableDesigner.Backend.Events.OutboxPattern; using TimetableDesigner.Backend.Events.Providers.RabbitMQ; using TimetableDesigner.Backend.Services.Authentication.API; using TimetableDesigner.Backend.Services.Authentication.API.Validators; @@ -28,6 +29,7 @@ public static class Program builder.Services.AddHelpers(); builder.Services.AddValidators(); builder.Services.AddMediatR(x => x.RegisterServicesFromAssembly(typeof(Program).Assembly)); + builder.Services.AddWorkers(); WebApplication app = builder.Build(); @@ -41,6 +43,12 @@ public static class Program app.Run(); } + private static IServiceCollection AddWorkers(this IServiceCollection services) + { + services.AddHostedService>(); + return services; + } + private static IServiceCollection AddHelpers(this IServiceCollection services) { services.AddSingleton(); diff --git a/TimetableDesigner.Backend.Services.Authentication/TimetableDesigner.Backend.Services.Authentication.csproj b/TimetableDesigner.Backend.Services.Authentication/TimetableDesigner.Backend.Services.Authentication.csproj index b89be32..c92c4f5 100644 --- a/TimetableDesigner.Backend.Services.Authentication/TimetableDesigner.Backend.Services.Authentication.csproj +++ b/TimetableDesigner.Backend.Services.Authentication/TimetableDesigner.Backend.Services.Authentication.csproj @@ -18,8 +18,9 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + +