Skip to content

Commit 8c4cf67

Browse files
committed
Weave async iterator methods (#624, #597)
Async iterators (async IAsyncEnumerable<T> with yield) are lowered with AsyncIteratorStateMachineAttribute rather than AsyncStateMachineAttribute, so ProcessType skipped them and their awaits kept resuming on the captured context. Recognise both attributes in GetAsyncStateMachineKind/Type so the iterator body is woven like any other state machine. Adds behavioural tests for the iterator producer plus the await foreach / await using consumer paths, and an IL snapshot of the woven types.
1 parent 6613cfb commit 8c4cf67

11 files changed

Lines changed: 5377 additions & 2 deletions
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#if NET
2+
using Fody;
3+
4+
[ConfigureAwait(false)]
5+
public class AsyncEnumerable
6+
{
7+
// Issue #624: an async iterator method (lowered with AsyncIteratorStateMachineAttribute)
8+
// must have its internal awaits configured. The only await here is the unconfigured
9+
// Task.Delay, so the flag reflects whether the iterator body was woven.
10+
public async IAsyncEnumerable<int> Producer(SynchronizationContext context)
11+
{
12+
SynchronizationContext.SetSynchronizationContext(context);
13+
await Task.Delay(10);
14+
yield return 1;
15+
await Task.Delay(10);
16+
yield return 2;
17+
}
18+
19+
// Issue #597: consuming an IAsyncEnumerable<T> with `await foreach` awaits the
20+
// ValueTask<bool>/ValueTask returned by MoveNextAsync/DisposeAsync. The source's own
21+
// await is configured explicitly so only this consumer's awaits are under test.
22+
public async Task AwaitForeach(SynchronizationContext context)
23+
{
24+
SynchronizationContext.SetSynchronizationContext(context);
25+
// ReSharper disable once UnusedVariable
26+
await foreach (var item in Source())
27+
{
28+
}
29+
}
30+
31+
static async IAsyncEnumerable<int> Source()
32+
{
33+
await Task.Delay(10).ConfigureAwait(false);
34+
yield return 1;
35+
}
36+
37+
// Issue #597: consuming an IAsyncDisposable with `await using` awaits the ValueTask
38+
// returned by DisposeAsync. The disposable's own await is configured explicitly so
39+
// only this consumer's await is under test.
40+
public async Task AwaitUsing(SynchronizationContext context)
41+
{
42+
SynchronizationContext.SetSynchronizationContext(context);
43+
await using (new AsyncDisposable())
44+
{
45+
}
46+
}
47+
}
48+
49+
public class AsyncDisposable : IAsyncDisposable
50+
{
51+
public async ValueTask DisposeAsync()
52+
{
53+
await Task.Delay(10).ConfigureAwait(false);
54+
Disposed = true;
55+
}
56+
57+
public static bool Disposed;
58+
}
59+
#endif

ConfigureAwait.Fody/CecilExtensions.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,20 @@ public static void InsertBefore(this ILProcessor processor, Instruction target,
4949
}
5050
}
5151

52+
// Async iterators (async IAsyncEnumerable<T> with yield) are lowered to a state machine
53+
// just like normal async methods, but the generated method carries
54+
// AsyncIteratorStateMachineAttribute rather than AsyncStateMachineAttribute.
55+
static bool IsAsyncStateMachineAttribute(this CustomAttribute attribute)
56+
{
57+
var fullName = attribute.AttributeType.FullName;
58+
return fullName is
59+
"System.Runtime.CompilerServices.AsyncStateMachineAttribute" or
60+
"System.Runtime.CompilerServices.AsyncIteratorStateMachineAttribute";
61+
}
62+
5263
public static AsyncStateMachineKind GetAsyncStateMachineKind(this MethodDefinition method)
5364
{
54-
if (method.CustomAttributes.Any(a => a.AttributeType.FullName == "System.Runtime.CompilerServices.AsyncStateMachineAttribute"))
65+
if (method.CustomAttributes.Any(IsAsyncStateMachineAttribute))
5566
return AsyncStateMachineKind.StateMachine;
5667

5768
if (method.ImplAttributes.HasFlag(MethodImplAttributes_Async))
@@ -63,7 +74,7 @@ public static AsyncStateMachineKind GetAsyncStateMachineKind(this MethodDefiniti
6374
public static TypeDefinition GetAsyncStateMachineType(this ICustomAttributeProvider provider)
6475
{
6576
var attribute = provider.CustomAttributes
66-
.FirstOrDefault(a => a.AttributeType.FullName == "System.Runtime.CompilerServices.AsyncStateMachineAttribute");
77+
.FirstOrDefault(IsAsyncStateMachineAttribute);
6778

6879
return (TypeDefinition)attribute?.ConstructorArguments[0].Value;
6980
}

Tests/AsyncEnumerableTests.cs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#if NET
2+
public partial class ModuleWeaverTests
3+
{
4+
// Issue #624: the async iterator body must be woven so its awaits do not resume on
5+
// the captured context.
6+
[Fact]
7+
public async Task AsyncEnumerable_Producer()
8+
{
9+
var context = testResult.GetInstance("FlagSynchronizationContext");
10+
var test = testResult.GetInstance("AsyncEnumerable");
11+
12+
Assert.False(context.Flag);
13+
14+
var enumerable = (IAsyncEnumerable<int>)test.Producer(context);
15+
// ReSharper disable once UnusedVariable
16+
await foreach (var item in enumerable)
17+
{
18+
}
19+
20+
Assert.False(context.Flag);
21+
}
22+
23+
// Issue #597: `await foreach` over an IAsyncEnumerable<T>.
24+
[Fact]
25+
public async Task AsyncEnumerable_AwaitForeach()
26+
{
27+
var context = testResult.GetInstance("FlagSynchronizationContext");
28+
var test = testResult.GetInstance("AsyncEnumerable");
29+
30+
Assert.False(context.Flag);
31+
32+
await test.AwaitForeach(context);
33+
34+
Assert.False(context.Flag);
35+
}
36+
37+
// Issue #597: `await using` over an IAsyncDisposable.
38+
[Fact]
39+
public async Task AsyncEnumerable_AwaitUsing()
40+
{
41+
var context = testResult.GetInstance("FlagSynchronizationContext");
42+
var test = testResult.GetInstance("AsyncEnumerable");
43+
44+
Assert.False(context.Flag);
45+
46+
await test.AwaitUsing(context);
47+
48+
Assert.False(context.Flag);
49+
}
50+
}
51+
#endif

0 commit comments

Comments
 (0)