Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 44 additions & 6 deletions docs/guide/command-line.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,12 @@ builder.Services.DisableAllWolverineMessagePersistence();
## Wolverine Diagnostics Commands <Badge type="tip" text="5.14" />

The `wolverine-diagnostics` command is an extensible parent command for deeper Wolverine-specific
inspection tools. Currently it exposes one sub-command: **`codegen-preview`**.
inspection tools.

::: tip
Both `codegen-preview` and `describe-routing` work without database or message-broker connectivity.
Wolverine automatically detects CLI codegen mode and stubs out persistence and transports.
:::

### codegen-preview

Expand Down Expand Up @@ -187,11 +192,44 @@ middleware calls in order, dependency resolution from the IoC container, and any
transaction-wrapping frames. This is identical to what `codegen preview` outputs, but scoped to
exactly one handler so the signal-to-noise ratio is much higher.

::: tip
`wolverine-diagnostics` works without database or message-broker connectivity for the same reason
as `codegen preview`: Wolverine automatically detects CLI codegen mode and stubs out persistence
and transports.
:::
### describe-routing <Badge type="tip" text="5.15" />

Inspect the message routing configuration for a specific message type or show a complete view of
all message routing in your application.

**Inspect routing for a single message type** (accepts full name, short name, or fuzzy match):

```bash
# Short class name
dotnet run -- wolverine-diagnostics describe-routing CreateOrder

# Fully-qualified name
dotnet run -- wolverine-diagnostics describe-routing MyApp.Orders.CreateOrder
```

The output for a single message type includes:

- **Local handler** — the handler class and method, if any
- **Routes table** — each destination with its type (local vs. external), endpoint mode
(Buffered/Durable/Inline), outbox enrollment, serialization format, and how the route was
resolved (local handler convention, explicit publish rule, transport routing convention, or
`[LocalQueue]` attribute)
- **Message-level attributes** — any `ModifyEnvelopeAttribute`-derived attributes (e.g.,
`[DeliverWithin]`) applied to the message class

**Show the complete routing topology** (all message types):

```bash
dotnet run -- wolverine-diagnostics describe-routing --all
```

The `--all` output includes:

- **Routing Conventions** — transport-level conventions registered via `RouteWith()`
- **Message Routing** table — every known message type with its destinations, mode, outbox status,
and serializer; unrouted types are flagged in yellow
- **Listeners** — all configured listening endpoints with name, mode, and parallelism
- **Senders** — all configured sending endpoints with name, mode, and subscription count

## Other Highlights

Expand Down
135 changes: 135 additions & 0 deletions src/Testing/CoreTests/Diagnostics/WolverineDiagnosticsCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Microsoft.Extensions.Hosting;
using Shouldly;
using Wolverine.Diagnostics;
using Wolverine.Runtime;
using Wolverine.Runtime.Handlers;
using Xunit;

Expand Down Expand Up @@ -141,12 +142,146 @@ public async Task codegen_preview_generates_code_for_handler()
DynamicCodeBuilder.WithinCodegenCommand = false;
}
}

// ── FindMessageType ─────────────────────────────────────────────────────────

[Fact]
public async Task find_message_type_by_exact_full_name()
{
var (messageTypes, graph) = await BuildMessageTypesAsync();
var found = WolverineDiagnosticsCommand.FindMessageType(
"CoreTests.Diagnostics.DiagnosticsTestMessage", messageTypes, graph);
found.ShouldBe(typeof(DiagnosticsTestMessage));
}

[Fact]
public async Task find_message_type_by_short_name()
{
var (messageTypes, graph) = await BuildMessageTypesAsync();
var found = WolverineDiagnosticsCommand.FindMessageType(
"DiagnosticsTestMessage", messageTypes, graph);
found.ShouldBe(typeof(DiagnosticsTestMessage));
}

[Fact]
public async Task find_message_type_fuzzy_contains()
{
var (messageTypes, graph) = await BuildMessageTypesAsync();
var found = WolverineDiagnosticsCommand.FindMessageType(
"TestMessage", messageTypes, graph);
found.ShouldBe(typeof(DiagnosticsTestMessage));
}

[Fact]
public async Task find_message_type_returns_null_for_unknown()
{
var (messageTypes, graph) = await BuildMessageTypesAsync();
var found = WolverineDiagnosticsCommand.FindMessageType(
"NonExistentXyzMessage", messageTypes, graph);
found.ShouldBeNull();
}

// ── describe-routing smoke tests ─────────────────────────────────────────

[Fact]
public async Task describe_routing_handled_message_is_known_to_handler_graph()
{
// Note: in MediatorOnly (lightweight) mode, local routing assignments are not populated,
// so runtime.RoutingFor() returns no routes. We verify discovery and CanHandle instead.
DynamicCodeBuilder.WithinCodegenCommand = true;
try
{
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.Discovery
.DisableConventionalDiscovery()
.IncludeType(typeof(DiagnosticsTestHandler));
})
.StartAsync();

var runtime = host.Services.GetRequiredService<IWolverineRuntime>();
var options = runtime.Options;
var messageTypes = options.Discovery.FindAllMessages(options.HandlerGraph).ToList();

var match = WolverineDiagnosticsCommand.FindMessageType(
"DiagnosticsTestMessage", messageTypes, options.HandlerGraph);

match.ShouldNotBeNull();
match.ShouldBe(typeof(DiagnosticsTestMessage));
options.HandlerGraph.CanHandle(typeof(DiagnosticsTestMessage))
.ShouldBeTrue("handler graph should know about the handled message type");
}
finally
{
DynamicCodeBuilder.WithinCodegenCommand = false;
}
}

[Fact]
public async Task describe_routing_for_unhandled_message_returns_no_routes()
{
DynamicCodeBuilder.WithinCodegenCommand = true;
try
{
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.Discovery.DisableConventionalDiscovery();
})
.StartAsync();

var runtime = host.Services.GetRequiredService<IWolverineRuntime>();
WolverineSystemPart.WithinDescription = true;
try
{
var routes = runtime.RoutingFor(typeof(DiagnosticsUnhandledMessage)).Routes;
routes.ShouldBeEmpty();
}
finally
{
WolverineSystemPart.WithinDescription = false;
}
}
finally
{
DynamicCodeBuilder.WithinCodegenCommand = false;
}
}

private static async Task<(IReadOnlyList<Type> messageTypes, HandlerGraph graph)> BuildMessageTypesAsync()
{
DynamicCodeBuilder.WithinCodegenCommand = true;
try
{
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.Discovery
.DisableConventionalDiscovery()
.IncludeType(typeof(DiagnosticsTestHandler));
})
.StartAsync();

var graph = host.Services.GetRequiredService<HandlerGraph>();
var options = host.Services.GetRequiredService<IWolverineRuntime>().Options;
var messageTypes = options.Discovery.FindAllMessages(graph);
return (messageTypes, graph);
}
finally
{
DynamicCodeBuilder.WithinCodegenCommand = false;
}
}
}

// ── Test fixtures ────────────────────────────────────────────────────────────

public record DiagnosticsTestMessage(string Text);

// Unhandled message — no handler registered
public record DiagnosticsUnhandledMessage(string Text);

public static class DiagnosticsTestHandler
{
public static void Handle(DiagnosticsTestMessage message)
Expand Down
Loading
Loading