A development-time web UI for MassTransit. Attach it to any app and you get a page that:
- lists every message type the app knows about. Types consumed by its consumers, job consumers and sagas are found automatically, and you can add more yourself.
- lets you search them by name, CamelCase initials (
SO→SubmitOrder), namespace or consumer. - pre-fills an editable JSON payload generated from the type's properties. Enum values are listed alongside.
- publishes the message, or sends it to a queue/exchange, through the app's own bus, with optional headers.
- keeps your edited drafts and a recent list per browser, so you can replay a payload with one click.
It dispatches through the app's IBus, so it works with any transport: RabbitMQ, Azure Service Bus, Amazon SQS, and the in-memory transport too. In-memory is the case where nothing outside the process can reach the bus.
TransitStop is only active in the Development environment by default.
From a local build:
<ProjectReference Include="..\..\transit-stop\TransitStop\TransitStop.csproj" />or pack it to a local feed and reference the package:
dotnet pack TransitStop -c Release -o C:/nuget-local<PackageReference Include="TransitStop" Version="0.1.0" />Tip – zero production footprint: reference it only in Debug builds and wrap the calls in
#if DEBUG:<PackageReference Include="TransitStop" Version="0.1.0" Condition="'$(Configuration)' == 'Debug'" />
builder.Services.AddMassTransit(x => { /* as usual */ });
builder.Services.AddTransitStop();
var app = builder.Build();
app.MapTransitStop(); // → http://localhost:<port>/transit-stop/
app.Run();MapTransitStop("/some/other/path") changes the route. It returns an endpoint convention builder, so .RequireAuthorization() and similar conventions work too.
TransitStop can bring its own small web server:
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMassTransit(x => { /* as usual */ });
builder.Services.AddTransitStop(o => o.UseStandaloneServer(port: 5380));
builder.Build().Run(); // → http://localhost:5380/The standalone server:
- listens on localhost only.
- is built from an empty builder, so the app's URLs, Kestrel settings and middleware don't affect it.
- logs an error and lets the app run on without TransitStop if the port is taken.
| Source | How |
|---|---|
| Consumed by this app | Automatic (DiscoverConsumedMessages = true). TransitStop reads the app's service registrations and collects the message types of IConsumer<T>, IConsumer<Batch<T>>, IJobConsumer<T>, InitiatedBy<T> / Orchestrates<T> / Observes<T, _>, and every Event<T> property of a MassTransitStateMachine. The UI shows which consumer handles each message. |
| A single type | o.AddMessage<OrderSubmitted>() |
| A contracts namespace | o.AddMessagesFromNamespaceContaining<OrderSubmitted>(). Use this for messages other services consume. |
| A whole assembly | o.AddMessagesFromAssemblyContaining<OrderSubmitted>(t => t.Name.EndsWith("Command")) |
| Hide some | o.Exclude(t => t.Namespace!.EndsWith(".Internal")) |
Assembly and namespace scans pick up public, non-abstract classes, records and interfaces. They skip consumers, sagas, state machines, exceptions, attributes and anything derived from a MassTransit class. Nested DTOs that live next to your messages, such as an Address record, are listed too. Hide them with Exclude if they get in the way.
builder.Services.AddTransitStop(o => o
.AddMessagesFromNamespaceContaining<OrderSubmitted>() // other services' messages
.Exclude(t => t.Name.EndsWith("Dto")));TransitStop turns your edited JSON into the message object, then hands that object to the bus. The bus serializes it as usual. By default the JSON is read with MassTransit's own System.Text.Json settings, plus enums written as names. Interface messages work out of the box.
If your bus uses Newtonsoft.Json with custom settings or converters, deserialize with those same settings:
builder.Services.AddTransitStop(o =>
{
JsonSerializerSettings settings = MyDefaults.MassTransitJsonSettings;
o.Deserializer = (json, type) => JsonConvert.DeserializeObject(json, type, settings);
});Newtonsoft cannot create interface messages from JSON. If you have interface contracts, keep the default deserializer, or handle
type.IsInterfaceseparately.
Sample payloads use camelCase by default and honour [JsonPropertyName], [JsonProperty] and [JsonIgnore]. To change the naming, set o.SampleNamingPolicy (null keeps C# names).
/ or Ctrl+K |
Focus search |
↑ ↓ Enter |
Pick a message from the search results and jump to the editor |
Ctrl+Enter |
Publish / Send |
Alt+Shift+F |
Format the JSON |
Tab |
Indent in the editor |
- Publish goes to every subscriber of the message type.
- Send goes to one destination. It accepts:
- a bare queue name (
order-service, which meansqueue:order-service) queue:nameorexchange:name- a full transport address (
rabbitmq://localhost/vhost/order-service)
- a bare queue name (
- Headers is an optional JSON object, e.g.
{"tenant": "acme"}, added to every dispatch. - Drafts: once you edit a payload it's remembered per message type (marked edited). Reset to sample brings back the generated one.
- Recent: click an entry to load that payload (and mode/destination) back into the editor.
- Drafts, recent dispatches and settings live in your browser's localStorage, separately for each app. Nothing is stored server-side.
- Each dispatch is also logged by the app (
TransitStop published SubmitOrder).
builder.Services.AddTransitStop(o =>
{
o.Title = "Order service"; // default: application name
o.EnableWhen = env => env.IsDevelopment() || env.IsEnvironment("Local");
o.DiscoverConsumedMessages = true;
o.AddMessagesFromNamespaceContaining<OrderSubmitted>();
o.Exclude(t => t.Name.EndsWith("Dto"));
o.Deserializer = (json, type) => /* ... */;
o.SampleNamingPolicy = JsonNamingPolicy.CamelCase;
o.UseStandaloneServer(5380); // hosts without HTTP
});| Option | Default | |
|---|---|---|
EnableWhen |
env.IsDevelopment() |
When false, MapTransitStop maps nothing (404) and the standalone server does not start. |
DiscoverConsumedMessages |
true |
List the messages this app's consumers and sagas consume. |
Deserializer |
MassTransit STJ + string enums | (json, type) => object |
SampleNamingPolicy |
camelCase | Property naming in generated samples. |
Title |
app name | Shown in the header and browser tab. |
UseStandaloneServer(port) |
off (port 5380 when called without arguments) |
Own Kestrel server on localhost:port. |
The UI uses a small JSON API, which also works from scripts. Paths are relative to the page, e.g. /transit-stop/:
# list message types (with generated samples)
curl http://localhost:5000/transit-stop/api/messages
# publish
curl -X POST http://localhost:5000/transit-stop/api/publish -H "Content-Type: application/json" -d '{
"messageType": "Orders.Contracts.SubmitOrder",
"message": { "orderId": "6f9619ff-8b86-d011-b42d-00cf4fc964ff", "customer": "ACME" },
"headers": { "tenant": "acme" }
}'
# send to a queue
curl -X POST http://localhost:5000/transit-stop/api/send -H "Content-Type: application/json" -d '{
"messageType": "Orders.Contracts.SubmitOrder",
"destination": "submit-order",
"message": { "customer": "ACME" }
}'messageType is the id from api/messages: the namespace-qualified type name.
- In-memory transport: only the process that hosts the bus can publish into it. That's why TransitStop runs inside your app rather than as a separate tool. With a broker such as RabbitMQ, any app attached to the broker can publish messages that other services consume. Add those contracts with
AddMessagesFromNamespaceContaining<T>(). - Generic messages: a closed generic like
Fault<SubmitOrder>is listed when a consumer or saga uses it. Open generics are not listed. MessageData<T>properties are left asnullin samples.- Requires .NET 8 or later and MassTransit 8.x.
- Security: TransitStop can put arbitrary messages on your bus. Keep
EnableWhenrestricted to development environments. If you widen it, protect the route, e.g.app.MapTransitStop().RequireAuthorization("Admins").
TransitStop.Demo is a small order shop on the in-memory bus:
dotnet run --project TransitStop.Demo # → http://localhost:5280/transit-stop/Things to try. The results show up in the console:
- Publish
SubmitOrder. The consumer totals it and publishesOrderSubmitted, which starts the order saga. - Publish
OrderShipped(an interface message) with the sameorderId. The saga finishes and sendsNotifyCustomerto thenotify-customerqueue. - Send
NotifyCustomeryourself. Switch to Send and use destinationnotify-customer. - Publish
SensorReadinga few times quickly. A batch consumer receives them together. CancelOrderhas no consumer in this app. It's listed only because the contracts namespace is added explicitly.
dotnet build TransitStop.slnx
dotnet pack TransitStop -c Release -o ./artifactsThe UI is a single self-contained HTML file (TransitStop/ui/index.html, embedded as a resource) with no external scripts or fonts, so it also works offline.