1

I have several methods which all end:

while (await cursor.MoveNextAsync())
{
    foreach (FieldServiceAppointment appointment in cursor.Current)
    {
        yield return appointment;
    }
}

For example:

public async IAsyncEnumerable<FieldServiceAppointment> GetEventWithWorkOrderType(
    string workOrderType, string customerId)
{
    using IAsyncCursor<FieldServiceAppointment> cursor = await FieldServiceAppointments.FindAsync(
        x => x.BusinessEventTypeCode == workOrderType
            && x.CustomerCode == customerId
        );

    while (await cursor.MoveNextAsync())
    {
        foreach (FieldServiceAppointment appointment in cursor.Current)
        {
            yield return appointment;
        }
    }
}

I'd like to remove this duplication.

If I try to refactor it to this:

public async IAsyncEnumerable<FieldServiceAppointment> GetEventWithWorkOrderType(
    string workOrderType, string customerId)
{
    using IAsyncCursor<FieldServiceAppointment> cursor = await FieldServiceAppointments.FindAsync(
        x => x.BusinessEventTypeCode == workOrderType
            && x.CustomerCode == customerId
        );
    YieldAppointments(cursor);
}

public async IAsyncEnumerable<FieldServiceAppointment> YieldAppointments(
    IAsyncCursor<FieldServiceAppointment> cursor)
{
    while (await cursor.MoveNextAsync())
    {
        foreach (FieldServiceAppointment appointment in cursor.Current)
        {
            yield return appointment;
        }
    }
}

It won't compile because I can't return a value from an iterator.

If I try to return yield return YieldAppointments(cursor);, it won't compile because:

Severity Code Description Project File Line Suppression State Error CS0266 Cannot implicitly convert type 'System.Collections.Generic.IAsyncEnumerable<DataAccessLayer.Entities.Praxedo.FieldServiceAppointment>' to 'DataAccessLayer.Entities.Praxedo.FieldServiceAppointment'. An explicit conversion exists (are you missing a cast?) DataAccessLayer C:\projects\EnpalPraxedoIntegration\DataAccessLayer\DbServices\FieldServiceAutomationDbService.cs 78 Active

So I tried to

yield return (IAsyncEnumerable<FieldServiceAppointment>) YieldAppointments(cursor);

and

yield return YieldAppointments(cursor) as IAsyncEnumerable <FieldServiceAppointment>;

either of which generate a compiler error of:

Severity Code Description Project File Line Suppression State Error CS0266 Cannot implicitly convert type 'System.Collections.Generic.IAsyncEnumerable<DataAccessLayer.Entities.Praxedo.FieldServiceAppointment>' to 'DataAccessLayer.Entities.Praxedo.FieldServiceAppointment'. An explicit conversion exists (are you missing a cast?) DataAccessLayer C:\projects\EnpalPraxedoIntegration\DataAccessLayer\DbServices\FieldServiceAutomationDbService.cs 78 Active

So then I tried:

public async IAsyncEnumerable<FieldServiceAppointment> GetEventWithWorkOrderType(
    string workOrderType, string customerId)
{
    using IAsyncCursor<FieldServiceAppointment> cursor = await FieldServiceAppointments.FindAsync(
        x => x.BusinessEventTypeCode == workOrderType
            && x.CustomerCode == customerId
        );
    yield return await YieldAppointments(cursor);
}

public async Task<FieldServiceAppointment> YieldAppointments(
    IAsyncCursor<FieldServiceAppointment> cursor)
{
    while (await cursor.MoveNextAsync())
    {
        foreach (FieldServiceAppointment appointment in cursor.Current)
        {
            yield return appointment;
        }
    }
}

but this won't compile because

Severity Code Description Project File Line Suppression State Error CS1624 The body of 'FieldServiceAutomationDbService.YieldAppointments(IAsyncCursor)' cannot be an iterator block because 'Task' is not an iterator interface type DataAccessLayer C:\projects\EnpalPraxedoIntegration\DataAccessLayer\DbServices\FieldServiceAutomationDbService.cs 81 Active

Is there a way to make this work?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Brian Kessler
  • 2,187
  • 6
  • 28
  • 58

2 Answers2

3

How about parameterising YieldAppointments with a Task that gives you a cursor?

public async IAsyncEnumerable<FieldServiceAppointment> YieldAppointments(Func<Task<IAsyncCursor<FieldServiceAppointment>>> cursorTask)
{
    using var cursor = await cursorTask();
    while (await cursor.MoveNextAsync())
    {
        foreach (FieldServiceAppointment appointment in cursor.Current)
        {
            yield return appointment;
        }
    }
}

Now you can write the first part of GetEventWithWorkOrderType (where you get the cursor, plus whatever other async operations that you might do) inside a lambda:

public IAsyncEnumerable<FieldServiceAppointment> GetEventWithWorkOrderType(string workOrderType, string customerId)
    => YieldAppointments(async () =>
        await FieldServiceAppointments.FindAsync(
            x => x.BusinessEventTypeCode == workOrderType
                && x.CustomerCode == customerId
        );
    );
Sweeper
  • 213,210
  • 22
  • 193
  • 313
1

One way to solve this problem is to use the AsyncEnumerableEx.Using method from the System.Interactive.Async package, and the ToAsyncEnumerable extension method from this answer by Tom Gringauz.

Below is the signature of the AsyncEnumerableEx.Using method:

public static IAsyncEnumerable<TSource> Using<TSource, TResource>(
    Func<Task<TResource>> resourceFactory,
    Func<TResource, ValueTask<IAsyncEnumerable<TSource>>> enumerableFactory)
    where TResource : IDisposable;

Below is the ToAsyncEnumerable extension method, copy-pasted from the aforementioned answer, enhanced with cancellation support:

public static async IAsyncEnumerable<T> ToAsyncEnumerable<T>(
    this IAsyncCursor<T> asyncCursor,
    [EnumeratorCancellation]CancellationToken cancellationToken = default)
{
    while (await asyncCursor.MoveNextAsync(cancellationToken).ConfigureAwait(false))
        foreach (var current in asyncCursor.Current)
            yield return current;
}

Here is how to combine these two methods, in order to solve your problem:

public IAsyncEnumerable<FieldServiceAppointment> GetEventWithWorkOrderType(
    string workOrderType, string customerId)
{
    return AsyncEnumerableEx.Using(() => FieldServiceAppointments
        .FindAsync(x => x.BusinessEventTypeCode == workOrderType &&
            x.CustomerCode == customerId),
        cursor => new ValueTask<IAsyncEnumerable<FieldServiceAppointment>>(cursor.ToAsyncEnumerable()));
}

The AsyncEnumerableEx.Using has an overly flexible enumerableFactory parameter, that (in your case) requires wrapping the cursor.ToAsyncEnumerable() invocation in a verbose and noisy ValueTask. You could get rid of this annoyance by wrapping the AsyncEnumerableEx.Using in your own Using method:

public static IAsyncEnumerable<TSource> Using<TSource, TResource>(
    Func<Task<TResource>> resourceFactory,
    Func<TResource, IAsyncEnumerable<TSource>> enumerableFactory)
    where TResource : IDisposable
{
    return AsyncEnumerableEx.Using(resourceFactory,
        resource => new ValueTask<IAsyncEnumerable<TSource>>(
            enumerableFactory(resource)));
}

With this method the solution becomes simpler and more readable:

public IAsyncEnumerable<FieldServiceAppointment> GetEventWithWorkOrderType(
    string workOrderType, string customerId)
{
    return Using(() => FieldServiceAppointments
        .FindAsync(x => x.BusinessEventTypeCode == workOrderType &&
            x.CustomerCode == customerId),
        cursor => cursor.ToAsyncEnumerable());
}

Alternative: Below is a self-sufficient and less abstract alternative. The FromAsyncCursor generic method takes an asyncCursorFactory, and returns a sequence that contains the documents emitted by the cursor:

public async static IAsyncEnumerable<TDocument> FromAsyncCursor<TDocument>(
    Func<Task<IAsyncCursor<TDocument>>> asyncCursorFactory,
    [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
    using var asyncCursor = await asyncCursorFactory();
    while (await asyncCursor.MoveNextAsync(cancellationToken).ConfigureAwait(false))
        foreach (var current in asyncCursor.Current)
            yield return current;
}

Usage example:

public IAsyncEnumerable<FieldServiceAppointment> GetEventWithWorkOrderType(
    string workOrderType, string customerId)
{
    return FromAsyncCursor(() => FieldServiceAppointments.FindAsync(
        x => x.BusinessEventTypeCode == workOrderType && x.CustomerCode == customerId));
}

The EnumeratorCancellation attribute makes it possible to consume the generated sequence, while observing a CancellationToken:

var cts = new CancellationTokenSource();
await foreach (var item in GetEventWithWorkOrderType("xxx", "yyy")
    .WithCancellation(cts.Token))
{
    //...
}

Although the GetEventWithWorkOrderType method does not accept a CancellationToken itself, the token is magically propagated to the FromAsyncCursor method, because of this attribute. All this is probably a bit academic though, because it's unlikely that a single MoveNextAsync will take so much time to fetch the next batch of documents, to make adding cancellation support a compelling proposition.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • This doesn't compile, but rather results in Severity Code Description Project File Line Suppression State Error CS1503 Argument 1: cannot convert from 'System.Collections.Generic.IAsyncEnumerable>' to 'System.Threading.Tasks.Task>' DataAccessLayer C:\projects\EnpalPraxedoIntegration\DataAccessLayer\DbServices\FieldServiceAutomationDbService.cs 72 Active – Brian Kessler Oct 21 '21 at 13:50
  • @BrianKessler hmm, see [this](https://dotnetfiddle.net/YLIM8D) fiddle. I made some assumptions about the API that you are using. My example compiles under these assumptions. – Theodor Zoulias Oct 21 '21 at 14:16
  • 1
    cheers for the response. I'm afraid I'm a bit too much of a C# newbie to figure out how to confirm your assumptions or supply the policy. ... but I think I figured out the problem.... I was trying to put all the relevant code into the class where I would use it, where most methods should be instance methods. But the extension method can't be an instance method or live in a non-static class... – Brian Kessler Oct 21 '21 at 15:16
  • 1
    @BrianKessler yeap, extension methods are only allowed in static classes. Btw using the tools made by the [Rx team](https://www.nuget.org/profiles/rxteam), can make anyone feel like a newbie. They are quite intimidating! – Theodor Zoulias Oct 21 '21 at 15:38
  • 1
    @BrianKessler FYI I added an alternative solution in the answer. – Theodor Zoulias Oct 21 '21 at 16:31
  • 1
    Awesome! I like this answer better, not only for being self-sufficient, but I think it is even more readable. :-) – Brian Kessler Oct 22 '21 at 09:46
  • 1
    @BrianKessler to be fair the `FromAsyncCursor` method is just a generalized version of the `YieldAppointments` method in [Sweeper's answer](https://stackoverflow.com/questions/69659993/in-c-how-can-i-extract-a-block-of-code-which-yields-the-return/69660348#69660348). :-) – Theodor Zoulias Oct 22 '21 at 10:05
  • 1
    Yes, I noticed that. I was originally tempted to accept his answer instead, but I found your answer more informative and more reusable. :-) – Brian Kessler Oct 22 '21 at 10:49