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
79 changes: 77 additions & 2 deletions docs/guide/command-line.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,9 @@ The available commands are:
help List all the available commands
resources Check, setup, or teardown stateful resources of this system
run Start and run this .Net application
storage Administer the Wolverine message storage

storage Administer the Wolverine message storage
wolverine-diagnostics Wolverine diagnostics tools for inspecting generated code and runtime behavior


Use dotnet run -- ? [command name] or dotnet run -- help [command name] to see usage help about a specific command

Expand Down Expand Up @@ -118,6 +119,80 @@ to help you troubleshoot issues in the future.
This functionality was originally built for consumption in the "CritterWatch" add on tool, but was requested by a [JasperFx Software](https://jasperfx.net)
client to provide a mechanism to detect any unintentional changes to Wolverine application configuration.

## CLI Commands Work Without External Connectivity

::: tip
This applies to `codegen write`, `codegen preview`, `describe`, and OpenAPI generation tools such as
`GetDocument.Insider` (Microsoft.Extensions.ApiDescription.Server). You do **not** need a running
database or message broker for these commands to succeed.
:::

Wolverine automatically detects when it is running in a metadata-only CLI mode and suppresses
persistence and transport initialization. No database connections or message broker connections
are opened. This allows commands like `codegen` and `describe` to work safely in CI pipelines or
developer machines that do not have external infrastructure available.

Detection is based on two signals:

1. **`DynamicCodeBuilder.WithinCodegenCommand`** — set by JasperFx when the `codegen` command is
used, either via `dotnet run -- codegen ...` or the `--start` flag.
2. **`ASPNETCORE_HOSTINGSTARTUPASSEMBLIES` environment variable** — contains `"GetDocument"` when
OpenAPI generation tools like `GetDocument.Insider` start the host.

When either condition is true, Wolverine applies the equivalent of "lightweight mode":
external transports are stubbed out, durability agents are disabled, and the durability
mode is set to `MediatorOnly`.

If you need to explicitly disable persistence initialization for other tooling (e.g., your own
OpenAPI generation pipeline), you can use the `DisableAllWolverineMessagePersistence()` extension:

```csharp
// In Program.cs or Startup.cs, guard with an environment check for your tooling
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`**.

### codegen-preview

Preview the full generated adapter code for a **specific** message handler or HTTP endpoint without
generating all handlers at once. This is useful when you want to understand exactly what middleware,
dependency resolution, or transaction wrapping Wolverine applies to a single entry point.

**Preview a message handler** (accepts fully-qualified name, short class name, or handler class name):

```bash
# Fully-qualified message type
dotnet run -- wolverine-diagnostics codegen-preview --handler MyApp.Orders.CreateOrder

# Short message type name (fuzzy match)
dotnet run -- wolverine-diagnostics codegen-preview --handler CreateOrder

# Handler class name
dotnet run -- wolverine-diagnostics codegen-preview --handler CreateOrderHandler
```

**Preview an HTTP endpoint** (requires Wolverine.HTTP; format: `"METHOD /path"`):

```bash
dotnet run -- wolverine-diagnostics codegen-preview --route "POST /api/orders"
dotnet run -- wolverine-diagnostics codegen-preview --route "GET /api/orders/{id}"
```

The output includes the full generated class — the `Handle` or `HandleAsync` override, all
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.
:::

## Other Highlights

* See the [code generation support](./codegen)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using JasperFx;
using JasperFx.CodeGeneration;
using JasperFx.Core.IoC;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
Expand Down Expand Up @@ -210,7 +211,7 @@ public static void MapWolverineEndpoints(this IEndpointRouteBuilder endpoints,

options.Policies.Add(new ProblemDetailsFromMiddleware());

if (Environment.CommandLine.Contains("codegen", StringComparison.OrdinalIgnoreCase))
if (DynamicCodeBuilder.WithinCodegenCommand)
{
options.WarmUpRoutes = RouteWarmup.Lazy;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using JasperFx;
using JasperFx.CodeGeneration;
using JasperFx.CodeGeneration.Model;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Wolverine.Tracking;
using Xunit;

namespace CoreTests.Bugs;

/// <summary>
/// Smoke tests for GitHub issue #2471: codegen and OpenAPI CLI commands should work
/// without database/transport connectivity.
/// </summary>
public class Bug_2471_codegen_without_connectivity
{
[Fact]
public void codegen_preview_works_without_database_connection()
{
// Simulate the codegen command setting WithinCodegenCommand = true before building host
DynamicCodeBuilder.WithinCodegenCommand = true;

try
{
// Build but do not start the host — the codegen command does this by default
// (without --start flag). No DB or transport connectivity is needed.
var host = Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.Discovery.DisableConventionalDiscovery()
.IncludeType(typeof(SimpleCodegenHandler2471));
})
.Build();

var collections = host.Services.GetServices<ICodeFileCollection>().ToArray();
collections.ShouldNotBeEmpty("Expected at least one ICodeFileCollection from Wolverine");

var builder = new DynamicCodeBuilder(host.Services, collections)
{
ServiceVariableSource = host.Services.GetService<IServiceVariableSource>()
};

// Should not throw even with no real database or transport configured
var code = builder.GenerateAllCode();
code.ShouldNotBeNullOrEmpty();
}
finally
{
DynamicCodeBuilder.WithinCodegenCommand = false;
}
}

[Fact]
public async Task host_startup_applies_lightweight_mode_automatically_during_codegen_command()
{
DynamicCodeBuilder.WithinCodegenCommand = true;

try
{
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.Discovery.DisableConventionalDiscovery()
.IncludeType(typeof(SimpleCodegenHandler2471));
})
.StartAsync();

var runtime = host.GetRuntime();

// Lightweight mode should have been applied automatically
runtime.Options.LightweightMode.ShouldBeTrue();
runtime.Options.ExternalTransportsAreStubbed.ShouldBeTrue();
runtime.Options.Durability.DurabilityAgentEnabled.ShouldBeFalse();
}
finally
{
DynamicCodeBuilder.WithinCodegenCommand = false;
}
}
}

public record SimpleCodegenMessage2471;

public static class SimpleCodegenHandler2471
{
public static void Handle(SimpleCodegenMessage2471 message)
{
}
}
156 changes: 156 additions & 0 deletions src/Testing/CoreTests/Diagnostics/WolverineDiagnosticsCommandTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
using JasperFx;
using JasperFx.CodeGeneration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Shouldly;
using Wolverine.Diagnostics;
using Wolverine.Runtime.Handlers;
using Xunit;

namespace CoreTests.Diagnostics;

/// <summary>
/// Unit and smoke tests for the WolverineDiagnosticsCommand added in GitHub issue #2467.
/// </summary>
public class WolverineDiagnosticsCommandTests
{
// ── RouteInputToFileName ──────────────────────────────────────────────────

[Theory]
[InlineData("POST /api/orders", "POST_api_orders")]
[InlineData("GET /api/orders/{id}", "GET_api_orders_id")]
[InlineData("DELETE /api/orders/{id}", "DELETE_api_orders_id")]
[InlineData("GET /", "GET")]
[InlineData("PUT /api/v1/users/{userId}/roles", "PUT_api_v1_users_userId_roles")]
[InlineData("/api/orders", "api_orders")] // no HTTP method prefix
[InlineData("POST /api/some-path", "POST_api_some_path")] // hyphens → underscores
public void route_input_to_file_name(string input, string expected)
{
WolverineDiagnosticsCommand.RouteInputToFileName(input).ShouldBe(expected);
}

// ── FindHandlerChain — exact and fuzzy matching ──────────────────────────

[Fact]
public void find_handler_chain_by_exact_full_name()
{
var chains = BuildTestChains();
var found = WolverineDiagnosticsCommand.FindHandlerChain(
"CoreTests.Diagnostics.DiagnosticsTestMessage", chains);
found.ShouldNotBeNull();
found.MessageType.ShouldBe(typeof(DiagnosticsTestMessage));
}

[Fact]
public void find_handler_chain_by_short_name()
{
var chains = BuildTestChains();
var found = WolverineDiagnosticsCommand.FindHandlerChain("DiagnosticsTestMessage", chains);
found.ShouldNotBeNull();
found.MessageType.ShouldBe(typeof(DiagnosticsTestMessage));
}

[Fact]
public void find_handler_chain_by_handler_class_name()
{
var chains = BuildTestChains();
var found = WolverineDiagnosticsCommand.FindHandlerChain("DiagnosticsTestHandler", chains);
found.ShouldNotBeNull();
found.MessageType.ShouldBe(typeof(DiagnosticsTestMessage));
}

[Fact]
public void find_handler_chain_fuzzy_contains_message_type()
{
var chains = BuildTestChains();
var found = WolverineDiagnosticsCommand.FindHandlerChain("TestMessage", chains);
found.ShouldNotBeNull();
found.MessageType.ShouldBe(typeof(DiagnosticsTestMessage));
}

[Fact]
public void find_handler_chain_returns_null_for_unknown()
{
var chains = BuildTestChains();
var found = WolverineDiagnosticsCommand.FindHandlerChain("NonExistentXyzHandler", chains);
found.ShouldBeNull();
}

private static HandlerChain[] BuildTestChains()
{
// Compile a real HandlerGraph so we get genuine HandlerChain objects.
DynamicCodeBuilder.WithinCodegenCommand = true;
try
{
var host = Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.Discovery
.DisableConventionalDiscovery()
.IncludeType(typeof(DiagnosticsTestHandler));
})
.Build();

// Accessing ICodeFileCollection triggers HandlerGraph.Compile()
host.Services.GetServices<ICodeFileCollection>().ToArray();

var graph = host.Services.GetRequiredService<HandlerGraph>();
return graph.AllChains().ToArray();
}
finally
{
DynamicCodeBuilder.WithinCodegenCommand = false;
}
}

// ── Full codegen-preview smoke test ──────────────────────────────────────

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

var services = host.Services;
var serviceVariableSource = services.GetService<JasperFx.CodeGeneration.Model.IServiceVariableSource>();
var graph = services.GetRequiredService<HandlerGraph>();
var chains = graph.AllChains().ToArray();

var chain = WolverineDiagnosticsCommand.FindHandlerChain("DiagnosticsTestMessage", chains);
chain.ShouldNotBeNull();

// Mimic what the command does: generate code for a single chain
var generatedAssembly = graph.StartAssembly(graph.Rules);
((JasperFx.CodeGeneration.ICodeFile)chain).AssembleTypes(generatedAssembly);
var code = generatedAssembly.GenerateCode(serviceVariableSource);

code.ShouldNotBeNullOrEmpty();
code.ShouldContain("DiagnosticsTestHandler");
}
finally
{
DynamicCodeBuilder.WithinCodegenCommand = false;
}
}
}

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

public record DiagnosticsTestMessage(string Text);

public static class DiagnosticsTestHandler
{
public static void Handle(DiagnosticsTestMessage message)
{
// intentionally empty — used only for code-generation testing
}
}
Loading
Loading