Have you ever wondered whether any code can run before Main(), the entry point of a .NET application?

  1. 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");
      }
    }
    
  2. 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 CodeAccessSecurityAttribute can be applied to methods, types, and assemblies. For example, we can define the following attribute in CasAssembly.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 point
    

    The 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.

  3. 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 exe or dll file and a set of .netmodule files. 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 dll or exe file, 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, as ilasm allows 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 with ilasm:

    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 point
    

    Update: in a year 2020, the C# 9 added [ModuleInitializer]: mark an internal or public parameterless static void method with this attribute. The compiler combines calls to all such methods into one module initializer, which runs once before other code in the module, including Main(). No custom assembly emission is needed.

  4. 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.AppDomainManager and 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.config file:

    <?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 AppDomainManager was 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=FooDomainManager
    

    The domain manager runs even before the application assembly is loaded, ahead of the assembly-level CAS checks.

  5. Startup hooks (modern .NET, post update from year 2026) The DOTNET_STARTUP_HOOKS environment variable lets you run managed code before the application’s entry point without modifying the application. Create a separate assembly, such as Hook.dll, containing a type named StartupHook in 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.dll
    

    The runtime calls StartupHook.Initialize() synchronously before Main, 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.