VitalRouter is a high-performance, zero-allocation in-memory messaging library for C#. It is specifically designed for environments where performance and decoupling are critical, such as Unity games or complex .NET applications.
By using simple attributes, you can implement asynchronous handlers, middleware (interceptors), and advanced sequence control. VitalRouter leverages Roslyn Source Generators to produce highly efficient code, promoting a clean, unidirectional control flow with minimal overhead.
Visit vitalrouter.hadashikick.jp for the full documentation.
- Zero Allocation: Optimized for high-frequency messaging without GC pressure.
- Thread-Safe: Designed for safe use across multiple threads.
- Unidirectional Flow: Promotes a predictable data flow through your application.
- Declarative Routing: Use attributes like
[Routes]and[Route]to define handlers. - Sequential Control: Declaratively queue, drop, or cancel overlapping async handlers — perfect for game cutscenes, dialogue, and tutorials.
- Async Interceptor Pipeline: Build sophisticated message processing chains.
- Versatile Compatibility: Works seamlessly in both Unity and standard .NET projects.
- DI Integration: Native support for VContainer (Unity) and Microsoft.Extensions.DependencyInjection.
VitalRouter follows a simple messaging flow. A Publisher sends a Command to a Router, which then dispatches it through optional Interceptors to one or more Handlers.
---
config:
theme: dark
---
graph LR
Publisher[Publisher] -- PublishAsync --> Router[Router]
subgraph Pipeline
Router -- next --> Interceptor1[Interceptor A]
Interceptor1 -- next --> Interceptor2[Interceptor B]
end
Interceptor2 -- Invoke --> Handler[Handler/Presenter]
Commands are lightweight data structures representing events or actions.
// record structs are recommended for zero-allocation messaging
public readonly record struct MoveCommand(Vector3 Destination) : ICommand;Tip
AOT/HybridCLR Note: While record struct is valid, ensure your AOT metadata is correctly generated for iOS/AOT environments if using tools like HybridCLR.
Use the [Routes] attribute on a partial class to receive commands.
[Routes]
public partial class PlayerPresenter
{
// Use ValueTask for performance in pure .NET projects
[Route]
public async ValueTask On(MoveCommand cmd)
{
await MoveToAsync(cmd.Destination);
}
// Use UniTask for Unity-specific async handling
[Route]
public async UniTask On(SomeAsyncCommand cmd)
{
await DoSomethingAsync();
}
}Connect your handler to a router and start sending commands.
var router = new Router();
var presenter = new PlayerPresenter();
// MapTo returns a Subscription (IDisposable)
using var subscription = presenter.MapTo(router);
// Publish a message
await router.PublishAsync(new MoveCommand(new Vector3(1, 0, 0)));You can also subscribe with lambdas instead of using the source generator.
// Simple subscription
router.Subscribe<MoveCommand>(cmd => { /* ... */ });
// Async subscription with ordering
router.SubscribeAwait<MoveCommand>(async (cmd, ct) =>
{
await DoSomethingAsync();
}, CommandOrdering.Sequential);
// Inline interceptors (Filters)
//
// `WithFilter(...)` returns a derived child router that owns the given filter.
// Subscribers registered on the child receive commands published on the parent
// (with the filter applied), just like an Rx `Where` chain forwards items from
// its source.
router
.WithFilter<MoveCommand>(async (cmd, context, next) =>
{
Console.WriteLine("Before");
await next(cmd, context);
Console.WriteLine("After");
})
.Subscribe(cmd => { /* ... */ });
// Publishing on the parent triggers the filter for subscribers on the child:
await router.PublishAsync(new MoveCommand(...));
// → "Before" → handler → "After"One of VitalRouter's standout features is declarative control over how concurrent async handlers run. This matters most in games, where dialogue, cutscenes, and tutorials must play in order — never overlapping.
Take a cutscene. You just publish commands, and VitalRouter plays them back-to-back:
public readonly record struct WalkCommand(Vector3 To) : ICommand;
public readonly record struct SpeakCommand(string Text) : ICommand;
public readonly record struct WaitCommand(float Seconds) : ICommand;
// `Sequential`: each command waits for the previous handler to finish.
[Routes(CommandOrdering.Sequential)]
public partial class CutscenePresenter : MonoBehaviour
{
[Route]
async UniTask On(WalkCommand cmd) => await character.WalkToAsync(cmd.To);
[Route]
async UniTask On(SpeakCommand cmd) => await dialogueView.ShowAsync(cmd.Text);
[Route]
async UniTask On(WaitCommand cmd) => await UniTask.Delay(TimeSpan.FromSeconds(cmd.Seconds));
}// Fire-and-forget. VitalRouter queues these and runs them strictly in order.
router.PublishAsync(new WalkCommand(stage.Center));
router.PublishAsync(new SpeakCommand("Hello there!"));
router.PublishAsync(new WaitCommand(0.5f));
router.PublishAsync(new SpeakCommand("Welcome to our little town."));Without ordering, all four handlers would start at the same time and the scene would be a mess. With Sequential, the character walks in, then speaks, then pauses, then speaks again — no manual coroutine chaining or hand-rolled state machine required.
Pick the strategy that fits each case:
| Ordering | Behavior | Typical use |
|---|---|---|
Parallel (default) |
Run all handlers concurrently | Independent reactions |
Sequential |
Queue, then run one at a time in order | Dialogue, cutscenes, tutorials |
Drop |
Ignore new commands while one is still running | Debounce buttons, prevent double-firing |
Switch |
Cancel the running handler, start the new one | "Latest wins" — re-targeting, search-as-you-type |
Ordering can be set globally, per Router, per [Routes] class, per [Route] method, or per SubscribeAwait call.
VitalRouter is highly optimized for Unity, especially when combined with UniTask.
When using MapTo in a MonoBehaviour, always bind the subscription to the object's lifecycle.
[Routes]
public partial class CharacterController : MonoBehaviour
{
private void Start()
{
// Bind the subscription to this GameObject's lifecycle
this.MapTo(Router.Default).AddTo(destroyCancellationToken);
}
[Route]
public void On(MoveCommand cmd)
{
transform.position = cmd.Destination;
}
}Important
Assembly Definition (.asmdef): You must reference VitalRouter in your .asmdef file for the Source Generator to process your [Routes] attributes.
UniTask is a fast async/await library for Unity. VitalRouter actively supports UniTask. Requires: UniTask >= 2.5.5
Tip
If UniTask is installed, the VITALROUTER_UNITASK_INTEGRATION flag is defined automatically, enabling optimized GC-free code paths.
- Unity projects: Unity 2022.2+ required for incremental source generator support.
- Standard .NET projects: .NET 6.0+.
Note
Since version 2.0, Unity distribution has moved to NuGet.
- Install NuGetForUnity.
- Search for and install the
VitalRouterpackages in the NuGet window.
Optional Extensions for Unity
Install the following package to use Unity-specific features, including integration with VContainer/UniTask.
https://github.com/hadashiA/VitalRouter.git?path=/src/VitalRouter.Unity/Assets/VitalRouter#2.7.1
You can pipeline async interceptors for published messages. This is a powerful general-purpose pattern for message processing.
R3 is a next-generation Reactive Extensions implementation for C#. VitalRouter integrates with R3.
Control command publishing via external MRuby scripts. MRuby fibers and C# async/await are fully integrated.
Based on large-scale production usage (e.g., HybridFrame):
- Prefer record structs: For commands that are pure data,
record structprovides equality comparison and zero-allocation benefits. - Explicit Lifecycles: Always use
.AddTo(destroyCancellationToken)or manualDispose()to avoid memory leaks and ghost event handling. - UniTask for Unity: Use
UniTaskorUniTaskVoidas return types in Unity handlers to leverage optimized pooling. UseValueTaskfor pure .NET projects. - Contextual Metadata: Use
PublishContextfor cross-cutting concerns (logging IDs, cancellation tokens, user permissions) instead of adding those fields to your command structs. - Sequential by Default for UI: Use
CommandOrdering.Sequentialfor UI animations or dialogue sequences to prevent race conditions. - DI Integration: In Unity, VContainer (>= 1.16.6) is highly recommended for managing router lifecycles and handler registration.
MIT
@hadashiA
