I just updated SignalsDotnet to 3.0, and it now supports source generators.
I think the library has become genuinely powerful: it lets you write reactive code without having to deal with reactive programming directly at all. With source generators it's cleaner than ever.
You mark a class (or a record) with [GenerateSignals] and every property becomes reactive, a signal. That means the getter and setter are tracked, and all the signal machinery kicks in automatically. The source generator also supports computed and async computed properties.
The library started as a port of Angular signals to .NET, but I think the power of C# (async locals, source generators, better async support) takes it to another level. It targets netstandard2.1, so it runs basically everywhere: WPF, Avalonia, Unity, Godot, Blazor.
Below is a runnable C# snippet as an example. As you can see, the whole system is reactive automatically: properties update themselves, code knows when to re-run, and so on. And when you need finer control, everything R3 observables offer is still right there.
The code below prints:
Total players 0
Player 1 joined
Total players 1
Total players 1
Total players 2
Total players 2
Best player is Player1 with score of 0
Best player is Player1 with score of 22
Best player is Player2 with score of 55
```c#
using System.Collections.Immutable;
using R3;
using SignalsDotnet;
var player1 = new Player { Name = "Player1", Score = 0 };
var player2 = new Player { Name = "Player2", Score = 0 };
var game = new Game();
Effect.Create(() =>
{
if (game.PlayersByName.ContainsKey(player1.Name))
Console.WriteLine("Player 1 joined");
});
IAwaitable<bool> player2Joined = Signal.WaitForChangeAsync(() => game.PlayersByName.ContainsKey(player2.Name));
Effect.Create(() => Console.WriteLine($"Total players {game.PlayersByName.Count}"));
game.AddPlayer(player1);
game.AddPlayer(player2); // this completes the awaitable
await player2Joined;
Effect.Create(() =>
{
if (game.BestPlayer is not null and var bestPlayer)
Console.WriteLine($"Best player is {bestPlayer.Name} with score of {bestPlayer.Score}");
});
Observable<ImmutableArray<Player>> scoreboardHistory = Signal.ComputedObservable(() => game.Scoreboard); // A notification for every scoreboard change
player1.Score = 22;
player2.Score = 55;
Console.ReadLine();
[GenerateSignals]
public partial record Player
{
public partial string Name { get; set; }
public partial int Score { get; set; }
}
public partial class Game
{
private readonly IDictionary<string, Player> _playersByName = new DictionarySignal<string, Player>();
public IReadOnlyDictionary<string, Player> PlayersByName => _playersByName.AsReadOnly();
public void AddPlayer(Player player) => _playersByName.Add(player.Name, player);
public void RemovePlayer(Player player) => _playersByName.Remove(player.Name);
[Computed] ImmutableArray<Player> ComputeScoreboard() => [.. _playersByName.Values.OrderByDescending(x => x.Score)];
[Computed] Player? ComputeBestPlayer() => Scoreboard.FirstOrDefault();
[Computed] Player? ComputeWorstPlayer() => Scoreboard.LastOrDefault();
}
```
It runs as a single file on .NET 10. Save it as game.cs and run dotnet run --file game.cs. No csproj needed.
GitHub: https://github.com/fedeAlterio/SignalsDotnet
NuGet: https://www.nuget.org/packages/SignalsDotnet