paint-brush
Una red de seguridad simple para controladores de eventos asíncronospor@devleader
814 lecturas
814 lecturas

Una red de seguridad simple para controladores de eventos asíncronos

por Dev Leader8m2023/02/14
Read on Terminal Reader

Demasiado Largo; Para Leer

Async void es la única excepción que parece que permitimos para la temida configuración de los 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 puedas decidir si tiene sentido.
featured image - Una red de seguridad simple para controladores de eventos asíncronos
Dev Leader HackerNoon profile picture

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. la temida configuración de vacío asíncrono .


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 aqui . De lo contrario, puedes mira el código en GitHub si desea clonarlo localmente para probarlo.

¡Un vídeo complementario!

haga clic aquí para ver el video!

El problema

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.


el articulo anterior abordó esto permitiéndonos ser creativos en el lado de la invocación de las cosas pero...


  • Es posible que necesitemos soporte para esto en objetos para los que no controlamos la invocación de eventos (es decir, se está conectando al evento de clic de un botón en su marco de interfaz de usuario favorito)


  • Algunas personas ven el uso del contexto dentro de esa solución como un truco (tampoco estoy en desacuerdo con eso).


  • … Específicamente, con los controladores de eventos, tenemos otros trucos más simples que podemos hacer para admitir los controladores de eventos asíncronos.


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.

Resolución de controladores de eventos asíncronos con Try/Catch

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:

  • Extremadamente simple Sin mecanismos complejos de entender.


  • No se requieren paquetes.


  • No necesita ser el propietario de la clase que genera el evento para que esto funcione. Esto significa que este enfoque funcionará para todos los objetos generadores de eventos existentes, incluidos los componentes de la interfaz de usuario de WinForms y WPF.


Contras:

  • Debes recordar hacer esto... en todas partes.


  • Es posible que a medida que su código evolucione con el tiempo, alguien pueda escribir accidentalmente lógica fuera del controlador de eventos try-catch que puede generar excepciones.


Dicho esto, esta solución es realmente simple, pero creo que podemos hacerlo un poco mejor.

Un enfoque (ligeramente) más elegante para mejorar los controladores de eventos asíncronos

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:


Salida del programa de ejemplo para controladores de eventos asíncronos

Ventajas:


  • Todavía muy simple. La función contenedora es *ligeramente* más compleja, pero sigue siendo muy básica.


  • No se requieren paquetes.


  • No necesita ser el propietario de la clase que genera el evento para que esto funcione. Esto significa que este enfoque funcionará para todos los objetos generadores de eventos existentes, incluidos los componentes de la interfaz de usuario de WinForms y WPF.


  • La intención es más obvia para trabajar con EventHandlers asíncronos debido a la sintaxis cuando se conecta el controlador al evento.


  • La desviación del código que eventualmente genera más excepciones seguirá estando envuelta dentro del intento/captura.


Contras:


  • ¡Aún tienes que acordarte de conectar esta cosa!

Pensamientos finales sobre los manejadores de eventos asíncronos

Si bien originalmente me dispuse a explorar formas interesantes de lidiar con el vacío asíncrono , los comentarios de los lectores fueron válidos en el sentido de que los ejemplos se centraron en los controladores de eventos asíncronos, y seguramente debe haber una forma más sencilla.


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 Programación Orientada a Aspectos (AoP) para inyectar este tipo de comportamiento en su aplicación para que no tenga que ir a recordar hacerlo.


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).