Skip to content

API Proposal: ExecutionContext.Restore(ExecutionContext? executionContext) #38011

@benaadams

Description

@benaadams

Background and Motivation

Alternative to the approved api #30867 "ExecutionContext.Run ref overloads"

awaiting a Task returning method that is itself not async does not guarantee undoing AsyncLocal/ExecutionContext changes (as it is the responsibility of the callee)

ExecutionContext does provide an callback based method that acts as a stack:

public sealed class ExecutionContext
{
    ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, object? state)
}

However using it to just undo changes is clunky (involves rearranging code into a callback); as well as applying the current context to run on, when its already on the current context.

It also doesn't provide a return value; and if it was guarding multiple awaits arranging to return a single Task (via using state as a reference) becomes complicated.

Rather than using it in a stack based "apply=>undo" manner; it would be useful if it could be restored directly inline.

Related ASP.NET issues dotnet/aspnetcore#15384, dotnet/aspnetcore#13991

Proposed API

namespace System.Threading
{
    public sealed class ExecutionContext
    {
        public static void Restore(ExecutionContext? executionContext);
    }
}

Usage Examples

var ec = ExecutionContext.Capture();
while (!cancelled)
{
    await NextRequest();

    await OnStartAsync();

    await ProcessRequestAsync();

    await OnCompleteAsync();

    ExecutionContext.Restore(ec);
}

Alternative Designs

#30867 "ExecutionContext.Run ref overloads"; this again requires contorting the code to the callback based pattern and is problematic for multiple awaits as regular .Run as well as introducing a large method that has generic code expansion.

Also guard the multiple awaits behind an explicitly async method:

while (!cancelled)
{
    await NextRequest();

    await ProcessUserCode();
}

async ValueTask ProcessUserCode()
{
    await OnStartAsync();

    await ProcessRequestAsync();

    await OnCompleteAsync();
}

However this introduces an extra statemachine per request and an additional allocation per request; as well as causing the ExecutionContext to be captured and restored per request; when really we just want to capture it at the start of request processing and only restore/reset it per request.

/cc @davidfowl @stephentoub

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions