Хорошее решение для ожидания в try/catch/наконец?

Мне нужно вызвать метод async в блоке catch, прежде чем снова добавить исключение (с его трассировкой стека) следующим образом:

try
{
    // Do something
}
catch
{
    // <- Clean things here with async methods
    throw;
}

Но, к сожалению, вы не можете использовать await в блоке catch или finally. Я узнал об этом, потому что компилятор не имеет возможности вернуться в блок catch, чтобы выполнить то, что после вашей инструкции await или что-то в этом роде...

Я попытался использовать Task.Wait() для замены await, и я зашел в тупик. Я искал в Интернете, как я мог этого избежать, и нашел этот сайт.

Так как я не могу изменить методы async и не знаю, используют ли они ConfigureAwait(false), я создал эти методы, которые принимают Func<Task>, который запускает метод async, когда мы находимся в другом потоке (чтобы избежать тупик) и ждет его завершения:

public static void AwaitTaskSync(Func<Task> action)
{
    Task.Run(async () => await action().ConfigureAwait(false)).Wait();
}

public static TResult AwaitTaskSync<TResult>(Func<Task<TResult>> action)
{
    return Task.Run(async () => await action().ConfigureAwait(false)).Result;
}

public static void AwaitSync(Func<IAsyncAction> action)
{
    AwaitTaskSync(() => action().AsTask());
}

public static TResult AwaitSync<TResult>(Func<IAsyncOperation<TResult>> action)
{
    return AwaitTaskSync(() => action().AsTask());
}

Итак, мои вопросы: как вы думаете, этот код в порядке?

Конечно, если у вас есть некоторые улучшения или вы знаете лучший подход, я слушаю!:)

Ответ 1

Вы можете перемещать логику за пределами блока catch и реконструировать исключение после, если необходимо, с помощью ExceptionDispatchInfo.

static async Task f()
{
    ExceptionDispatchInfo capturedException = null;
    try
    {
        await TaskThatFails();
    }
    catch (MyException ex)
    {
        capturedException = ExceptionDispatchInfo.Capture(ex);
    }

    if (capturedException != null)
    {
        await ExceptionHandler();

        capturedException.Throw();
    }
}

Таким образом, когда вызывающий объект проверяет свойство исключения StackTrace, он все еще записывает, где внутри TaskThatFails он был брошен.

Ответ 2

Вы должны знать, что с С# 6.0 можно использовать блоки await в catch и finally, поэтому вы действительно можете это сделать:

try
{
    // Do something
}
catch (Exception ex)
{
    await DoCleanupAsync();
    throw;
}

Новые функции С# 6.0, включая тот, который я только что упомянул перечислены здесь или как видео здесь.

Ответ 3

Если вам нужно использовать обработчики ошибок async, я бы рекомендовал что-то вроде этого:

Exception exception = null;
try
{
  ...
}
catch (Exception ex)
{
  exception = ex;
}

if (exception != null)
{
  ...
}

Проблема синхронной блокировки кода async (независимо от того, какой поток работает) заключается в том, что вы синхронно блокируете. В большинстве сценариев лучше использовать await.

Обновление:. Поскольку вам нужно реконструировать, вы можете использовать ExceptionDispatchInfo.

Ответ 4

Мы извлекли hvd отличный ответ в следующий многоразовый класс утилиты в нашем проекте:

public static class TryWithAwaitInCatch
{
    public static async Task ExecuteAndHandleErrorAsync(Func<Task> actionAsync,
        Func<Exception, Task<bool>> errorHandlerAsync)
    {
        ExceptionDispatchInfo capturedException = null;
        try
        {
            await actionAsync().ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            capturedException = ExceptionDispatchInfo.Capture(ex);
        }

        if (capturedException != null)
        {
            bool needsThrow = await errorHandlerAsync(capturedException.SourceException).ConfigureAwait(false);
            if (needsThrow)
            {
                capturedException.Throw();
            }
        }
    }
}

Можно использовать его следующим образом:

    public async Task OnDoSomething()
    {
        await TryWithAwaitInCatch.ExecuteAndHandleErrorAsync(
            async () => await DoSomethingAsync(),
            async (ex) => { await ShowMessageAsync("Error: " + ex.Message); return false; }
        );
    }

Не стесняйтесь улучшать именование, мы сохранили его намеренно многословным. Обратите внимание, что нет необходимости фиксировать контекст внутри обертки, поскольку он уже захвачен на сайте вызова, следовательно ConfigureAwait(false).