Injecting code before an application starts with .NET Startup Hooks

Injecting code before an application starts with .NET Startup Hooks

.NET Startup Hooks is a little-known feature that allows us to inject code into a target process before it starts running. This can be done without modifying the target process source code or recompiling it.

For .NET Core 3+, we want to provide a low-level hook that allows injecting managed code to run before the main application's entry point. This hook will make it possible for the host to customize the behavior of managed applications during process launch after they have been deployed.

Building a hook is quite simple. Run dotnet new classlib -n MyHook and replace the default class with the following content:

using System;

internal class StartupHook
{
    public static void Initialize()
    {
        Console.WriteLine("Hello from my hook!");
    }
}

Two things to notice here, the class is internal and there is no namespace. Let's run dotnet new console -n MyApp to create the target application. Modify the Program.cs file as follows:

using System;

namespace MyApp
{
    public class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(Message);
        }

        public static string Message = "Hello from my app!";
    }
}

Publish both projects with the following commands:

dotnet publish .\MyHook
dotnet publish .\MyApp

To inject our hook, create the DOTNET_STARTUP_HOOKS environment variable (a list of assemblies can be specified and delimited by : on Linux and ; on Windows):

$env:DOTNET_STARTUP_HOOKS="C:\Source\dotnet-hooks\MyHook\bin\Debug\net7.0\publish\MyHook.dll"

Run .\MyApp\bin\Debug\net7.0\MyApp.exe to see the following output:

Hello from my hook!
Hello from my app!

So, what else can we do with this hook? Let's see some examples.

Modify the console output

Go to the MyHook project and update the default class as follows:

internal class StartupHook
{
    public static void Initialize()
    {
        Console.SetOut(new InvertedTextWriter(Console.Out));
    }
}

public class InvertedTextWriter : TextWriter
{
    private readonly TextWriter _writer;

    public InvertedTextWriter(TextWriter baseTextWriter)
    {
        _writer = baseTextWriter;
    }

    public override Encoding Encoding => _writer.Encoding;

    public override void Write(string value)
    {
        _writer.Write(string.Concat(value.Reverse()));
    }
}

Run dotnet publish .\MyHook and .\MyApp\bin\Debug\net7.0\MyApp.exe to see:

!ppa ym morf olleH

Access to static fields

Go to the MyHook project and update the default class as follows:

using System.Reflection;

internal class StartupHook
{
    public static void Initialize()
    {
        var program = Assembly.GetEntryAssembly()?.DefinedTypes
                             .FirstOrDefault(t => t.Name == "Program");
        var property = program?.DeclaredFields
                                   .FirstOrDefault(p => p.Name == "Message");
        property?.SetValue(null, "Text changed by the hook!");
    }
}

Run dotnet publish .\MyHook and .\MyApp\bin\Debug\net7.0\MyApp.exe to see:

Text changed by the hook!

This example is quite scary because there is no limit to what can be changed.

Monitor your application

Go to the MyHook project and update the default class as follows:

using System.Reflection;

internal class StartupHook
{
    public static void Initialize()
    {
        new Thread(MetricsPoller)
        {
            IsBackground = true,
            Name = "Poller"
        }.Start();
    }

    private static void MetricsPoller()
    {
        while (true)
        {
            var gen0 = GC.CollectionCount(0);
            var gen1 = GC.CollectionCount(1);
            var gen2 = GC.CollectionCount(2);

            Console.WriteLine($"Generation 0 {gen0}");
            Console.WriteLine($"Generation 1 {gen1}");
            Console.WriteLine($"Generation 2 {gen2}");

            Thread.Sleep(3000);
        }
    }
}

Go to the MyApp project and update the Program.cs file as follows:

using System;

namespace MyApp
{
    public class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(Message);
            Console.ReadLine();
        }

        public static string Message = "Hello from my app!";
    }
}

Run dotnet publish .\MyApp, dotnet publish .\MyHook and .\MyApp\bin\Debug\net7.0\MyApp.exe to see:

Hello from my app!
Generation 0 0
Generation 1 0
Generation 2 0
Generation 0 0
Generation 1 0
Generation 2 0

Creativity is the only limit to what we can do with this feature. Here you can find the official documentation. Thank you, and happy coding.