Solemos pensar que el punto de entrada de una aplicación .NET, ya sea el método estático Main()
del clásico Program.cs
o el nuevo Program.cs
de .NET 6 que utiliza top level statements, es lo primero que se ejecuta en nuestras aplicaciones, en realidad no es así. Hay vida antes de que nuestra aplicación empiece a correr ;)
Aunque obviamente su utilidad es limitada y no es algo que necesitaremos hacer a menudo (o probablemente nunca), es conveniente saber que existen varias formas de insertar código que se ejecute antes que lo que siempre hemos considerado el entry point de nuestra aplicación, y es lo que veremos en este post.
1. Constructores estáticos
La primera fórmula consiste en usar el constructor estático de la clase Program
, que es donde se encuentra en entry point. En aplicaciones de consola, escritorio, web, etc., esta clase es la que contiene el método estático Main()
que tradicionalmente ha actuado como punto de entrada del programa.
Dado que el constructor estático de una clase se ejecuta antes que cualquier otra cosa en ella, bastaría con insertar aquí nuestro código personalizado:
internal class Program
{
static Program() => Console.WriteLine("Hello from Program static constructor");
static void Main(string[] args) => Console.WriteLine("Hello from Main()");
}
El resultado de la ejecución de este código será:
Hello from Program static constructor
Hello from Main()
Si usamos .NET 6 o superior, la cosa cambiará un poco, pues la clase Program.cs
está algo escondida tras la magia de los top level statements de C#9. Pero de forma similar a la anterior, podríamos introducir el constructor estático de la siguiente manera:
// File: Program.cs
Console.WriteLine("Hello from Main()")
public partial class Program
{
static Program() => Console.WriteLine("Hello from Program static constructor");
}
El resultado obtenido por consola será el mismo que antes:
Hello from Program static constructor
Hello from Main()
2. Inicializadores de módulos
Otra posibilidad es el uso de inicializadores de módulos, una característica introducida también en C# 9 que permite ejecutar código justo cuando el ensamblado en el que se encuentre sea cargado en memoria. Este código debe encontrarse en métodos estáticos void
sin parámetros, decorados con el atributo [ModuleInitializer]
.
Los module initializers se ejecutan una única vez, cuando el ensamblado se carga para que cualquiera de los componentes definidos en el mismo sean utilizados. Por ejemplo, imaginad que tenemos una biblioteca .NET llamada MyClassLibrary
con las dos clases siguientes:
public class Initializer
{
[ModuleInitializer]
public static void Initialize()
{
Console.WriteLine("Hello from MyClassLibrary module initializer!");
}
}
public class MyClass
{
public MyClass() => Console.WriteLine("Hello from MyClass constructor");
}
Si referenciamos esta biblioteca desde nuestro proyecto principal y hacemos una referencia a MyClass
en su método principal, veremos que el inicializador de módulo de cla clase Initializer
se está ejecutando:
internal class Program
{
static Program() => Console.WriteLine("Hello from Program static constructor");
static void Main(string[] args)
{
Console.WriteLine("Hello from Main()");
var foo = new MyClass(); // Clase definida en MyClassLibrary
}
}
La salida será la siguiente. Fijaos que el inicializador de MyClassLibrary
se ha ejecutado al principio, para dejar el módulo inicializado antes de ser utilizado; tras ello, se ejecutó el constructor estático de Program
seguido del método Main()
y finalmente el constructor de MyClass
:
Hello from MyClassLibrary module initializer!
Hello from Program static constructor
Hello from Main()
Hello from MyClass constructor
Si además introdujésemos un constructor estático en la clase MyClass
, veríamos que éste se ejecuta en el orden esperado, justo antes del constructor de instancia:
Hello from MyClassLibrary module initializer!
Hello from Program static constructor
Hello from Main()
Hello from MyClass static constructor
Hello from MyClass constructor
El código de los inicializadores de módulos se ejecutará también si el ensamblado ha sido cargado de forma dinámica. Así, en el siguiente código, el método marcado con [ModuleInitializer]
será ejecutado al intentar usarlo:
// path contiene la ruta al archivo .dll del ensamblado
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(path);
var c = assembly.CreateInstance("MyClassLibrary.MyClass");
// Aquí ya se habrá ejecutado el module initializer
// y luego el constructor de MyClass
...
3. Startup hooks
Una opción más para ejecutar código antes del entry point es utilizar los startup hooks. A diferencia del caso anterior, donde los inicializadores estaban incluidos en ensamblados referenciados directamente desde el proyecto principal, en esta ocasión se trata de algo que podemos modificar dinámicamente en cada entorno de ejecución.
La idea consiste en establecer en la variable de entorno DOTNET_STARTUP_HOOKS
la ruta (o rutas, separadas por punto y coma) de una serie de ensamblados en cuyo interior se encuentra el código de inicialización a ejecutar, que debe encontrarse obligatoriamente definido en el método estático Initialize()
de una clase interna llamada StartupHook
.
Un ejemplo sería el siguiente código en una biblioteca de clases, llamada por ejemplo 'MyHook':
// Esta clase debe definirse en la "raíz" del proyecto,
// por lo que no la definimos dentro de un namespace concreto.
internal class StartupHook
{
public static void Initialize()
{
Console.WriteLine("Hello from the startup hook!");
}
}
Este código se ejecutará al arrancar cualquier aplicación .NET (Core), en su mismo proceso, antes que los constructores estáticos e incluso que los inicializadores de módulos, siempre que la ruta hacia su ensamblado se encuentre en la variable de entorno DOTNET_STARTUP_HOOKS
, como podemos ver a continuación con un proyecto simple que únicamente saluda al usuario por consola:
C:\HelloWorld\bin\debug\net6.0\>HelloWorld.exe
Hello world!
C:\HelloWorld\bin\debug\net6.0\>SET DOTNET_STARTUP_HOOKS=C:\Hooks\MyHook.dll
C:\HelloWorld\bin\debug\net6.0\>HelloWorld.exe
Hello from the startup hook!
Hello world!
Esta capacidad de ejecución previa de un ensamblado externo puede resultar interesante, sobre todo para hacer cosas raras. Por ejemplo, imaginad una aplicación de consola como la siguiente:
internal class Program
{
public static string Text { get; set; } = "Hello world!";
static void Main(string[] args)
{
Console.WriteLine(Text);
}
}
Aunque previsiblemente su ejecución finalizará mostrando por pantalla el texto "Hello world!", sería posible adjuntarle un startup hook que modifique este comportamiento utilizando reflection para alterar el valor justo antes de que se ejecute:
internal class StartupHook
{
public static void Initialize()
{
var program = Assembly.GetEntryAssembly()?.DefinedTypes
.FirstOrDefault(t => t.Name == "Program");
var textProperty = program?.DeclaredProperties
.FirstOrDefault(p => p.Name == "Text");
textProperty?.SetValue(null, "Text changed by the hook!");
}
}
De esta forma, una vez incluida la ruta hacia el ensamblado que contiene esta clase en la variable de entorno DOTNET_STARTUP_HOOKS
, lo que veríamos por consola sería lo siguiente:
C:\HelloWorld\bin\debug\net6.0\>HelloWorld.exe
Hello world!
C:\HelloWorld\bin\debug\net6.0\>SET DOTNET_STARTUP_HOOKS=C:\Hooks\MyHook.dll
C:\HelloWorld\bin\debug\net6.0\>HelloWorld.exe
Text changed by the hook!
Podéis ver otros usos divertidos de los startup hooks en este post de Kevin Gosse, donde pone del revés la salida por consola, o sobrescribe Array.Empty()
, entre otras diabluras ;)
Y si queréis leer algo más sobre los temas que hemos visto en el post, ahí van algunas referencias:
- Executing code before Main in .NET
Gérald Barré - Module Initializers In C# 9
Khalid Abuhakmeh - How to use .NET module initializers in a concrete real use case
Daniel Cazzulino - Module initializers - C# 9.0 specification proposals
Microsoft - Startup hooks
Microsoft
Publicado en Variable not found.
Aún no hay comentarios, ¡sé el primero!
Enviar un nuevo comentario