kmusick kmusick - 3 months ago 27
C# Question

C# Reactive Extensions (rx) FirstOrDefault enumerates entire collection

It seems that the expected behavior of FirstOrDefault is to complete after finding an item that matches the predicate and the expected behavior of concat is to evaluate lazily. However, the following example enumerates the entire collection even though the predicate matches the first item.

Unit Test:

[Fact]
public void FirstOrDefault_NotLoadedFirstInTable_OnlyBuilds1Entity()
{
var table = new EntityTable
{
Rows = Enumerable.Range(0, 5).Select(i => new EntityTableRow { Id = i }).ToList()
};
var configRepository = Substitute.For<IConfigRepository>();
configRepository.Table<EntityTable>().Returns(table);
var repo = GetEntityRepository(configRepository);

Entity result = null;
repo.Entities.FirstOrDefaultAsync(i => i.RowId == 0).Subscribe(i => result = i);

repo.buildCalled.Should().Be(1);
}


Code:

private List<Entity> InMemory = new List<Entity>();
public IObservable<Entity> Entities { get; }

public BaseObservableRepository(IConfigRepository configRepository)
{
_ConfigRepository = configRepository
Entities = Observable.Defer(() => GetObservable().Concat());
}
protected override IEnumerable<IObservable<Entity>> GetObservable()
{
var rows = _ConfigRepository.Table<EntityTable>().Rows.Where(i => InMemory.Any(e => e.RowId == i.Id) == false);
return rows.Select(i => Observable.Return(BuildEntity(i)));
}

public int buildCalled = 0;
public Entity BuildEntity(EntityTableRow entityRow)
{
buildCalled++;
return new Entity { RowId = entityRow.Id, StringVal = entityRow.StringVal };
}


Is this the expected behavior? Is there a way to defer the enumeration of the objects (specifically the building in this case) until truly neede

Answer

The following is Linqpad-friendly code equivalent to what you have:

void Main()
{
    var entities = Observable.Defer(() => GetObservable().Concat());
    Entity result = null;
    var first = entities.FirstOrDefaultAsync(i => i.RowId == 1).Subscribe(i => result = i);
    result.Dump();
    buildCalled.Dump();
}

// Define other methods and classes here

public IEnumerable<IObservable<Entity>> GetObservable()
{
    var rows = new List<EntityTableRow>
    {
        new EntityTableRow { Id = 1, StringVal = "One"},
        new EntityTableRow { Id = 2, StringVal = "Two"},
    };
    return rows.Select(i => Observable.Return(BuildEntity(i)));
}

public int buildCalled = 0;
public Entity BuildEntity(EntityTableRow entityRow)
{
    buildCalled++;
    return new Entity { RowId = entityRow.Id, StringVal = entityRow.StringVal };
}

public class Entity
{
    public int RowId { get; set; }
    public string StringVal { get; set; }
}

public class EntityTableRow
{
    public int Id { get; set; }
    public string StringVal { get; set; }
}

If you change GetObservable to the following, you'll get the desired result:

public IObservable<IObservable<Entity>> GetObservable()
{
    var rows = new List<EntityTableRow>
    {
        new EntityTableRow { Id = 1, StringVal = "One"},
        new EntityTableRow { Id = 2, StringVal = "Two"},
    };
    return rows.ToObservable().Select(i => Observable.Return(BuildEntity(i)));
}

It appears the implementation of Concat<TSource>(IEnumerable<IObservable<TSource>>) is eager in evaluating the enumerable, whereas the implementation of Concat<TSource>(IObservable<IObservable<TSource>>) and ToObservable<TSource>(IEnumerable<TSource>) maintain laziness appropriately. I can't say I know why.