You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/articles/cqrs-pipeline.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -98,7 +98,7 @@ No additional wiring is needed – the validator is picked up automatically from
98
98
99
99
### Request Logging
100
100
101
-
`RequestLoggingPipelineBehavior` logs the start and completion of every request, attaching the serialised error to the logging scope when the result is a failure.
101
+
`RequestLoggingPipelineBehavior` logs the start and completion of requests whose response type is `Result` (or `Result<T>`), attaching the serialised error to the logging scope when the result is a failure. Requests with any other response type are not covered by this behavior.
Copy file name to clipboardExpand all lines: docs/articles/inbox-pattern.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,7 +4,7 @@ Message delivery is **at-least-once**: a broker can redeliver a message (after a
4
4
5
5
The inbox pattern makes consumers **idempotent**: it records which messages have already been processed and skips duplicates. `Vulthil.Messaging.Inbox` provides this as a consume filter, with the processed-marker written in the **same transaction** as the consumer's business changes — so on a relational store, processing is exactly-once.
6
6
7
-
This is an *idempotent receiver*, not a store-and-forward inbox: it rides on top of the transport's existing retry, fault, and dead-letter machinery rather than re-implementing them. Bounding a consumer that keeps failing is therefore **not** the guard's job — on RabbitMQ a poison delivery is retried up to the queue's `MaxRetryCount`, then a `Fault<T>` is published and the message is nacked to the dead-letter exchange. The filter deliberately ignores `IMessageContext.RetryCount` and adds no max-attempts of its own, keeping retry policy in one place (the transport).
7
+
This is an *idempotent receiver*, not a store-and-forward inbox: it rides on top of the transport's existing retry, fault, and dead-letter machinery rather than re-implementing them. Bounding a consumer that keeps failing is therefore **not** the guard's job — on RabbitMQ a persistently-failing delivery is retried per the consumer's [retry policy](messaging.md#retries) (its own registration's, falling back to the queue default), then a `Fault<T>` is published and the delivery is dead-lettered when a dead-letter queue is configured. The filter deliberately ignores `IMessageContext.RetryCount` and adds no max-attempts of its own, keeping retry policy in one place (the transport).
Copy file name to clipboardExpand all lines: docs/articles/outbox-pattern.md
+20-3Lines changed: 20 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,14 +4,19 @@ The transactional outbox pattern guarantees that domain events raised by aggrega
4
4
5
5
## How It Works
6
6
7
-
1. During `SaveChangesAsync`, a `SaveChangesInterceptor` serialises every pending domain event into an `OutboxMessage` row in the same database transaction.
7
+
1. During `SaveChanges`/`SaveChangesAsync` (both the synchronous and asynchronous paths are intercepted), a `SaveChangesInterceptor` serialises every pending domain event into an `OutboxMessage` row in the same database transaction.
8
8
2. The aggregate root's event collection is cleared.
9
9
3. A background service (`OutboxBackgroundService`) relays unprocessed outbox messages — woken immediately once the captured rows are durable (on transaction commit, or right after a non-transactional `SaveChanges` that captured domain events) for low latency, and polling on an interval as the backstop.
10
10
4. Each message is routed by its `OutboxDestination` to the registered `IOutboxDispatcher` that handles it (in-process domain events by default, or the broker — see below).
11
11
5. Successfully relayed messages are marked as processed; failures are retried up to the configured maximum, after which the message is dead-lettered — its `FailedOnUtc` timestamp is set, it is no longer relayed, and an error is logged with the last failure (the `Error` column).
12
12
13
13
This guarantees **at-least-once delivery** because the event and the business data are committed atomically.
14
14
15
+
Relay order is best-effort, not a contract: messages are fetched oldest-first (`OccurredOnUtc`, then `Id`), but a
16
+
failed message does not block the rest of its batch — the batch keeps dispatching, so later messages overtake the
17
+
failure, which is retried on a later cycle (no head-of-line blocking). With `EnableParallelPublishing`, order
18
+
within a batch is not preserved at all. Consumers that need ordering must not rely on the outbox for it.
19
+
15
20
## Configuration
16
21
17
22
### Enable outbox processing during DbContext registration
@@ -54,6 +59,11 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
54
59
55
60
`BaseDbContext` owns the `OutboxMessages``DbSet`; apply the provider-optimized mapping in `OnModelCreating` by calling your provider's extension — `ApplyNpgsqlOutbox()`, `ApplyMySqlOutbox()`, or `ApplyCosmosOutbox()` (as shown above). The agnostic `ApplyOutbox()` is available for custom providers.
56
61
62
+
Because the `DbSet` lives on the base class, EF Core's convention discovery puts the `OutboxMessage` entity into
63
+
**every** derived context's model — and its migrations — even when outbox processing is never enabled for that
64
+
context. A context that will never use the outbox can opt out with `modelBuilder.Ignore<OutboxMessage>()` in its
65
+
`OnModelCreating`; an outbox-enabled context must keep the mapping (the relay store refuses to run without it).
66
+
57
67
Only one outbox-enabled `DbContext` is supported per host: the relay and retention background services resolve a
58
68
single `IOutboxStore`, so calling `EnableOutboxProcessing()` for a second context throws an
59
69
`InvalidOperationException` at startup instead of silently leaving the first context's messages unrelayed. The
@@ -71,7 +81,7 @@ running unprotected.
71
81
|`EnableParallelPublishing`|`false`| Publish messages in parallel within a batch (each dispatch runs in its own DI scope) |
72
82
|`MaxDegreeOfParallelism`| 4 | Maximum concurrent dispatches when `EnableParallelPublishing` is enabled |
73
83
|`OutboxProcessingDelaySeconds`| 2 | Base polling delay between processing cycles |
74
-
|`MaxDelaySeconds`| 60 | Maximum back-off delay when no messages are found|
84
+
|`MaxDelaySeconds`| 60 | Maximum back-off delay when a cycle relays nothing (no pending messages, or every fetched message failed)|
75
85
|`EnableTracing`|`true`| Carry the originating trace identifier when publishing |
76
86
77
87
## Observability
@@ -105,7 +115,7 @@ Processed and dead-lettered rows remain in the `OutboxMessages` table after rela
105
115
|`SweepInterval`| 1 hour | Delay between sweeps |
106
116
|`BatchSize`| 1000 | Rows deleted per batch within a sweep |
107
117
108
-
The sweep deletes rows whose `ProcessedOnUtc`**or**`FailedOnUtc` is older than `RetentionPeriod`; **pending rows are never touched**. It runs through the registered `IOutboxStore` when it implements `IOutboxRetentionStore` (the EF Core store does) — every store deletes at most `BatchSize` rows per call (relational providers select the oldest eligible keys and delete them set-based with `ExecuteDelete`), so a large backlog is drained in bounded, short-lived batches, and the **same sweep covers Cosmos** (a Cosmos container TTL is not used, because it cannot tell a pending row from a relayed one and could expire an undelivered message).
118
+
The sweep deletes rows whose `ProcessedOnUtc`**or**`FailedOnUtc` is older than `RetentionPeriod`; **pending rows are never touched**. It runs through the registered `IOutboxStore` when it implements `IOutboxRetentionStore` (the EF Core store does; a custom store that doesn't gets a one-time warning and the sweep is skipped) — every store deletes at most `BatchSize` rows per call (relational providers select the oldest eligible keys and delete them set-based with `ExecuteDelete`), so a large backlog is drained in bounded, short-lived batches, and the **same sweep covers Cosmos** (a Cosmos container TTL is not used, because it cannot tell a pending row from a relayed one and could expire an undelivered message).
109
119
110
120
## Custom Outbox Store
111
121
@@ -156,6 +166,13 @@ Capture is relational-only (it enlists in the ambient transaction); the relay wo
156
166
the general publish/send **filter pipeline** (`IPublishFilter`, registered via `AddPublishFilter<T>()`), which is the
157
167
publish-side counterpart to consume filters and can host other cross-cutting concerns.
158
168
169
+
Two pipeline consequences to know: capturing **short-circuits** the pipeline, so publish filters registered after
170
+
`AddTransactionalOutbox()` never see a captured message; and the relay publishes captured messages straight through
171
+
the transport, **bypassing the publish-filter pipeline entirely** (nothing runs twice, but nothing else observes the
172
+
relayed publish either). Register any filter that must observe every outgoing message *before*
173
+
`AddTransactionalOutbox()`. Request/reply is unaffected — requests never flow through the publish filters and are
174
+
never captured (a request awaiting a synchronous reply cannot be deferred to a relay).
175
+
159
176
The capture preserves the publish faithfully: the message id, correlation id, routing key, send destination, custom
160
177
headers, and the current trace context (`TraceParent`/`TraceState`, when tracing is enabled) are stored with the row
161
178
and re-applied on relay — the relayed publish runs under an activity linked to the capturing trace. Headers persist
Copy file name to clipboardExpand all lines: docs/articles/packages/index.md
+16-4Lines changed: 16 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -14,7 +14,9 @@ Focused usage guidance for each package in `src`. Pick the page that matches the
14
14
## Infrastructure
15
15
16
16
-[Vulthil.SharedKernel.Infrastructure](vulthil-sharedkernel-infrastructure.md) – `BaseDbContext`, generic repository, and outbox composition entry point.
17
-
-[Vulthil.SharedKernel.Infrastructure.Relational](vulthil-sharedkernel-infrastructure-relational.md) – relational outbox strategy and `MigrateAsync` for EF Core providers.
-[Vulthil.SharedKernel.Outbox.EntityFrameworkCore](vulthil-sharedkernel-outbox-entityframeworkcore.md) – the EF Core implementation of the outbox engine.
19
+
-[Vulthil.SharedKernel.Infrastructure.Relational](vulthil-sharedkernel-infrastructure-relational.md) – relational outbox store base and `MigrateAsync` for EF Core providers.
18
20
-[Vulthil.SharedKernel.Infrastructure.Npgsql](vulthil-sharedkernel-infrastructure-npgsql.md) – `UseNpgsql` provider wiring with PostgreSQL-tuned outbox.
19
21
-[Vulthil.SharedKernel.Infrastructure.MySql](vulthil-sharedkernel-infrastructure-mysql.md) – `UseMySql` provider wiring with MySQL-tuned outbox.
20
22
-[Vulthil.SharedKernel.Infrastructure.Cosmos](vulthil-sharedkernel-infrastructure-cosmos.md) – `UseCosmosDb` provider wiring with Cosmos-specific outbox.
@@ -23,14 +25,24 @@ Focused usage guidance for each package in `src`. Pick the page that matches the
23
25
24
26
-[Vulthil.SharedKernel.Api](vulthil-sharedkernel-api.md) – minimal API endpoint conventions and `Result` → HTTP mapping.
25
27
28
+
## Hosting
29
+
30
+
-[Vulthil.Extensions.Hosting](vulthil-extensions-hosting.md) – `IRestartableHostedService`, the marker for hosted services that can be paused and resumed cleanly.
31
+
26
32
## Messaging
27
33
28
34
-[Vulthil.Messaging.Abstractions](vulthil-messaging-abstractions.md) – transport-agnostic consumer and publisher contracts.
29
-
-[Vulthil.Messaging](vulthil-messaging.md) – queue/consumer registration and hosted processing.
35
+
-[Vulthil.Messaging](vulthil-messaging.md) – queue/consumer registration, hosted processing, and the transport-author SDK.
30
36
-[Vulthil.Messaging.RabbitMq](vulthil-messaging-rabbitmq.md) – RabbitMQ transport implementation.
37
+
-[Vulthil.Messaging.Outbox](vulthil-messaging-outbox.md) – transactional bus-publish outbox bridging the outbox engine to the broker.
0 commit comments