System.InvalidOperationException 将 GetAwaiter().GetResult() 与 ServiceBusReceiver.PeekMessagesAsync 一起使用时
System.InvalidOperationException when using GetAwaiter().GetResult() with ServiceBusReceiver.PeekMessagesAsync
上下文
我们正在使用 GetAwaiter().GetResult()
,因为 PowerShell 的 Cmdlet.ProcessRecord()
不支持 async/await。
代码示例
class Program
{
static async Task Main(string[] args)
{
var topicPath = "some-topic";
var subscriptionName = "some-subscription";
var connectionString = "some-connection-string";
var subscriptionPath = EntityNameHelper.FormatSubscriptionPath(
topicPath,
subscriptionName
);
var serviceBusClient = new ServiceBusClient(connectionString);
var receiver = serviceBusClient.CreateReceiver(queueName: subscriptionPath);
// This one works. :-)
await foreach (var item in GetMessages(receiver, maxMessagesPerFetch: 5))
{
Console.WriteLine("async/await: " + item);
}
// This one explodes.
var enumerator = GetMessages(receiver, maxMessagesPerFetch: 5).GetAsyncEnumerator();
while (enumerator.MoveNextAsync().GetAwaiter().GetResult())
{
// Unhandled exception. System.InvalidOperationException: Operation is not valid due to the current state of the object.
// at NonSync.IAsyncEnumerable.Program.GetMessages(ServiceBusReceiver receiver, Int32 maxMessagesPerFetch)+System.Threading.Tasks.Sources.IValueTaskSource<System.Boolean>.GetResult()
// at NonSync.IAsyncEnumerable.Program.Main(String[] args) in C:\dev\mediavalet\MediaValet.Learning\entropy\NonSynchronousDotNet\NonSync.IAsyncEnumerable\Program.cs:line 42
// at NonSync.IAsyncEnumerable.Program.<Main>(String[] args)
Console.WriteLine("GetAwaiter().GetResult(): " + enumerator.Current);
}
}
public static async IAsyncEnumerable<string> GetMessages(
ServiceBusReceiver receiver,
int maxMessagesPerFetch
)
{
yield return "Foo";
var messages = await receiver.PeekMessagesAsync(maxMessagesPerFetch);
yield return "Bar";
}
}
问题
这是怎么回事?我们如何在不更改的情况下修复它?GetMessages
?
根据 ValueTask<TResult>
结构的文档:
The following operations should never be performed on a ValueTask<TResult>
instance:
• Awaiting the instance multiple times.
• Calling AsTask
multiple times.
• Using .Result
or .GetAwaiter().GetResult()
when the operation hasn't yet completed, or using them multiple times.
• Using more than one of these techniques to consume the instance.
If you do any of the above, the results are undefined.
您可以使用 AsTask
方法将 ValueTask<bool>
转换为 Task<bool>
:
while (enumerator.MoveNextAsync().AsTask().GetAwaiter().GetResult())
此答案通过代码示例补充了 Theodor 的答案。我们的具体问题是我们在 ValueTask
完成之前调用了 GetResult()
。文档指定不允许这样做:
A ValueTask instance may only be awaited once, and consumers may not read Result until the instance has completed. If these limitations are unacceptable, convert the ValueTask to a Task by calling AsTask. (Emphasis added).
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
var enumerator = GetAsyncEnumerable().GetAsyncEnumerator();
while (true)
{
var moveNext = enumerator.MoveNextAsync();
var moveNextAwaiter = moveNext.GetAwaiter();
Console.WriteLine("ValueTask.IsCompleted: {0}", moveNext.IsCompleted);
try
{
if (moveNextAwaiter.GetResult())
{
Console.WriteLine("IAsyncEnumerator.Current: {0}", enumerator.Current);
continue;
}
Console.WriteLine("Done! We passed the end of the collection.");
break;
}
catch (InvalidOperationException)
{
Console.WriteLine("Boom! GetResult() before the ValueTask completed.");
continue;
}
}
async IAsyncEnumerable<int> GetAsyncEnumerable()
{
yield return 1;
await Task.Delay(1000);
yield return 2; // <---- We never access this, because GetResult() explodes.
yield return 3;
}
输出:
ValueTask.IsCompleted: True
IAsyncEnumerator.Current: 1
ValueTask.IsCompleted: False
Boom! GetResult() before the ValueTask completed.
ValueTask.IsCompleted: True
IAsyncEnumerator.Current: 3
ValueTask.IsCompleted: True
Done! We passed the end of the collection.
上下文
我们正在使用 GetAwaiter().GetResult()
,因为 PowerShell 的 Cmdlet.ProcessRecord()
不支持 async/await。
代码示例
class Program
{
static async Task Main(string[] args)
{
var topicPath = "some-topic";
var subscriptionName = "some-subscription";
var connectionString = "some-connection-string";
var subscriptionPath = EntityNameHelper.FormatSubscriptionPath(
topicPath,
subscriptionName
);
var serviceBusClient = new ServiceBusClient(connectionString);
var receiver = serviceBusClient.CreateReceiver(queueName: subscriptionPath);
// This one works. :-)
await foreach (var item in GetMessages(receiver, maxMessagesPerFetch: 5))
{
Console.WriteLine("async/await: " + item);
}
// This one explodes.
var enumerator = GetMessages(receiver, maxMessagesPerFetch: 5).GetAsyncEnumerator();
while (enumerator.MoveNextAsync().GetAwaiter().GetResult())
{
// Unhandled exception. System.InvalidOperationException: Operation is not valid due to the current state of the object.
// at NonSync.IAsyncEnumerable.Program.GetMessages(ServiceBusReceiver receiver, Int32 maxMessagesPerFetch)+System.Threading.Tasks.Sources.IValueTaskSource<System.Boolean>.GetResult()
// at NonSync.IAsyncEnumerable.Program.Main(String[] args) in C:\dev\mediavalet\MediaValet.Learning\entropy\NonSynchronousDotNet\NonSync.IAsyncEnumerable\Program.cs:line 42
// at NonSync.IAsyncEnumerable.Program.<Main>(String[] args)
Console.WriteLine("GetAwaiter().GetResult(): " + enumerator.Current);
}
}
public static async IAsyncEnumerable<string> GetMessages(
ServiceBusReceiver receiver,
int maxMessagesPerFetch
)
{
yield return "Foo";
var messages = await receiver.PeekMessagesAsync(maxMessagesPerFetch);
yield return "Bar";
}
}
问题
这是怎么回事?我们如何在不更改的情况下修复它?GetMessages
?
根据 ValueTask<TResult>
结构的文档:
The following operations should never be performed on a
ValueTask<TResult>
instance:• Awaiting the instance multiple times.
• CallingAsTask
multiple times.
• Using.Result
or.GetAwaiter().GetResult()
when the operation hasn't yet completed, or using them multiple times.
• Using more than one of these techniques to consume the instance.If you do any of the above, the results are undefined.
您可以使用 AsTask
方法将 ValueTask<bool>
转换为 Task<bool>
:
while (enumerator.MoveNextAsync().AsTask().GetAwaiter().GetResult())
此答案通过代码示例补充了 Theodor 的答案。我们的具体问题是我们在 ValueTask
完成之前调用了 GetResult()
。文档指定不允许这样做:
A ValueTask instance may only be awaited once, and consumers may not read Result until the instance has completed. If these limitations are unacceptable, convert the ValueTask to a Task by calling AsTask. (Emphasis added).
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
var enumerator = GetAsyncEnumerable().GetAsyncEnumerator();
while (true)
{
var moveNext = enumerator.MoveNextAsync();
var moveNextAwaiter = moveNext.GetAwaiter();
Console.WriteLine("ValueTask.IsCompleted: {0}", moveNext.IsCompleted);
try
{
if (moveNextAwaiter.GetResult())
{
Console.WriteLine("IAsyncEnumerator.Current: {0}", enumerator.Current);
continue;
}
Console.WriteLine("Done! We passed the end of the collection.");
break;
}
catch (InvalidOperationException)
{
Console.WriteLine("Boom! GetResult() before the ValueTask completed.");
continue;
}
}
async IAsyncEnumerable<int> GetAsyncEnumerable()
{
yield return 1;
await Task.Delay(1000);
yield return 2; // <---- We never access this, because GetResult() explodes.
yield return 3;
}
输出:
ValueTask.IsCompleted: True
IAsyncEnumerator.Current: 1
ValueTask.IsCompleted: False
Boom! GetResult() before the ValueTask completed.
ValueTask.IsCompleted: True
IAsyncEnumerator.Current: 3
ValueTask.IsCompleted: True
Done! We passed the end of the collection.