Skip to content

Commit

Permalink
Add outbox between domain events and their publishing
Browse files Browse the repository at this point in the history
  • Loading branch information
Kishotta committed May 27, 2024
1 parent f8c2e7f commit ade94b6
Show file tree
Hide file tree
Showing 35 changed files with 650 additions and 54 deletions.
4 changes: 4 additions & 0 deletions src/Evently/Api/Evently.Api/modules.users.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
"ConfidentialClientId": "evently-confidential-client",
"ConfidentialClientSecret": "1anQtMszBLyFhvNuhQBIXfFqVFhArjpO",
"PublicClientId": "evently-public-client"
},
"Outbox": {
"IntervalInSeconds": 15,
"BatchSize": "20"
}
}
}
4 changes: 4 additions & 0 deletions src/Evently/Api/Evently.Api/modules.users.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
"ConfidentialClientId": "",
"ConfidentialClientSecret": "",
"PublicClientId": ""
},
"Outbox": {
"IntervalInSeconds": 5,
"BatchSize": "50"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public static Action<IServiceProvider, DbContextOptionsBuilder> StandardOptions(
optionsBuilder.MigrationsHistoryTable(HistoryRepository.DefaultTableName, schema);
}).UseSnakeCaseNamingConvention()
.AddInterceptors(
serviceProvider.GetRequiredService<PublishDomainEventsInterceptor>(),
serviceProvider.GetRequiredService<InsertOutboxMessagesInterceptor>(),
serviceProvider.GetRequiredService<WriteAuditLogInterceptor>());
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
<PackageReference Include="MassTransit" Version="8.2.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.5" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.5" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
<PackageReference Update="SonarAnalyzer.CSharp" Version="9.25.1.91650">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.9.0" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Npgsql;
using Quartz;
using StackExchange.Redis;

namespace Evently.Common.Infrastructure;
Expand All @@ -35,10 +36,16 @@ public static IServiceCollection AddInfrastructure(

services.AddScoped<IDbConnectionFactory, DbConnectionFactory>();

services.TryAddSingleton<PublishDomainEventsInterceptor>();
services.TryAddSingleton<InsertOutboxMessagesInterceptor>();

services.TryAddSingleton<IDateTimeProvider, DateTimeProvider>();

services.AddQuartz();
services.AddQuartzHostedService(options =>
{
options.WaitForJobsToComplete = true;
});

try
{
IConnectionMultiplexer connectionMultiplexer = ConnectionMultiplexer.Connect(cacheConnectionString);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Evently.Common.Domain;
using Evently.Common.Infrastructure.Serialization;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Newtonsoft.Json;

namespace Evently.Common.Infrastructure.Outbox;

public sealed class InsertOutboxMessagesInterceptor : SaveChangesInterceptor
{
public override async ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = new CancellationToken())
{
if(eventData.Context is not null)
InsertOutboxMessages(eventData.Context);

return await base.SavingChangesAsync(eventData, result, cancellationToken);
}
private static void InsertOutboxMessages(
DbContext context)
{
var outboxMessages = context
.ChangeTracker
.Entries<Entity>()
.Select(entry => entry.Entity)
.SelectMany(entity =>
{
var domainEvents = entity.GetDomainEvents();
entity.ClearDomainEvents();
return domainEvents;
})
.Select(domainEvent => new OutboxMessage
{
Id = domainEvent.Id,
Type = domainEvent.GetType().Name,
Content = JsonConvert.SerializeObject(domainEvent, SerializerSettings.Instance),
OccuredAtUtc = domainEvent.OccuredAtUtc,
})
.ToList();

context.Set<OutboxMessage>().AddRange(outboxMessages);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Evently.Common.Infrastructure.Outbox;

public sealed class OutboxMessage
{
public Guid Id { get; init; }
public string Type { get; init; } = string.Empty;
public string Content { get; init; } = string.Empty;
public DateTime OccuredAtUtc { get; init; }
public DateTime? ProcessedAtUtc { get; init; }
public string? Error { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Evently.Common.Infrastructure.Outbox;

public sealed class OutboxMessageConfiguration : IEntityTypeConfiguration<OutboxMessage>
{
public void Configure(EntityTypeBuilder<OutboxMessage> builder)
{
builder.ToTable("outbox_messages");

builder.HasKey(outboxMessage => outboxMessage.Id);

builder.Property(outBoxMessage => outBoxMessage.Content)
.HasMaxLength(3000)
.HasColumnType("jsonb");
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Newtonsoft.Json;

namespace Evently.Common.Infrastructure.Serialization;

public static class SerializerSettings
{
public static readonly JsonSerializerSettings Instance = new()
{
TypeNameHandling = TypeNameHandling.All,
MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead,
};
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using Evently.Common.Domain;
using Evently.Common.Domain.Auditing;
using Evently.Modules.Attendance.Domain.Tickets;

namespace Evently.Modules.Attendance.Domain.Attendees;

[Auditable]
public sealed class Attendee : Entity
{
public Guid Id { get; private set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using Evently.Common.Domain;
using Evently.Common.Domain.Auditing;

namespace Evently.Modules.Attendance.Domain.Events;

[Auditable]
public sealed class Event : Entity
{
public Guid Id { get; private set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using Evently.Common.Domain;
using Evently.Common.Domain.Auditing;
using Evently.Modules.Attendance.Domain.Attendees;
using Evently.Modules.Attendance.Domain.Events;

namespace Evently.Modules.Attendance.Domain.Tickets;

[Auditable]
public sealed class Ticket : Entity
{
public Guid Id { get; private set; }
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,23 @@ protected override void Up(MigrationBuilder migrationBuilder)
table.PrimaryKey("pk_events", x => x.id);
});

migrationBuilder.CreateTable(
name: "outbox_messages",
schema: "attendance",
columns: table => new
{
id = table.Column<Guid>(type: "uuid", nullable: false),
type = table.Column<string>(type: "text", nullable: false),
content = table.Column<string>(type: "jsonb", maxLength: 3000, nullable: false),
occured_at_utc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
processed_at_utc = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
error = table.Column<string>(type: "text", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_outbox_messages", x => x.id);
});

migrationBuilder.CreateTable(
name: "tickets",
schema: "attendance",
Expand Down Expand Up @@ -123,6 +140,10 @@ protected override void Down(MigrationBuilder migrationBuilder)
name: "audit_logs",
schema: "attendance");

migrationBuilder.DropTable(
name: "outbox_messages",
schema: "attendance");

migrationBuilder.DropTable(
name: "tickets",
schema: "attendance");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,42 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.ToTable("audit_logs", "attendance");
});

modelBuilder.Entity("Evently.Common.Infrastructure.Outbox.OutboxMessage", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uuid")
.HasColumnName("id");

b.Property<string>("Content")
.IsRequired()
.HasMaxLength(3000)
.HasColumnType("jsonb")
.HasColumnName("content");

b.Property<string>("Error")
.HasColumnType("text")
.HasColumnName("error");

b.Property<DateTime>("OccuredAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("occured_at_utc");

b.Property<DateTime?>("ProcessedAtUtc")
.HasColumnType("timestamp with time zone")
.HasColumnName("processed_at_utc");

b.Property<string>("Type")
.IsRequired()
.HasColumnType("text")
.HasColumnName("type");

b.HasKey("Id")
.HasName("pk_outbox_messages");

b.ToTable("outbox_messages", "attendance");
});

modelBuilder.Entity("Evently.Modules.Attendance.Domain.Attendees.Attendee", b =>
{
b.Property<Guid>("Id")
Expand Down
Loading

0 comments on commit ade94b6

Please sign in to comment.