Is there life before Main?
Have you ever wondered whether any code can run before Main(), the entry point of a .NET application?
-
Static constructor of the type that declares the entry point. Although the rules for type initialization vary between CLR versions, an explicit static constructor runs before the entry point is called, regardless of whether the type has any static fields or whether they are accessed:
class Program { static Program() { System.Console.WriteLine("type initializer"); } private static void Main() { System.Console.WriteLine("entry point"); } } -
CAS attributes. Code Access Security was deprecated in .NET 4.0, but it still works, albeit with considerable overhead: calling a method with a CAS attribute can be three or four orders of magnitude slower than an ordinary method call. An attribute derived from
CodeAccessSecurityAttributecan be applied to methods, types, and assemblies. For example, we can define the following attribute inCasAssembly.dll:using System; using System.Security; using System.Security.Permissions; [AttributeUsage( AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Constructor | AttributeTargets.Method, AllowMultiple=true, Inherited=false)] public sealed class FooAttribute : CodeAccessSecurityAttribute { public FooAttribute(SecurityAction action = SecurityAction.Demand) : base(action) { } public string Level { get; set; } public override IPermission CreatePermission() { Console.WriteLine("cas: {0} level", Level); return null; // return no permission object } }Then use it in
ConsoleApplication.exe:using System; using System.Security.Permissions; [assembly: Foo(SecurityAction.RequestMinimum, Level="assembly")] [Foo(Level="type")] class Program { [Foo(Level="constructor")] static Program() { Console.WriteLine("type initializer"); } [Foo(Level="method")] private static void Main() { Console.WriteLine("entry point"); } }The output is:
cas: assembly level cas: type level cas: type level cas: constructor level type initializer cas: type level cas: type level cas: method level entry pointThe attribute needs to be in a separate assembly because an assembly-level CAS attribute requires the assembly containing its type to be fully loadable when permissions are checked. The type-level check occurs twice: a method call requires permission to access both the type and the specific method, and checking access to the method also requires access to its declaring type.
We have now executed arbitrary code while the assembly is being loaded, before the static constructor of
Program. There is another option, too. -
Module initializers. Every .NET application consists of assemblies, and each assembly can contain multiple modules. Visual Studio only supports single-module assemblies, so modules are easy to overlook. A multifile assembly consists of an
exeordllfile and a set of.netmodulefiles. One useful property is that modules can be loaded on demand, which may help when an assembly contains many resources that are not all needed immediately.The main module, stored in the
dllorexefile, contains a special type, usually called<Module>, which occupies the first entry in the type definition table. This type can hold global variables and methods, asilasmallows it to do. It can also have a static constructor: this is the module initializer. It runs after the assembly is loaded but before code in the module executes, including the static constructor of the type declaring the entry point.We can demonstrate this by generating an assembly with a module initializer using
System.Reflection.Emit, or by writing IL and compiling it withilasm:using System; using System.Reflection; using System.Reflection.Emit; static class Program { private static void Main() { // create a dynamic assembly in the current application domain var assemblyBuilder = AppDomain.CurrentDomain .DefineDynamicAssembly(new AssemblyName("FooAssembly"), AssemblyBuilderAccess.RunAndSave); // define a single module in the dynamic assembly const string moduleName = "test.exe"; var moduleBuilder = assemblyBuilder .DefineDynamicModule("FooModule", moduleName); // create a global method for the module initializer var cctor = moduleBuilder.DefineGlobalMethod(".cctor", MethodAttributes.RTSpecialName | MethodAttributes.SpecialName | MethodAttributes.Static, null, null); var il2 = cctor.GetILGenerator(); il2.EmitWriteLine("module initializer"); il2.Emit(OpCodes.Ret); moduleBuilder.CreateGlobalFunctions(); // define a static class, represented as abstract sealed in IL var fooType = moduleBuilder.DefineType("Program", TypeAttributes.Abstract | TypeAttributes.Sealed); // define the static constructor var ctorBuilder = fooType.DefineTypeInitializer(); var il = ctorBuilder.GetILGenerator(); il.EmitWriteLine("type initializer"); il.Emit(OpCodes.Ret); // define the static Main() method as the entry point var mainBuilder = fooType.DefineMethod("Main", MethodAttributes.Public | MethodAttributes.Static, CallingConventions.Standard, typeof(void), null); il = mainBuilder.GetILGenerator(); il.EmitWriteLine("entry point"); il.Emit(OpCodes.Ret); fooType.CreateType(); // set the entry point and save the assembly as an exe file assemblyBuilder.SetEntryPoint( mainBuilder, PEFileKinds.ConsoleApplication); assemblyBuilder.Save(moduleName); } }Running the generated assembly produces:
module initializer type initializer entry pointUpdate: in a year 2020, the C# 9 added
[ModuleInitializer]: mark aninternalorpublicparameterlessstatic voidmethod with this attribute. The compiler combines calls to all such methods into one module initializer, which runs once before other code in the module, includingMain(). No custom assembly emission is needed. -
Application domain manager. This option was suggested by CLR expert @tr_tr_mitya. The documentation and this blog post explain it in more detail. In brief, it is a managed counterpart to a CLR host: a special type that controls assembly loading and application domain creation, along with some aspects of security and remoting.
An application domain manager derives from
System.AppDomainManagerand is defined in a strong-named assembly installed in the GAC. For example:using System; public class FooDomainManager : AppDomainManager { public override void InitializeNewDomain(AppDomainSetup info) { Console.WriteLine("init domain"); } }Starting with .NET 4.0, an application can select this manager in its
App.configfile:<?xml version="1.0"?> <configuration> <runtime> <!-- full name of the assembly containing the AppDomainManager --> <appDomainManagerAssembly value="DomainManager, Version=1.0.0.0, Culture=neutral, PublicKeyToken=679fb76896252e34"/> <!-- fully qualified name of the AppDomainManager type --> <appDomainManagerType value="FooDomainManager"/> </runtime> </configuration>Before .NET 4.0, the convenient way to select an
AppDomainManagerwas through environment variables, typically set system-wide. It can also be configured through the Windows registry or the unmanaged hosting API when the CLR is hosted in an unmanaged application:set APPDOMAIN_MANAGER_ASM=DomainManager, Version=1.0.0.0, PublicKeyToken=679fb7... set APPDOMAIN_MANAGER_TYPE=FooDomainManagerThe domain manager runs even before the application assembly is loaded, ahead of the assembly-level CAS checks.
-
Startup hooks (modern .NET, post update from year 2026) The
DOTNET_STARTUP_HOOKSenvironment variable lets you run managed code before the application’s entry point without modifying the application. Create a separate assembly, such asHook.dll, containing a type namedStartupHookin the global namespace:internal static class StartupHook { public static void Initialize() { System.Console.WriteLine("before Main"); } }Then point the runtime to it when launching the application. On Unix:
DOTNET_STARTUP_HOOKS=/absolute/path/Hook.dll dotnet App.dllThe runtime calls
StartupHook.Initialize()synchronously beforeMain, on the same thread. You can specify multiple hook assemblies, separated by:on Unix or;on Windows; they run in the order listed. This is useful for adding instrumentation or telemetry to an existing application without rebuilding it.