Establish the facts about runtime async, define the problem, and produce a design note. This story
decides nothing. It exists so that the design discussion starts from evidence rather than from
assumptions, and so that a later session can break the work into implementable stories.
Status of the feature in .NET 11
Runtime async is present in .NET 11 but is not enabled for user code.
The runtime side is on. CoreCLR supports it with no environment variable, and the framework
libraries are compiled with it: a scan of the 181 assemblies in the release candidate 1 shared
framework found 899 methods carrying MethodImplOptions.Async across 27 assemblies, including 107
in System.Private.CoreLib.
The compiler side is off for user code. In both Preview 7 and release candidate 1 the compiler emits
classic state machines unless <Features>runtime-async=on</Features> is set. The <UseRuntimeAsync>
MSBuild property has no effect, because the release candidate 1 SDK contains no reference to it.
RuntimeFeature.Async is absent from both the reference and implementation assemblies. No release
note for release candidate 1 mentions the feature. The tracking issue
dotnet/runtime#109632 states the requirement explicitly, and still lists
open work including stack trace formatting and function pointers to async methods.
Opting in is cheap and silent. One MSBuild property on a net11.0 project is sufficient. No
EnablePreviewFeatures, no LangVersion change, no environment variable, and no warning of any
kind. The same property in a Directory.Build.props switches an entire repository at once.
Verified facts
All confirmed against the .NET 11 SDK with the feature flag set, first on Preview 7 and re-run on
release candidate 1 with identical results.
- Runtime async adds no IL instruction. An
await is an ordinary call whose operand is a
MethodSpec token for AsyncHelpers.Await<T>, AwaitAwaiter<TAwaiter> or
UnsafeAwaitAwaiter<TAwaiter>. The method carries implementation flag 0x2000. PostSharp's
instruction reader needs no new opcode support.
- There is no
AsyncStateMachineAttribute, no state machine type and no MoveNext. A
Task<T>-returning method leaves a value of type T on the stack at ret, not a Task<T>.
async void, async iterators and ordinary iterators keep classic state machines. Both forms occur
in the same type, so any decision must be taken per method rather than per assembly or per target
framework.
- Wrapping the body in a
try block with synchronous advice in catch and finally handlers is
valid: it compiles, keeps real Clause and Finally handlers, and executes with the correct
ordering on both the successful and the throwing path.
- Roslyn lowers
await inside a finally into a catch clause plus an
ExceptionDispatchInfo.Capture(...).Throw() re-throw, moving the awaited operation out of the
handler. PostSharp will read a catch where the source had a finally.
AsyncHelpers gained four Await overloads between Preview 7 and release candidate 1. Recognition
should match on declaring type and method name, not on an enumerated overload list.
What looks tractable
Advising the method boundary looks straightforward. The runtime-async body spans the whole logical
execution of the async method, so wrapping it produces entry advice at method start and exit advice
after the awaited work completes. The classic form needs ApplyToStateMachine and a MoveNext
rewrite for the same result.
Advising await points also looks tractable, and possibly simpler than the classic form. A suspension
point is a single call in a flat method body, so injection is ordinary instruction insertion. None
of the machinery that makes the classic implementation difficult applies, because that difficulty
comes from the state machine. This is an observation, not a result: it has not been implemented or
tested.
Open problems
- The evaluation stack must be effectively empty at a suspension point. Roslyn spills a pending
argument into a local rather than leaving it on the stack across the Await call.
- Locals introduced by PostSharp must survive a suspension. Byref variables, byref-like structures
and pinning locals may not be hoisted, and reading one after a suspension point produces null with
no diagnostic.
- The fast path bypasses the suspension. When the awaiter reports completion, a
brtrue branch
skips the AsyncHelpers call entirely.
- The two await encodings differ. A
Task<T> await is a single call returning the value; a custom
awaitable keeps the GetAwaiter, get_IsCompleted, AsyncHelpers, GetResult protocol.
- Whether the runtime tolerates arbitrary injected IL around the
AsyncHelpers call is untested.
- The existing public API has no defined meaning here. This covers
ApplyToStateMachine, which has
no referent without a state machine; SemanticallyAdvisedMethodKinds.Async, which does not match,
so a runtime-async method currently falls through to ReturnsAwaitable by accident; and the type
of MethodExecutionArgs.ReturnValue at the exit point.
MethodInterceptionAspect replaces the whole invocation and has no state machine to generate for
ProceedAsync. One candidate is extracting the body into a separate method marked
[MethodImpl(MethodImplOptions.Async)] and awaiting it.
StateMachineKind and IStateMachineInfo are SDK surface. IStateMachineInfo is marked
[InternalImplement], so adding members to it is not a user-facing breaking change, but the
representation still needs deciding.
- What PostSharp should do when an aspect cannot be honoured is undecided: report an error, reduce
the advice silently, or reduce it with a warning.
Priority and the feedback dependency
This work stays in scope but is not critical, because the feature is not enabled by default and no
customer reaches it without asking. The reduced pressure is an opportunity to design the aspect
model properly.
One dependency is worth naming. Customer feedback only arrives if customers can tell something is
wrong. Today a customer who enables runtime async gets silently misclassified methods and invalid IL,
with no diagnostic from the compiler or from PostSharp. Detection is therefore the mechanism that
produces the feedback this schedule relies on, and it is cheap, since it is a single implementation
flag on the method. An early diagnostic is the one piece of this work worth separating from the rest
of the design.
Suggested first experiment
Run PostSharp over a runtime-async assembly with a trivial OnMethodBoundaryAspect and inspect the
result with ilverify. Then attempt an injection around an AsyncHelpers call. That tests problems
1 to 5 at once.
Analysis: Documentation/DotNet11-CSharp15-Impact.md in postsharp/PostSharp.
— Claude for Gael
Establish the facts about runtime async, define the problem, and produce a design note. This story
decides nothing. It exists so that the design discussion starts from evidence rather than from
assumptions, and so that a later session can break the work into implementable stories.
Status of the feature in .NET 11
Runtime async is present in .NET 11 but is not enabled for user code.
The runtime side is on. CoreCLR supports it with no environment variable, and the framework
libraries are compiled with it: a scan of the 181 assemblies in the release candidate 1 shared
framework found 899 methods carrying
MethodImplOptions.Asyncacross 27 assemblies, including 107in
System.Private.CoreLib.The compiler side is off for user code. In both Preview 7 and release candidate 1 the compiler emits
classic state machines unless
<Features>runtime-async=on</Features>is set. The<UseRuntimeAsync>MSBuild property has no effect, because the release candidate 1 SDK contains no reference to it.
RuntimeFeature.Asyncis absent from both the reference and implementation assemblies. No releasenote for release candidate 1 mentions the feature. The tracking issue
dotnet/runtime#109632 states the requirement explicitly, and still lists
open work including stack trace formatting and function pointers to async methods.
Opting in is cheap and silent. One MSBuild property on a
net11.0project is sufficient. NoEnablePreviewFeatures, noLangVersionchange, no environment variable, and no warning of anykind. The same property in a
Directory.Build.propsswitches an entire repository at once.Verified facts
All confirmed against the .NET 11 SDK with the feature flag set, first on Preview 7 and re-run on
release candidate 1 with identical results.
awaitis an ordinarycallwhose operand is aMethodSpectoken forAsyncHelpers.Await<T>,AwaitAwaiter<TAwaiter>orUnsafeAwaitAwaiter<TAwaiter>. The method carries implementation flag0x2000. PostSharp'sinstruction reader needs no new opcode support.
AsyncStateMachineAttribute, no state machine type and noMoveNext. ATask<T>-returning method leaves a value of typeTon the stack atret, not aTask<T>.async void, async iterators and ordinary iterators keep classic state machines. Both forms occurin the same type, so any decision must be taken per method rather than per assembly or per target
framework.
tryblock with synchronous advice incatchandfinallyhandlers isvalid: it compiles, keeps real
ClauseandFinallyhandlers, and executes with the correctordering on both the successful and the throwing path.
awaitinside afinallyinto acatchclause plus anExceptionDispatchInfo.Capture(...).Throw()re-throw, moving the awaited operation out of thehandler. PostSharp will read a
catchwhere the source had afinally.AsyncHelpersgained fourAwaitoverloads between Preview 7 and release candidate 1. Recognitionshould match on declaring type and method name, not on an enumerated overload list.
What looks tractable
Advising the method boundary looks straightforward. The runtime-async body spans the whole logical
execution of the async method, so wrapping it produces entry advice at method start and exit advice
after the awaited work completes. The classic form needs
ApplyToStateMachineand aMoveNextrewrite for the same result.
Advising await points also looks tractable, and possibly simpler than the classic form. A suspension
point is a single
callin a flat method body, so injection is ordinary instruction insertion. Noneof the machinery that makes the classic implementation difficult applies, because that difficulty
comes from the state machine. This is an observation, not a result: it has not been implemented or
tested.
Open problems
argument into a local rather than leaving it on the stack across the
Awaitcall.and pinning locals may not be hoisted, and reading one after a suspension point produces null with
no diagnostic.
brtruebranchskips the
AsyncHelperscall entirely.Task<T>await is a single call returning the value; a customawaitable keeps the
GetAwaiter,get_IsCompleted,AsyncHelpers,GetResultprotocol.AsyncHelperscall is untested.ApplyToStateMachine, which hasno referent without a state machine;
SemanticallyAdvisedMethodKinds.Async, which does not match,so a runtime-async method currently falls through to
ReturnsAwaitableby accident; and the typeof
MethodExecutionArgs.ReturnValueat the exit point.MethodInterceptionAspectreplaces the whole invocation and has no state machine to generate forProceedAsync. One candidate is extracting the body into a separate method marked[MethodImpl(MethodImplOptions.Async)]and awaiting it.StateMachineKindandIStateMachineInfoare SDK surface.IStateMachineInfois marked[InternalImplement], so adding members to it is not a user-facing breaking change, but therepresentation still needs deciding.
the advice silently, or reduce it with a warning.
Priority and the feedback dependency
This work stays in scope but is not critical, because the feature is not enabled by default and no
customer reaches it without asking. The reduced pressure is an opportunity to design the aspect
model properly.
One dependency is worth naming. Customer feedback only arrives if customers can tell something is
wrong. Today a customer who enables runtime async gets silently misclassified methods and invalid IL,
with no diagnostic from the compiler or from PostSharp. Detection is therefore the mechanism that
produces the feedback this schedule relies on, and it is cheap, since it is a single implementation
flag on the method. An early diagnostic is the one piece of this work worth separating from the rest
of the design.
Suggested first experiment
Run PostSharp over a runtime-async assembly with a trivial
OnMethodBoundaryAspectand inspect theresult with
ilverify. Then attempt an injection around anAsyncHelperscall. That tests problems1 to 5 at once.
Analysis:
Documentation/DotNet11-CSharp15-Impact.mdinpostsharp/PostSharp.— Claude for Gael