[Article] Programación Asincrónica en .Net Core (Parte 1)

Siempre queremos crear aplicaciones rápidas con un gran rendimiento. Por esto, es muy importante conocer los principios asincrónos que nos brinda .Net. Veremos los conceptos principales, problemas comunes, escenarios complejos  y que palabras claves que nos brinda C# para poder trabajar asincrónicamente.

Programación asincrónica en .NET

Para comenzar, no importa en que estemos trabajando en ASP.Net, en una aplicación de consola, Windows Presentation Foundation, aplicaciones Windows Forms o Xamarin los principios asincrónicos podremos aplicarlos en cualquier entorno .Net.

Antes de ver en detalle cómo funciona, veamos una aplicación que no aprovecha estas características. Usaremos un aplicación Windows Presentation Foundation para ver en detalle lo que sucede al tener conexiones sincrónicas.

Lo que hace nuestra aplicaciones es buscar personas por el color de sus ojos. Tenemos un TextBox y un Botón para buscar. También una grilla donde veremos los resultados, un cuadro de texto para escribir y una barra de progreso. Nuestra aplicación consulta a una Api para recuperar los datos. Veamos que sucede.

El código que se ejecuta al presionar el botón buscar es el siguiente:

var client = new WebClient();
var content = client.DownloadString($"https://localhost:44356/api/customers/search/" + search);
var data = JsonConvert.DeserializeObject<IEnumerable<Customer>>(content);

CustomersGrid.ItemsSource = data;

Estamos utilizando un clase que ha que hoy se encuentra de deshuso que es WebClient. Esta clase tiene el problema que bloquea el hilo de ejecución principal que es donde se encuentra la interfaz de usuario haciendo que también se bloquee.

Entonces ¿Qué podemos hacer? bien, primero utilizaremos una una clase que está diseña para hacer uso de tecnología asincrónica que es HttpClient. Esta clase posee el método GetAsync, y como su nombre lo indica, es una operación asincrónica dando a entender que no bloqueara el hilo de ejecución principal donde se encuentra la interfaz.

try {
	using (var client = new HttpClient()) {

		var result = await client.GetAsync($"https://localhost:44356/api/customers/search/" + search);
        var content = await result.Content.ReadAsStringAsync();
        var data = JsonConvert.DeserializeObject<IEnumerable<Customer>>(content);

        CustomersGrid.ItemsSource = data;
    }
} catch (Exception ex) { }

Podemos ver que en nuestro nuevo código se encuentra la palabra clave await. Este es el mejor camino para indicar que deseamos volver a esta parte del código una vez que los datos se cargan desde la API que estamos consultando. Veamos el comportamiento ahora que el código está escrito para no generar bloqueos:

Como podemos ver nuestra UI ahora no se encuentra bloqueada al consultar un servicio. También podemos ver la barra de progreso corriendo sin interrupciones mientras se espera la resolución de los datos.

Realmente, como seguramente has notado, con unos pequeños cambios en nuestra aplicación podemos aprovechar los principios de asincronismo brindando una mejor experiencia de usuarios en este caso.

Async y Await en .NET

Veamos el código completo de la ejecución del código donde se encuentra declarado el evento click del botón:

private async void button_Click(object sender, RoutedEventArgs e)
{

	try
	{
		using (var client = new HttpClient())
        {

			var result = await client.GetAsync($"https://localhost:44356/api/customers/search/" + search);
            var content = await result.Content.ReadAsStringAsync();
            var data = JsonConvert.DeserializeObject<IEnumerable<Customer>>(content);

            CustomersGrid.ItemsSource = data;
		}
     }catch (Exception ex) { }

}

Pueden notar que luego de la palabra private aparece la palabra async, esto indica que el evento será asincrónico, pero no será suficiente para que nuestro que funcione correctamente. Para esto debemos usar la palabra await, que como comentamos anteriormente, es la encargada de notificar que debe esperar hasta que se termine de ejecutar.

Supongamos que solamente tenemos la palabra async y no usamos nunca await. Visual Studio nos indicará que estará esperando la palabra await dentro del método.

Otro punto que debemos tener en cuenta es el método. Como podemos ver el método ahora se llamada GetAsync(), de hecho, devuelve una tarea del tipo HttpResponseMessage, hablaremos más adelante en el post, por ahora, solo diremos que hará que se ejecute en un hilo diferente.

Los principios asincrónicos de los que hablamos en nuestras aplicaciones no solo están destinados a aplicaciones de Windows o aplicaciones móviles. Estos principios asincrónicos son adecuados para cualquier tipo de operaciones de E / S. Por ejemplo, acceso a disco, acceso a bases de datos, memoria o servicios como del ejemplo.

También podemos aplicar el mismo principio al código del lado del servidor en ASP.NET. Veamos el código de la API rest que está siendo invocada, la cual, nos devuelve la lista de personas.

[Route("api/customers/search/{valor}")]        
public async Task<IList<Customer>> Get(string valor)
{
	List<Customer> items;

	using (StreamReader r = new StreamReader(@"customers.json"))
	{
		var json = await r.ReadToEndAsync();
		items = JsonConvert.DeserializeObject<List<Customer>>(json);
	}
   
	return items.Where(p => p.EyeColor == valor).ToList();
}

Vemos que tenemos declarado el método del controlador como asíncrono. También, el tipo será Task. Esto quiere decir que ASP.Net sabra que hay una operación es asíncrona en curso y luego, cuando todos los resultados estén disponibles, serán devueltos por el método. 

El gran beneficio que nos brinda dentro de ASP.Net es reducir la carga excesiva en los servidores web sin interrumpir la ejecución normal dejando seguir atendiendo peticiones mientras está esperando la resolución de datos o accesos externos.

Comprendiendo el flujo

Hasta el momento vimos cómo declarar nuestros métodos como asincrónicos, vimos las palabras claves como await  y entendimos como las operaciones se ejecutan en un hilo separado y no en el hilo principal de las aplicaciones.

Await, como mencionamos anteriormente, nos permite recuperar el resultado de una operación asincrónica cuando esté disponible. También nos asegura que no hay excepciones o problemas con la tarea que se está ejecutando actualmente. Esto quiere decir que no solamente es la mejor manera de esperar, sino también, validar la operación actual.
Aquí es donde entra el término Continuation. Continuation es el bloque de código que se ejecuta después de la palabra await.

Como vemos en la imagen, nuestro método invocado GetAsync(…)  está marcado con la palabra await  esto significa que lo que se encuentra marcado en rojo es nuestro bloque de Continuation. Nuevamente, donde se encuentra el método RadAsStringAsync(),  se encuentra marcado con await, esto quiere decir que las siguiente 2 lineas serian nuestro Continuation.

Hagamos un cambio para probar si hay algún problema, por ejemplo, que no encuentra ningun registro. Nuestro código se vería de la siguiente manera:

using (var client = new HttpClient())
{
    var result = await client.GetAsync($"https://localhost:44356/api/customers/search/" + search);
    
    try {

        result.EnsureSuccessStatusCode();

        var content = await result.Content.ReadAsStringAsync();
        var data = JsonConvert.DeserializeObject<IEnumerable<Customer>>(content);

        CustomersGrid.ItemsSource = data;

        } catch (Exception ex) {
            txtOut.Text += ex.Message;
        }
}

Como vemos en el código, nuestro método realiza la llamada asincrónica pero no devolverá un error. Para esto, cambiamos nuestro Try..Catch y dentro invocamos result.EnsureSuccessStatusCode(). Este valida el código del response , al no ser 200 lanzará una excepción. Por último, esta será capturada por el catch escribiendo en el cuadro de texto el error.

Claramente podemos ver que luego de la palabra await el código se ejecutará dando la sensación que no debemos preocuparnos por trabajar en otro hilo y que si este tiene algún problema podremos validar y recuperar el contexto de donde estamos ejecutando nuestro código.

Conclusión

En nuestras aplicaciones en muy importante que entendamos y comencemos a implementar estos patrones asincrónicos. No solamente por el hecho que nuestras aplicaciones tendrán menos bloqueos y tendrán una mejor experiencia de usuario, si no también, que estos bloqueos pueden producir costos adicionales. Si estamos trabajando en la nube no debemos olvidar que cuando más desperdicio de los recursos hagamos, más deberemos pagar. Esta es un buen camino para ahorrar costo con nuestras aplicaciones.

En el próximo post veremos cómo crear nuestros propios métodos asincrónicos, como manejar excepciones y las mejores prácticas para ello.

Fernando Sonego

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *