Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 30 de noviembre de 2021
.NET

Cuando aparece una nueva versión de C# o .NET, los titulares de las noticias, tweets y posts suelen girar en torno a las novedades más destacadas, aquellas que suponen cambios importantes respecto a versiones anteriores. Pero es habitual que, aparte de éstas, se añadan otras características más pequeñas que muchas veces pasan desapercibidas.

Esto es lo que ocurre con al atributo [CallerArgumentExpression], una joyita de C#10 que puede ayudarnos a hacer más descriptivos los mensajes de error y trazas que guardamos cuando aparece un error en nuestras aplicaciones.

Y para entenderlo bien, comencemos viendo un ejemplo de la situación previa a la aparición de este atributo. Creemos una nueva aplicación de consola .NET 6 e introducimos el siguiente código en Program.cs; no hace falta nada más, gracias nuevas características de C# 10 como top level statements e implicit usings:

Console.WriteLine(Divide(10, 5));
Console.WriteLine(Divide(10, DateTime.Now.Second % 2));

int Divide(int a, int b)
{
    if (b == 0)
        throw new ArgumentOutOfRangeException(nameof(b), "El divisor no puede ser cero");
    return a / b;
}

Como podéis intuir, la idea es que la aplicación explote cuando la ejecutemos en un segundo par. Si corremos la aplicación y todo va bien, el resultado sería el siguiente:

C:\Projects\Test>dotnet run
2
10
C:\Projects\Test>_

Sin embargo, si la ejecutamos en un segundo par, el resultado obtenido será el mostrado a continuación:

C:\Projects\Test>dotnet run
2
Unhandled exception. 
System.ArgumentOutOfRangeException: El divisor no puede ser cero (Parameter 'b')
at Program.<<Main>$>g__Divide|0_0(Int32 a, Int32 b) in C:\Projects\Test\Program.cs:line 6
   at Program.<Main>$(String[] args) in C:\Projects\Test\Program.cs:line 2

C:\Projects\Test>_   

La excepción deja bien claro de lo que se trata, pues ofrece información textual del problema y el parámetro cuyo valor es inválido. Incluso, examinando el stack trace, podemos llegar a ver en cuál de las dos llamadas se ha introducido el problema. Sin embargo, si simplemente disponemos del mensaje de error, nos costará algo más de trabajo ver el origen del problema.

Y aquí es donde entra en juego [CallerArgumentExpression], un atributo que podemos aplicar a un parámetro string del método para que el framework lo pueble automáticamente con la expresión del código fuente que ha generado el valor enviado al método. What?

Reformulemos el método Divide() para hacer uso de esta característica y seguro que lo vemos más claro:

int Divide(int a, int b, [CallerArgumentExpression("b")]string? divisorExpression = null)
{
    if (b == 0)
        throw new ArgumentOutOfRangeException(nameof(b), 
                  $"El divisor no puede ser cero. Expresión: {divisorExpression}"
        );

    return a / b;
}

Fijaos que el parámetro divisorExpression (o como le queramos llamar) tiene null como valor por defecto, para no hacer obligatorio su uso en la llamada al método, y con su atributo [CallerArgumentExpression("b")] indicamos al framework que debe introducir aquí la expresión usada para enviar el valor al parámetro b.

Si ahora ejecutamos en el segundo apropiado, veremos que el mensaje de error es más claro, pues incluye la expresión cuya evaluación provocó el problema:

C:\Projects\Test>dotnet run
2
Unhandled exception. 
System.ArgumentOutOfRangeException: El divisor no puede ser cero. 
Expresión: DateTime.Now.Second % 2 (Parameter 'b')
   at Program.<<Main>$>g__Divide|0_0(Int32 a, Int32 b, String divisorExpression) 
   in C:\Projects\Test\Program.cs:line 9
   at Program.<Main>$(String[] args) in C:\Projects\Test\Program.cs:line 4
   
C:\Projects\Test>_

Así, simplemente observando el mensaje del error podríamos determinar cuál fue la causa, sin necesidad de acudir al código fuente.

En .NET 6, podemos ver el uso de este atributo en ArgumentNullException.ThrowIfNull(), la nueva fórmula rápida para evitar la entrada de valores nulos en nuestros métodos:

public void MyMethod(object first, object second)
{
    ArgumentNullException.ThrowIfNull(first);
    ArgumentNullException.ThrowIfNull(second);
    // ...
}

Obviamente, hay que saber cuándo usarlo, porque tiene sus peligros. Dado que lo que aparece ahí es el código fuente tal cual ha sido escrito, éste podría llegar a mostrar información confidencial o importante. Imaginad este ejemplo, que aunque sea algo exagerado, creo que sirve como muestra:

...
var x = Divide(100000, 345678 /* Secret key */ - y);

Si b vale 345678, el resultado que obtendríamos por consola sería el siguiente, donde podéis ver que se muestra la expresión de la invocación, incluida la clave secreta e incluso el comentario presente en el código fuente:

C:\Projects\Test>dotnet run
2
Unhandled exception. 
System.ArgumentOutOfRangeException: El divisor no puede ser cero. 
Expresión: 345678 /* Secret key */ - y (Parameter 'b')
   at Program.<<Main>$>g__Divide|0_0(Int32 a, Int32 b, String divisorExpression) 
   in C:\Projects\Test\Program.cs:line 11
   at Program.<Main>$(String[] args) in C:\Projects\Test\Program.cs:line 6

C:\Projects\Test>_

Esta información podría quedar registrada en trazas y ser visible por cualquiera que tuviera acceso a ellas. 

De la misma forma, si descompilamos la aplicación, en el código obtenido veremos que la expresión es incluida en la llamada, por lo que podríamos tener también acceso a esta información si usamos una herramienta de este tipo con los ensamblados de la aplicación:

Program.<<Main>$>Eg__Divide|0_0(100000, 345678 - y, "345678 /* Secret key */ - y");

Publicado en Variable not found.

Aún no hay comentarios, ¡sé el primero!