Appearance
Computed<T>: The Core Abstraction
This document covers Computed<T>, IComputed, extension methods, ComputedRegistry, and invalidation source tracking.
Overview
Computed<T> is Fusion's central abstraction — an immutable, cached result of a computation that:
- Stores either a value or an error
- Tracks its consistency state (Computing → Consistent → Invalidated)
- Knows its dependencies and dependants
- Can notify when it becomes invalidated
- Can produce an updated version of itself
IComputed Interface
csharp
public interface IComputed : IResult, IHasVersion<ulong>
{
ComputedOptions Options { get; }
ComputedInput Input { get; }
Type OutputType { get; }
ConsistencyState ConsistencyState { get; }
Result Output { get; }
InvalidationSource InvalidationSource { get; }
event Action<Computed> Invalidated;
void Invalidate(bool immediately = false, ...);
ValueTask<Computed> UpdateUntyped(CancellationToken cancellationToken = default);
Task UseUntyped(CancellationToken cancellationToken = default);
}Computed<T> Properties
Value Access
csharp
T Value { get; } // Throws if Error is set
T? ValueOrDefault { get; } // Returns default(T) if Error is set
Exception? Error { get; } // The error, if computation failed
bool HasValue { get; } // True if no error
bool HasError { get; } // True if error
// Deconstruct for pattern matching
var (value, error) = computed;
var (value, error, version) = computed;Metadata
csharp
ulong Version { get; } // Unique version number
ComputedOptions Options { get; } // Configuration options
ComputedInput Input { get; } // The input that produced this computed
ConsistencyState ConsistencyState { get; } // Current stateConsistencyState
Every Computed<T> goes through these states:
| State | Description |
|---|---|
Computing | Currently being computed (mutable during this phase) |
Consistent | Computation complete, value is current |
Invalidated | Marked as outdated, needs recomputation |
csharp
// Check state via extension methods
computed.IsConsistent() // true if Consistent
computed.IsInvalidated() // true if Invalidated
computed.IsComputing() // true if Computing
computed.IsConsistentOrComputing() // true if not InvalidatedStatic Methods
Computed.Current
Access the currently computing Computed<T> from within a compute method:
csharp
[ComputeMethod]
public virtual async Task<Data> GetData()
{
var current = Computed.Current; // Nullable — returns null when no current computed is tracked
// or
var current = Computed.GetCurrent<Data>(); // Throws if no current computed is tracked
}Computed.Capture
Capture the Computed<T> produced by a compute method call:
csharp
var computed = await Computed.Capture(() => service.GetData());
// computed is Computed<Data>
// TryCapture returns null if capture fails
var computed = await Computed.TryCapture(() => service.GetData());Computed.GetExisting
Get the cached Computed<T> without triggering computation:
csharp
var existing = Computed.GetExisting(() => service.GetData());
// Returns null if not cached, never computesComputed.New
Create a standalone computed value (useful for tests):
csharp
var computed = Computed.New(async ct => {
await Task.Delay(100, ct);
return 42;
});Context Scopes
Control how compute method calls interact with dependency tracking:
csharp
// Suppress dependency capture — calls won't register as dependencies
using (Computed.BeginIsolation()) {
// This call happens, but won't make the current computed depend on it
var data = await service.GetData();
}
// Capture mode — explicitly capture the computed produced by a call
using var scope = Computed.BeginCapture();
await service.GetData();
var captured = scope.Context.GetCaptured<Data>();
// Capture existing only — get cached computed without triggering computation
using var scope = Computed.BeginCaptureExisting();
_ = service.GetData(); // Synchronous if cached
var existing = scope.Context.TryGetCaptured<Data>(); // null if not cachedWhen to use BeginIsolation():
- Fetching data for logging or metrics without creating dependencies
- Calling compute methods from non-compute code where dependencies don't make sense
- Breaking potential dependency cycles
- Implementing "fire and forget" patterns where you don't want invalidation to propagate
Invalidation.Begin() leaks into spawned work via ExecutionContext
Invalidation.Begin() (and Invalidation.IsActive) is AsyncLocal-based, so it flows through ExecutionContext the same way any other ambient async state does. If code inside the using block (or a synchronous Invalidated event handler, which itself runs inside the invalidation pass) starts background work with Task.Run or a similar continuation, that work captures the current ExecutionContext snapshot — disposing the using block afterwards doesn't undo it. A compute method called later from that spawned work "runs" in invalidation mode: it skips its body, silently returns default(T), and invalidates the corresponding cached value instead of computing it. Guard such spawned work with Computed.BeginIsolation(), the same way framework code protects itself.
Extension Methods
Update & Use
csharp
// Get the latest version (recomputes if invalidated)
Computed<T> updated = await computed.Update(ct);
// Use as a dependency in current computation
T value = await computed.Use(ct);
// Use even if inconsistent (for showing stale data)
T value = await computed.Use(allowInconsistent: true, ct);Invalidation
csharp
// Invalidate immediately
computed.Invalidate();
computed.Invalidate(immediately: true);
// Invalidate after delay
computed.Invalidate(TimeSpan.FromSeconds(30));
// With explicit source for debugging
computed.Invalidate(new InvalidationSource("MyReason"));Waiting for Changes
csharp
// Wait until invalidated
await computed.WhenInvalidated(ct);
// Wait until value satisfies predicate
var updated = await computed.When(x => x > 100, ct);
// With custom update delayer
var updated = await computed.When(
x => x > 100,
FixedDelayer.Get(1), // 1 second between checks
ct);Observing Changes
csharp
// Stream of computed values as they change
await foreach (var c in computed.Changes(ct)) {
Console.WriteLine($"New value: {c.Value}");
}
// With custom delayer
await foreach (var c in computed.Changes(FixedDelayer.Get(0.5), ct)) {
// ...
}Invalidation Chain Navigation
csharp
// Get the root cause of invalidation
Computed origin = computed.GetInvalidationOrigin();Invalidation Event
Subscribe to know when a computed becomes invalidated:
csharp
computed.Invalidated += c => {
Console.WriteLine($"Invalidated: {c}");
};
// If already invalidated, handler fires immediatelyKeep dependency chains shallow
Invalidation propagates from a dependency to its dependants by synchronous recursion — it's the hottest path in the library, so it isn't implemented as an iterative, heap-based traversal. A dependency chain depth on the order of 10^3 risks a stack overflow. Dependency graphs are expected to stay shallow in practice; if you find yourself building deliberately deep chains (e.g. a linked list of computed values), restructure it instead of relying on deep recursive invalidation.
ComputedRegistry
ComputedRegistry is the global cache of all Computed<T> instances.
Accessing the Registry
csharp
// Get cached computed by input
Computed? cached = ComputedRegistry.Get(input);
// Enumerate all cached inputs
foreach (var input in ComputedRegistry.Keys) { ... }
// Invalidate everything (useful for tests)
ComputedRegistry.InvalidateEverything();
// Force prune dead entries
await ComputedRegistry.Prune();Events
csharp
// Called when a new Computed is registered
ComputedRegistry.OnRegister += computed => { ... };
// Called when a Computed is unregistered (after invalidation)
ComputedRegistry.OnUnregister += computed => { ... };
// Called on every access (for monitoring)
ComputedRegistry.OnAccess += (computed, isNew) => { ... };Configuration
csharp
// Before first use of Fusion:
ComputedRegistry.Settings.InitialCapacity = 10000;
ComputedRegistry.Settings.ConcurrencyLevel = 64;Metrics
ComputedRegistry.Metrics exposes OpenTelemetry metrics:
computed.registry.key.count— number of cached entriescomputed.registry.node.count— nodes in dependency graphcomputed.registry.edge.count— edges in dependency graphcomputed.registry.pruned.*— pruning statistics
Invalidation Source Tracking
Fusion can track why a computed was invalidated, which is invaluable for debugging.
InvalidationSource
Every invalidated Computed<T> has an InvalidationSource:
csharp
if (computed.IsInvalidated()) {
var source = computed.InvalidationSource;
Console.WriteLine($"Invalidated by: {source}");
}Tracking Modes
Configure via Invalidation.TrackingMode:
csharp
// At startup:
Invalidation.TrackingMode = InvalidationTrackingMode.WholeChain;| Mode | Description | Memory Impact |
|---|---|---|
None | No tracking, InvalidationSource returns Unknown | Lowest |
OriginOnly | Tracks original source only (default) | Low |
WholeChain | Tracks full invalidation chain | Higher |
Using WholeChain Mode
When WholeChain is enabled, you can walk the entire invalidation chain:
csharp
Invalidation.TrackingMode = InvalidationTrackingMode.WholeChain;
// Later, when debugging:
var computed = await Computed.Capture(() => service.GetData());
// ... some time passes, computed gets invalidated ...
if (computed.IsInvalidated()) {
// Get the direct source
var source = computed.InvalidationSource;
// Get the original root cause
var origin = source.Origin;
Console.WriteLine($"Root cause: {origin}");
// Walk the whole chain
foreach (var s in source) {
Console.WriteLine($" <- {s}");
}
// Or format as string
Console.WriteLine(source.ToString(InvalidationSourceFormat.WholeChain));
// Output: "GetUser(123) <- GetUserList() <- UserService.cs:42"
}InvalidationSourceFormat
csharp
computed.ToString(InvalidationSourceFormat.Default); // Direct source
computed.ToString(InvalidationSourceFormat.Origin); // Root cause only
computed.ToString(InvalidationSourceFormat.WholeChain); // Full chainCustom Invalidation Sources
Provide explicit sources when invalidating:
csharp
using (Invalidation.Begin()) {
// Source is auto-captured from caller location
_ = service.GetData(id);
}
// Or explicit:
computed.Invalidate(new InvalidationSource("Cache expired"));
computed.Invalidate(InvalidationSource.ForCurrentLocation());Predefined Sources
Fusion uses predefined sources for internal invalidations:
csharp
InvalidationSource.Unknown // When tracking is disabled
InvalidationSource.Cancellation // Cancellation-triggered
InvalidationSource.InitialState // Initial state invalidation
// ... and others for specific internal operationsComputedSource
ComputedSource (and its typed counterpart ComputedSource<T>) is a self-contained producer of Computed<T> values driven by a delegate you provide, rather than by a [ComputeMethod] call — ComputedState is built on top of it.
.Computed can transiently return a Computing instance
ComputedSource.Computed (and ComputedSource<T>.Computed) publish the next Computed<T> instance before it starts computing, so that the in-flight instance is reachable and can be invalidated while it's still computing. This means a direct read of .Computed may observe an instance whose ConsistencyState is Computing — and Output/Value throw on such an instance. Prefer Use() or Update() (which await completion of the current computation), or check ConsistencyState explicitly before reading Output/Value off .Computed directly.
ComputedImpl (Advanced)
ComputedImpl is a static class in ActualLab.Fusion.Internal that exposes low-level operations on Computed instances. These methods are primarily for framework extensions and advanced scenarios.
Internal API
These methods bypass normal safety checks. Use only when you understand the implications.
| Method | Description |
|---|---|
TrySetValue(computed, value) | Set the output value of a computing computed |
TrySetError(computed, exception) | Set an error on a computing computed |
TrySetOutput(computed, result) | Set output as a Result (value or error) |
StartAutoInvalidation(computed) | Start the auto-invalidation timer |
RenewTimeouts(computed, isNew) | Renew keep-alive timeouts |
CancelTimeouts(computed) | Cancel keep-alive timeouts |
GetDependencies(computed) | Get all dependencies of a computed |
GetDependants(computed) | Get all dependants (reverse dependencies) |
AddDependency(computed, dependency) | Manually add a dependency |
RemoveDependant(computed, dependant) | Remove a dependant |
PruneDependants(computed) | Remove invalidated dependants |
CopyDependenciesTo(computed, buffer) | Copy dependencies to a buffer |
IsTransientError(computed, error) | Check if error should trigger retry |
Example — inspecting the dependency graph:
csharp
using ActualLab.Fusion.Internal;
var computed = await Computed.Capture(() => service.GetAggregatedData());
// Get direct dependencies
var dependencies = ComputedImpl.GetDependencies(computed);
foreach (var dep in dependencies)
Console.WriteLine($"Depends on: {dep.Input}");
// Get dependants (who depends on this computed)
var dependants = ComputedImpl.GetDependants(computed);
foreach (var (input, version) in dependants)
Console.WriteLine($"Depended on by: {input}");Tips
- Computed values are immutable — after
Consistent, the value never changes; get a new version viaUpdate() - Use
Capturefor debugging — it's the easiest way to inspect the computed graph - Enable
WholeChaintemporarily — turn it on when debugging invalidation issues, off in production - Don't hold old computed values — they reference the dependency graph, preventing GC
- Prefer
WhenInvalidatedover polling — it's more efficient and immediate - Use
Changes()for reactive UIs — it handles update delays and retries automatically
