dotnet / dotnet/reactive

[feature request] Implement ToObservable for CancellationToken

Open
#1,773 6 comments 0 reactions 0 assignees View on GitHub
[area] Rx
Dominant language
C#
Stars
7.2k
Forks
798
PR merge metrics
No merged PRs in 30d

Description

#### Feature request

A function to convert a `CancellationToken` token into an observable that emit `OnError` with `OperationCanceledException` when the token is signaled. It can possibly be implemented like this:

```c#
public static class RxExt
{
public static IObservable ToObservable(this CancellationToken @this)
{
if (@this.IsCancellationRequested)
{
return Observable.Throw(new OperationCanceledException(@this));
}
if (!@this.CanBeCanceled)
{
return Observable.Never();
}
return Observable.Create(
observer => @this.Register(
(_, token) => observer.OnError(new OperationCanceledException(token)),
null));
}
}
```

> Which next library version (i.e., patch, minor or major)?

Patch or Minor

> What are the platform(s), environment(s) and related component version(s)?

All supported platforms

> How commonly is this feature needed (one project, several projects, company-wide, global)?

Company-wide

> Please describe the feature.

Here's one possible use case, coupled with `TakeUntil` (I'm sure there are many others). Here, the `Finally` action is called upon the first item emit or when the token is signaled.

This code here is a bit contrived (I might as well use [`IObservable.ToTask`](https://docs.microsoft.com/en-us/previous-versions/dotnet/reactive-extensions/hh229587(v=vs.103)) or `IObservable.RunAsync`) but in our real-life Rx piple-lines this use case does makes sense:

```C#
IDisposable RunUntilCancelled(IObservable sequence, Action action, CancellationToken token)
{
return sequence
.FirstAsync()
.TakeUntil(token.ToObservable())
.Finally(action)
.Publish()
.Connect();
}

async Task test_CancellationToken_ToObservable()
{
var sequence = Observable
.Interval(TimeSpan.FromMilliseconds(1000))
.Do(n => Console.WriteLine(n))
.Skip(3);

var sw = Stopwatch.StartNew();
var tcs = new TaskCompletionSource();

using var cts = new CancellationTokenSource(2500);
using var subscription = RunUntilCancelled(sequence, tcs.SetResult, cts.Token);

await tcs.Task;
var lapse = sw.ElapsedMilliseconds;
Trace.Assert(lapse >= 2500 && lapse < 3000);
}
```

**Updated**, the initial version was rather naive, [a race condition has emerged](https://stackoverflow.com/a/72496607/1768303) since I stated using it. Here is an updated version aiming to solve it, and also giving a choice whether to use `OnError` or `OnNext`:

```c#
public static class RxExt
{
public static IObservable ToObservable(
this CancellationToken @this,
bool useOnError = true)
{
if (@this.IsCancellationRequested)
{
return useOnError ?
Observable.Throw(new OperationCanceledException(@this)) :
Observable.Return(@this);
}

if (!@this.CanBeCanceled)
{
return Observable.Never();
}

return Observable.Create(
observer =>
{
int disposed = 0;
bool IsListening() => Interlocked.CompareExchange(ref disposed, 0, 0) == 0;

var rego = @this.Register((_, token) =>
{
if (useOnError)
{
if (IsListening())
{
observer.OnError(new OperationCanceledException(token));
}
}
else
{
if (IsListening())
{
observer.OnNext(token);
}
if (IsListening())
{
observer.OnCompleted();
}
}
}, state: null);

return Disposable.Create(() =>
{
if (Interlocked.CompareExchange(ref disposed, 1, 0) == 0)
{
rego.Unregister();
}
});
});
}
}
```

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.