15

Is there a convenient way to use an async function as the predicate of a Where operator on an observable?

For example, if I have a nice tidy but possibly long-running function defined like this:

Task<int> Rank(object item);

Is there a trick to passing it to Where and maintaining the asynchronous execution? As in:

myObservable.Where(async item => (await Rank(item)) > 5)

In the past, when I've needed to do this, I've resorted to using SelectMany and projecting those results into a new type along with the original value and then doing the filtering based on that.

myObservable.SelectMany(async item => new 
  {
    ShouldInclude = (await Rank(item)) > 5,
    Item = item
  })
  .Where(o => o.ShouldInclude)
  .Select(o => o.Item);

I think that's terribly unreadable, though and I feel like there must be a cleaner way.

MojoFilter
  • 12,256
  • 14
  • 53
  • 61

2 Answers2

13

I think that's terribly unreadable

Yes, but you can fix that by encapsulating it into a helper method. If you call it Where, you will get exactly the syntax you wanted:

public static IObservable<T> Where<T>(
    this IObservable<T> source, Func<T, Task<bool>> predicate)
{
    return source.SelectMany(async item => new 
        {
            ShouldInclude = await predicate(item),
            Item = item
        })
        .Where(x => x.ShouldInclude)
        .Select(x => x.Item);
}
svick
  • 236,525
  • 50
  • 385
  • 514
2

Alternatively, you could use something like this:

public static IObservable<T> Where<T>(
    this IObservable<T> source, Func<T, Task<bool>> predicate)
{
    return source.SelectMany(item => 
        predicate(item).ToObservable()
            .Select(include => include ? Observable.Return(item) : Observable.Empty<T>())
        );
}

Which is basically how Where works, if you definited it in terms of SelectMany`:

public static IObservable<T> Where<T>(
    this IObservable<T> source, Func<T, bool> predicate)
{
    return source.SelectMany(item => predicate(item) ? Observable.Return(item) : Observable.Empty<T>());
}

Actually though, @svick's answer has less closures. :)

cwharris
  • 17,835
  • 4
  • 44
  • 64
  • Why are you using `Map()` instead of the more common name (at least in C#) `Select()`? – svick Aug 16 '14 at 09:27
  • `map` is the standard function programming term. `Select` is a legacy term left over from the original LINQ to SQL implementation. `Map` doesn't exist in the .NET version of Rx, as far as I know, but does exist in RxJS. Basically, I'm just used to writing `map` and not `Select`. – cwharris Aug 17 '14 at 22:13
  • 2
    Actually, [`Map()` does exist](https://github.com/Reactive-Extensions/Rx.NET/blob/master/Rx.NET/Source/System.Reactive.Observable.Aliases/Observable.Aliases.cs#L41) in the `System.Reactive.Observable.Aliases` namespace. – svick Aug 17 '14 at 22:38
  • Ahh, in that case I'll probably start using it in .NET to be consistent with the rest of the world. :) – cwharris Aug 17 '14 at 23:03