在 C# 中,如何提取生成 return 的代码块?

In C#, how can I extract a block of code which yields the return?

我有几个方法都结束了:

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

例如:

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;
        }
    }
}

我想删除这个重复项。

如果我尝试将其重构为:

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;
        }
    }
}

它不会编译,因为我不能 return 来自迭代器的值。

如果我尝试 return yield return YieldAppointments(cursor);,它不会编译,因为:

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

所以我尝试

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

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

其中任何一个都会产生以下编译器错误:

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

然后我尝试了:

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;
        }
    }
}

但这不会编译因为

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

有没有办法让它工作?

用一个 Task 参数化 YieldAppointments 给你一个光标怎么样?

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;
        }
    }
}

现在您可以在 lambda 中编写 GetEventWithWorkOrderType 的第一部分(获取光标的位置,以及您可能执行的任何其他异步操作):

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

解决此问题的一种方法是使用 Tom Gringauz 的 AsyncEnumerableEx.Using method from the System.Interactive.Async package, and the ToAsyncEnumerable extension method from

下面是AsyncEnumerableEx.Using方法的签名:

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

下面是 ToAsyncEnumerable 扩展方法,从上述答案复制粘贴,增强了取消支持:

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;
}

以下是如何结合这两种方法来解决您的问题:

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()));
}

AsyncEnumerableEx.Using 有一个过于灵活的 enumerableFactory 参数,(在您的情况下)需要将 cursor.ToAsyncEnumerable() 调用包装在冗长且嘈杂的 ValueTask 中。您可以通过将 AsyncEnumerableEx.Using 包装在您自己的 Using 方法中来摆脱这种烦恼:

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)));
}

使用此方法,解决方案变得更简单且更具可读性:

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

备选方案: 下面是一个自给自足且不那么抽象的备选方案。 FromAsyncCursor 泛型方法采用 asyncCursorFactory 和 returns 包含游标发出的文档的序列:

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;
}

用法示例:

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

EnumeratorCancellation 属性可以使用生成的序列,同时观察 CancellationToken:

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

虽然 GetEventWithWorkOrderType 方法本身不接受 CancellationToken,但由于此属性,令牌会神奇地传播到 FromAsyncCursor 方法。不过,所有这些都可能有点学术性,因为单个 MoveNextAsync 不太可能花费这么多时间来获取下一批文档,从而使添加取消支持成为一个令人信服的提议。