From 1735d714bb4259211e0cb4078ffba574199265ff Mon Sep 17 00:00:00 2001 From: filzrev <103790468+filzrev@users.noreply.github.com> Date: Mon, 19 Feb 2024 10:09:30 +0900 Subject: [PATCH] chore: add doc generation supports --- .github/workflows/build-doc.yml | 88 + docs/.gitignore | 2 + docs/articles/index.md | 1864 +++++++++++++++++++++ docs/articles/toc.yml | 1 + docs/docfx.json | 59 + docs/index.md | 18 + docs/reference_factory.md | 95 -- docs/reference_operator.md | 291 ---- docs/templates/cysharp/public/favicon.ico | Bin 0 -> 1445 bytes docs/templates/cysharp/public/logo.png | Bin 0 -> 3185 bytes docs/templates/cysharp/public/main.css | 26 + docs/templates/cysharp/public/main.js | 10 + docs/toc.yml | 6 + 13 files changed, 2074 insertions(+), 386 deletions(-) create mode 100644 .github/workflows/build-doc.yml create mode 100644 docs/.gitignore create mode 100644 docs/articles/index.md create mode 100644 docs/articles/toc.yml create mode 100644 docs/docfx.json create mode 100644 docs/index.md delete mode 100644 docs/reference_factory.md delete mode 100644 docs/reference_operator.md create mode 100644 docs/templates/cysharp/public/favicon.ico create mode 100644 docs/templates/cysharp/public/logo.png create mode 100644 docs/templates/cysharp/public/main.css create mode 100644 docs/templates/cysharp/public/main.js create mode 100644 docs/toc.yml diff --git a/.github/workflows/build-doc.yml b/.github/workflows/build-doc.yml new file mode 100644 index 00000000..3d747d61 --- /dev/null +++ b/.github/workflows/build-doc.yml @@ -0,0 +1,88 @@ +name: Build document and deploy to GitHub Page + +on: + push: + branches: ["main"] + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: true + +defaults: + run: + shell: pwsh + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + defaults: + run: + working-directory: ./docs + steps: + # Checkout + - name: Checkout + uses: actions/checkout@v4 + # Install docfx as .NET global tools + - name: Install docfx + run: | + dotnet tool install docfx -g + # Run `docfx metadata` command + - name: Generate metadata + run: | + docfx metadata + # Run `docfx build` command + - name: Build document + run: | + docfx build + # Run `docfx pdf` command + - name: Build PDF files + run: | + docfx pdf + # Upload docfx output site + - name: Upload docfx build results to artifacts + uses: actions/upload-artifact@v4 + with: + name: wwwroot + path: docs/_site + if-no-files-found: error + + # Deploy site contents to GitHub Pages + publish-pages: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + # Setup GitHub Pages + - name: Setup Pages + uses: actions/configure-pages@v4 + + # Download artifacts + - name: Download artifact + id: download + uses: actions/download-artifact@v4 + with: + name: wwwroot + path: temp/wwwroot + + # Upload content to GitHub Pages + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ${{steps.download.outputs.download-path}} + + # Deploy to GitHub Pages + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 00000000..1c35ccf5 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +_site +api diff --git a/docs/articles/index.md b/docs/articles/index.md new file mode 100644 index 00000000..5f0115d2 --- /dev/null +++ b/docs/articles/index.md @@ -0,0 +1,1864 @@ +# R3 + +The new future of [dotnet/reactive](https://github.com/dotnet/reactive/) and [UniRx](https://github.com/neuecc/UniRx), which support many platforms including [Unity](#unity), [Godot](#godot), [Avalonia](#avalonia), [WPF](#wpf), [WinForms](#winforms), [Stride](#stride), [LogicLooper](#logiclooper), [MAUI](#maui), [MonoGame](#monogame). + + +I have over 10 years of experience with Rx, experience in implementing a custom Rx runtime ([UniRx](https://github.com/neuecc/UniRx)) for game engine, and experience in implementing an asynchronous runtime ([UniTask](https://github.com/Cysharp/UniTask/)) for game engine. Based on those experiences, I came to believe that there is a need to implement a new Reactive Extensions for .NET, one that reflects modern C# and returns to the core values of Rx. + +* Stopping the pipeline at OnError is a billion-dollar mistake. +* IScheduler is the root of poor performance. +* Frame-based operations, a missing feature in Rx, are especially important in game engines. +* Single asynchronous operations should be entirely left to async/await. +* Synchronous APIs should not be implemented. +* Query syntax is a bad notation except for SQL. +* The Necessity of a subscription list to prevent subscription leaks (similar to a Parallel Debugger) +* Backpressure should be left to [IAsyncEnumerable](https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/generate-consume-asynchronous-stream) and [Channels](https://devblogs.microsoft.com/dotnet/an-introduction-to-system-threading-channels/). +* For distributed processing and queries, there are [GraphQL](https://graphql.org/), [Kubernetes](https://kubernetes.io/), [Orleans](https://learn.microsoft.com/en-us/dotnet/orleans/), [Akka.NET](https://getakka.net/), [gRPC](https://grpc.io/), [MagicOnion](https://github.com/Cysharp/MagicOnion). + +In other words, LINQ is not for EveryThing, and we believe that the essence of Rx lies in the processing of in-memory messaging (LINQ to Events), which will be our focus. We are not concerned with communication processes like [Reactive Streams](https://www.reactive-streams.org/). + +To address the shortcomings of dotnet/reactive, we have made changes to the core interfaces. In recent years, Rx-like frameworks optimized for language features, such as [Kotlin Flow](https://kotlinlang.org/docs/flow.html) and [Swift Combine](https://developer.apple.com/documentation/combine), have been standardized. C# has also evolved significantly, now at C# 12, and we believe there is a need for an Rx that aligns with the latest C#. + +Improving performance was also a theme in the reimplementation. For example, this is the result of the terrible performance of IScheudler and the performance difference caused by its removal. + +![image](https://github.com/Cysharp/ZLogger/assets/46207/68a12664-a840-4725-a87c-8fdbb03b4a02) + +`Observable.Range(1, 10000).Subscribe()` + +You can also see interesting results in allocations with the addition and deletion to Subject. + +![image](https://github.com/Cysharp/ZLogger/assets/46207/2194c086-37a3-44d6-8642-5fd0fa91b168) + +`x10000 subject.Subscribe() -> x10000 subscription.Dispose()` + +This is because dotnet/reactive has adopted ImmutableArray (or its equivalent) for Subject, which results in the allocation of a new array every time one is added or removed. Depending on the design of the application, a large number of subscriptions can occur (we have seen this especially in the complexity of games), which can be a critical issue. In R3, we have devised a way to achieve high performance while avoiding ImmutableArray. + +Core Interface +--- +This library is distributed via NuGet, supporting .NET Standard 2.0, .NET Standard 2.1, .NET 6(.NET 7) and .NET 8 or above. + +> PM> Install-Package [R3](https://www.nuget.org/packages/R3) + +Some platforms(WPF, Avalonia, Unity, Godot) requires additional step to install. Please see [Platform Supports](#platform-supports) section in below. + +R3 code is almostly same as standard Rx. Make the Observable via factory methods(Timer, Interval, FromEvent, Subject, etc...) and chain operator via LINQ methods. Therefore, your knowledge about Rx and documentation on Rx can be almost directly applied. If you are new to Rx, the [ReactiveX](https://reactivex.io/intro.html) website and [Introduction to Rx.NET](https://introtorx.com/) would be useful resources for reference. + +```csharp +using R3; + +var subscription = Observable.Interval(TimeSpan.FromSeconds(1)) + .Select((_, i) => i) + .Where(x => x % 2 == 0) + .Subscribe(x => Console.WriteLine($"Interval:{x}")); + +var cts = new CancellationTokenSource(); +_ = Task.Run(() => { Console.ReadLine(); cts.Cancel(); }); + +await Observable.Timer(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(3)) + .TakeUntil(cts.Token) + .ForEachAsync(x => Console.WriteLine($"Timer")); + +subscription.Dispose(); +``` + +The surface API remains the same as normal Rx, but the interfaces used internally are different and are not `IObservable/IObserver`. + +`IObservable` being the dual of `IEnumerable` is a beautiful definition, but it was not very practical in use. + +```csharp +public abstract class Observable +{ + public IDisposable Subscribe(Observer observer); +} + +public abstract class Observer : IDisposable +{ + public void OnNext(T value); + public void OnErrorResume(Exception error); + public void OnCompleted(Result result); // Result is () | Exception +} +``` + +The biggest difference is that in normal Rx, when an exception occurs in the pipeline, it flows to `OnError` and the subscription is unsubscribed, but in R3, it flows to `OnErrorResume` and the subscription is not unsubscribed. + +I consider the automatic unsubscription by OnError to be a bad design for event handling. It's very difficult and risky to resolve it within an operator like Retry, and it also led to poor performance (there are many questions and complex answers about stopping and resubscribing all over the world). Also, converting OnErrorResume to OnError(OnCompleted(Result.Failure)) is easy and does not degrade performance, but the reverse is impossible. Therefore, the design was changed to not stop by default and give users the choice to stop. + +Since the original Rx contract was `OnError | OnCompleted`, it was changed to `OnCompleted(Result result)` to consolidate into one method. Result is a readonly struct with two states: `Failure(Exception) | Success()`. + +The reason for changing to an abstract class instead of an interface is that Rx has implicit complex contracts that interfaces do not guarantee. By making it an abstract class, we fully controlled the behavior of Subscribe, OnNext, and Dispose. This made it possible to manage the list of all subscriptions and prevent subscription leaks. + +![image](https://github.com/Cysharp/ZLogger/assets/46207/149abca5-6d84-44ea-8373-b0e8cd2dc46a) + +Subscription leaks are a common problem in applications with long lifecycles, such as GUIs or games. Tracking all subscriptions makes it easy to prevent leaks. + +Internally, when subscribing, an Observer is always linked to the target Observable and doubles as a Subscription. This ensures that Observers are reliably connected from top to bottom, making tracking certain and clear that they are released on OnCompleted/Dispose. In terms of performance, because the Observer itself always becomes a Subscription, there is no need for unnecessary IDisposable allocations. + +TimeProvider instead of IScheduler +--- +In traditional Rx, `IScheduler` was used as an abstraction for time-based processing, but in R3, we have discontinued its use and instead opted for the [TimeProvider](https://learn.microsoft.com/en-us/dotnet/api/system.timeprovider?view=net-8.0) introduced in .NET 8. For example, the operators are defined as follows: + +```csharp +public static Observable Interval(TimeSpan period, TimeProvider timeProvider); +public static Observable Delay(this Observable source, TimeSpan dueTime, TimeProvider timeProvider) +public static Observable Debounce(this Observable source, TimeSpan timeSpan, TimeProvider timeProvider) // same as Throttle in dotnet/reactive +``` + +Originally, `IScheduler` had performance issues, and the internal implementation of dotnet/reactive was peppered with code that circumvented these issues using `PeriodicTimer` and `IStopwatch`, leading to unnecessary complexity. These can be better expressed with TimeProvider (`TimeProvider.CreateTimer()`, `TimeProvider.GetTimestamp()`). + +While TimeProvider is an abstraction for asynchronous operations, excluding the Fake for testing purposes, `IScheduler` included synchronous schedulers like `ImmediateScheduler` and `CurrentThreadScheduler`. However, these were also meaningless as applying them to time-based operators would cause blocking, and `CurrentThreadScheduler` had poor performance. + +![image](https://github.com/Cysharp/ZLogger/assets/46207/68a12664-a840-4725-a87c-8fdbb03b4a02) +`Observable.Range(1, 10000).Subscribe()` + +In R3, anything that requires synchronous execution (like Range) is treated as Immediate, and everything else is considered asynchronous and handled through TimeProvider. + +As for the implementation of TimeProvider, the standard TimeProvider.System using the ThreadPool is the default. For unit testing, FakeTimeProvider (Microsoft.Extensions.TimeProvider.Testing) is available. Additionally, many TimeProvider implementations are provided for different platforms, such as DispatcherTimerProvider for WPF and UpdateTimerProvider for Unity, enhancing ease of use tailored to each platform. + +Frame based operations +--- +In GUI applications, there's the message loop, and in game engines, there's the game loop. Platforms that operate based on loops are not uncommon. The idea of executing something after a few seconds or frames fits very well with Rx. Just as time has been abstracted through TimeProvider, we introduced a layer of abstraction for frames called FrameProvider, and added frame-based operators corresponding to all methods that accept TimeProvider. + +```csharp +public static Observable IntervalFrame(int periodFrame, FrameProvider frameProvider); +public static Observable DelayFrame(this Observable source, int frameCount, FrameProvider frameProvider) +public static Observable DebounceFrame(this Observable source, int frameCount, FrameProvider frameProvider) +``` + +The effectiveness of frame-based processing has been proven in Unity's Rx implementation, [neuecc/UniRx](https://github.com/neuecc/UniRx), which is one of the reasons why UniRx has gained strong support. + +There are also several operators unique to frame-based processing. + +```csharp +// push OnNext every frame. +Observable.EveryUpdate().Subscribe(x => Console.WriteLine(x)); + +// take value until next frame +_eventSoure.TakeUntil(Obserable.NextFrame()).Subscribe(); + +// polling value changed +Observable.EveryValueChanged(this, x => x.Width).Subscribe(x => WidthText.Text = x.ToString()); +Observable.EveryValueChanged(this, x => x.Height).Subscribe(x => HeightText.Text = x.ToString()); +``` + +`EveryValueChanged` could be interesting, as it converts properties without Push-based notifications like `INotifyPropertyChanged`. + +![](https://cloud.githubusercontent.com/assets/46207/15827886/1573ff16-2c48-11e6-9876-4e4455d7eced.gif)` + +Subjects(ReactiveProperty) +--- +In R3, there are four types of Subjects: `Subject`, `ReactiveProperty`, `ReplaySubject`, and `ReplayFrameSubject`. `ReactiveProperty` corresponds to what would be a `BehaviorSubject`, but with the added functionality of eliminating duplicate values. Since you can choose to enable or disable duplicate elimination, it effectively becomes a superior alternative to `BehaviorSubject`, leading to the removal of `BehaviorSubject`. + +`ReactiveProperty` has equivalents in other frameworks as well, such as [Android LiveData](https://developer.android.com/topic/libraries/architecture/livedata) and [Kotlin StateFlow](https://developer.android.com/kotlin/flow/stateflow-and-sharedflow), particularly effective for data binding in UI contexts. In .NET, there is a library called [runceel/ReactiveProperty](https://github.com/runceel/ReactiveProperty), which I originally created. + +Unlike dotnet/reactive's Subject, all Subjects in R3 (Subject, ReactiveProperty, ReplaySubject, ReplayFrameSubject) are designed to call OnCompleted upon disposal. This is because R3 is designed with a focus on subscription management and unsubscription. By calling OnCompleted, it ensures that all subscriptions are unsubscribed from the Subject, the upstream source of events, by default. If you wish to avoid calling OnCompleted, you can do so by calling `Dispose(false)`. + +`ReactiveProperty` is mutable, but it can be converted to a read-only `ReadOnlyReactiveProperty`. Following the [guidance for the Android UI Layer](https://developer.android.com/topic/architecture/ui-layer), the Kotlin code below is + +```kotlin +class NewsViewModel(...) : ViewModel() { + + private val _uiState = MutableStateFlow(NewsUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + ... +} +``` + +can be adapted to the following R3 code. + +```csharp +class NewsViewModel +{ + ReactiveProperty _uiState = new(new NewsUiState()); + public ReadOnlyReactiveProperty UiState => _uiState; +} +``` + +In R3, we use a combination of a mutable private field and a readonly public property. + +By inheriting `ReactiveProperty` and overriding `OnValueChanging` and `OnValueChanged`, you can customize behavior, such as adding validation. + +```csharp +// Since the primary constructor sets values to fields before calling base, it is safe to call OnValueChanging in the base constructor. +public sealed class ClampedReactiveProperty(T initialValue, T min, T max) + : ReactiveProperty(initialValue) where T : IComparable +{ + private static IComparer Comparer { get; } = Comparer.Default; + + protected override void OnValueChanging(ref T value) + { + if (Comparer.Compare(value, min) < 0) + { + value = min; + } + else if (Comparer.Compare(value, max) > 0) + { + value = max; + } + } +} + +// For regular constructors, please set `callOnValueChangeInBaseConstructor` to false and manually call it once to correct the value. +public sealed class ClampedReactiveProperty2 + : ReactiveProperty where T : IComparable +{ + private static IComparer Comparer { get; } = Comparer.Default; + + readonly T min, max; + + // callOnValueChangeInBaseConstructor to avoid OnValueChanging call before min, max set. + public ClampedReactiveProperty2(T initialValue, T min, T max) + : base(initialValue, EqualityComparer.Default, callOnValueChangeInBaseConstructor: false) + { + this.min = min; + this.max = max; + + // modify currentValue manually + OnValueChanging(ref GetValueRef()); + } + + protected override void OnValueChanging(ref T value) + { + if (Comparer.Compare(value, min) < 0) + { + value = min; + } + else if (Comparer.Compare(value, max) > 0) + { + value = max; + } + } +} +``` + +Additionally, `ReactiveProperty` supports serialization with `System.Text.JsonSerializer` in .NET 6 and above. For earlier versions, you need to implement `ReactivePropertyJsonConverterFactory` under the existing implementation and add it to the Converter. + +Disposable +--- +To bundle multiple IDisposables (Subscriptions), it's good to use Disposable's methods. In R3, depending on the performance, + +```csharp +Disposable.Combine(IDisposable d1, ..., IDisposable d8); +Disposable.Combine(params IDisposable[]); +Disposable.CreateBuilder(); +CompositeDisposable +DisposableBag +``` + +five types are available for use. In terms of performance advantages, the order is `Combine(d1,...,d8) (>= CreateBuilder) > Combine(IDisposable[]) >= CreateBuilder > DisposableBag > CompositeDisposable`. + +When the number of subscriptions is statically determined, Combine offers the best performance. Internally, for less than 8 arguments, it uses fields, and for 9 or more arguments, it uses an array, making Combine especially efficient for 8 arguments or less. + +```csharp +public partial class MainWindow : Window +{ + IDisposable disposable; + + public MainWindow() + { + var d1 = Observable.IntervalFrame(1).Subscribe(); + var d2 = Observable.IntervalFrame(1).Subscribe(); + var d3 = Observable.IntervalFrame(1).Subscribe(); + + disposable = Disposable.Combine(d1, d2, d3); + } + + protected override void OnClosed(EventArgs e) + { + disposable.Dispose(); + } +} +``` + +If there are many subscriptions and it's cumbersome to hold each one in a variable, `CreateBuilder` can be used instead. At build time, it combines according to the number of items added to it. Since the Builder itself is a struct, there are no allocations. + +```csharp +public partial class MainWindow : Window +{ + IDisposable disposable; + + public MainWindow() + { + var d = Disposable.CreateBuilder(); + Observable.IntervalFrame(1).Subscribe().AddTo(ref d); + Observable.IntervalFrame(1).Subscribe().AddTo(ref d); + Observable.IntervalFrame(1).Subscribe().AddTo(ref d); + + disposable = d.Build(); + } + + protected override void OnClosed(EventArgs e) + { + disposable.Dispose(); + } +} +``` + +For dynamically added items, using `DisposableBag` is advisable. This is an add-only struct with only `Add/Clear/Dispose` methods. It can be used relatively quickly and with low allocation by holding it in a class field and passing it around by reference. However, it is not thread-safe. + +```csharp +public partial class MainWindow : Window +{ + DisposableBag disposable; // DisposableBag is struct, no need new and don't copy + + public MainWindow() + { + Observable.IntervalFrame(1).Subscribe().AddTo(ref disposable); + Observable.IntervalFrame(1).Subscribe().AddTo(ref disposable); + Observable.IntervalFrame(1).Subscribe().AddTo(ref disposable); + } + + void OnClick() + { + Observable.IntervalFrame(1).Subscribe().AddTo(ref disposable); + } + + protected override void OnClosed(EventArgs e) + { + disposable.Dispose(); + } +} +``` + +`CompositeDisposable` is a class that also supports `Remove` and is thread-safe. It is the most feature-rich, but comparatively, it has the lowest performance. + +```csharp +public partial class MainWindow : Window +{ + CompositeDisposable disposable = new CompositeDisposable(); + + public MainWindow() + { + Observable.IntervalFrame(1).Subscribe().AddTo(disposable); + Observable.IntervalFrame(1).Subscribe().AddTo(disposable); + Observable.IntervalFrame(1).Subscribe().AddTo(disposable); + } + + void OnClick() + { + Observable.IntervalFrame(1).Subscribe().AddTo(disposable); + } + + protected override void OnClosed(EventArgs e) + { + disposable.Dispose(); + } +} +``` + +Additionally, there are other utilities for Disposables as follows. + +``` +Disposable.Create(Action); +Disposable.Dispose(...); +SingleAssignmentDisposable +SingleAssignmentDisposableCore // struct +SerialDisposable +SerialDisposableCore// struct +``` + +Subscription Management +--- +Managing subscriptions is one of the most crucial aspects of Rx, and inadequate management can lead to memory leaks. There are two patterns for unsubscribing in Rx. One is by disposing of the IDisposable (Subscription) returned by Subscribe. The other is by receiving OnCompleted. + +In R3, to enhance subscription cancellation on both fronts, it's now possible to bundle subscriptions using a variety of Disposable classes for Subscriptions, and for OnCompleted, the upstream side of events (such as Subject or Factory) has been made capable of emitting OnCompleted. Especially, Factories that receive a TimeProvider or FrameProvider can now take a CancellationToken. + +```csharp +public static Observable Interval(TimeSpan period, TimeProvider timeProvider, CancellationToken cancellationToken) +public static Observable EveryUpdate(FrameProvider frameProvider, CancellationToken cancellationToken) +``` + +When cancelled, OnCompleted is sent, and all subscriptions are unsubscribed. + +### ObservableTracker + +R3 incorporates a system called ObservableTracker. When activated, it allows you to view all subscription statuses. + +``` +ObservableTracker.EnableTracking = true; // default is false +ObservableTracker.EnableStackTrace = true; + +using var d = Observable.Interval(TimeSpan.FromSeconds(1)) + .Where(x => true) + .Take(10000) + .Subscribe(); + +// check subscription +ObservableTracker.ForEachActiveTask(x => +{ + Console.WriteLine(x); +}); +``` + +``` +TrackingState { TrackingId = 1, FormattedType = Timer._Timer, AddTime = 2024/01/09 4:11:39, StackTrace =... } +TrackingState { TrackingId = 2, FormattedType = Where`1._Where, AddTime = 2024/01/09 4:11:39, StackTrace =... } +TrackingState { TrackingId = 3, FormattedType = Take`1._Take, AddTime = 2024/01/09 4:11:39, StackTrace =... } +``` + +Besides directly calling `ForEachActiveTask`, making it more accessible through a GUI can make it easier to check for subscription leaks. Currently, there is an integrated GUI for Unity, and there are plans to provide a screen using Blazor for other platforms. + +ObservableSystem, UnhandledExceptionHandler +--- +For time-based operators that do not specify a TimeProvider or FrameProvider, the default Provider of ObservableSystem is used. This is settable, so if there is a platform-specific Provider (for example, DispatcherTimeProvider in WPF), you can swap it out to create a more user-friendly environment. + +```csharp +public static class ObservableSystem +{ + public static TimeProvider DefaultTimeProvider { get; set; } = TimeProvider.System; + public static FrameProvider DefaultFrameProvider { get; set; } = new NotSupportedFrameProvider(); + + static Action unhandledException = DefaultUnhandledExceptionHandler; + + // Prevent +=, use Set and Get method. + public static void RegisterUnhandledExceptionHandler(Action unhandledExceptionHandler) + { + unhandledException = unhandledExceptionHandler; + } + + public static Action GetUnhandledExceptionHandler() + { + return unhandledException; + } + + static void DefaultUnhandledExceptionHandler(Exception exception) + { + Console.WriteLine("R3 UnhandleException: " + exception.ToString()); + } +} +``` + +In CUI environments, by default, the FrameProvider will throw an exception. If you want to use FrameProvider in a CUI environment, you can set either `NewThreadSleepFrameProvider`, which sleeps in a new thread for a specified number of seconds, or `TimerFrameProvider`, which executes every specified number of seconds. + +### UnhandledExceptionHandler + +When an exception passes through OnErrorResume and is not ultimately handled by Subscribe, the UnhandledExceptionHandler of ObservableSystem is called. This can be set with `RegisterUnhandledExceptionHandler`. By default, it writes to `Console.WriteLine`, but it may need to be changed to use `ILogger` or something else as required. + +Result Handling +--- +The `Result` received by OnCompleted has a field `Exception?`, where it's null in case of success and contains the Exception in case of failure. + +```csharp +// Typical processing code example +void OnCompleted(Result result) +{ + if (result.IsFailure) + { + // do failure + _ = result.Exception; + } + else // result.IsSuccess + { + // do success + } +} +``` + +To generate a `Result`, in addition to using `Result.Success` and `Result.Failure(exception)`, Observer has OnCompleted() and OnCompleted(exception) as shortcuts for Success and Failure, respectively. + +```csharp +observer.OnCompleted(Result.Success); +observer.OnCompleted(Result.Failure(exception)); + +observer.OnCompleted(); // same as Result.Success +observer.OnCompleted(exception); // same as Result.Failure(exception) +``` + +Unit Testing +--- +For unit testing, you can use [FakeTimeProvider](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.time.testing.faketimeprovider) of Microsoft.Extensions.TimeProvider.Testing. + +Additionally, in R3, there is a collection called LiveList, which allows you to obtain subscription statuses as a list. Combining these two features can be very useful for unit testing. + +```csharp +var fakeTime = new FakeTimeProvider(); + +var list = Observable.Timer(TimeSpan.FromSeconds(5), fakeTime).ToLiveList(); + +fakeTime.Advance(TimeSpan.FromSeconds(4)); +list.AssertIsNotCompleted(); + +fakeTime.Advance(TimeSpan.FromSeconds(1)); +list.AssertIsCompleted(); +list.AssertEqual([Unit.Default]); +``` + +For FrameProvider, a `FakeFrameProvider` is provided as standard, and it can be used in the same way as `FakeTimeProvider`. + +```csharp +var cts = new CancellationTokenSource(); +var frameProvider = new FakeFrameProvider(); + +var list = Observable.EveryUpdate(frameProvider, cts.Token) + .Select(_ => frameProvider.GetFrameCount()) + .ToLiveList(); + +list.AssertEqual([]); // list.Should().Equal(expected); + +frameProvider.Advance(); +list.AssertEqual([0]); + +frameProvider.Advance(3); +list.AssertEqual([0, 1, 2, 3]); + +cts.Cancel(); +list.AssertIsCompleted(); // list.IsCompleted.Should().BeTrue(); + +frameProvider.Advance(); +list.AssertEqual([0, 1, 2, 3]); +list.AssertIsCompleted(); +``` + +Interoperability with `IObservable` +--- +`Observable` is not `IObservable`. You can convert both by these methods. + +* `public static Observable ToObservable(this IObservable source)` +* `public static IObservable AsSystemObservable(this Observable source)` + +Interoperability with `async/await` +--- +R3 has special integration with `async/await`. First, all methods that return a single asynchronous operation have now become ***Async methods, returning `Task`. + +Furthermore, you can specify special behaviors when asynchronous methods are provided to Where/Select/Subscribe. + +| Name | ReturnType | +| --- | --- | +| **SelectAwait**(this `Observable` source, `Func>` selector, `AwaitOperations` awaitOperations = AwaitOperation.Sequential, `Boolean` configureAwait = True) | `Observable` | +| **WhereAwait**(this `Observable` source, `Func>` predicate, `AwaitOperations` awaitOperations = AwaitOperation.Sequential, `Boolean` configureAwait = True) | `Observable` | +| **SubscribeAwait**(this `Observable` source, `Func` onNextAsync, `AwaitOperations` awaitOperations = AwaitOperation.Sequential, `Boolean` configureAwait = True) | `IDisposable` | +| **SubscribeAwait**(this `Observable` source, `Func` onNextAsync, `Action` onCompleted, `AwaitOperations` awaitOperations = AwaitOperation.Sequential, `Boolean` configureAwait = True) | `IDisposable` | +| **SubscribeAwait**(this `Observable` source, `Func` onNextAsync, `Action` onErrorResume, `Action` onCompleted, `AwaitOperations` awaitOperations = AwaitOperation.Sequential, `Boolean` configureAwait = True) | `IDisposable` | + +```csharp +public enum AwaitOperation +{ + /// All values are queued, and the next value waits for the completion of the asynchronous method. + Sequential, + /// Drop new value when async operation is running. + Drop, + /// If the previous asynchronous method is running, it is cancelled and the next asynchronous method is executed. + Switch, + /// All values are sent immediately to the asynchronous method. + Parallel, + /// All values are sent immediately to the asynchronous method, but the results are queued and passed to the next operator in order. + SequentialParallel, + /// Only the latest value is queued, and the next value waits for the completion of the asynchronous method. + Latest, +} +``` + +```csharp +// for example... +// Drop enables prevention of execution by multiple clicks +button.OnClickAsObservable() + .SelectAwait(async (_, ct) => + { + var req = await UnityWebRequest.Get("https://google.com/").SendWebRequest().WithCancellation(ct); + return req.downloadHandler.text; + }, AwaitOperation.Drop) + .SubscribeToText(text); +``` + +Additionally, the following time-related filtering methods can also accept asynchronous methods. + +| Name | ReturnType | +| --- | --- | +| **Debounce**(this `Observable` source, `Func` throttleDurationSelector, `Boolean` configureAwait = true) | `Observable` | +| **ThrottleFirst**(this `Observable` source, `Func` sampler, `Boolean` configureAwait = true) | `Observable` | +| **ThrottleLast**(this `Observable` source, `Func` sampler, `Boolean` configureAwait = true) | `Observable` | + +Concurrency Policy +--- +The composition of operators is thread-safe, and it is expected that the values flowing through OnNext are on a single thread. In other words, if OnNext is issued on multiple threads, the operators may behave unexpectedly. This is the same as with dotnet/reactive. + +ObservableCollections +--- +As a special collection for monitoring changes in collections and handling them in R3, the [ObservableCollections](https://github.com/Cysharp/ObservableCollections)'s `ObservableCollections.R3` package is available. + +It has `ObservableList`, `ObservableDictionary`, `ObservableHashSet`, `ObservableQueue`, `ObservableStack`, `ObservableRingBuffer`, `ObservableFixedSizeRingBuffer` and these observe methods. + +```csharp +Observable> IObservableCollection.ObserveAdd() +Observable> IObservableCollection.ObserveRemove() +Observable> IObservableCollection.ObserveReplace() +Observable> IObservableCollection.ObserveMove() +Observable> IObservableCollection.ObserveReset() +``` + +XAML Platforms(`BindableReactiveProperty`) +--- +For XAML based application platforms, R3 provides `BindableReactiveProperty` that can bind observable property to view like [Android LiveData](https://developer.android.com/topic/libraries/architecture/livedata) and [Kotlin StateFlow](https://developer.android.com/kotlin/flow/.stateflow-and-sharedflow). It implements [INotifyPropertyChanged](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.inotifypropertychanged) and [INotifyDataErrorInfo](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.inotifydataerrorinfo). + +Simple usage, expose `BindableReactiveProperty` via `new` or `ToBindableReactiveProperty`. + +Here is the simple In and Out BindableReactiveProperty ViewModel, Xaml and code-behind. In xaml, `.Value` to bind property. + +```csharp +public class BasicUsagesViewModel : IDisposable +{ + public BindableReactiveProperty Input { get; } + public BindableReactiveProperty Output { get; } + + public BasicUsagesViewModel() + { + Input = new BindableReactiveProperty(""); + Output = Input.Select(x => x.ToUpper()).ToBindableReactiveProperty(""); + } + + public void Dispose() + { + Disposable.Dispose(Input, Output); + } +} +``` + +```xml + + + + + + + + + +``` + +```csharp +namespace WpfApp1; + +public partial class MainWindow : Window +{ + public MainWindow() + { + InitializeComponent(); + } + + protected override void OnClosed(EventArgs e) + { + (this.DataContext as IDisposable)?.Dispose(); + } +} +``` + +![image](https://github.com/Cysharp/R3/assets/46207/01c3738f-e941-412e-b517-8e7867d6f709) + +BindableReactiveProperty also supports validation via DataAnnotation or custom logic. If you want to use DataAnnotation attribute, require to call `EnableValidation()` in field initializer or `EnableValidation(Expression selfSelector)` in constructor. + +```csharp +public class ValidationViewModel : IDisposable +{ + // Pattern 1. use EnableValidation to enable DataAnnotation validation in field initializer + [Range(0.0, 300.0)] + public BindableReactiveProperty Height { get; } = new BindableReactiveProperty().EnableValidation(); + + [Range(0.0, 300.0)] + public BindableReactiveProperty Weight { get; } + + IDisposable customValidation1Subscription; + public BindableReactiveProperty CustomValidation1 { get; set; } + + public BindableReactiveProperty CustomValidation2 { get; set; } + + public ValidationViewModel() + { + // Pattern 2. use EnableValidation(Expression) to enable DataAnnotation validation + Weight = new BindableReactiveProperty().EnableValidation(() => Weight); + + // Pattern 3. EnableValidation() and call OnErrorResume to set custom error meessage + CustomValidation1 = new BindableReactiveProperty().EnableValidation(); + customValidation1Subscription = CustomValidation1.Subscribe(x => + { + if (0.0 <= x && x <= 300.0) return; + + CustomValidation1.OnErrorResume(new Exception("value is not in range.")); + }); + + // Pattern 4. simplified version of Pattern3, EnableValidation(Func) + CustomValidation2 = new BindableReactiveProperty().EnableValidation(x => + { + if (0.0 <= x && x <= 300.0) return null; // null is no validate result + return new Exception("value is not in range."); + }); + } + + public void Dispose() + { + Disposable.Dispose(Height, Weight, CustomValidation1, customValidation1Subscription, CustomValidation2); + } +} +``` + +```xml + + + + + + + + +``` + +![image](https://github.com/Cysharp/R3/assets/46207/f80149e6-1573-46b5-9a77-b78776dd3527) + +### ReactiveCommand + +`ReactiveCommand` is observable [ICommand](https://learn.microsoft.com/en-us/dotnet/api/system.windows.input.icommand) implementation. It can create from `Observable canExecuteSource`. + +```csharp +public class CommandViewModel : IDisposable +{ + public BindableReactiveProperty OnCheck { get; } // bind to CheckBox + public ReactiveCommand ShowMessageBox { get; } // bind to Button + + public CommandViewModel() + { + OnCheck = new BindableReactiveProperty(); + ShowMessageBox = OnCheck.ToReactiveCommand(_ => + { + MessageBox.Show("clicked"); + }); + } + + public void Dispose() + { + Disposable.Combine(OnCheck, ShowMessageBox); + } +} +``` + +```xml + + + + + +