<h1>FluidBus</h1>
<p>A lightweight, modular event bus for .NET 10 built around two dispatch models: <strong>Router</strong> (protocol-based routing with sync/async support) and <strong>React</strong> (channel-based reactive subscriptions), plus a <strong>CallBack</strong> registry and a built-in <strong>Benchmark</strong> toolkit.</p>
<p>Zero external dependencies.</p>
<h2>Architecture</h2>
<pre><code>FluidBus
├── FluidBus.Core # Interfaces, protocols, tasks, errors
├── FluidBus.Router # Protocol-based event routing (sync/async)
├── FluidBus.React # Reactive channel-based event dispatch
├── FluidBus.CallBack # Named callback registry
├── FluidBus.Benchmark # Benchmarking utilities
└── FBus # Unified facade
</code></pre>
<h3>Router vs React</h3>
<p>| | Router | React |
|---|---|---|
| Dispatch | Protocol-based (sync/async) | Channel-based (always async) |
| Handler count | One handler per event type | Multiple subscribers per channel |
| Registration | Manual via <code>FRouter.Register()</code> | Auto-subscribe on instantiation |
| Matching | Exact event type | All subscribers on the channel |
| Use case | Command / request patterns | Broadcast / observer patterns |</p>
<h2>Getting started</h2>
<p>Reference <code>FluidBus</code> in your project. The <code>FBus</code> facade exposes all modules:</p>
<pre><code class="language-csharp">using FluidBus;
FBus.Route(routeEvent); // Router dispatch
FBus.React(reactEvent); // React dispatch
FBus.CallBack("on_complete", someData); // Execute a named callback
FBus.Bench("my scenario", 1000, 100, () => { }); // Benchmark a scenario
</code></pre>
<hr />
<h2>FluidBus.Core</h2>
<p>Shared foundation used by all modules.</p>
<h3>Instructions</h3>
<p>Instructions carry the data and logic that handlers execute. They use the <code>FluidCallBack</code> delegate:</p>
<pre><code class="language-csharp">public delegate object? FluidCallBack(object? data);
</code></pre>
<p>Inherit from <code>FluidInstruction<T></code>:</p>
<pre><code class="language-csharp">using FluidBus.Core.Abstracts;
public class PrintInstruction : FluidInstruction<string>
{
public PrintInstruction(string? data, params FluidCallBack[] methods)
: base(data, methods) { }
}
</code></pre>
<p>Each instruction can hold multiple callbacks (deduplicated by ID), executed sequentially. An <code>OnResult</code> event fires after execution.</p>
<h3>Protocols</h3>
<p>Protocols define the execution strategy for the Router:</p>
<pre><code class="language-csharp">public enum ExecutionStrategy { Sync = 0, Async = 1 }
</code></pre>
<p>A built-in <code>BusProtocol.System</code> (sync) is always available. Create custom protocols:</p>
<pre><code class="language-csharp">using FluidBus.Core.Protocols;
public class AsyncProtocol : BusProtocol
{
public override ExecutionStrategy Strategy => ExecutionStrategy.Async;
public AsyncProtocol() : base("ASYNC") { }
}
</code></pre>
<h3>Tasks</h3>
<p><code>FluidTask</code> wraps <code>Task.Run()</code> with state tracking (<code>Running</code>, <code>Completed</code>, <code>Failed</code>, <code>Cancelled</code>) and a fluent <code>OnComplete()</code> continuation API.</p>
<h3>Error hierarchy</h3>
<p>All exceptions inherit from <code>FluidBusError</code> and expose <code>.DisplayMessage()</code>.</p>
<p>| Exception | Thrown when |
|---|---|
| <code>DispatchException</code> | Async dispatch fails or unknown <code>ExecutionStrategy</code> |
| <code>ProtocolNotFoundException</code> | Event protocol has no registered port |
| <code>HandlerNotFoundException</code> | No handler registered for a given event type |
| <code>DuplicateHandlerException</code> | Handler already registered for that event type |
| <code>InstructionException</code> | <code>Execute()</code> called with no callbacks or null data |
| <code>ChannelException</code> | Channel write with no subscribers, or subscriber failure |
| <code>HandlerLinqException</code> | Handler registry error |</p>
<hr />
<h2>FluidBus.Router</h2>
<p>The Router dispatches events through <strong>protocols</strong> to registered <strong>handlers</strong>, matched by event type. One handler per event type.</p>
<h3>1. Create a custom event</h3>
<pre><code class="language-csharp">using FluidBus.Core.Interfaces;
using FluidBus.Core.Protocols;
using FluidBus.Router.Abstracts;
public class UserCreatedEvent : RouteEvent
{
public UserCreatedEvent(string id, BusProtocol protocol, params IFluidInstruction[] instrs)
: base($"{nameof(UserCreatedEvent)}::{id}", protocol, instrs) { }
}
</code></pre>
<h3>2. Create a custom handler</h3>
<pre><code class="language-csharp">using FluidBus.Core.Interfaces;
using FluidBus.Router.Abstracts;
public class UserCreatedHandler : RouteHandler<UserCreatedEvent>
{
public UserCreatedHandler(string id)
: base($"{nameof(UserCreatedEvent)}::{id}") { }
public override bool Handle(IFluidEvent evt)
{
Console.WriteLine($"[{Id}] Handling event {evt.Id}");
return base.Handle(evt);
}
}
</code></pre>
<h3>3. Register and publish</h3>
<pre><code class="language-csharp">using FluidBus;
using FluidBus.Core.Abstracts;
using FluidBus.Core.Protocols;
using FluidBus.Router.Core;
// Register the handler
FRouter.Register(new UserCreatedHandler("user_handler"));
// Create an instruction with a callback
var instruction = new PrintInstruction("Hello from FluidBus!", msg =>
{
Console.WriteLine(msg);
return null;
});
// Publish on the System protocol (sync)
FBus.Route(new UserCreatedEvent("evt_1", BusProtocol.System, instruction));
</code></pre>
<h3>Custom protocols</h3>
<pre><code class="language-csharp">// Register a port for your protocol
FRouter.AddPort(new AsyncProtocol());
// Events using this protocol will dispatch asynchronously
FBus.Route(new UserCreatedEvent("evt_2", new AsyncProtocol(), instruction));
</code></pre>
<h3>Dispatch flow</h3>
<pre><code>FBus.Route(event)
-> FRouter.Publish(event)
-> Lookup port by event.Protocol
-> Lookup handler by event type (HandlerLinq)
-> RouterPort.Dispatch(event, handler)
├─ Sync: event.Dispatch(handler) — blocking
└─ Async: FluidTask wrapping event.Dispatch(handler)
-> handler.Handle(event)
-> Execute each instruction's callbacks sequentially
</code></pre>
<hr />
<h2>FluidBus.React</h2>
<p>React uses <strong>channels</strong> (<code>System.Threading.Channels</code>) instead of protocols. Handlers auto-subscribe to their event type's channel on creation. Events are dispatched asynchronously to all subscribers.</p>
<h3>1. Create a custom event</h3>
<pre><code class="language-csharp">using FluidBus.Core.Interfaces;
using FluidBus.React.Abstracts;
public class ChatMessageEvent : ReactEvent
{
public ChatMessageEvent(string id, params IFluidInstruction[] instrs)
: base($"{nameof(ChatMessageEvent)}::{id}", instrs) { }
}
</code></pre>
<h3>2. Create a custom handler</h3>
<p>Handlers subscribe automatically to their channel on instantiation:</p>
<pre><code class="language-csharp">using FluidBus.Core.Interfaces;
using FluidBus.React.Abstracts;
public class ChatMessageHandler : ReactHandler<ChatMessageEvent>
{
public ChatMessageHandler(string id)
: base($"{nameof(ChatMessageEvent)}::{id}") { }
public override bool Handle(IFluidEvent evt)
{
Console.WriteLine($"[{Id}] Received message");
return base.Handle(evt);
}
}
</code></pre>
<h3>3. Register and publish</h3>
<pre><code class="language-csharp">using FluidBus;
using FluidBus.Core.Abstracts;
using FluidBus.React.Core;
// Instantiating the handler auto-subscribes it to the ChatMessageEvent channel
var handler = new ChatMessageHandler("chat_listener");
// Optionally register it for lifecycle management
FReact.RegisterHandler(handler);
// Publish — all subscribed handlers are notified asynchronously
var instruction = new PrintInstruction("New message!", msg =>
{
Console.WriteLine(msg);
return null;
});
FBus.React(new ChatMessageEvent("msg_1", instruction));
// Drop a handler
FReact.DropHandler(handler);
</code></pre>
<h3>Dispatch flow</h3>
<pre><code>FBus.React(event)
-> FReact.Publish(event)
-> GetOrCreateChannel(event type)
-> channel.Write(event) — enqueued in unbounded Channel<T>
-> Background reader loop (per channel)
-> Broadcast to all ReactReceive subscribers
-> handler.Handle(event)
-> Execute each instruction's callbacks
</code></pre>
<hr />
<h2>FluidBus.CallBack</h2>
<p>A simple named callback registry using the <code>FluidCallBack</code> delegate.</p>
<pre><code class="language-csharp">using FluidBus.Core.Abstracts;
using FluidBus.CallBack.Core;
// Register a callback
FCallBack.RegisterCallBack("on_complete", data =>
{
Console.WriteLine($"Completed with: {data}");
return data;
});
// Execute by name (through the facade)
FBus.CallBack("on_complete", "some result");
// Remove a callback
FCallBack.DropCallBack("on_complete");
</code></pre>
<p>Returns <code>null</code> silently if the callback doesn't exist.</p>
<hr />
<h2>FluidBus.Benchmark</h2>
<p>Built-in benchmarking with warmup support and nanosecond precision (<code>Stopwatch.GetTimestamp()</code>).</p>
<pre><code class="language-csharp">using FluidBus;
using FluidBus.Benchmark.Core;
BenchResult result = FBus.Bench("route 1000 events", iterations: 1000, warmup: 100, () =>
{
FBus.Route(myEvent);
});
result.Print(); // Prints iterations, duration (ms), avg ns/iteration
</code></pre>
<p><code>BenchResult</code> exposes: <code>Iteration</code>, <code>Warmup</code>, <code>Case</code>, <code>Start</code>, <code>End</code>, <code>Duration</code> (ms).</p>
<hr />
<h2>License</h2>
<p><a href="LICENSE">Business Source License 1.1</a> - See LICENSE file for details.</p>
Patchnotes
-
2026-01-15 v2.0.1 refactor
Want to dig deeper?
A specific use case for this project?
Integration, fork, custom feature — let's talk.