Build real-time apps (Blazor included) with less than 1% of extra code responsible for real-time updates. Host 10-1000x faster APIs relying on transparent and nearly 100% consistent caching. We call it DREAM, or Distributed REActive Memoization, and it's here to turn real-time on!
Fusion is a .NET library that implements
đŠ Distributed REActive Memoization (DREAM) â a novel abstraction somewhat similar to MobX or Flux, but designed to deal with an arbitrary large state spanning across your backend microservices, API servers, and reaching even every client of your app.
Fusion solves a set of infamously hard problems with a single hammer:
Problem | So you donât need⊠|
---|---|
đ Caching | Redis, memcached, ⊠|
đ€č Real-time cache invalidation | No good solutions - itâs an infamously hard problem |
đ Real-time updates | SignalR, WebSockets, gRPC, ⊠|
đ€Ź Network chattiness | A fair amount of code |
đ Offline mode support | A fair amount of code |
đ± Client-side state management | MobX, Flux/Redux, Recoil, ⊠|
đ° Single codebase for Blazor WebAssembly, Server, and Hybrid/MAUI | No good alternatives |
And the best part is: Fusion does all of that transparently for you, so Fusion-based code is almost identical to a code that doesnât involve it. All you need is to:
IComputeService
(a tagging interface) on your Fusion service to ensure call intercepting proxy is generated for it in compile time.[ComputeMethod]
+ declare them as virtual
serviceCollection.AddFusion().AddService<MyService>()
The magic happens when [ComputeMethod]
-s are invoked:
(serviceInstance, method, args...)
cache key) is still consistent, Fusion returns it instantly, without letting the method to run.[ComputeMethod]
call triggered during the evaluation of another [ComputeMethod]
call.The second step allows Fusion to track which values are expected to change when one of them changes. Itâs quite similar to lot traceability, but implemented for arbitrary functions rather than manufacturing processes.
The last piece of a puzzle is Computed.Invalidate()
block allowing to tag cached results as âinconsistent with the ground truthâ. Here is how you use it:
var avatars = await GetUserAvatars(userId);
using (Computed.Invalidate()) {
// Any [ComputeMethod] invoked inside this block doesn't run normally,
// but invalidates the result of the identical call instead.
// Such calls complete synchronously and return completed Task<TResult>,
// so you don't need to await them.
_ = userService.GetUser(userId);
foreach (var avatar in avatars)
_ = userAvatarService.GetAvatar(userId, avatar.Id);
}
The invalidation is always transitive: if GetUserProfile(3)
calls GetUserAvatar("3:ava1")
, and GetUserAvatar("3:ava1")
gets invalidated, GetUserProfile(3)
gets invalidated as well.
To make it work, Fusion maintains a dictionary-like structure that tracks recent and âobservedâ call results:
(serviceInstance, method, call arguments...)
Computing
, Consistent
, Invalidated
) and dependent-dependency links. Computed<T>
instances are nearly immutable: once constructed, they can only transition to Inconsistent
state.You can âpullâ the Computed<T>
instance âbackingâ certain call like this:
var computed1 = await Computed.Capture(() => GetUserProfile(3));
// You can await await for its invalidation:
await computed1.WhenInvalidated();
Assert.IsFalse(computed1.IsConsistent());
// And recompute it:
var computed2 = await computed1.Recompute();
So any Computed<T>
is observable. Moreover, it can be a âreplicaâ of a remote Computed<T>
instance that mirrors its state in your local process, so the dependency graph can be distributed. To make it work, Fusion uses its own WebSocket-based RPC protocol, which is quite similar to any other RPC protocol:
Step 3 doesnât change much in terms of network traffic: itâs either zero or one extra message per call (i.e. 3 messages instead of 2 in the worst case). But this small addition allows Compute Service Clients to know precisely when a given cached call result becomes inconsistent.
The presence of step 3 makes a huge difference: any cached & still consistent result is as good as the data youâll get from the remote server, right? So itâs totally fine to resolve a call that âhitsâ such a result locally, incurring no network round-trip!
Finally, any Compute Service Client behaves as a similar local Compute Service. Look at this code:
string GetUserName(id)
=> (await userService.GetUser(id)).Name;
You canât tell whether userService
here is a local compute service or a compute service client, right?
IUserService
). The implementations are different though: Fusion service client is registered via fusion.AddClient<TInterface>()
vs fusion.AddServer<TInterface, TService>()
for the server.userService
terminates instantly if its previous result is still consistentGetUserName
is a method of another computed service (a local one), computed value backing GetUser(id)
call that it makes would automatically extend Fusionâs dependency graph for GetUserName(id)
call!So Fusion abstracts away the âplacementâ of a service, and does it much better than conventional RPC proxies: Fusion proxies arenât âchattyâ by default!
If you prefer slides, check out
âWhy real-time web apps need Blazor and Fusion?â talk -
it explains how many problems we tackle are connected, how Fusion addresses the root cause, and how to code a simplified version of Fusionâs key abstraction in C#.
The slides are slightly outdated - e.g. now Fusion clients use
Stl.Rpc
rather than HTTP to communicate with the server, but all the concepts they cover are still intact.
Quick Start, Cheat Sheet, and the Tutorial are the best places to start from.
Check out Samples; some of them are covered further in this document.
All of this sounds way too good to be true, right? Thatâs why there are lots of visual proofs in the remaining part of this document. But if youâll find anything concerning in Fusionâs source code or samples, please feel free to grill us with questions on Discord!
Letâs start with some big guns:
Check out Actual Chat â a very new chat app built by the minds behind Fusion.
Actual Chat fuses real-time audio, live transcription, and AI assistance
to let you communicate with utmost efficiency.
With clients for WebAssembly, iOS, Android, and Windows, it boasts nearly
100% code sharing across these platforms.
Beyond real-time updates, several of its features, like offline mode,
are powered by Fusion.Weâre posting some code examples from Actual Chat codebase here,
so join this chat to learn how we use it in a real app.
Now, the samples:
Below is Fusion+Blazor Sample
delivering real-time updates to 3 browser windows:
Play with
live version of this sample right now!
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 Actual Chat:
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 way higher though, coz the calls you see on this chart are the calls which âmade itâ to the server - in other words, they werenât eliminated by the client / its Fusion services.
A small synthetic benchmark in Fusion test suite
compares ârawâ Entity Framework Core-based
Data Access Layer (DAL) against its version relying on Fusion:
Calls/s | PostgreSQL | MariaDB | SQL Server | Sqlite |
---|---|---|---|---|
Single reader | 1.02K | 645.77 | 863.33 | 3.79K |
960 readers (high concurrency) | 12.96K | 14.52K | 16.66K | 16.50K |
Single reader + Fusion | 9.54M | 9.28M | 9.05M | 8.92M |
960 readers + Fusion | 145.95M | 140.29M | 137.70M | 141.40M |
The raw output for this test on Ryzen Threadripper 3960X is here. The number of readers looks crazy at first, but it is tweaked to maximize the output for non-Fusion version of DAL (the readers are asynchronous, so they mostly wait for DB response there).
Fusionâs transparent caching ensures every API call result your code produces is cached, and moreover, even when such results are recomputed, they mostly use other cached dependencies instead of hitting a much slower storage (DB in this case).
And interestingly, even when there are no âlayersâ of dependencies (think only âlayer zeroâ is there), Fusion manages to speed up the API calls this test runs by 8,000 to 12,000 times.
msbuild
, but for your method call results: whatâs computed and consistent is never recomputed.Stl.Interception
library to intercept method calls, and although there is no benchmark yet, these are the fastest call interceptors available on .NET - theyâre marginally faster than e.g. the ones provided by Castle.DynamicProxy. They donât box call arguments and require just 1 allocation per call.Stl.Rpc
- a part of Fusion responsible for its RPC calls. Its preliminary benchmark results show it is ~ 1.5x faster than SignalR, and ~ 3x faster than gRPC.Stl.Rpc
uses 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.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:
Check out âScaling Fusion Servicesâ part of the Tutorial to see a much more robust description of how Fusion scales.
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 GetNonFusionData(key1);
var v2 = await GetNonFusionData(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 (Computed.Invalidate()) {
// 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 Computed Value, and thus it captures dependencies when it runs and instantly returns the result, if the current computed value is still consisntent.
Compute services are registered ~ almost like 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 like 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 Blazor component displays
"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 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:
Real-time typically implies you use events to deliver change
notifications to every client which state might be impacted by
this change, so you have to:
And Fusion solves all these problems using a single abstraction allowing it
to identifying and track data dependencies automatically.
Fusion allows you to create truly independent UI components.
You can embed them in any part of UI without any need
to worry of 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 Compute Service Client, which not simply proxies the calls, but also completely
kills the chattiness youâd expect from a regular client-side proxy.
P.S. If youâve already spent some time learning about Fusion,
please help us to make it better by completing Fusion Feedback Form
(1âŠ3 min).