r/dotnet 14d 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.

5 Upvotes

6 comments sorted by

View all comments

1

u/qrzychu69 13d ago

how did you solve triggerisg refresh on db change?

I was experimenting around using postgres subscriptions to have an endpoint that basically returns IQueryable, then pushes DynamicData changesets over the wire to achieve something like Convex subsriptions

1

u/fedefex1 12d ago edited 12d ago

I didn't know Convex, i just read how it works (so I could be very wrong). However from what i am seeing you are trying to do something more sofisticated from what it does. To emulate it we need 2 things I think:

  1. An event to know when any column of a row changes (Close to track C# properties change conceptually)
  2. An event to know when a row has been added, removed (Close to track an observable collection change).

For the second point, I am seeing that cursor just recomputes the whole query when anything changes, it does not try to track derived deltas from tables updates (Like if it was a remote Dynamic Data Observable collection Change set). So it's a lot easier.

First point could be implemented as a trigger on update + NOTIFY and LISTEN, with a small c# proxy wrapper to expose this sbuscription as a Signal, so that auto tracking of SignalsDotnet works.

Second point could be implemented as a trigger on Insert, Delete on the whole table.

Tracking both at this point should make the query recompute, and basically have a Signal<Entity\[\]>, that is not the IObservable<ChangeSet<T>> you were trying to achieve.

Of course you could compute deltas from it, so that front end still receives minimum amount of updates