Подтвердить что ты не робот

Как сохранить поведение ожидания с помощью TaskCompletionSource.SetException?

(Это новая попытка в этом вопросе, которая теперь лучше показывает проблему.)

Скажем, у нас есть сбойная задача (var faultedTask = Task.Run(() => { throw new Exception("test"); });), и мы ее ждем. await распакует AggregateException и выбросит основное исключение. Он будет бросать faultedTask.Exception.InnerExceptions.First().

В соответствии с исходным кодом для ThrowForNonSuccess он сделает это, выполнив любой сохраненный ExceptionDispatchInfo предположительно, чтобы сохранить хорошие трассировки стека. Он не распаковывает AggregateException, если нет ExceptionDispatchInfo.

Этот факт был для меня неожиданным, потому что в документации указано, что первое исключение всегда выбрасывается: https://msdn.microsoft.com/en-us/library/hh156528.aspx?f=255&MSPPError=-2147217396 Оказывается, await может бросать AggregateException, хотя это не документированное поведение.

Это становится проблемой, когда мы хотим создать прокси-задачу и установить ее исключение:

var proxyTcs = new TaskCompletionSource<object>();
proxyTcs.SetException(faultedTask.Exception);
await proxyTcs.Task;

Это вызывает AggregateException, тогда как await faultedTask; вызовет исключение теста.

Как я могу создать прокси-задачу, которую я могу выполнить по желанию, и это будет отражать поведение исключений, которое было в исходной задаче?

Исходное поведение:

  • await выдаст первое внутреннее исключение.
  • Все исключения по-прежнему доступны через Task.Exception.InnerExceptions. (В более ранней версии этого вопроса было исключено это требование.)

Вот тест, в котором суммируются выводы:

[TestMethod]
public void ExceptionAwait()
{
    ExceptionAwaitAsync().Wait();
}

static async Task ExceptionAwaitAsync()
{
    //Task has multiple exceptions.
    var faultedTask = Task.WhenAll(Task.Run(() => { throw new Exception("test"); }), Task.Run(() => { throw new Exception("test"); }));

    try
    {
        await faultedTask;
        Assert.Fail();
    }
    catch (Exception ex)
    {
        Assert.IsTrue(ex.Message == "test"); //Works.
    }

    Assert.IsTrue(faultedTask.Exception.InnerExceptions.Count == 2); //Works.

    //Both attempts will fail. Uncomment attempt 1 to try the second one.
    await Attempt1(faultedTask);
    await Attempt2(faultedTask);
}

static async Task Attempt1(Task faultedTask)
{
    var proxyTcs = new TaskCompletionSource<object>();
    proxyTcs.SetException(faultedTask.Exception);

    try
    {
        await proxyTcs.Task;
        Assert.Fail();
    }
    catch (Exception ex)
    {
        Assert.IsTrue(ex.Message == "test"); //Fails.
    }
}

static async Task Attempt2(Task faultedTask)
{
    var proxyTcs = new TaskCompletionSource<object>();
    proxyTcs.SetException(faultedTask.Exception.InnerExceptions.First());

    try
    {
        await proxyTcs.Task;
        Assert.Fail();
    }
    catch (Exception ex)
    {
        Assert.IsTrue(ex.Message == "test"); //Works.
    }

    Assert.IsTrue(proxyTcs.Task.Exception.InnerExceptions.Count == 2); //Fails. Should preserve both exceptions.
}

Мотивация для этого вопроса заключается в том, что я пытаюсь построить функцию, которая скопирует результат одной задачи на TaskCompletionSource. Это вспомогательная функция, которая часто используется при написании функций комбинатора задач. Важно, чтобы клиенты API не могли обнаружить разницу между исходной задачей и задачей прокси.

4b9b3361

Ответ 1

Оказывается, что ожидание может вызывать AggregateException, хотя это не документированное поведение.

Нет, что поведение, когда первое вложенное исключение является AggregateException.

В принципе, когда вы вызываете TaskCompletionSource.SetException(Exception), он обматывает это исключение в AggregateException.

Если вы хотите сохранить несколько исключений, просто используйте перегрузку SetException, которая принимает IEnumerable<Exception>:

proxyTcs.SetException(faultedTask.Exception.InnerExceptions);