Search code examples
c#multithreadingasync-awaitsimple-injector

Simple Injector "instance is requested outside the context of an active (Async Scoped) scope" when using `ContinueWith`


I have a block of code that throws:

is registered using the 'Async Scoped' lifestyle, but the instance is requested outside the context of an active (Async Scoped) scope`

  1. The above is thrown when I use return for the Task (at higher volumes, but ok at lower processing volumes)
  2. It however works OK regardless of thru-put request volume when I await the Task, Simple Injector does not throw.

I appreciate this may be more of a async focused question as opposed to Simple Injector, but any insights why replacing return with await resolves this?

Thanks in advance, I am concerned whilst "working" using await is it potentially hiding a larger issue.

Background:

I have the following loop that deques items (tasks to be dispatched) by a worker:

void Loop()
{
    while (cancellationToken.IsCancellationRequested == false)
    {
        item = await this.queue.Reader.ReadAsync(cancellationToken);
        
        await DispatchItemAsync(item, (item, dispatcher) =>
        {
            return dispatcher
                .SendAsync(((ISendItem)item).GetHandler, item.Message, item.CancellationToken)
                .ContinueWith(t => item.TaskCompletionSourceWrapper.SetResult(t.Result), TaskContinuationOptions.RunContinuationsAsynchronously);
        });
    }
}

The DispatchItemAsync from the above loop is below:

protected override async Task DispatchItemAsync(
    IQueueItem item, Func<IQueueItem, IThreadStrategy, Task> dispatchFunc)
{
    // cast the passed item from channel queue
    var queueItemWithStack = item as IQueueItemWithStack;

    using (AsyncScopedLifestyle.BeginScope(this.container))
    {
        var dispatcher = container.GetInstance<InParallel>();
        // the above is an interface of delegates that is used to call functions
        
        // return throws SimpleInjector outside of scope exception (intermittent,
        // always for high request volume)
        return dispatchFunc(queueItemWithStack, dispatcher);

        // using await no exception is thrown
        // await dispatchFunc(queueItemWithStack, dispatcher);
    }
}

With InParallel containing the function which is called by the dispatchFunc line, the below is (eventually via a chain) called:

public Task<object> SendAsync(
    Func<SendFunction> getHandler,
    object request,
    CancellationToken cancellationToken = default)
{
    return this
        .inCaller
        .SendAsync(getHandler, request, cancellationToken)
        .ContinueWith(t =>
        { 
            // snip some code
            // the below throws if DispatchItemAsync call us with return
            // but is OK if DispatchItemAsync called us with await instead
            return t.Result;
        });
}

The exception occurs on the above ContinueWith when the t.Result is accessed:

CommandHandler is registered using the 'Async Scoped' lifestyle, but the instance is requested outside the context of an active (Async Scoped) scope. Please see https://simpleinjector.org/scoped for more information about how apply lifestyles and manage scopes.


Solution

  • By not awaiting a Task, you are executing the Task in parallel with the primary operation. Parallel means the code becomes multi threaded.

    This means that the original Scope will likely get disposed before the Task finished executing. This causes the exception you are experiencing. But in other cases, the Scope might get disposed of during the execution of the Task. This will cause objects getting disposed of while the Task is running. This can cause weird multi-threading issues.

    What this means is that, in your case, you should absolutely await the Task that is returned from dispatchFunc.