DEV Community

Cover image for Angular Signals, But Better Now for .NET
Federico Alterio
Federico Alterio

Posted on

Angular Signals, But Better Now for .NET

C# is Great for async

Did you know that C# is where async-await and reactive extensions was born?

Before rxjs made Observable popular in JavaScript, C# already had IObservable — complete with powerful LINQ operators (without that pipe cerimony, thanks to extension methods).
The same goes for promises and abort signals: they’re essentially JavaScript re-implementations of Task and CancellationToken.

C# has some of the best async support out there. In fact, it still offers capabilities that JavaScript lacks — async locals, custom awaitables.

And yet, one thing has been missing..

Signals.

That’s where SignalsDotnet comes in.

SignalsDotnet

SignalsDotnet brings the mental model and ergonomics of Angular Signals into the .NET ecosystem — without abandoning what makes C# great. It’s a modern reactive primitive designed for .NET developers who are tired of boilerplate-heavy INotifyPropertyChanged, leaky subscriptions, and over-engineered state management.

At its core, a signal is just a value — but one that knows who depends on it.

When a signal changes, everything that derives from it updates automatically. No manual wiring. No += and -=.
C#

var a = new Signal<int>(2);
var b = new Signal<int>(3);

var sum = Signal.Computed(() => a.Value + b.Value);

Console.WriteLine(sum.Value); // 5

a.Value = 10;

Console.WriteLine(sum.Value); // 13
Enter fullscreen mode Exit fullscreen mode

Here’s the (quasi) equivalent minimal JavaScript example using Angular-style signals:

Angular

const a = signal(2);
const b = signal(3);

const sum = computed(() => a() + b());
Enter fullscreen mode Exit fullscreen mode

Angular signals are tied to Angular’s lifecycle and scheduling.
SignalsDotnet is framework-agnostic: everything is computed synchronously and immediately, with no hidden batching or render cycle — closer to an Rx Subject.

Async computed

Do you remember when I said C# has one of the best async support?
What happens if our computation function is async?
Look at this:

C#

var a = new Signal<int>(2);
var b = new Signal<int>(3);

var computed = Signal.AsyncComputed(async ct =>
{
    // Access signal BEFORE await
    int first = a.Value;

    // Simulate async work
    await Task.Delay(500, ct);

    // Access signal AFTER await
    int second = b.Value;

    return first + second;
},  defaultValue: 0,
    ConcurrentChangeStrategy.CancelCurrent);
Enter fullscreen mode Exit fullscreen mode

You can notice that the b signal is accessed after the await. This is possible because C# has the concept of ExecutionContext and AsyncLocal, which effectively gives a “context” to our async call stack.

What this means in practice: even if the thread is doing other work while awaiting, C# still knows the computation depends on both a and b. The dependency tracking survives the suspension of the async method.

In contrast, JavaScript signals or Angular resources lose that kind of implicit context across async boundaries
This is an attempt to replicate an async computed in Angular, using resources
Angular

const a = signal(2);
const b = signal(3);

const computed = resource({
  params: () => ({ a: a() }), // only tracks a before async

  loader: async ({ params, abortSignal }) => {
    // Access signal BEFORE await
    const first = params.a;

    // Simulate async work
    await new Promise((resolve) => setTimeout(resolve, 500));

    // Access signal AFTER await
    const second = b(); // this access is NOT tracked automatically

    return first + second;
  },
  initialValue: 0,
});
Enter fullscreen mode Exit fullscreen mode

To make this work in Angular, you would have to either:

  • Declare the dependency on b before it’s accessed in the async function,
  • Split the computation into multiple computed signals or resources, or
  • Fallback to RxJS for more flexible dynamic tracking.

All of these approaches defeat a bit the purpose of computed signals, which are supposed to automatically track dependencies without extra boilerplate.

C# would have no problem to track also in while loops, for loops or whatever, it just works.

Can you imagine doing that with angular resources?

var numbers = new[]
{
    new Signal<int>(1),
    new Signal<int>(2),
    new Signal<int>(3)
};

// AsyncComputed that sums all signals, with async/await inside the loop
var asyncSum = Signal.AsyncComputed(async ct =>
{
    int total = 0;
    for (int i = 0; i < numbers.Length; i++)
    {
        total += numbers[i].Value;       // dependency tracked
        await Task.Delay(100, ct);       // async work inside loop
    }
    return total;
}, defaultValue: 0, ConcurrentChangeStrategy.CancelCurrent);
Enter fullscreen mode Exit fullscreen mode

There’s no easy counterpart in Angular — even in RxJS, because loops become recursion.

SignalsDotnet: Signal Types at a Glance

Signal Type Description Example
Signal<T> Writable, reactive value var count = new Signal<int>(0);
IReadOnlySignal<T> Read-only / computed signal var fullName = Signal.Computed(() => $"{first.Value} {last.Value}");
IAsyncReadOnlySignal<T> Async computed signal var asyncSum = Signal.AsyncComputed(async ct => { await Task.Delay(100); return count.Value * 2; });
ISignal<T> (Linked) Writable computed signal that can be overridden var linked = Signal.Linked(() => source.Value * 2); linked.Value = 100;
CollectionSignal<TObservableCollection> Deeply reactive wrapper around an ObservableCollection<T> var people = new CollectionSignal<ObservableCollection<Person>>();
DictionarySignal<TKey, TValue> Reactive dictionary with fine‑grained key tracking var scores = new DictionarySignal<string, int>(); scores["a"] = 1;
Effect Runs side effects when dependencies change Signal.Effect(() => Console.WriteLine(count.Value));

Highlights:

  • Automatic dependency tracking — signals automatically subscribe to only the signals they use.
  • Supports computed, async computed with cancellation, and linked writable computed signals.
  • CollectionSignal listens not just to the collection reference, but also to its internal changes (add/remove/clear).
  • DictionarySignal tracks keys independently so computed signals only rerun when the specific keys they depend on change.
  • Fully framework‑agnostic — works in console apps, WPF, MAUI, Blazor, Unity, Godot, etc.

Bonus: All SignalsDotnet signals are Observables. That means they integrate seamlessly with all Rx (R3), giving you the full power of operators like Select, Where, CombineLatest, and more.


Any feedback, suggestions, or experiences are more than welcome — I hope you give SignalsDotnet a try!

Top comments (0)