Cuando hablamos de manejadores de eventos asíncronos, lo primero que nos viene a la mente a muchos de nosotros es que es la única excepción que parece que permitimos.
Cuando había escrito sobre esto antes, estaba emocionado de que estaba explorando una solución que implicaba permitir que existiera el vacío asíncrono (sin querer arrancarme el resto del cabello).
Para mí, se trataba mucho más de algunos trucos inteligentes que podemos usar para superar los controladores de eventos asíncronos que de proporcionar soluciones para evitar el problema por completo.
Sin embargo, dicho esto, hubo mucha tracción en el artículo, por lo que estoy muy agradecido, y algunas personas expresaron opiniones de que preferirían resolver los manejadores de eventos asíncronos de una manera diferente.
Pensé que este era un gran punto, así que quería idear un enfoque alternativo que no corrija el vacío asíncrono, pero que te permita evitarlo (¿ves lo que hice allí?) por completo mientras resolvía algunos de los desafíos. con manejadores de eventos asíncronos.
En este artículo, presentaré otra solución que puede probar en su propio código. Abordaremos los pros y los contras desde mi perspectiva con respecto a cómo se puede usar para que pueda decidir si tiene sentido para su caso de uso.
También puede encontrar código interactuable en .NET fiddle right
El problema al que nos enfrentamos con los manejadores de eventos asíncronos es que la firma de los eventos a los que podemos suscribirnos en C# de manera predeterminada se parece a esto:
void TheObject_TheEvent(object sender, EventArgs e);
Y notará que al haber anulado el anverso de esta firma, nos vemos obligados a usar void en nuestros propios controladores para poder suscribirnos al evento.
Esto significa que si desea que su controlador ejecute código asíncrono/en espera, deberá esperar dentro de su método de vacío... Lo que introduce el gran patrón de vacío asíncrono aterrador que se nos dice que evitemos como la peste.
¿Y por qué? Porque async void interrumpe la capacidad de las excepciones para que surjan correctamente y, como resultado, puede causar muchos dolores de cabeza.
En mi opinión, simple es mejor... así que si leíste mi artículo anterior sobre async void y tu objetivo era realmente solo lidiar con EventHandlers, esto debería ayudarte.
En función de las condiciones establecidas anteriormente, el manejo de excepciones se descompone en el límite del vacío asíncrono. Si tiene una excepción que necesita burbujear cruzando este límite, entonces se divertirá.
Y por diversión, quiero decir que si te gusta depurar por qué las cosas no funcionan y no tienes una indicación clara de qué es lo que está fallando, entonces realmente lo pasarás genial.
Entonces, ¿cuál es la forma más fácil de solucionar esto?
Evitemos que las excepciones puedan cruzar este límite en primer lugar usando una herramienta simple a la que tenemos acceso: try/catch.
objectThatRaisesEvent.TheEvent += async (s, e) => { // if the try catch surrounds EVERYTHING in the handler, no exception can bubble up try { await SomeTaskYouWantToAwait(); } catch (Exception ex) { // TODO: put your exception handling stuff here } // no exception can escape here if the try/catch surrounds the entire handler body }
Como se indica en el código anterior, si coloca un bloque try/catch alrededor de TODO el cuerpo de su controlador de eventos, entonces puede evitar que cualquier excepción surja a través de ese límite de vacío asíncrono. En la superficie, es bastante simple y no requiere nada especial para implementar esto.
Ventajas:
Contras:
Dicho esto, esta solución es realmente simple, pero creo que podemos hacerlo un poco mejor.
Una mejora que creo que podemos hacer con respecto a la solución propuesta inicialmente es que podemos hacer que sea un poco más explícito que tenemos un controlador de eventos asíncrono que debería estar a salvo de las excepciones burbujeantes.
Este enfoque también evitará que la desviación del código con el tiempo provoque que el código problemático se ejecute fuera del controlador de eventos. Sin embargo, no abordará el hecho de que debe recordar agregar esto manualmente.
Veamos el código:
static class EventHandlers { public static EventHandler<TArgs> TryAsync<TArgs>( Func<object, TArgs, Task> callback, Action<Exception> errorHandler) where TArgs : EventArgs => TryAsync<TArgs>( callback, ex => { errorHandler.Invoke(ex); return Task.CompletedTask; }); public static EventHandler<TArgs> TryAsync<TArgs>( Func<object, TArgs, Task> callback, Func<Exception, Task> errorHandler) where TArgs : EventArgs { return new EventHandler<TArgs>(async (object s, TArgs e) => { try { await callback.Invoke(s, e); } catch (Exception ex) { await errorHandler.Invoke(ex); } }); } }
El código anterior utiliza literalmente el mismo enfoque para evitar que las excepciones crucen el límite de vacío asíncrono. Simplemente tratamos de atrapar el cuerpo del controlador de eventos, pero ahora lo hemos agrupado en un método explícitamente dedicado para reutilizarlo.
Así es como se vería aplicarlo:
someEventRaisingObject.TheEvent += EventHandlers.TryAsync<EventArgs>( async (s, e) => { Console.WriteLine("Starting the event handler..."); await SomeTaskToAwait(); Console.WriteLine("Event handler completed."); }, ex => Console.WriteLine($"[TryAsync Error Callback] Our exception handler caught: {ex}"));
Podemos ver que ahora tenemos un delegado con una firma de tarea asíncrona para trabajar, y podemos estar seguros de que cualquier cosa que coloquemos dentro de eso tendrá un intento/captura dentro del método auxiliar que vimos anteriormente.
Aquí hay una captura de pantalla que muestra la devolución de llamada del controlador de errores capturando correctamente la excepción:
Ventajas:
Contras:
Si bien originalmente me dispuse a explorar
En este artículo, exploramos lo que podría argumentar que es la forma más simple de hacer que sus EventHandlers asíncronos se comporten correctamente, y la solución refinada (en mi opinión) solo tiene el inconveniente de que debe recordar usarla.
Un comentarista había sugerido que uno podría explorar
Existen algunos marcos AoP en tiempo de compilación, pero lo dejaré como un ejercicio para usted como lector (porque también es un ejercicio para que yo haga un seguimiento).