ActualLab.Fusion is a successor of Stl.Fusion. It works similarly to a method call middleware that transparently "plugs" a rich set of features into every call made to Fusion services – caching, invalidation, RPC, real-time sync, and many other capabilities, all without changing how you write your code.
You can think of Fusion as make or msbuild, but operating on functions and their outputs instead of source files and build artifacts. Like MSBuild, Fusion uses lazy computation:
- When something changes, dependent results are immediately marked as inconsistent – and this signal propagates all the way to remote clients
- Recomputation only happens when you actually request the result
Fusion solves a set of infamously hard problems with a 🦄 single abstraction:
| Problem | Fusion's Answer | So you don't need... |
|---|---|---|
| ⚡ Caching | In-memory memoization by call arguments | Redis, memcached, ... |
| 🔄 Cache invalidation | Automatic dependency tracking + cascading invalidation | Manual tracking – an infamously hard problem |
| 🪄 Real-time updates | Fusion client automatically propagates server-side cache invalidation back to the client making it aware of state changes on the server side | SignalR, WebSockets, custom pub/sub, ... |
| Client-side persistent cache support (IndexedDB, SQLite) is integrated right into the Fusion RPC client, so you get offline mode for free! | Service workers, sync logic, conflict resolution, ... | |
| 🤬 Network chattiness | Persistent cache enables speculative execution on the client side, allowing ActualLab.Rpc to batch hundreds of RPC calls into a single transmission frame |
Request batching, debouncing, manual optimization, ... |
| 📡 Network traffic | ActualLab.Rpc is 2-5x faster than gRPC and SignalR even in its "raw" mode; Fusion integration, if enabled, turns it into an efficiency beast by adding "cache match" responses |
Protocol tuning, custom serialization, ... |
| 🧩 Client-side state management | Same abstractions everywhere: Computed<T>, MutableState<T>, and compute methods |
MobX, Flux/Redux, Recoil, ... |
| 💰 Single codebase | Single codebase for Blazor Server, WebAssembly, and MAUI (iOS, Android, Windows, macOS, and more) | Platform-specific code, multiple implementations, ... |
The best part: you get all of this without turning your code into a mess. You can think of Fusion as a call middleware or a decorator. That's why Fusion-based code looks as if there is no Fusion at all! So you can focus on building your app and ship faster — and save yourself from dealing with a 2–3× larger codebase and a plethora of "why is it stale?" bugs, which are among the hardest to debug.
Documentation is the best place to start from.
If you prefer video, check out:
ActualLab.Fusion Video![]() |
ActualLab.Rpc Video![]() |
|---|
- Clone Fusion Samples repository:
git clone [email protected]:ActualLab/Fusion.Samples.git - Follow the instructions from README.md to build and run everything.
Below is Fusion+Blazor Sample delivering real-time updates to 3 browser windows:
The sample supports both Blazor Server and Blazor WebAssembly hosting modes. And even if you use different modes in different windows, Fusion still keeps in sync literally every bit of a shared state there, including the sign-in state:
Yes, it's incredibly fast. Here is an RPC call duration distribution for one of the most frequent calls on Voxt.ai:
IChats.GetTile reads a small "chat tile" – typically 5 entries pinned to a specific ID range, so it can be efficiently cached. And even for these calls the typical response time is barely measurable: every X-axis mark is 10x larger than the previous one, so the highest peak you see is at 0.03ms!
The next bump at ~4-5ms is when the service actually goes to the DB – i.e., it's the time you'd expect to see without Fusion. The load would be much higher though, because the calls you see on this chart are only the calls that "made it" to the server – in other words, they weren't eliminated by the client and its Fusion services.
Our benchmarks show Fusion delivering over 300 million calls per second on a consumer CPU (AMD Ryzen 9 9950X3D) with its transparent caching:
| Scenario | Without Fusion | With Fusion | Speedup |
|---|---|---|---|
| Local DAL, almost no writes (peak performance) | 38.61K calls/s | 313.75M calls/s | >8,000x |
| Local repo-like service, non-stop writes | 136.40K calls/s | 264.71M calls/s | ~1,941x |
| Remote repo-like service, non-stop writes | 100.33K calls/s (REST) | 222.06M calls/s | ~2,214x |
These aren't typos – Fusion makes your services thousands of times faster by eliminating redundant computation, RPC, and database access.
Note that these benchmarks test Fusion method calls with no dependency chains. Real-life Fusion-based API services typically call other compute services, forming deep dependency graphs. Each layer multiplies the savings (i.e. when something is recomputed, it's typically recomputed just partially), so real-world speedups are often even higher than what you see here.
ActualLab.Rpc is an extendable RPC protocol powering Fusion's distributed features. It isn't "coupled" to Fusion, so you can use it independently. It outperforms all major alternatives on plain RPC tests, especially on call tests and small-item streaming tests:
| Framework | Calls/s | Streaming |
|---|---|---|
| ActualLab.Rpc | 7.34M | 95.39M items/s |
| SignalR | 5.35M | 17.15M items/s |
| gRPC | 1.11M | 38.25M items/s |
So it's significantly faster than gRPC and SignalR, both for calls and for streaming.
- The concept itself is all about eliminating any unnecessary computation. Think
msbuild, but for your method call results: what's computed and consistent is never recomputed. - Fusion caches call results in memory, so if there's a cache hit, the result is instantly available. No round-trips to external caches, no serialization/deserialization, etc.
- Moreover, there is no cloning: what's cached is the .NET object or struct returned from a call, so any call result is "shared". This is much more CPU cache-friendly than, e.g., deserializing a new copy on every hit.
- Fusion uses its own
ActualLab.Interceptionlibrary for method interception. Unlike Castle.DynamicProxy and similar libraries that box arguments and allocate heavily, our interceptors require just 1 allocation per call with zero boxing – making them the fastest on .NET. ActualLab.Rpcuses the fastest serializers available on .NET – MemoryPack by default (it doesn't require runtime IL Emit), though you can also use MessagePack (it's slightly faster, but requires IL Emit) or anything else you prefer.- All critical execution paths in Fusion are heavily optimized. The archived version of this page shows that the performance of local compute services is currently 10x better than it was a few years ago.
Yes. Fusion does something similar to what any MMORPG game engine does: even though the complete game state is huge, it's still possible to run the game in real time for 1M+ players, because every player observes a tiny fraction of a complete game state, and thus all you need is to ensure the observed part of the state fits in RAM.
And that's exactly what Fusion does:
- It spawns the observed part of the state on-demand (i.e. when you call a Compute Service method)
- Ensures the dependency graph backing this part of the state stays in memory while someone uses it
- Destroys what's unobserved.
To use Fusion, you need to:
- Reference the
ActualLab.FusionNuGet package - "Implement"
IComputeService(a tagging interface) on your service - Mark methods requiring caching/invalidation with
[ComputeMethod]and declare them asvirtual - Register the service via
services.AddFusion().AddService<MyService>()
A typical Compute Service looks as follows:
public class ExampleService : IComputeService
{
[ComputeMethod]
public virtual async Task<string> GetValue(string key)
{
// This method reads the data from non-Fusion "sources",
// so it requires invalidation on write (see SetValue)
return await File.ReadAllTextAsync(_prefix + key);
}
[ComputeMethod]
public virtual async Task<string> GetPair(string key1, string key2)
{
// This method uses only other [ComputeMethod]-s or static data,
// thus it doesn't require invalidation on write
var v1 = await GetValue(key1);
var v2 = await GetValue(key2);
return $"{v1}, {v2}";
}
public async Task SetValue(string key, string value)
{
// This method changes the data read by GetValue and GetPair,
// but since GetPair uses GetValue, it will be invalidated
// automatically once we invalidate GetValue.
await File.WriteAllTextAsync(_prefix + key, value);
using (Invalidation.Begin()) {
// This is how you invalidate what's changed by this method.
// Call arguments matter: you invalidate only a result of a
// call with matching arguments rather than every GetValue
// call result!
_ = GetValue(key);
}
}
}[ComputeMethod] indicates that every time you call this method, its result is "backed" by a Computed Value, and thus it captures dependencies when it runs and instantly returns the result if the current computed value is still consistent.
Compute services are registered similarly to singletons:
var services = new ServiceCollection();
var fusion = services.AddFusion(); // It's ok to call it many times
// ~ Like service.AddSingleton<[TService, ]TImplementation>()
fusion.AddService<ExampleService>();Check out CounterService from HelloBlazorServer sample to see the actual code of compute service.
Now, I guess you're curious how the UI code looks with Fusion. You'll be surprised, but it's as simple as it could be:
// MomentsAgoBadge.razor
@inherits ComputedStateComponent<string>
@inject IFusionTime _fusionTime
<span>@State.Value</span>
@code {
[Parameter]
public DateTime Value { get; set; }
protected override Task<string> ComputeState()
=> _fusionTime.GetMomentsAgo(Value);
}MomentsAgoBadge is a Blazor component that displays an
"N [seconds/minutes/...] ago" string. The code above is almost identical to its
actual code,
which is a bit more complex due to null handling.
You see it uses IFusionTime – one of the built-in compute services that provides GetUtcNow and GetMomentsAgo methods. As you might guess, the results of these methods are invalidated automatically; check out FusionTime service to see how it works.
But what's important here is that MomentsAgoBadge is inherited from
ComputedStateComponent –
an abstract type which provides ComputeState method. As you might guess, this method behaves like a Compute Method.
ComputedStateComponent<T> exposes State property (of ComputedState<T> type),
which allows you to get the most recent output of ComputeState() via its
Value property. "State" is another key Fusion abstraction – it implements a "wait for invalidation and recompute" loop
similar to this one:
var computed = await Computed.Capture(_ => service.Method(...));
while (true) {
await computed.WhenInvalidated();
computed = await computed.Update();
}The only difference is that it does this in a more robust way - in particular, it allows you to control the delays between the invalidation and the update, access the most recent non-error value, etc.
Finally, ComputedStateComponent automatically calls StateHasChanged()
once its State gets updated to make sure the new value is displayed.
So if you use Fusion, you don't need to code any reactions in the UI. Reactions (i.e. partial updates and re-renders) happen automatically due to dependency chains that connect your UI components with the data providers they use, which in turn are connected to data providers they use, and so on - till the very basic "ingredient providers", i.e. compute methods that are invalidated on changes.
If you want to see a few more examples of similarly simple UI components, check out:
- Counter.razor - a Blazor component that uses CounterService from HelloBlazorServer sample
- ChatMessageCountBadge.razor and AppUserBadge.razor from Board Games.
Real-time typically implies you use events to deliver change notifications to every client whose state might be impacted by a change, so you have to:
- Know which clients to notify about a particular event. This alone is a fairly hard problem - in particular, you need to know what every client "sees" now. Sending events for anything that's out of the "viewport" (e.g. a post you may see, but don't see right now) doesn't make sense, because it's a huge waste that severely limits the scalability. Similarly to MMORPG, the "visible" part of the state is tiny in comparison to the "available" one for most of web apps too.
- Apply events to the client-side state. This seems easy too, but note that you should do the same on the server side as well, and keeping the logic in two completely different handlers in sync for every event is a source of potential problems in the future.
- Make the UI properly update its event subscriptions on every client-side state change. This is what client-side code has to do to ensure p.1 properly works on the server side. And again, this looks like a solvable problem on paper, but things get much more complex if you want to ensure your UI provides a truly eventually consistent view. Just think in which order you'd run "query the initial data" and "subscribe to the subsequent events" actions to see some issues here.
- Throttle down the rate of certain events (e.g. "like" events for every popular post). Easy on paper, but more complex if you want to ensure the user sees eventually consistent view on your system. In particular, this implies that every event you send "summarizes" the changes made by it and every event you discard, so likely, you'll need a dedicated type, producer, and handlers for each of such events.
And Fusion solves all these problems using a single abstraction that allows it to identify and track data dependencies automatically.
Fusion allows you to create truly independent UI components. You can embed them in any part of the UI without any need to worry about how they'll interact with each other.
This makes Fusion a perfect fit for micro-frontends on Blazor: the ability to create loosely coupled UI components is paramount there.
Besides that, if your invalidation logic is correct, Fusion guarantees that your UI state is eventually consistent.
You might think all of this works only in Blazor Server mode. But no, all these UI components work in Blazor WebAssembly mode as well, which is another unique feature Fusion provides. Any Compute Service can be substituted with a Compute Service Client, which doesn't simply proxy the calls, but also completely eliminates the chattiness you'd expect from a regular client-side proxy.
- Read Documentation
- Check out Samples
- Join Fusion Place to ask questions and track project updates.




