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

18 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, 8 de abril de 2025
Persona al teléfono que no sabe quién le está llamando

En C#, existen atributos que no están diseñados para añadir metadatos consumibles en tiempo de ejecución, sino que están pensados para ser interpretados por el compilador.

Entre ellos, se encuentran los atributos englobados en la categoría "caller information", que ofrecen la posibilidad de que un método o función conozca información sobre el método que lo ha llamado, como el nombre del archivo, el número de línea, el nombre del método o la expresión de argumento.

Esto puede ser muy interesante en diversos escenarios, pero probablemente el ejemplo más claro lo encontramos en la generación de trazas y puntos de control, puesto que, en lugar de tener que escribir manualmente el nombre del método o el número de línea, podemos obtenerlos de forma automática.

Lo vemos a continuación.

Los atributos "caller info"

Estos atributos se encuentran definidos en el espacio de nombres System.Runtime.CompilerServices y son los siguientes:

  • CallerFilePathAttribute: Obtiene el nombre del archivo que contiene el código del método que ha llamado al método actual.
  • CallerLineNumberAttribute: Obtiene el número de línea en el archivo fuente en el que se ha realizado la llamada al método actual.
  • CallerMemberNameAttribute: Obtiene el nombre del método (o propiedad, si es un getter o setter) que ha llamado al método actual.
  • CallerArgumentExpressionAttribute: Obtiene la expresión que se ha utilizado en la llamada para generar el valor del argumento cuyo nombre se pasa como parámetro al atributo.

Para utilizarlos, simplemente debemos añadirlos como argumentos opcionales en la firma del método o función que los va a consumir, como en el siguiente ejemplo:

using System.Runtime.CompilerServices;

public static class ConsoleLogger
{
    public static void Log(string message = "",
        [CallerMemberName] string memberName = "",
        [CallerFilePath] string sourceFilePath = "",
        [CallerLineNumber] int sourceLineNumber = 0,
        [CallerArgumentExpression("message")] string callerArgument = "")
    {
        Console.WriteLine($"Message: {message}");
        Console.WriteLine($"Member name: {memberName}");
        Console.WriteLine($"Source file path: {sourceFilePath}");
        Console.WriteLine($"Source line number: {sourceLineNumber}");
        Console.WriteLine($"Caller argument: {callerArgument}");
    }
}

Fijaos que CallerArgumentExpression necesita un argumento adicional, que es el nombre del argumento que se quiere obtener. En este caso, queremos obtener la expresión que se ha utilizado para generar el valor del argumento message. Podéis leer más sobre este atributo, así como algunas precauciones a tener en cuenta, en este post.

Teniendo definida la clase anterior, podemos llamar al método estático ConsoleLogger.Log() desde cualquier punto del proyecto, y obtendremos por consola información sobre el origen de la llamada, como el nombre del método, el archivo donde se ha definido, el número de línea o la expresión usada como argumento.

Por ejemplo, imaginad que tenemos una aplicación de consola con el siguiente Program.cs:

var x = new Foo();
x.DoSomething();

public class Foo
{
    public void DoSomething()
    {
        ConsoleLogger.Log("Hello" + " world!");
    }
}

Al ejecutarla, veremos por consola la siguiente información:

Message: Hello world!
Member name: DoSomething
Source file path: C:\CallerInfo\Program.cs
Source line number: 8
Caller argument: "Hello" + " world!"

Como se puede intuir, el compilador ha completado la llamada al método Log() con la información que conoce sobre el método que la ha realizado. Al revisar sus parámetros, ha sustituido aquellos que tienen un atributo "caller info" por los valores que corresponden al método que ha realizado la llamada.

De hecho, el código que ha generado el compilador realmente para la clase Foo que hemos visto anteriormente es el siguiente (le he añadido comentarios para aclarar qué es cada cosa):

public class Foo
{
    public void DoSomething()
    {
        ConsoleLogger.Log(
           "Hello world!",     // El argumento pasado originalmente al método
           "DoSomething",      // Método actual
           "C:\\CallerInfo\\Program.cs",  // Archivo fuente
           8,                             // Línea de código de la invocación
           "\"Hello\" + \" world\"");     // Expresión usada para generar el argumento
                                          // para el parámetro "message"
    }
}

Esto implica que esta técnica no tiene coste en tiempo de ejecución, puesto que toda la información se resuelve y se introduce en las llamadas en tiempo de compilación. Por tanto, es una forma muy eficiente de obtener información sobre el contexto de ejecución de un método.

Publicado en Variable not found.

2 Comentarios:

MontyCLT dijo...

Desconocía la mayoría de estos atributos (sólo conocía el de la expresión), pero he usado la clase StackFrame para algo parecido en mi framework.

El código en GitHub:
https://github.com/iokode/OpinionatedFramework/blob/main/src/Foundation/Logging/ILogging.Caller.cs

José María Aguilar dijo...

Hola Iván!

Sí, consultando la traza de llamadas también puedes obtener algunos estos datos (de hecho, tengo en el tintero algún post sobre esta técnica), excepto el CallerArgumentExpression, que sólo está disponible en tiempo de compilación.

La diferencia principal es el momento de resolución (runtime vs compilación), por lo que la opción de los atributos será más eficiente para los escenarios simples.

Gracias por comentar!