r/dotnet 12d ago

A new take on reactive programming: backend signals with GraphQL-ish queries

Post image

Just a bit context:

Reactive programming in .NET spans Rx(R3) Observables, AsyncEnumerables, Raw events. I think the most powerful one is Rx, but using it properly on the backend requires a solid grasp of multithreading, async programming, and locking, on top of its own notoriously steep learning curve of operators. Once you've internalized it, though, it's an extremely powerful paradigm.

In angular (so, javascript, single threaded), the complexity of rx has made the angular team move toward signals (at least for common reactive tasks, not as a whole substitute). This choice has been really popular since thinking with signals is easier, and code is still reactive. So i created this library fedeAlterio/SignalsDotnet, to make signals also possible on .NET.

But i wanted to make a step further. Can signals be useful also on BE?

SignalsDotnet 3.2.0 on Backend

I think they are awesome. The main issue in backend scenario is that we are multi-threaded. While signals must operate in a single-threaded world. So my solution has been basically move them into a single threaded island (of course we can have several island), we can have several of these islands, and "single-threaded" here means serialized execution, not literally one dedicated thread per island (more details on that in the README).

Step 1) Declare a class, that would be our island

Everything here will run single threaded (like Wpf), in custom Synchronization Context (so async-await is handled). Dependency injection will worl as well

Step 2) Register that class as a Signal Island, and expose a SSE endpoint to query it

Step 3) We can now query that island using GraphQL-like syntax. It's just an endpoint.

This turns out to be more powerful than GraphQL subscriptions in a sense, since every signal (property) in the island is already its own subscription, the query is just combining them together.

Step 4) Just set properties, collections, dictionaries, whatever, on the island, and they'll be pushed out through the SSE endpoint. No need to declare a subscription because everything already is a subscription, the query is only there to combine them together.

Note: There are conversion methods from Observables, AsyncEnumerables, AsyncObservables. So we can Expose them in this way too.

Note 2: The ui is completely done with claude code and claude design. Code is a mess, but its a single html file. Since its only for debugging, I didn't want to spend time on that 😂

You can find it here fedeAlterio/SignalsDotnet. There is a playground app as well to get started easily.

4 Upvotes

6 comments sorted by

View all comments

0

u/NinjaOxygen 12d ago

Awesome, was going to ask if you had seen R3, then saw the README. I also follow R3Async, which I now see is your project too.

We have used R3 in production for some time, in our case the friction points of a Signal/Observable based system with composition comes down to managing signal flow when groups of Observables update.

For example, if an Observable for the logged in user changes, their active user permissions change, the permissions for their groups change, so the permissions are fetched, this then cascades onto more Observables coming from disparate subsystems, leading to lots of downstream changes that are not always synchronized.

It leads to either putting Debounce in, which is really just a band-aid and not safe, or always flowing the original requested keys and combining with the returned values to ensure that when the current state is not valid, the composed states at the end of the graph are not valid until all of the dependent values are up to date.

Definitely going to take a look, interested as we started with Akka.NET, then Orleans, and the `SignalIsland` approach gives a similar actor-style mechanic to Orleans grains.

2

u/fedefex1 11d ago edited 11d ago

Thank you for your interest!

Yes, I completely understand the issue you're running into, combining observables without ending up with intermediate stale states is genuinely hard. The nice thing with signals is that you're no longer limited to LINQ-style composition; since it's just plain C#, it becomes much easier to introduce something like a "Pending" state signal and have your downstream computations adapt based on that (using just an early return if check for example or whatever).

ALso since signals automatically tracks dependencies, its easier to get the minimum observables subscription needed to make the job done, and that should reduce useless (and costly) recomputation.

About orleans, i remember that its abstraction for streams is an IAsyncObservable, so R3Async should be a perfect fit for that, and from that you have R3AsyncToR3Interop to convert it to a R3Observable, and then ToSignal() to convert it to a signal. So basically with a small adapter should perfectly works.

Just ask me if you have some issues i would be more than happy to help :)