Skip to content

Commit 82566b2

Browse files
authored
Merge pull request #226 from Vulthil/feature/review-fixes
Feature/review fixes
2 parents 2812b01 + 5655df0 commit 82566b2

121 files changed

Lines changed: 2186 additions & 1070 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.Packages.props

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="$(FrameworkVersion)" />
2929
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="$(FrameworkVersion)" />
3030
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="$(NpgsqlEfCoreVersion)" />
31+
<!-- MySQL: Aspire/Pomelo integration on net9; the Microting fork (a Pomelo fork) fills the EF Core 10 / net10 gap until the official Pomelo/Aspire packages ship it. -->
32+
<PackageVersion Include="Aspire.Pomelo.EntityFrameworkCore.MySql" Version="$(AspireVersion)" />
33+
<PackageVersion Include="Microting.EntityFrameworkCore.MySql" Version="10.0.9" />
3134
<PackageVersion Include="Microsoft.EntityFrameworkCore.Cosmos" Version="$(FrameworkVersion)" />
3235
<PackageVersion Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="4.14.0" />
3336
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.9" />

docs/articles/cqrs-pipeline.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ Pipeline behaviors wrap every handler invocation, allowing you to add cross-cutt
8080

8181
### Validation
8282

83-
`ValidationPipelineBehavior` runs all registered `IValidator<TCommand>` instances before the handler executes. When validation fails, it short-circuits and returns a `Result` containing a `ValidationError`:
83+
`ValidationPipelineBehavior` runs all registered `IValidator<TCommand>` instances before the handler executes. When validation fails it short-circuits: a command returning `Result` or `Result<T>` receives a failed result containing a `ValidationError`, while a command with any other response type throws a `ValidationException` (there is no in-band way to represent failure for a non-result response):
8484

8585
```csharp
8686
public sealed class CreateUserCommandValidator : AbstractValidator<CreateUserCommand>
@@ -102,7 +102,7 @@ No additional wiring is needed – the validator is picked up automatically from
102102

103103
### Transactional
104104

105-
`TransactionalPipelineBehavior` wraps `ITransactionalCommand` handlers inside a database transaction. If the handler succeeds, the transaction commits; if it throws, the transaction rolls back.
105+
`TransactionalPipelineBehavior` wraps `ITransactionalCommand` handlers inside a database transaction. If the handler returns a successful `Result`, the transaction commits; if it returns a failed `Result` or throws, the transaction rolls back. (The non-generic `ITransactionalCommand` marker returns `Result` and is wrapped the same way as `ITransactionalCommand<Result>`.)
106106

107107
```csharp
108108
// Mark a command as transactional

docs/articles/domain-modeling.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,10 @@
1414

1515
## Defining an Entity
1616

17-
Entities are compared by identity, not by attribute values:
17+
Entities are compared by identity, not by attribute values. Give the identifier value equality — a `record` or `readonly record struct` — so two entities with the same id compare as equal:
1818

1919
```csharp
20-
public sealed class UserId(Guid value)
21-
{
22-
public Guid Value { get; } = value;
23-
}
20+
public sealed record UserId(Guid Value);
2421

2522
public sealed class User : AggregateRoot<UserId>
2623
{

docs/articles/inbox-pattern.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
7979
public DbSet<InboxMessage> InboxMessages => Set<InboxMessage>();
8080

8181
protected override void OnModelCreating(ModelBuilder modelBuilder)
82-
=> modelBuilder.ApplyConfiguration(new InboxMessageEntityConfiguration());
82+
=> modelBuilder.ApplyRelationalInbox();
8383
}
8484
```
8585

@@ -91,11 +91,11 @@ The consumer and the store must share the same scoped `DbContext` instance (the
9191

9292
### Cosmos store
9393

94-
The Cosmos store is wired the same way — apply `CosmosInboxMessageEntityConfiguration` and call `AddCosmosInbox<AppDbContext>()`:
94+
The Cosmos store is wired the same way — apply the Cosmos mapping with `ApplyCosmosInbox()` and call `AddCosmosInbox<AppDbContext>()`:
9595

9696
```csharp
9797
protected override void OnModelCreating(ModelBuilder modelBuilder)
98-
=> modelBuilder.ApplyConfiguration(new CosmosInboxMessageEntityConfiguration());
98+
=> modelBuilder.ApplyCosmosInbox();
9999
```
100100

101101
```csharp

docs/articles/outbox-pattern.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ The transactional outbox pattern guarantees that domain events raised by aggrega
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).
11-
5. Successfully relayed messages are marked as processed; failures are retried up to the configured maximum.
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).
1212

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

@@ -26,7 +26,7 @@ builder.AddDbContext<AppDbContext>(config =>
2626
.EnableOutboxProcessing(o =>
2727
{
2828
o.BatchSize = 10; // Messages fetched per poll cycle
29-
o.MaxRetries = 3; // Retry limit before a message is abandoned
29+
o.MaxRetries = 3; // Retries before a message is dead-lettered
3030
});
3131
});
3232
```
@@ -43,18 +43,25 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
4343
typeof(AppDbContext).Assembly;
4444

4545
public DbSet<User> Users => Set<User>();
46+
47+
protected override void OnModelCreating(ModelBuilder modelBuilder)
48+
{
49+
base.OnModelCreating(modelBuilder);
50+
modelBuilder.ApplyNpgsqlOutbox();
51+
}
4652
}
4753
```
4854

49-
The `OutboxMessage` entity configuration is applied automatically by `BaseDbContext.OnModelCreating`.
55+
`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.
5056

5157
## Outbox Processing Options
5258

5359
| Property | Default | Description |
5460
|---|---|---|
5561
| `BatchSize` | 10 | Number of messages fetched per poll cycle |
56-
| `MaxRetries` | 3 | Maximum publish attempts before a message is abandoned |
57-
| `EnableParallelPublishing` | `false` | Publish messages in parallel within a batch |
62+
| `MaxRetries` | 3 | Publish attempts before a message is dead-lettered (`FailedOnUtc` set, no longer relayed) |
63+
| `EnableParallelPublishing` | `false` | Publish messages in parallel within a batch (each dispatch runs in its own DI scope) |
64+
| `MaxDegreeOfParallelism` | 4 | Maximum concurrent dispatches when `EnableParallelPublishing` is enabled |
5865
| `OutboxProcessingDelayInSeconds` | 2 | Base polling delay between processing cycles |
5966
| `MaxDelaySeconds` | 60 | Maximum back-off delay when no messages are found |
6067
| `EnableTracing` | `true` | Carry the originating trace identifier when publishing |
@@ -137,7 +144,7 @@ OutboxBackgroundService polls
137144
138145
OutboxProcessor deserialises & publishes via IDomainEventPublisher
139146
140-
Message marked as processed (or retried on failure)
147+
Message marked as processed (or retried on failure, then dead-lettered after MaxRetries)
141148
```
142149

143150
## When to Use

docs/articles/packages/vulthil-messaging-inbox-cosmos.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ deduplication of redeliveries over **idempotent-by-design** consumer writes.
1313

1414
## Pattern
1515

16-
- Your `DbContext` implements `ISaveInboxMessages` and applies `CosmosInboxMessageEntityConfiguration`
16+
- Your `DbContext` implements `ISaveInboxMessages` and applies the Cosmos inbox mapping via `ApplyCosmosInbox()`
1717
- The marker is a self-contained document keyed + partitioned by `MessageId` in its own container; a duplicate insert conflicts and is treated as already-processed
1818
- The marker is written after the consumer's own commit (no ambient transaction) — keep consumer writes idempotent
1919

@@ -25,7 +25,7 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options) : DbCon
2525
public DbSet<InboxMessage> InboxMessages => Set<InboxMessage>();
2626

2727
protected override void OnModelCreating(ModelBuilder modelBuilder)
28-
=> modelBuilder.ApplyConfiguration(new CosmosInboxMessageEntityConfiguration());
28+
=> modelBuilder.ApplyCosmosInbox();
2929
}
3030
```
3131

docs/articles/packages/vulthil-messaging-inbox-relational.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public sealed class AppDbContext(DbContextOptions<AppDbContext> options)
2626
public DbSet<InboxMessage> InboxMessages => Set<InboxMessage>();
2727

2828
protected override void OnModelCreating(ModelBuilder modelBuilder)
29-
=> modelBuilder.ApplyConfiguration(new InboxMessageEntityConfiguration());
29+
=> modelBuilder.ApplyRelationalInbox();
3030
}
3131
```
3232

docs/articles/packages/vulthil-sharedkernel-api.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,10 @@ app.MapOpenApiEndpoints();
4848

4949
### Controller-based endpoints
5050

51-
Controllers can return typed results directly for OpenAPI documentation:
51+
Derive from `BaseController` for the standard `[ApiController]`/route conventions and a `Logger` property resolved for the concrete controller type (no constructor logger argument required). Controllers can return typed results directly for OpenAPI documentation:
5252

5353
```csharp
54-
public sealed class UsersController(ILogger<UsersController> logger, ISender sender)
55-
: BaseController(logger)
54+
public sealed class UsersController(ISender sender) : BaseController
5655
{
5756
[HttpGet("{id:guid}")]
5857
public async Task<Results<Ok<UserDto>, ValidationProblem, NotFound, Conflict, ProblemHttpResult>> Get(Guid id)
@@ -66,8 +65,7 @@ public sealed class UsersController(ILogger<UsersController> logger, ISender sen
6665
Or use `IActionResult` with model-state error translation:
6766

6867
```csharp
69-
public sealed class UsersController(ILogger<UsersController> logger, ISender sender)
70-
: BaseController(logger)
68+
public sealed class UsersController(ISender sender) : BaseController
7169
{
7270
[HttpGet("{id:guid}")]
7371
public async Task<IActionResult> Get(Guid id)

docs/articles/packages/vulthil-sharedkernel-infrastructure-cosmos.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Use `Vulthil.SharedKernel.Infrastructure.Cosmos` to run the shared infrastructur
1111
## Pattern
1212

1313
- Call `UseCosmosDb("connectionName")` on the database infrastructure configurator – it both registers the EF Core context and selects the Cosmos outbox strategy
14-
- Configure the Cosmos-specific entity model for `OutboxMessage` via `OutboxMessageEntityConfiguration` (applied automatically when `EnableOutboxProcessing` runs)
14+
- Configure the Cosmos-specific entity model for `OutboxMessage` via the `ApplyCosmosOutbox()` model-builder extension (call it from your `OnModelCreating`)
1515
- Order between `UseCosmosDb` and `EnableOutboxProcessing` does not matter; the configurator defers the underlying call until the full chain has executed
1616

1717
## Usage

docs/articles/packages/vulthil-sharedkernel-infrastructure-mysql.md

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,57 @@
11
# Vulthil.SharedKernel.Infrastructure.MySql
22

3-
Use `Vulthil.SharedKernel.Infrastructure.MySql` to select the MySQL-tuned outbox strategy when your `DbContext` runs against MySQL.
3+
Use `Vulthil.SharedKernel.Infrastructure.MySql` to wire a MySQL-backed `DbContext` together with the MySQL-tuned outbox strategy.
44

55
## When to use
66

77
- MySQL is the underlying database for an application's primary `DbContext`
8-
- Outbox processing should use the MySQL strategy
8+
- Outbox processing should use the MySQL strategy (row-level locking via `FOR UPDATE SKIP LOCKED`)
99

1010
## Pattern
1111

12-
- Call `UseMySql()` on the database infrastructure configurator to swap in the MySQL outbox strategy
13-
- Register the `DbContext` itself with your preferred MySQL EF Core integration (e.g. `Pomelo.EntityFrameworkCore.MySql`) – `UseMySql` only switches the outbox strategy and does not register the EF Core provider
14-
- Apply migrations on startup with `MigrateAsync<TDbContext>()` from `Vulthil.SharedKernel.Infrastructure.Relational` (pulled in transitively)
12+
- Call `UseMySql("ConnectionStringKey")` on the database infrastructure configurator – it registers a pooled EF Core context for the named connection string, selects the MySQL outbox strategy, and (when outbox processing is enabled) wires the commit-time relay trigger
13+
- Order between `UseMySql` and `EnableOutboxProcessing` does not matter; the configurator defers the underlying registration until the full chain has executed
14+
- Retrying execution strategies are fully supported – the outbox processor runs its transactional unit inside the context's execution strategy (`Database.CreateExecutionStrategy().ExecuteAsync`)
15+
16+
## Provider
17+
18+
The MySQL EF Core provider is resolved per target framework:
19+
20+
- **.NET 9** – the Aspire client integration [`Aspire.Pomelo.EntityFrameworkCore.MySql`](https://www.nuget.org/packages/Aspire.Pomelo.EntityFrameworkCore.MySql), which resolves the connection string and adds health checks, telemetry, and connection resiliency (parity with the Npgsql and Cosmos integrations)
21+
- **.NET 10** – the API-compatible [`Microting.EntityFrameworkCore.MySql`](https://www.nuget.org/packages/Microting.EntityFrameworkCore.MySql) fork of Pomelo, registered directly, until the official Pomelo and Aspire packages ship EF Core 10 support
22+
23+
On .NET 10 the MySQL server version is detected from the connection at startup via `ServerVersion.AutoDetect`, so the database must be reachable when the host starts.
1524

1625
## Usage
1726

1827
### Registration
1928

2029
```csharp
21-
builder.Services.AddDbContext<AppDbContext>(options =>
22-
options.UseMySql(
23-
builder.Configuration.GetConnectionString("Default"),
24-
ServerVersion.AutoDetect(builder.Configuration.GetConnectionString("Default"))));
25-
2630
builder.AddDbContext<AppDbContext>(config => config
27-
.UseMySql()
31+
.UseMySql("Default")
2832
.EnableOutboxProcessing(o =>
2933
{
3034
o.BatchSize = 25;
3135
o.MaxRetries = 3;
3236
}));
3337
```
3438

39+
### Configuring the integration
40+
41+
`UseMySql` takes an optional `configure` delegate. Because the Aspire integration only exists on .NET 9, its type differs per target framework — the Aspire `PomeloEntityFrameworkCoreMySqlSettings` on .NET 9, and the EF Core `MySqlDbContextOptionsBuilder` on .NET 10:
42+
43+
```csharp
44+
// .NET 9 (Aspire) — toggle health checks, tracing, retries, command timeout:
45+
config.UseMySql("Default", settings => settings.DisableHealthChecks = true);
46+
47+
// .NET 10 (Microting) — configure EF Core/Pomelo options:
48+
config.UseMySql("Default", mySql => mySql.CommandTimeout(30));
49+
```
50+
3551
### Applying migrations on startup
3652

53+
`Vulthil.SharedKernel.Infrastructure.Relational` (pulled in transitively) exposes `MigrateAsync`:
54+
3755
```csharp
3856
var app = builder.Build();
3957
await app.MigrateAsync<AppDbContext>();

0 commit comments

Comments
 (0)