Carl's Notes
Life complexity navigation algorithms

TIL: ReactiveUI Gives No Guarantees About the Order in Which Observers Are Notified

I’m rewriting my Svelte/Go app for peronsal finances in C# using Avalonia. One popular choice is to use reactive programming through rx.net and with the help of the ReactiveUI library.

ReactiveUI provides a construct called ObservableAsPropertyHelper<T> (read more) to make it easy to create properties that depend on the value of other properties.

The documentation even encourages the use of ObservableAsPropertyHelper<T> (“OAPH” from now on):

When a property’s value depends on another property, a set of properties, or an observable stream, rather than set the value explicitly, use ObservableAsPropertyHelper with WhenAny wherever possible.

It provides the following example:

firstName = this
    .WhenAnyValue(x => x.Name)
    .Select(name => name.Split(' ')[0])
    .ToProperty(this, nameof(FirstName));

A more complete example would look like this:

class Person : ReactiveObject 
{
  private string _name;
  public string Name 
  {
    get => _name;
    set => this.RaiseAndSetIfChanged(ref _name, value);
  }

  private ObservableAsPropertyHelper<string> _firstName;
  public string FirstName => _firstName.Value;
  
  public Person() 
  {
    _firstName = this
      // Whenever the value of the property Person.Name changes...
      .WhenAnyValue(x => x.Name)
      // ...then compute a first name out of the new value...
      .Select(name => name.Split(' ')[0])
      // ...and use the computed value to set FirstName, 
      // raising a notification in the process.
      ToProperty(this, nameof(FirstName));
  }
}

This looks like you’re setting up an invariant: when inspecting the object’s state from outside, FirstName will always be equal to the first part of Name, until the first space.

However, this is not true. ReactiveUI makes no guarantees that your OAPH will be set before external observers observing changes to Name get notified.

I had a situation that looked like this: A view model maintained a list of Person-like objects. It also had one property providing a status based on the state of all these Persons. To update this status, I set things up like this in the constructor:

class ViewModel : ReactiveObject 
{
  private ObservableCollection<Person> _people;
  private string _status;

  public string Status
  {
    get => _status;
    private set => this.RaiseAndSetIfChanged(ref _status, value);
  }

  public ViewModel()
  {
    // In the collection '_people', ...
    _people
      // ... listen to changes in the collection ...
      .ToObservableChangeSet()
      // ... limited to changes to the items' Name property ...
      .AutoRefresh(x => x.Name)
      // ... and calculate the status based on the values. Note that
      // I don't care about what kind of change happened: Any change should
      // trigger this listener.
      .Subscribe(_ => Status = 
        _people.Any(x => x.FirstName == "") 
        ? "Something is weird" 
        : "OK");
  {
}

So, listening to any changes in the _people collection, I recalculated the value of Status by iterating over the objects.

Note how this code reacts to changes to Name but then uses FirstName for its calculation. This is important.

With this very example, it turns out that the Status listener will be called before the OAPH’s listener (I’m using ReactiveUI v20.1.63). When giving all Person instances a Name different from "" for the first time, Status may still be "Something is weird" at the end of it because of this ordering issue.

I’m writing “may” above because the ordering can change, in particular when running inside a unit test framework. In my specific case, the app itself actually worked fine, but when writing a test to verify this behaviour, I stumbled upon this state inconsistency due to this difference.

On the other hand, listening to any single Person through person.WhenAnyValue(x => x.Name) does not exhibit this behaviour, so it seems like something specific to collections and AutoRefresh.

So the takeaways are:

I’ve submitted a bug on GitHub about this. It’s quite possible my expectations are wrong about the order in which listeners are called, but then I think the documentation should be more explicit about it.

Read another post