Fusion is a .NET library that implements Distributed REActive Memoization (DREAM) โ a novel technique that gracefully solves a number of well-known problems:
Problem | So you don't need... |
---|---|
๐ฑ Client-side state management | Fluxor, Redux, MobX, Recoil, ... |
๐ Real-time updates | SignalR, WebSockets, gRPC streaming, ... |
๐ In-memory cache | Redis, memcached, ... |
๐คน Real-time cache invalidation | No good solutions - it's an infamously hard problem |
๐ช Automatic & transparent pub/sub | A fair amount of code |
๐คฌ Network chattiness | A fair amount of code |
๐ฐ Single codebase for Blazor WebAssembly, Server, and Hybrid | No good alternatives |
So what DREAM means?
- Memoization is a way
to speed up function calls by caching their output for a given input.
Fusion provides a transparent memoization for any function, so
when you call
GetUser(id)
multiple times, its actual computation happens just once for everyid
assuming there is enough RAM to cache every result. - Reactive
part of your Fusion-based code reacts to changes by triggering
invalidations. Invalidation is a call to memoizing function inside
a special
using (Computed.Invalidate()) { ... }
block, which marks the cached result for some specific call (e.g.GetUser(3)
) as inconsistent with the ground truth, which guarantees it will be recomputed on the next actual (non-invalidating) call. Suprisingly, invalidation is cascading: ifGetUserPic("[email protected]")
callsGetUser(3)
, its result will be invalidated as well in this case, and the same will happen with every other computed result that depends onGetUser(3)
directly or indirectly. This means Fusion tracks dependencies between cached computation results; the dependency graph is built and updated in the runtime, and this process is completely transparent for the developers. - The dependency graph can be
Distributed:
Fusion allows you to create invalidation-aware caching RPC clients
for any of such functions.
- They eliminate network chattiness by re-using locally cached results while it's known they aren't invalidated on the server side yet. The pub/sub, delivery, and processing of invalidation messages happens automatically and transparently for you.
- Moreover, such clients register their results in Fusion's dependency graph
like any other Fusion functions, so if you client-side code declares
GetUserName(id) => server.GetUser(id).Name
function,GetUserName(id)
result will be invalidated onceGetUser(id)
gets invalidated on the server side. And that's what powers all real-time UI updates on the client side in Fusion samples.
Lot traceability is probably the best real-world analogy of how this approach works. It allows to identify every product (computed value) that uses certain ingredient (another computed value), and consequently, even every buyer of a product (think UI control) that uses certain ingredient. So if you want every consumer to have the most up-to-date version of every product they bought (think real-time UI), lot traceability makes this possible.
And assuming every purchase order triggers the whole build chain and uses the most recent ingredients, merely notifying the consumers they can buy a newer version of their ๐ฑ is enough. It's up to them to decide when to update - they can do this immediately or postpone this till the next ๐ฐ, but the important piece is: they are aware the product they have is obsolete now.
We know all of this sounds weird. 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!
If you prefer slides and ๐ต detective stories, check out
"Why real-time web apps need Blazor and Fusion?" talk -
it explains how all these problems are connected and
describes how you can code a simplified version of
Fusion's key abstraction in C#.
And if you prefer text - just continue reading!
"What is your evidence?"*
This 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 piece of shared state there, including sign-in state:
A small benchmark in Fusion test suite compares "raw" Entity Framework Core-based Data Access Layer (DAL) against its version relying on Fusion:
The speedup you see is:
- ~31,500x for Sqlite EF Core Provider
- ~1,000x for In-memory EF Core Provider
- These numbers are slightly smaller on the most recent Fusion version, but the difference is still huge.
Fusion's transparent caching ensures every computation your code runs re-uses as many of cached dependencies as possible & caches its own output. As you can see, this feature allows to speed up even a very basic logic (fetching a single random user) using in-memory EF Core provider by 1000x, and the more complex logic you have, the larger performance gain is.
There are 4 components:
- Compute Services are services exposing methods "backed" by Fusion's
version of "computed observables". When such methods run, they produce
Computed Values (instances of
IComputed<T>
) under the hood, even though the results they return are usual (i.e. are of their return type). ButIComputed<T>
instances are cached and reused on future calls to the same method with the same arguments; moreover, they form dependency graphs, so once some "deep"IComputed<T>
gets invalidated, all of its dependencies are invalidated too. - Replica Services are remote proxies of Compute Services.
They substitute Compute Services they "replicate" on the client side
exposing their interface, but more importantly, they also "connect"
IComputed<T>
instances they create on the client with their server-side counterparts. Any Replica Service is also a Compute Service, so any other client-side Compute Service method that calls it becomes dependent on its output too. And since any Compute Service never runs the same computation twice (unless it is invalidated), they kill any network chattiness. - State - more specifically,
IComputedState<T>
andIMutableState<T>
. States are quite similar to observables in Knockout or MobX, but designed to follow Fusion game rules. And yes, you mostly use them in UI and almost never - on the server-side. - And finally, there is
IComputed<T>
โ an observable Computed Value that's in some ways similar to the one you can find in Knockout, MobX, or Vue.js, but very different, if you look at its fundamental properties.
IComputed<T>
is:
- Thread-safe
- Asynchronous โ any Computed Value is computed asynchronously; Fusion APIs dependent on this feature are also asynchronous.
- Almost immutable โ once created, the only change that may happen to it is transition
to
IsConsistent() == false
state - GC-friendly โ if you know about
Pure Computed Observables
from Knockout, you understand the problem.
IComputed<T>
solves it even better โ dependent-dependency relationships are explicit there, and the reference pointing from dependency to dependent is weak, so any dependent Computed Value is available for GC unless it's referenced by something else (i.e. used).
All of this makes it possible to use IComputed<T>
on the server side โ
you don't have to synchronize access to it, you can use it everywhere, including
async functions, and you don't need to worry about GC.
Check out how Fusion differs from SignalR โ this post takes a real app example (Slack-like chat) and describes what has to be done in both these cases to implement it.
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 of this part of the state stays in memory
- Destroys every part of the dependency graph that isn't "used" by one of "observed" components.
Check out "Scaling Fusion Services" part of the Tutorial to see a much more robust description of how Fusion scales.
Most of Fusion-based code lives in Compute Services.
Such services are resolved via DI containers to their Fusion-generated proxies
producing Computed Values while they run.
Proxies cache and reuse these IComputed<T>
instances on future calls to
the same method with the same arguments.
A typical Compute Service looks as follows:
public class ExampleService
{
[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);
}
}
}
As you might guess:
[ComputeMethod]
indicates that every time you call this method, its result is "backed" by Computed Value, and thus it captures dependencies (depends on results of any other compute method it calls) and allows other compute methods to depend on its own results. This attribute works only when you register a service as Compute Service in IoC container and the method it is applied to is async and virtual.Computed.Invalidate()
call creates a "scope" (IDisposable
) which makes every[ComputeMethod]
you call inside it to invalidate the result for this call, which triggers synchronous cascading (i.e. recursive) invalidation of every dependency it has, except remote ones (they are invalidated asynchronously).
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.AddComputeService<ExampleService>();
Check out CounterService from HelloBlazorServer sample to see the actual code of compute service.
Note: Most of Fusion Samples use attribute-based service registration, which is just another way of doing the same. So you might need to look for
[ComputeService]
attribute there to find out which compute services are registered there.
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. It is used in a few samples,
including Board Games.
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 is a [Compute Method] too,
so captures its dependencies & its result gets invalidated once
cascading invalidation from one of its "ingredients" "hits" it.
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 which state might be impacted by this change. Which means 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. Kind of an easy problem too, but note that you should do the same on 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 future.
- Make UI to 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 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 allowing it to identifying and track data dependencies automatically.
Fusion allows you to create truly independent UI components. You can embed them in any parts of UI you like 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 Replica Service on the client, which not simply proxies the calls, but also completely kills the chattiness you'd expect from a regular client-side proxy. So if you need to support both modes, Fusion is currently the only library solving this problem gracefully.
Replica Service's RPC protocol is actually an extension to
regular Web API, which kicks in only when a client submits a
special header, but otherwise the endpoint acts as a regular one.
So any of such APIs is callable even without Fusion! Try to
open this page in one window in
and call โ/apiโ/Sumโ/Accumulate
and /api/Sum/GetAccumulator
on this Swagger page in another window.
- Check out Samples, Tutorial, Slides, or go to Documentation Home
- Join our Discord Server to ask questions and track project updates.
- Fusion: 1st birthday, 1K+ stars on GitHub, System.Text.Json support in v1.4
- Popular UI architectures compared & how Blazor+Fusion UI fits in there
- Fusion: Current State and Upcoming Features
- The Ungreen Web: Why our web apps are terribly inefficient?
- Why real-time UI is inevitable future for web apps?
- How similar is Fusion to SignalR?
- How similar is Fusion to Knockout / MobX?
- Fusion In Simple Terms
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).