Skip to content

feat(messaging): Outbox dispatcher + Inbox + RabbitMQ publisher (fase 1) - #1

Merged
thomasmoreira merged 2 commits into
mainfrom
feat/messaging-outbox-inbox
Jun 8, 2026
Merged

thomasmoreira merged 2 commits into
mainfrom
feat/messaging-outbox-inbox

Conversation

@thomasmoreira

@thomasmoreira thomasmoreira commented Jun 8, 2026 •

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Outbox pattern implemented for reliable, exactly-once event publishing to RabbitMQ
    • Background dispatcher service to automatically deliver pending events with configurable poll interval and batch size
    • New event serialization and publishing components with DI integration
  • Tests

    • End-to-end integration test verifying single-delivery and outbox mark-as-processed behavior against real PostgreSQL and RabbitMQ
  • Chores

    • Bumped core dependencies (Microsoft.Extensions, EF Core, PostgreSQL driver, RabbitMQ client, Testcontainers)

Implementa o núcleo de mensageria confiável dos BuildingBlocks (spec §5/§10),
sem tocar nos serviços ainda.

Messaging (sem dependência de EF, para evitar ciclo):
- OutboxRecord, IEventSerializer + JsonEventSerializer
- IOutboxProcessor (recebe o publish como callback; a transação vive no Persistence)
- OutboxDispatcher (BackgroundService, polling, escopo por iteração, logging source-gen)
- RabbitMqEventPublisher (RabbitMQ.Client v7, publisher confirms, Id do evento -> message-id)
- DI: AddRabbitMqPublisher / AddOutboxDispatcher

Persistence (depende de Messaging):
- Configs EF snake_case (outbox/inbox), payload jsonb, índice parcial em pending
- MessagingDbContext base, EfOutbox (mesma UoW), EfInbox (dedup por PK)
- EfOutboxProcessor: SELECT ... FOR UPDATE SKIP LOCKED em transação; marca processed
  só após o confirm
- DI: AddOutboxInbox

Testes:
- OutboxDispatcherTests (Testcontainers: Postgres + RabbitMQ reais) prova publish
  exactly-once com message-id correto e 2ª drenagem no-op

Pacotes ajustados para versões reais (EF 10.0.8, Npgsql 10.0.2, RabbitMQ.Client 7.2.1,
Testcontainers 4.12.0). Build verde; Unit 4/4; Integration 1 passou + 3 skipped.
@coderabbitai

coderabbitai Bot commented Jun 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fa38ddc5-8b58-40fb-9fdd-b1e359c6ff3d

📥 Commits

Reviewing files that changed from the base of the PR and between 4caaa82 and eb84895.

📒 Files selected for processing (3)
  • src/BuildingBlocks/Messaging/JsonEventSerializer.cs
  • src/BuildingBlocks/Messaging/RabbitMqEventPublisher.cs
  • src/BuildingBlocks/Persistence/EfOutboxProcessor.cs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/BuildingBlocks/Messaging/JsonEventSerializer.cs
  • src/BuildingBlocks/Persistence/EfOutboxProcessor.cs
  • src/BuildingBlocks/Messaging/RabbitMqEventPublisher.cs

📝 Walkthrough

Walkthrough

This PR introduces a transactional outbox pattern for reliable event publishing to RabbitMQ. It adds messaging contracts and types, a JSON event serializer, a RabbitMQ publisher with confirmations, a background OutboxDispatcher, EF Core schema and implementations for outbox/inbox processing with row locking, DI registration extensions, package/version updates, and integration tests validating exactly-once delivery.

Changes

Outbox Pattern with RabbitMQ Integration

Layer / File(s) Summary
Messaging Contracts and Data Types
src/BuildingBlocks/Messaging/IEventPublisher.cs, src/BuildingBlocks/Messaging/IEventSerializer.cs, src/BuildingBlocks/Messaging/IOutboxProcessor.cs, src/BuildingBlocks/Messaging/OutboxRecord.cs
IEventPublisher updated to accept OutboxRecord. New IEventSerializer and IOutboxProcessor interfaces added. OutboxRecord models serialized outbox rows.
Event Serialization
src/BuildingBlocks/Messaging/JsonEventSerializer.cs
JsonEventSerializer implements IEventSerializer using System.Text.Json (Web defaults), deriving the event Type from runtime CLR type and producing the JSON Payload.
RabbitMQ Publisher
src/BuildingBlocks/Messaging/RabbitMqOptions.cs, src/BuildingBlocks/Messaging/RabbitMqEventPublisher.cs
RabbitMqOptions exposes host/port/credentials and exchange. RabbitMqEventPublisher publishes OutboxRecord to a durable topic exchange with publisher confirmations and lazy, thread-safe AMQP channel initialization.
Outbox Dispatcher Service
src/BuildingBlocks/Messaging/OutboxDispatcherOptions.cs, src/BuildingBlocks/Messaging/OutboxDispatcher.cs
OutboxDispatcherOptions configures poll interval and batch size. OutboxDispatcher is a hosted BackgroundService that polls and calls IOutboxProcessor.ProcessPendingAsync(..., publisher.PublishAsync, ...); exposes DrainAsync for tests and logs poll errors without stopping.
EF Core Persistence Context and Schema
src/BuildingBlocks/Persistence/BuildingBlocks.Persistence.csproj, src/BuildingBlocks/Persistence/MessagingDbContext.cs, src/BuildingBlocks/Persistence/Configurations/OutboxMessageConfiguration.cs, src/BuildingBlocks/Persistence/Configurations/InboxMessageConfiguration.cs
Adds MessagingDbContext with Outbox and Inbox DbSets. OutboxMessageConfiguration maps JSONB payload and defines a partial index for pending rows. InboxMessageConfiguration maps inbox table with MessageId primary key.
Persistence Implementations
src/BuildingBlocks/Persistence/EfOutbox.cs, src/BuildingBlocks/Persistence/EfInbox.cs, src/BuildingBlocks/Persistence/EfOutboxProcessor.cs
EfOutbox serializes events and adds OutboxMessage to the context (no commit). EfInbox checks/marks processed message IDs. EfOutboxProcessor selects pending rows with FOR UPDATE SKIP LOCKED, publishes via injected callback, marks rows processed only after successful publish, and commits the transaction.
Dependency Injection Wiring
src/BuildingBlocks/Messaging/BuildingBlocks.Messaging.csproj, src/BuildingBlocks/Messaging/MessagingServiceCollectionExtensions.cs, src/BuildingBlocks/Persistence/PersistenceServiceCollectionExtensions.cs
Adds project package refs for messaging, AddRabbitMqPublisher to register RabbitMqEventPublisher, AddOutboxDispatcher to register the hosted dispatcher, and AddOutboxInbox to register serializer, ef outbox/inbox, and processor services.
Integration Tests and Dependencies
Directory.Packages.props, tests/Integration/Tests.Integration.csproj, tests/Integration/OutboxDispatcherTests.cs
Central package versions bumped (Microsoft.Extensions 10.0.8, EF Core 10.0.8, Npgsql 0.10.2, RabbitMQ.Client 7.2.1, Testcontainers 4.12.0). OutboxDispatcherTests uses Testcontainers to verify an OrderPlaced event is published exactly once and the outbox row is marked processed.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hop through code with careful paws,
Outbox rows and RabbitMQ laws,
I serialize, I queue, I send,
Once and only—no loop to mend,
A tiny rabbit tests and nods, applause!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.13% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main changes: introducing outbox dispatcher, inbox, and RabbitMQ publisher components as phase 1 of a messaging system.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/messaging-outbox-inbox

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (8)
src/BuildingBlocks/Messaging/IOutboxProcessor.cs (1)

3-8: 💤 Low value

Consider removing database-specific implementation detail from interface documentation.

The documentation mentions FOR UPDATE SKIP LOCKED, which is a Postgres-specific locking mechanism. While the current implementation uses Postgres, the interface abstraction should remain database-agnostic. Consider rephrasing to describe the behavior (e.g., "locks selected rows to prevent concurrent processing") rather than the specific SQL syntax.

📝 Suggested documentation improvement
 /// <summary>
-/// Drains a batch of pending outbox rows inside a single DB transaction that holds a
-/// <c>FOR UPDATE SKIP LOCKED</c> lock on the selected rows. The transaction (and the EF
-/// dependency) lives in the persistence layer; the publish side is injected as a callback
-/// so Messaging stays free of any EF reference (avoids the circular dependency).
+/// Drains a batch of pending outbox rows inside a single DB transaction that locks
+/// selected rows to prevent concurrent processing. The transaction (and the EF dependency)
+/// lives in the persistence layer; the publish side is injected as a callback so Messaging
+/// stays free of any EF reference (avoids the circular dependency).
 /// </summary>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/BuildingBlocks/Messaging/IOutboxProcessor.cs` around lines 3 - 8, Update
the XML doc on the IOutboxProcessor interface to remove the Postgres-specific
phrase "FOR UPDATE SKIP LOCKED" and instead describe the behavior: that the
method drains a batch of pending outbox rows inside a single database
transaction which locks the selected rows to prevent concurrent processing,
while keeping the publish callback injection and EF/persistence separation
wording intact (edit the summary for the method/ interface named
IOutboxProcessor to be database-agnostic and describe the locking behaviour
rather than specific SQL syntax).
src/BuildingBlocks/Messaging/JsonEventSerializer.cs (1)

12-12: 💤 Low value

JsonSerializerDefaults.Web uses camelCase property naming.

The JsonSerializerDefaults.Web preset configures property names to be camelCase (e.g., orderId instead of OrderId). Ensure downstream consumers expect this convention, or explicitly document it as part of the message contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/BuildingBlocks/Messaging/JsonEventSerializer.cs` at line 12, The
JsonEventSerializer currently uses JsonSerializerDefaults.Web which applies
camelCase property names (JsonSerializerOptions Options =
new(JsonSerializerDefaults.Web)); update the serializer to make naming explicit
and align with the message contract: either set Options.PropertyNamingPolicy
explicitly (e.g., JsonNamingPolicy.CamelCase if consumers expect camelCase, or
null/Default if PascalCase is required) and remove reliance on the Web preset,
or add clear documentation in the message contract for JsonEventSerializer that
downstream consumers must expect camelCase property names; adjust any
serialization/deserialization tests and consumers accordingly to match the
chosen policy.
tests/Integration/OutboxDispatcherTests.cs (1)

99-111: 💤 Low value

Consider adding validation for UserInfo parsing edge cases.

The password parsing logic at line 102 could throw IndexOutOfRangeException if uri.UserInfo is empty or malformed. While Testcontainers should always provide a valid connection string, adding a guard would make the test more robust.

🛡️ Proposed defensive fix
     private IOptions<RabbitMqOptions> BuildRabbitOptions()
     {
         var uri = new Uri(_rabbit.GetConnectionString());
-        var parts = uri.UserInfo.Split(':', 2);
+        if (string.IsNullOrEmpty(uri.UserInfo))
+        {
+            throw new InvalidOperationException("RabbitMQ connection string missing UserInfo");
+        }
+        
+        var parts = uri.UserInfo.Split(':', 2);
         return Options.Create(new RabbitMqOptions
         {
             Host = uri.Host,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/Integration/OutboxDispatcherTests.cs` around lines 99 - 111, The
UserInfo parsing in BuildRabbitOptions can throw when uri.UserInfo is
empty/malformed; update BuildRabbitOptions to defensively handle that by
checking string.IsNullOrEmpty(uri.UserInfo) before splitting, or by using a
split that safely yields zero/one/two elements, then set Username and Password
from the resulting parts with fallbacks (empty string) if parts.Length < 1 or <
2; ensure you reference BuildRabbitOptions, uri.UserInfo and the parts array
when making the guard so Username/Password never cause an
IndexOutOfRangeException.
src/BuildingBlocks/Persistence/Configurations/OutboxMessageConfiguration.cs (2)

18-18: ⚡ Quick win

Consider adding a check constraint for non-negative attempts.

Adding a check constraint ensures data integrity at the database level, preventing invalid retry counts.

🛡️ Suggested enhancement
-builder.Property(x => x.Attempts).HasColumnName("attempts");
+builder.Property(x => x.Attempts)
+    .HasColumnName("attempts")
+    .HasCheckConstraint("CK_outbox_attempts_nonnegative", "attempts >= 0");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/BuildingBlocks/Persistence/Configurations/OutboxMessageConfiguration.cs`
at line 18, Add a DB-level check constraint to ensure OutboxMessage attempts
cannot be negative: in OutboxMessageConfiguration after the existing
builder.Property(x => x.Attempts).HasColumnName("attempts") call, add a
HasCheckConstraint on the entity (using OutboxMessageConfiguration's builder)
such as a constraint name like "CK_OutboxMessage_Attempts_NonNegative" with
condition referencing the attempts column (e.g., "attempts >= 0") so the
database enforces non-negative values for the Attempts property.

12-13: ⚡ Quick win

Consider explicit column type for Id.

Explicitly configuring the Id column type as uuid improves schema clarity and ensures consistent behavior across environments.

📝 Suggested enhancement
 builder.HasKey(x => x.Id);
-builder.Property(x => x.Id).HasColumnName("id");
+builder.Property(x => x.Id)
+    .HasColumnName("id")
+    .HasColumnType("uuid");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/BuildingBlocks/Persistence/Configurations/OutboxMessageConfiguration.cs`
around lines 12 - 13, The Id column lacks an explicit DB type; update the
OutboxMessageConfiguration to set the column type for the Id property by
extending the builder.Property(x => x.Id) configuration (the same property
referenced by builder.HasKey(x => x.Id)) to include a HasColumnType("uuid") call
so the column is created as uuid in the schema.
src/BuildingBlocks/Persistence/Configurations/InboxMessageConfiguration.cs (1)

13-14: ⚡ Quick win

Consider explicit column type for MessageId.

While EF Core will infer the column type from the C# property, explicitly configuring it improves clarity and prevents migration drift if the property type changes.

📝 Suggested enhancement

If MessageId is a Guid:

 builder.HasKey(x => x.MessageId);
-builder.Property(x => x.MessageId).HasColumnName("message_id");
+builder.Property(x => x.MessageId)
+    .HasColumnName("message_id")
+    .HasColumnType("uuid");

If MessageId is a string:

 builder.HasKey(x => x.MessageId);
-builder.Property(x => x.MessageId).HasColumnName("message_id");
+builder.Property(x => x.MessageId)
+    .HasColumnName("message_id")
+    .HasMaxLength(255);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/BuildingBlocks/Persistence/Configurations/InboxMessageConfiguration.cs`
around lines 13 - 14, The mapping for MessageId in InboxMessageConfiguration
currently only sets the column name; explicitly configure the column type to
prevent migration drift: update the builder.Property(x => x.MessageId) chain to
include HasColumnType with the appropriate SQL type (e.g., "uuid" or
"uniqueidentifier" for a Guid, or a sized text type like "varchar(36)" /
"nvarchar(36)" for a string) while keeping builder.HasKey(x => x.MessageId) and
the HasColumnName("message_id") call.
src/BuildingBlocks/Persistence/EfOutboxProcessor.cs (1)

41-47: 💤 Low value

Consider documenting at-least-once delivery semantics in failure scenarios.

The current implementation provides exactly-once publishing in the happy path. However, if publishAsync succeeds (message confirmed by RabbitMQ) but SaveChanges or CommitAsync subsequently fails, the row remains pending and will be re-published on the next poll, resulting in at-least-once delivery.

This is inherent to the outbox pattern with a separate message broker and requires idempotent consumers. Consider adding a note to the class documentation to make this explicit for future maintainers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/BuildingBlocks/Persistence/EfOutboxProcessor.cs` around lines 41 - 47,
Update the EfOutboxProcessor class documentation to explicitly state that while
the happy path provides single publish and removal, failures after publishAsync
(e.g., SaveChangesAsync or CommitAsync failing) will leave the outbox row
pending and lead to message re-publish on the next poll, resulting in
at-least-once delivery semantics; mention that consumers must be idempotent and
reference the relevant methods publishAsync, SaveChangesAsync and CommitAsync in
the doc comment so future maintainers understand the failure mode and needed
precautions.
src/BuildingBlocks/Messaging/MessagingServiceCollectionExtensions.cs (1)

20-27: ⚡ Quick win

Consider guarding against multiple dispatcher registrations.

AddHostedService (line 25) does not prevent duplicate registrations like TryAddSingleton does. Calling AddOutboxDispatcher multiple times will register multiple OutboxDispatcher instances that run concurrently and compete for work via FOR UPDATE SKIP LOCKED.

While functionally safe, multiple instances within a single process create unnecessary overhead (duplicate polling, scope creation, and queries) unless explicitly desired for in-process horizontal scaling.

Consider using TryAddEnumerable to prevent accidental double-registration:

🛡️ Suggested guard using TryAddEnumerable
     services.Configure(configure ?? (_ => { }));
-    services.AddHostedService<OutboxDispatcher>();
+    services.TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService, OutboxDispatcher>());
     return services;

Alternatively, document that multiple registrations are supported if in-process scaling is an intentional design choice.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/BuildingBlocks/Messaging/MessagingServiceCollectionExtensions.cs` around
lines 20 - 27, The AddOutboxDispatcher extension currently calls
services.AddHostedService<OutboxDispatcher>() which allows duplicate
registrations; change it to register the dispatcher with TryAddEnumerable to
prevent accidental double-registration by using
TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService,
typeof(OutboxDispatcher) / new ServiceDescriptor(typeof(IHostedService),
typeof(OutboxDispatcher), ServiceLifetime.Singleton)>) so that
AddOutboxDispatcher (and OutboxDispatcher) are guarded against multiple
registrations while keeping the existing services.Configure(configure) behavior;
update the registration call referencing AddOutboxDispatcher, OutboxDispatcher,
AddHostedService and TryAddEnumerable accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/BuildingBlocks/Messaging/JsonEventSerializer.cs`:
- Around line 14-21: JsonEventSerializer.Serialize currently uses
message.GetType().Name as the event discriminator (in method Serialize and for
IntegrationEvent messages), which is fragile; change it to use a more stable
discriminator such as message.GetType().FullName (falling back to Name if
FullName is null), and sanitize common artifacts (replace '+' with '.' and strip
generic backtick suffixes) so generic and nested types produce reasonable
strings; additionally add a TODO comment and/or plan to implement a robust type
registry/attribute-based mapping (e.g., EventTypeAttribute) for future stable
identifiers and ensure consumers use the same mapping for deserialization.

In `@src/BuildingBlocks/Messaging/RabbitMqEventPublisher.cs`:
- Around line 88-101: The DisposeAsync method currently disposes _channel,
_connection and _gate sequentially so an exception from _channel.DisposeAsync()
can prevent disposing the others; update RabbitMqEventPublisher.DisposeAsync to
ensure each resource is disposed regardless of earlier failures by wrapping each
await _channel.DisposeAsync(), await _connection.DisposeAsync(), and
_gate.Dispose() in their own try-catch (or using a try/finally that guarantees
subsequent disposals), catching and optionally logging exceptions so disposal
proceeds for _connection and _gate even if _channel disposal fails.
- Around line 22-43: The PublishAsync method uses record.Type and record.Payload
without validating them; add defensive null/empty checks at the start of
PublishAsync (after ArgumentNullException.ThrowIfNull(record)) to throw
ArgumentException/ArgumentNullException with clear messages if record.Type is
null/empty or record.Payload is null; update error messages to reference
OutboxRecord.Id for context, and only proceed to call EnsureChannelAsync,
construct BasicProperties, and call channel.BasicPublishAsync when these
validations pass.

In `@src/BuildingBlocks/Persistence/BuildingBlocks.Persistence.csproj`:
- Line 13: The project TFM is set to net10.0 in the TargetFramework element of
BuildingBlocks.Persistence.csproj which requires a .NET 10 SDK (preview) not
guaranteed in current CI/dev images; either pin the required preview SDK by
adding/updating a global.json that references the exact .NET 10 preview SDK
version used in your build matrix, or change the TargetFramework in
BuildingBlocks.Persistence.csproj from net10.0 to a GA TFM your CI already
supports (e.g., net8.0 or net7.0) and update any API usage accordingly so
local/CI builds no longer depend on an unavailable preview SDK.

In `@src/BuildingBlocks/Persistence/EfOutboxProcessor.cs`:
- Around line 35-39: EfOutboxProcessor currently reads OutboxMessage.Attempts
but never updates it; implement attempt tracking and backoff by incrementing
Attempts and recording a LastAttemptAt timestamp on each process try, performing
a backoff check before calling publishAsync, and only setting ProcessedAt after
a successful publish; specifically, in the loop that iterates pending messages
(where publishAsync(new OutboxRecord(...)) is called) update message.Attempts++
and message.LastAttemptAt = DateTimeOffset.UtcNow before attempting publish,
skip/pause attempts if (now - message.LastAttemptAt) <
backoffForAttempts(message.Attempts), and on successful publish set
message.ProcessedAt = UtcNow, while on repeated failures when message.Attempts
exceeds a poison threshold mark it as failed/dead-letter (or set ProcessedAt and
a Poisoned flag) so poison-message handling works as intended.

---

Nitpick comments:
In `@src/BuildingBlocks/Messaging/IOutboxProcessor.cs`:
- Around line 3-8: Update the XML doc on the IOutboxProcessor interface to
remove the Postgres-specific phrase "FOR UPDATE SKIP LOCKED" and instead
describe the behavior: that the method drains a batch of pending outbox rows
inside a single database transaction which locks the selected rows to prevent
concurrent processing, while keeping the publish callback injection and
EF/persistence separation wording intact (edit the summary for the method/
interface named IOutboxProcessor to be database-agnostic and describe the
locking behaviour rather than specific SQL syntax).

In `@src/BuildingBlocks/Messaging/JsonEventSerializer.cs`:
- Line 12: The JsonEventSerializer currently uses JsonSerializerDefaults.Web
which applies camelCase property names (JsonSerializerOptions Options =
new(JsonSerializerDefaults.Web)); update the serializer to make naming explicit
and align with the message contract: either set Options.PropertyNamingPolicy
explicitly (e.g., JsonNamingPolicy.CamelCase if consumers expect camelCase, or
null/Default if PascalCase is required) and remove reliance on the Web preset,
or add clear documentation in the message contract for JsonEventSerializer that
downstream consumers must expect camelCase property names; adjust any
serialization/deserialization tests and consumers accordingly to match the
chosen policy.

In `@src/BuildingBlocks/Messaging/MessagingServiceCollectionExtensions.cs`:
- Around line 20-27: The AddOutboxDispatcher extension currently calls
services.AddHostedService<OutboxDispatcher>() which allows duplicate
registrations; change it to register the dispatcher with TryAddEnumerable to
prevent accidental double-registration by using
TryAddEnumerable(ServiceDescriptor.Singleton<IHostedService,
typeof(OutboxDispatcher) / new ServiceDescriptor(typeof(IHostedService),
typeof(OutboxDispatcher), ServiceLifetime.Singleton)>) so that
AddOutboxDispatcher (and OutboxDispatcher) are guarded against multiple
registrations while keeping the existing services.Configure(configure) behavior;
update the registration call referencing AddOutboxDispatcher, OutboxDispatcher,
AddHostedService and TryAddEnumerable accordingly.

In `@src/BuildingBlocks/Persistence/Configurations/InboxMessageConfiguration.cs`:
- Around line 13-14: The mapping for MessageId in InboxMessageConfiguration
currently only sets the column name; explicitly configure the column type to
prevent migration drift: update the builder.Property(x => x.MessageId) chain to
include HasColumnType with the appropriate SQL type (e.g., "uuid" or
"uniqueidentifier" for a Guid, or a sized text type like "varchar(36)" /
"nvarchar(36)" for a string) while keeping builder.HasKey(x => x.MessageId) and
the HasColumnName("message_id") call.

In `@src/BuildingBlocks/Persistence/Configurations/OutboxMessageConfiguration.cs`:
- Line 18: Add a DB-level check constraint to ensure OutboxMessage attempts
cannot be negative: in OutboxMessageConfiguration after the existing
builder.Property(x => x.Attempts).HasColumnName("attempts") call, add a
HasCheckConstraint on the entity (using OutboxMessageConfiguration's builder)
such as a constraint name like "CK_OutboxMessage_Attempts_NonNegative" with
condition referencing the attempts column (e.g., "attempts >= 0") so the
database enforces non-negative values for the Attempts property.
- Around line 12-13: The Id column lacks an explicit DB type; update the
OutboxMessageConfiguration to set the column type for the Id property by
extending the builder.Property(x => x.Id) configuration (the same property
referenced by builder.HasKey(x => x.Id)) to include a HasColumnType("uuid") call
so the column is created as uuid in the schema.

In `@src/BuildingBlocks/Persistence/EfOutboxProcessor.cs`:
- Around line 41-47: Update the EfOutboxProcessor class documentation to
explicitly state that while the happy path provides single publish and removal,
failures after publishAsync (e.g., SaveChangesAsync or CommitAsync failing) will
leave the outbox row pending and lead to message re-publish on the next poll,
resulting in at-least-once delivery semantics; mention that consumers must be
idempotent and reference the relevant methods publishAsync, SaveChangesAsync and
CommitAsync in the doc comment so future maintainers understand the failure mode
and needed precautions.

In `@tests/Integration/OutboxDispatcherTests.cs`:
- Around line 99-111: The UserInfo parsing in BuildRabbitOptions can throw when
uri.UserInfo is empty/malformed; update BuildRabbitOptions to defensively handle
that by checking string.IsNullOrEmpty(uri.UserInfo) before splitting, or by
using a split that safely yields zero/one/two elements, then set Username and
Password from the resulting parts with fallbacks (empty string) if parts.Length
< 1 or < 2; ensure you reference BuildRabbitOptions, uri.UserInfo and the parts
array when making the guard so Username/Password never cause an
IndexOutOfRangeException.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a556cdfa-eba4-4127-832e-740a17898d42

📥 Commits

Reviewing files that changed from the base of the PR and between a8bba30 and 4caaa82.

📒 Files selected for processing (22)
  • Directory.Packages.props
  • src/BuildingBlocks/Messaging/BuildingBlocks.Messaging.csproj
  • src/BuildingBlocks/Messaging/IEventPublisher.cs
  • src/BuildingBlocks/Messaging/IEventSerializer.cs
  • src/BuildingBlocks/Messaging/IOutboxProcessor.cs
  • src/BuildingBlocks/Messaging/JsonEventSerializer.cs
  • src/BuildingBlocks/Messaging/MessagingServiceCollectionExtensions.cs
  • src/BuildingBlocks/Messaging/OutboxDispatcher.cs
  • src/BuildingBlocks/Messaging/OutboxDispatcherOptions.cs
  • src/BuildingBlocks/Messaging/OutboxRecord.cs
  • src/BuildingBlocks/Messaging/RabbitMqEventPublisher.cs
  • src/BuildingBlocks/Messaging/RabbitMqOptions.cs
  • src/BuildingBlocks/Persistence/BuildingBlocks.Persistence.csproj
  • src/BuildingBlocks/Persistence/Configurations/InboxMessageConfiguration.cs
  • src/BuildingBlocks/Persistence/Configurations/OutboxMessageConfiguration.cs
  • src/BuildingBlocks/Persistence/EfInbox.cs
  • src/BuildingBlocks/Persistence/EfOutbox.cs
  • src/BuildingBlocks/Persistence/EfOutboxProcessor.cs
  • src/BuildingBlocks/Persistence/MessagingDbContext.cs
  • src/BuildingBlocks/Persistence/PersistenceServiceCollectionExtensions.cs
  • tests/Integration/OutboxDispatcherTests.cs
  • tests/Integration/Tests.Integration.csproj

Comment thread src/BuildingBlocks/Messaging/JsonEventSerializer.cs
Comment thread src/BuildingBlocks/Messaging/RabbitMqEventPublisher.cs
Comment thread src/BuildingBlocks/Messaging/RabbitMqEventPublisher.cs
</ItemGroup>

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check installed .NET SDK versions and validate target framework

# Check installed SDKs
dotnet --list-sdks

# Check if the project builds successfully
dotnet build src/BuildingBlocks/Persistence/BuildingBlocks.Persistence.csproj --configuration Debug 2>&1 | head -20

Repository: thomasmoreira/distributed-consistency-lab

Length of output: 193


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Print the relevant csproj section around the TargetFramework
FILE="src/BuildingBlocks/Persistence/BuildingBlocks.Persistence.csproj"
if [ -f "$FILE" ]; then
  sed -n '1,60p' "$FILE" | cat -n
else
  echo "Missing file: $FILE"
  exit 1
fi

Repository: thomasmoreira/distributed-consistency-lab

Length of output: 721


🌐 Web query:

.NET target framework monikers net10.0 released preview as of 2025

💡 Result:

As of 2026-06-08, the.NET target framework moniker (TFM) for.NET 10 is net10.0 [1]..NET 10 was released in 2025, with its first preview (Preview 1) announced on February 25, 2025 [2]. It subsequently underwent a standard preview release cycle throughout the year before the General Availability (GA) version, 10.0.0, was officially released on November 11, 2025 [3][4][5]. The net10.0 TFM is currently the standard moniker for targeting the stable.NET 10 framework [1].

Citations:


Clarify net10.0 support: it’s .NET 10 (preview in early 2025), not a March-2025 GA TFM

src/BuildingBlocks/Persistence/BuildingBlocks.Persistence.csproj targets net10.0. .NET 10’s preview started in Feb 2025, with GA arriving later (Nov 2025), so a March-2025 build environment must have the matching .NET 10 preview SDK available for net10.0 or the build will fail.

Make CI/dev builds use the correct SDK (e.g., via a pinned global.json), or switch the TFM to a GA framework your supported build matrix already provides.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/BuildingBlocks/Persistence/BuildingBlocks.Persistence.csproj` at line 13,
The project TFM is set to net10.0 in the TargetFramework element of
BuildingBlocks.Persistence.csproj which requires a .NET 10 SDK (preview) not
guaranteed in current CI/dev images; either pin the required preview SDK by
adding/updating a global.json that references the exact .NET 10 preview SDK
version used in your build matrix, or change the TargetFramework in
BuildingBlocks.Persistence.csproj from net10.0 to a GA TFM your CI already
supports (e.g., net8.0 or net7.0) and update any API usage accordingly so
local/CI builds no longer depend on an unavailable preview SDK.

Comment thread src/BuildingBlocks/Persistence/EfOutboxProcessor.cs
…guro)

Resolve os pontos válidos da review automatizada do PR:
- RabbitMqEventPublisher: valida record.Type/Payload não-nulos antes de publicar
- RabbitMqEventPublisher: DisposeAsync com finally aninhado garante que conexão e
  semáforo são liberados mesmo se o dispose do canal lançar (sem leak)
- Documenta limitações conscientes como <remarks>: discriminador GetType().Name
  (frágil a rename; ok para records planos) e ausência de backoff/poison handling
  (attempts não incrementado; head-of-line blocking) — ambas fora do escopo da fase 1

Falso positivo descartado: net10.0 não é preview (GA nov/2025, SDK pinado, CI verde).
Build verde; unit 4/4; integration exactly-once passou.
@thomasmoreira

Copy link
Copy Markdown
Owner Author

@thomasmoreira
thomasmoreira merged commit 344e72e into main Jun 8, 2026
2 checks passed
@thomasmoreira
thomasmoreira deleted the feat/messaging-outbox-inbox branch June 8, 2026 14:39
thomasmoreira added a commit that referenced this pull request Jun 10, 2026
feat(messaging): Outbox dispatcher + Inbox + RabbitMQ publisher (fase 1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant