Skip to content

Commit fb9b9b5

Browse files
authored
docs: reconcile package docs and articles with current behavior (#334)
* docs: reconcile package docs and articles with current behavior * chore: give each nuget package content-specific tags
1 parent 5da89a2 commit fb9b9b5

83 files changed

Lines changed: 343 additions & 136 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
<Copyright>Copyright © Vulthil</Copyright>
4141
<PackageLicenseExpression>MIT</PackageLicenseExpression>
4242
<PackageProjectUrl>https://github.com/Vulthil/Vulthil.SharedKernel</PackageProjectUrl>
43-
<PackageTags>shared-kernel;ddd;cqrs;result-pattern;dotnet</PackageTags>
43+
<!-- PackageTags are set per project so each package's tags describe its own contents. -->
4444
<RepositoryType>git</RepositoryType>
4545
<RepositoryUrl>https://github.com/Vulthil/Vulthil.SharedKernel</RepositoryUrl>
4646
</PropertyGroup>

docs/articles/cqrs-pipeline.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ No additional wiring is needed – the validator is picked up automatically from
9898

9999
### Request Logging
100100

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.
102102

103103
### Transactional
104104

docs/articles/getting-started.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,26 @@ The packages are organised into layers so you can adopt only what you need:
1212
| Domain | `Vulthil.Results` | Railway-oriented `Result<T>` for explicit error handling |
1313
| Application | `Vulthil.SharedKernel.Application` | Commands, queries, handlers, and pipeline behaviors |
1414
| Infrastructure | `Vulthil.SharedKernel.Infrastructure` | EF Core base context, generic repository, and outbox processing |
15-
| Infrastructure | `Vulthil.SharedKernel.Infrastructure.Relational` | Shared relational support and the default relational outbox strategy |
15+
| Infrastructure | `Vulthil.SharedKernel.Outbox` | EF-free transactional outbox engine (relay, dispatchers, `IOutboxStore`) |
16+
| Infrastructure | `Vulthil.SharedKernel.Outbox.EntityFrameworkCore` | EF Core implementation of the outbox engine |
17+
| Infrastructure | `Vulthil.SharedKernel.Infrastructure.Relational` | Shared relational support and the relational outbox store base |
1618
| Infrastructure | `Vulthil.SharedKernel.Infrastructure.Npgsql` | PostgreSQL provider integration (`UseNpgsql`) |
1719
| Infrastructure | `Vulthil.SharedKernel.Infrastructure.MySql` | MySQL provider integration (`UseMySql`) |
1820
| Infrastructure | `Vulthil.SharedKernel.Infrastructure.Cosmos` | Azure Cosmos DB provider integration (`UseCosmosDb`) |
1921
| API | `Vulthil.SharedKernel.Api` | Minimal API endpoint conventions and `Result` → HTTP mapping |
22+
| Hosting | `Vulthil.Extensions.Hosting` | `IRestartableHostedService` marker for cleanly pausable hosted services |
2023
| Messaging | `Vulthil.Messaging.Abstractions` | Transport-agnostic consumer and publisher contracts |
21-
| Messaging | `Vulthil.Messaging` | Queue/consumer registration and hosted service orchestration |
24+
| Messaging | `Vulthil.Messaging` | Queue/consumer registration, hosted orchestration, and the transport SDK |
2225
| Messaging | `Vulthil.Messaging.RabbitMq` | RabbitMQ transport implementation |
26+
| Messaging | `Vulthil.Messaging.Outbox` | Transactional bus-publish outbox (captures publishes into the outbox) |
27+
| Messaging | `Vulthil.Messaging.Inbox` | Idempotent-receiver consume filter for exactly/effectively-once processing |
28+
| Messaging | `Vulthil.Messaging.Inbox.EntityFrameworkCore` | Shared EF Core inbox primitives |
29+
| Messaging | `Vulthil.Messaging.Inbox.Relational` | Relational idempotency store (transactional exactly-once) |
30+
| Messaging | `Vulthil.Messaging.Inbox.Cosmos` | Cosmos idempotency store (effectively-once) |
2331
| Testing | `Vulthil.xUnit` | Base test classes, auto-mocking, and Testcontainers integration |
32+
| Testing | `Vulthil.xUnit.Cosmos` | Cosmos DB emulator fixture for `Vulthil.xUnit` |
2433
| Testing | `Vulthil.Messaging.TestHarness` | In-memory messaging test harness |
25-
| Testing | `Vulthil.Extensions.Testing` | Shared assertion and setup helpers |
34+
| Testing | `Vulthil.Extensions.Testing` | Framework-agnostic polling and HTTP response helpers (no xUnit dependency) |
2635

2736
## Minimal Example
2837

docs/articles/inbox-pattern.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Message delivery is **at-least-once**: a broker can redeliver a message (after a
44

55
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.
66

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).
88

99
## How It Works
1010

docs/articles/messaging.md

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -305,17 +305,19 @@ Or in code:
305305
m.ConfigureMessagingOptions(opts => opts.ConsumeFilters.EnableLogging = false);
306306
```
307307

308-
The filter stays registered in DI; only its behavior is skipped, so it's still
309-
resolvable in unit tests if you want to assert against it.
308+
The toggle is applied when `AddMessaging` composes the pipeline: a disabled filter is
309+
never registered in DI at all (there is no per-delivery flag check), so flipping the
310+
flag after registration has no effect.
310311

311312
### Idempotent receivers (inbox)
312313

313314
`Vulthil.Messaging.Inbox` ships a consume filter that deduplicates redelivered
314-
messages, giving exactly-once processing on top of at-least-once delivery. It is a
315-
transaction-owning filter: the consumer runs inside an `IIdempotencyStore`
316-
transaction so the processed-marker and the consumer's writes commit atomically.
317-
Opt a message type in with `AddIdempotentInbox<TMessage>()`. See the
318-
[Inbox Pattern](inbox-pattern.md) article for the full design.
315+
messages on top of at-least-once delivery. The consumer invocation is handed to an
316+
`IIdempotencyStore`, which owns the unit: with a transactional (relational) store the
317+
processed-marker and the consumer's writes commit atomically — exactly-once
318+
processing — while a store without cross-partition transactions (e.g. Cosmos) is
319+
effectively-once. Opt a message type in with `AddIdempotentInbox<TMessage>()`. See
320+
the [Inbox Pattern](inbox-pattern.md) article for the full design.
319321

320322
## Ordered Processing (per-aggregate)
321323

@@ -523,6 +525,17 @@ await publisher.PublishAsync(new OrderCreatedEvent(orderId), ctx =>
523525
Fault publishing is best-effort: a failure to publish the fault is logged and never prevents the
524526
original delivery from being settled (nacked for dead-lettering).
525527

528+
### Poison deliveries
529+
530+
Deliveries that fail **before** a consumer can run bypass the retry and fault machinery entirely, and
531+
no `Fault<T>` is published for them:
532+
533+
- **Unknown message type** (no consumer/plan matches the delivery's type identity): the delivery is
534+
**acked and dropped permanently**, with a warning log — it does not reach the dead-letter queue.
535+
- **Undeserializable body** (malformed JSON, or a body that deserializes to `null`): the delivery is
536+
**nacked without requeue**, with an error log. When the queue has a dead-letter queue configured
537+
(`q.UseDeadLetterQueue()`), the broker moves it there; otherwise it is discarded.
538+
526539
## Routing Keys
527540

528541
Routing keys flow through two distinct configuration sites, one on each side of the wire:

docs/articles/outbox-pattern.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,19 @@ The transactional outbox pattern guarantees that domain events raised by aggrega
44

55
## How It Works
66

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.
88
2. The aggregate root's event collection is cleared.
99
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.
1010
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).
1111
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).
1212

1313
This guarantees **at-least-once delivery** because the event and the business data are committed atomically.
1414

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+
1520
## Configuration
1621

1722
### Enable outbox processing during DbContext registration
@@ -54,6 +59,11 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
5459

5560
`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.
5661

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+
5767
Only one outbox-enabled `DbContext` is supported per host: the relay and retention background services resolve a
5868
single `IOutboxStore`, so calling `EnableOutboxProcessing()` for a second context throws an
5969
`InvalidOperationException` at startup instead of silently leaving the first context's messages unrelayed. The
@@ -71,7 +81,7 @@ running unprotected.
7181
| `EnableParallelPublishing` | `false` | Publish messages in parallel within a batch (each dispatch runs in its own DI scope) |
7282
| `MaxDegreeOfParallelism` | 4 | Maximum concurrent dispatches when `EnableParallelPublishing` is enabled |
7383
| `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) |
7585
| `EnableTracing` | `true` | Carry the originating trace identifier when publishing |
7686

7787
## Observability
@@ -105,7 +115,7 @@ Processed and dead-lettered rows remain in the `OutboxMessages` table after rela
105115
| `SweepInterval` | 1 hour | Delay between sweeps |
106116
| `BatchSize` | 1000 | Rows deleted per batch within a sweep |
107117

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).
109119

110120
## Custom Outbox Store
111121

@@ -156,6 +166,13 @@ Capture is relational-only (it enlists in the ambient transaction); the relay wo
156166
the general publish/send **filter pipeline** (`IPublishFilter`, registered via `AddPublishFilter<T>()`), which is the
157167
publish-side counterpart to consume filters and can host other cross-cutting concerns.
158168

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+
159176
The capture preserves the publish faithfully: the message id, correlation id, routing key, send destination, custom
160177
headers, and the current trace context (`TraceParent`/`TraceState`, when tracing is enabled) are stored with the row
161178
and re-applied on relay — the relayed publish runs under an activity linked to the capturing trace. Headers persist

docs/articles/packages/index.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ Focused usage guidance for each package in `src`. Pick the page that matches the
1414
## Infrastructure
1515

1616
- [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.
17+
- [Vulthil.SharedKernel.Outbox](vulthil-sharedkernel-outbox.md) – the EF-free transactional outbox engine (relay, dispatchers, `IOutboxStore` seam).
18+
- [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.
1820
- [Vulthil.SharedKernel.Infrastructure.Npgsql](vulthil-sharedkernel-infrastructure-npgsql.md)`UseNpgsql` provider wiring with PostgreSQL-tuned outbox.
1921
- [Vulthil.SharedKernel.Infrastructure.MySql](vulthil-sharedkernel-infrastructure-mysql.md)`UseMySql` provider wiring with MySQL-tuned outbox.
2022
- [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
2325

2426
- [Vulthil.SharedKernel.Api](vulthil-sharedkernel-api.md) – minimal API endpoint conventions and `Result` → HTTP mapping.
2527

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+
2632
## Messaging
2733

2834
- [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.
3036
- [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.
38+
- [Vulthil.Messaging.Inbox](vulthil-messaging-inbox.md) – idempotent-receiver consume filter (`IIdempotencyStore` contract).
39+
- [Vulthil.Messaging.Inbox.EntityFrameworkCore](vulthil-messaging-inbox-entityframeworkcore.md) – shared EF Core inbox primitives (`InboxMessage`, `ISaveInboxMessages`).
40+
- [Vulthil.Messaging.Inbox.Relational](vulthil-messaging-inbox-relational.md) – relational idempotency store (transactional exactly-once).
41+
- [Vulthil.Messaging.Inbox.Cosmos](vulthil-messaging-inbox-cosmos.md) – Cosmos idempotency store (effectively-once).
3142

3243
## Testing
3344

34-
- [Vulthil.xUnit](vulthil-xunit.md) – reusable xUnit base classes and auto-mocking.
45+
- [Vulthil.xUnit](vulthil-xunit.md) – reusable xUnit base classes, auto-mocking, containers, and HTTP mocks.
46+
- [Vulthil.xUnit.Cosmos](vulthil-xunit-cosmos.md) – Cosmos DB emulator fixture for `Vulthil.xUnit`.
3547
- [Vulthil.Messaging.TestHarness](vulthil-messaging-testharness.md) – in-memory messaging test harness.
36-
- [Vulthil.Extensions.Testing](vulthil-extensions-testing.md)shared assertion and setup helpers.
48+
- [Vulthil.Extensions.Testing](vulthil-extensions-testing.md)framework-agnostic polling and HTTP response helpers.

docs/articles/packages/toc.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
href: vulthil-sharedkernel-infrastructure-cosmos.md
2323
- name: Vulthil.SharedKernel.Api
2424
href: vulthil-sharedkernel-api.md
25+
- name: Vulthil.Extensions.Hosting
26+
href: vulthil-extensions-hosting.md
2527
- name: Vulthil.Messaging.Abstractions
2628
href: vulthil-messaging-abstractions.md
2729
- name: Vulthil.Messaging

0 commit comments

Comments
 (0)