Как сохранить ожидаемое поведение с 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; бросил бы тестовое исключение.

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

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

  1. await бросит первое внутреннее исключение.
  2. Все исключения по-прежнему доступны через 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 не могли обнаружить разницу между исходной задачей и задачей-посредником.

1 ответ

Решение

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

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

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

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

proxyTcs.SetException(faultedTask.Exception.InnerExceptions);
Другие вопросы по тегам