¿Una buena solución para esperar en try / catch / finalmente?

94

Necesito llamar a un asyncmétodo en un catchbloque antes de lanzar nuevamente la excepción (con su seguimiento de pila) como este:

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

Pero desafortunadamente no puedes usarlo awaiten un bloque catcho finally. Aprendí que es porque el compilador no tiene forma de retroceder en un catchbloque para ejecutar lo que está después de tu awaitinstrucción o algo así ...

Traté de usar Task.Wait()para reemplazar awaity obtuve un punto muerto. Busqué en la Web cómo podía evitarlo y encontré este sitio .

Como no puedo cambiar los asyncmétodos ni sé si se usan ConfigureAwait(false), creé estos métodos que toman un Func<Task>método que inicia un método asíncrono una vez que estamos en un hilo diferente (para evitar un punto muerto) y espera a que se complete:

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());
}

Entonces mi pregunta es: ¿Crees que este código está bien?

Por supuesto, si tiene algunas mejoras o conoce un enfoque mejor, ¡lo escucho! :)

user2397050
fuente
2
El uso awaiten un bloque de captura está realmente permitido desde C # 6.0 (vea mi respuesta a continuación)
Adi Lester
3
Mensajes de error de C # 5.0 relacionados : CS1985 : No se puede esperar en el cuerpo de una cláusula catch. CS1984 : No se puede esperar en el cuerpo de una cláusula
DavidRR

Respuestas:

173

Puede mover la lógica fuera del catchbloque y volver a lanzar la excepción después, si es necesario, usando 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();
    }
}

De esta forma, cuando la persona que llama inspecciona la StackTracepropiedad de la excepción , aún registra dónde TaskThatFailsse lanzó.

Sam Harwell
fuente
1
¿Cuál es la ventaja de conservar en ExceptionDispatchInfolugar de Exception(como en la respuesta de Stephen Cleary)?
Varvara Kalinina
2
Puedo adivinar que si decides volver a lanzar el Exception, ¿perderás todo el anterior StackTrace?
Varvara Kalinina
1
@VarvaraKalinina Exactamente.
54

Debe saber que desde C # 6.0, es posible usar awaitin catchy finallyblocks, por lo que de hecho podría hacer esto:

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

Las nuevas características de C # 6.0, incluida la que acabo de mencionar, se enumeran aquí o en un video aquí .

Adi Lester
fuente
El soporte para await en los bloques de captura / finalmente en C # 6.0 también se indica en Wikipedia.
DavidRR
4
@DavidRR. la Wikipedia no es autorizada. Es solo otro sitio web entre millones en lo que respecta a esto.
user34660
Si bien esto es válido para C # 6, la pregunta se etiquetó como C # 5 desde el principio. Esto me hace preguntarme si tener esta respuesta aquí es confuso o si deberíamos eliminar la etiqueta de versión específica en estos casos.
julealgon
16

Si necesita utilizar asynccontroladores de errores, le recomiendo algo como esto:

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

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

El problema con el bloqueo sincrónico en el asynccódigo (independientemente del hilo en el que se esté ejecutando) es que está bloqueando sincrónicamente. En la mayoría de los escenarios, es mejor usar await.

Actualización: ya que necesita volver a lanzar, puede usar ExceptionDispatchInfo.

Stephen Cleary
fuente
1
Gracias, pero lamentablemente ya conozco este método. Eso es lo que hago normalmente, pero no puedo hacer esto aquí. Si simplemente uso throw exception;en la ifdeclaración, se perderá el seguimiento de la pila.
user2397050
3

Se extrajeron gran respuesta de hvd a la siguiente clase de utilidad reutilizables en nuestro proyecto:

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();
            }
        }
    }
}

Uno lo usaría de la siguiente manera:

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

Siéntase libre de mejorar el nombre, lo mantuvimos intencionalmente detallado. Tenga en cuenta que no es necesario capturar el contexto dentro del contenedor, ya que ya está capturado en el sitio de la llamada, por lo tanto ConfigureAwait(false).

mrts
fuente