Skip to content

Add an async-aware Proceed extension for behaviors #263

Description

@kzu

Summary

Behaviors see a Task or ValueTask as an ordinary return value. A reusable behavior cannot hard-code the result type, and Native AOT cannot look that result up with reflection.

Add ProceedExtension.Proceed so a behavior can write:

return invocation.Proceed(next, async (outcome, again) =>
{
    if (outcome.Exception is IOException)
        return await again();

    return ProceedOutcome.FromValue((int)outcome.Value! + 1, outcome.Elapsed);
});

API

  • IMethodReturn Proceed(this IMethodInvocation invocation, ExecuteHandler next, ProceedHandler callback) in ProceedExtension.cs.
  • ProceedHandler is ValueTask<ProceedOutcome> (ProceedOutcome outcome, ProceedAgain again).
  • ProceedAgain calls next once more and returns that attempt. It does not re-enter the current behavior.
  • ProceedOutcome exposes Value, Exception, and Elapsed. FromValue and FromException build a replacement attempt. For Task<T> and ValueTask<T>, Value is T, not the awaitable.

Behavior

  • Call next once before the callback. again may call it again. Attempts do not overlap.
  • Synchronous members, constructors, ref returns, and non-awaitables (IAsyncEnumerable<T>, custom awaitables) run the callback before Proceed returns. The callback's ValueTask must already be completed. Otherwise throw InvalidOperationException.
  • Preserve ref and out values on synchronous returns.
  • A throw from the target or from next becomes outcome.Exception. When the callback keeps that exception, Proceed returns CreateExceptionReturn, so the stunt still throws on the caller thread.
  • Task, ValueTask, Task<T>, and ValueTask<T> return a new awaitable of the same declared kind immediately. The callback runs when that attempt finishes. Unwrap with await and ConfigureAwait(false). A faulted or canceled awaitable stays faulted or canceled, with the original exception, and IMethodReturn.Exception stays null.
  • A null task or value task is a synchronous failure (NullReferenceException).
  • ValueTask and ValueTask<T> are single-consumption. Each Proceed awaits the attempt it received and returns a new awaitable, so an outer behavior awaits that new one.
  • Elapsed includes the time spent awaiting the attempt.

Native AOT

AsyncRegistry.Register<T>() registers the closed adapters for Task<T> and ValueTask<T>. The generator emits that call from the stunt static constructor for every closed type argument a generated member returns. Non-generic Task and ValueTask adapters ship with the library.

When dynamic code is unavailable and the adapter is missing, throw NotSupportedException naming AsyncRegistry.Register<{T}>() and a module initializer. Open generic intercepted methods stay on the existing ST015 path and do not receive a closed adapter.

Acceptance

  • A synchronous callback can replace a value or recover an exception, and again can retry.
  • A faulted Task<T> preserves exception identity. A canceled task stays canceled.
  • Two behaviors compose over one ValueTask<T> and consume it once.
  • A class stunt reports a base-call exception and can adjust a base Task<T>.
  • A generated stunt for Task<int> and ValueTask<string> contains AsyncRegistry.Register<int>() and AsyncRegistry.Register<string>().

Back this issue
Back this issue

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions