README.md

July 3, 2026 · View on GitHub

MemoizR

MemoizR-logo
Streamlined Concurrency

.NET NuGet NuGet

"The world is still short on languages that deal super elegantly and inherently and intuitively with concurrency" Mads Torgersen Lead Designer of C# (https://www.youtube.com/watch?v=Nuw3afaXLUc&t=4402s)

MemoizR is a library for .NET that brings the power of Dynamic Lazy Memoization and Declarative Structured Concurrency to your fingertips. It streamlines complex multi-threaded scenarios, making them easier to write, maintain, and reason about.

Inspired From Stephen Cleary — Asynchronous streams https://www.youtube.com/watch?v=-Tq4wLyen7Q&t=706s

compared towhich isMemoizR/Signals
IEnumerablesynchronousasynchronous
Tasksingle valuemulti value
Observablepush basedpush-pull
IAsyncEnumerablepull basedpush-pull

Key Features

  • Dynamic Lazy Memoization: Calculate values only when needed, avoiding unnecessary computations and optimizing performance.
  • Declarative Structured Concurrency: Easily manage complex concurrency scenarios with straightforward configuration, effortless maintenance, robust error handling, and seamless cancellation.
  • Dependency Graph:Automatically track dependencies between your data, ensuring that only necessary computations are performed.
  • Automatic Synchronization: Work with shared state without the hassle of manual synchronization.
  • Performance Optimization: Benefit from memoization for read-heavy scenarios and lazy evaluation for write-heavy scenarios. Inspiration

MemoizR draws inspiration from various sources

  • Reactively and Solid: Dynamic lazy memoization concepts.
  • VHDL: Synchronization mechanisms.
  • ReactiveX: Reactive programming paradigms.
  • Structured Concurrency: Principles for well-structured concurrent code. Special thanks to @mfp22 for the idea of signal operators!

Advantages over ReactiveX and Dataflow

MemoizR offers several advantages over traditional concurrency libraries:

Implicit Subscription Handling

No need to manage subscriptions manually; MemoizR automatically tracks and synchronizes dependencies. Implicit LinkTo: Dependencies are automatically linked based on your code's structure, simplifying data flow setup. Simplified Error Handling: Structured concurrency makes error handling more robust and easier to reason about.

Usage

Basic Memoization

// Setup
var f = new MemoFactory();
var v1 = f.CreateSignal(1);
var m1 = f.CreateMemoizR(async() => await v1.Get());
var m2 = f.CreateMemoizR(async() => await v1.Get() * 2);
var m3 = f.CreateMemoizR(async() => await m1.Get() + await m2.Get());

// Get Value manually
await m3.Get(); // Calculates m1 + 2 * m1 => (1 + 2 * 1) = 3

// Change
await Task.Run(async () => await v1.Set(2));
// Synchronization is handled by MemoizR
await m3.Get(); // Calculates m1 + 2 * m1 => (1 + 2 * 2) = 6
await m3.Get(); // No operation, result is still 6

await v1.Set(3); // Setting v1 does not trigger evaluation of the graph
await v1.Set(2); // Setting v1 does not trigger evaluation of the graph
await m3.Get(); // No operation, result is still 6 (because the last time the graph was evaluated, v1 was already 2)

Dynamic Graphs

MemoizR can handle dynamic changes in the dependency graph:

var m3 = f.CreateMemoizR(async() => await v1.Get() ? await m1.Get() : await m2.Get());

Declarative Structured Concurrency

var f = new MemoFactory("DSC");

var child1 = f.CreateConcurrentMapReduce(
    async c =>
    {
        await Task.Delay(3000, c.Token);
        return 3;
    });

// all tasks get canceled if one fails
var c1 = f.CreateConcurrentMapReduce(
    async c =>
    {
        await child1.Get();
        return 4;
    });

var x = await c1.Get();

Resources

A concurrent job can own resources. These resources will be disposed by the job after all its work is done.

var groupTask = f.CreateConcurrentMapReduce(async group =>
{
    group.AddResource(myDisposableResource);

    return await myDisposableResource.DoWorkAsync(group.Token);
});
await groupTask.Get(); // First, waits for all tasks to complete; then, disposes myDisposableResource.

All exceptions raised by disposal of any resource are ignored.

Reactivity

var f = new MemoFactory();
var v1 = f.CreateSignal(1);
var m1 = f.CreateMemoizR(async() => await v1.Get());
var m2 = f.CreateMemoizR(async() => await v1.Get() * 2);
var r1 = f.CreateReaction(m1, m2, (val1, val2) => val1 + val2);

Causality Stamps (preparation for distributed graphs)

Every node carries a causality stamp recording exactly which signal versions its current value reflects, captured atomically with the value — the groundwork for glitch-free synchronization of distributed graphs (#39, inspired by Interval Tree Clocks):

var (value, stamp) = await m1.GetWithStamp(); // the (value, stamp) pair of one publication
stamp.TryGetTrigger(v1.Id, out var trigger);  // which version of v1 the value reflects
var glitchFree = stamp.IsConsistentWith(otherStamp); // agreement on all shared signals
var payload = stamp.Serialize();              // compact, deterministic wire format

Stamps are space-efficient (uniform regions collapse, ITC-style), reset-resilient (a restarted graph's stamps are never confused with their pre-restart incarnation), and documented in docs/architecture/causality-trigger-clock.md. A runnable two-peer bridge — stale/pull protocol, glitch barrier, late-delivery dropping and reset detection — lives in samples/DistributedGraphSample.

Data-race safety (strict mode)

MemoizR publishes value references tear-free across concurrent flows, but only an immutable (or thread-safe) type makes the object behind the reference safe to share. Strict mode — the runtime analog of Swift's Sendable checking — validates this at node creation:

var f = new MemoFactory("strict", MemoFactoryOptions.StrictSendableChecks);

record Person(string Name, int Age);          // init-only members => Sendable
var p = f.CreateSignal(new Person("Ada", 36)); // ok
var xs = f.CreateSignal(ImmutableArray.Create(1, 2, 3)); // ok

f.CreateSignal(new List<int>()); // throws: List<int> is not Sendable
                                 // (writable instance field '_items')

Types the structural check cannot prove (internally synchronized ones) can opt in with [Sendable], the analog of Swift's @unchecked Sendable. Code that must only run inside a serialized graph evaluation can assert it dynamically — the preconditionIsolated() analog:

f.AssertEvaluationIsolated(); // throws outside a Get/Set/recompute/reaction update

The same discipline is checked at build time by analyzers bundled in the NuGet package (severity configurable per rule via .editorconfig):

RuleFlags
MZR001A non-Sendable value type at a Create* call — the compile-time mirror of strict mode
MZR002A computation writing captured locals, fields, or statics — lift that state into a Signal
MZR003Signal.Set inside a computation, which throws InvalidOperationException at runtime

Reaction side effects can be pinned to an executor — the analog of Swift's custom actor executors (SE-0392). AddSynchronizationContext(uiContext) covers UI threads; AddExecutor(new DedicatedThreadExecutor()) gives a single-threaded isolation seat whose installed SynchronizationContext keeps async continuations on its thread; any custom IExecutor works, per factory or per BuildReaction():

using var executor = new DedicatedThreadExecutor();
var f = new MemoFactory().AddExecutor(executor);
var r = f.BuildReaction().CreateReaction(m1, v =>
{
    executor.AssertIsolated(); // the executor-flavored preconditionIsolated()
    // touch executor-isolated state safely
});

Actor engine (experimental)

The Swift-actor-style core suggested by issue #36: graph bookkeeping runs as serialized turns of a per-context GraphActor instead of under locks, while computations stay parallel. Same semantics, no monitors, no deadlock surface — plus a read-evidence commit guard that closes a staleness window the lock engine leaves open:

var f = new MemoFactory();
var v = f.CreateActorSignal(1);
var m = f.CreateActorMemoizR(async () => await v.Get() * 2);
await m.Get(); // 2 — lazy, memoized, generation-guarded, actor-isolated bookkeeping

Actor nodes only interoperate with actor nodes of the same context (enforced at the type level); signals/memos only for now.

See ADR 0003 (runtime layer), ADR 0004 (analyzers), ADR 0005 (executors), and ADR 0006 (actor engine) for the design and its limits.

WPF / UI threads

With the MemoizR.Wpf package the whole dependency graph keeps evaluating on the thread pool; only each reaction's action is dispatched to the WPF UI thread:

var f = new MemoFactory().AddWpfDispatcher(); // uses Application.Current.Dispatcher

var v1 = f.CreateSignal(1);
var m1 = f.CreateMemoizR(async () => await v1.Get() * 2); // computed on worker threads

// Dependencies are passed as separate parameters so they are evaluated in parallel on the
// thread pool; only the action below runs on the UI thread, with the already-computed values.
var r1 = f.CreateReaction(m1, v => viewModel.Value = v);

await v1.Set(5); // safe from any thread

A specific Dispatcher can be supplied with f.AddWpfDispatcher(dispatcher). On other UI stacks, register the UI SynchronizationContext directly from the UI thread with f.AddSynchronizationContext(SynchronizationContext.Current!) (MemoizR.Reactive); reactions built from the factory then follow the same contract: dependencies on the thread pool, action on the registered context. Both routes wrap the context in a SynchronizationContextExecutor, the IExecutor seat described above.

Try it out! Experiment with MemoizR online: https://dotnetfiddle.net/Widget/EWtptc

Example From: Khalid Abuhakmeh

Testing

Run the test suite with dotnet test. Thread interleavings of the locking code are explored systematically with Microsoft Coyote; see docs/Coyote.md for how to rewrite the assemblies and run the Coyote test.