{"slug": "exploring-the-net-boot-process-via-host-tracing", "title": "Exploring the .NET boot process via host tracing", "summary": "The article explains how to enable host tracing in .NET by setting the `COREHOST_TRACE=1` environment variable, which provides detailed diagnostic logs about the early boot process of a .NET application. It describes how to configure trace output to a file using `COREHOST_TRACEFILE` and adjust verbosity with `COREHOST_TRACE_VERBOSITY`. The post then uses this tracing feature to explore the startup sequence, including the roles of the `dotnet` muxer, `hostfxr`, and `hostpolicy` components in loading the .NET runtime.", "body_md": "In this post we take a look a look at how you can enable diagnostics for the .NET host itself that you can use to debug issues running your .NET applications. We then use the tracing diagnostics to explore the boot process of a simple .NET application.\n\n[Understanding the boot process with tracing](#understanding-the-boot-process-with-tracing)\n\nThe main focus of this post is to show the [the host tracing feature](https://github.com/dotnet/runtime/blob/25cae043b11fa5e4fbda011376a7ad403438bd62/docs/design/features/host-tracing.md) available in modern .NET. This isn't \"tracing\" like OpenTelemetry or APM solutions with activities and spans, this is\n\n*old school*tracing, i.e. logging.😄\n\nHost tracing provides you detailed diagnostic information about the very early steps of a .NET application's \"boot\" process. This can be useful if you're trying to understand why your application is using the \"wrong\" version of .NET, for example. You won't need it often, but it can be invaluable when things aren't working the way you expect!\n\nIn this post I'm going to explore the startup process for a simple .NET app by looking at the host tracing output. It's going to be intentionally verbose, but it will give you an idea of what's available.\n\nEnabling host tracing requires setting a single environment variable: `COREHOST_TRACE=1`\n\n. By default this writes the traces to `stderr`\n\n, but you can redirect that output to a file by setting `COREHOST_TRACEFILE`\n\nto one of two values:\n\n`COREHOST_TRACEFILE=<file_path>`\n\nappends the logs to the file`<path>`\n\n. The file is created if it doesn't already exist, but the*directory*it's in must exist. Relative paths are relative to the working directory.`COREHOST_TRACEFILE=<dir_path>`\n\n(.NET 10+ only), if the directory`<dir_path>`\n\nexists, The file`<exe_name>.<pid>.log`\n\nis appended to.\n\nYou can also control the verbosity of the logs by setting `COREHOST_TRACE_VERBOSITY=<level>`\n\nwhere `<level>`\n\nis a value from `1`\n\nto `4`\n\n, `4`\n\nbeing the most verbose, and `1`\n\nbeing only errors.\n\nTo test it out, I created a simple console app, built it, and ran it with tracing enabled:\n\n```\ndotnet new console\ndotnet build\n\n# Enable tracing\n$env:COREHOST_TRACE=1\n$env:COREHOST_TRACEFILE=\"host_trace.log\"\ndotnet bin\\Debug\\net9.0\\MyApp.dll\n```\n\nWith that in mind, let's explore the boot process of a .NET app.\n\n[Loading applications with modern .NET](#loading-applications-with-modern-net)\n\nWhen I think about modern .NET applications, I often think of three main divisions:\n\n- The .NET\n**runtime**, the CoreCLR, which is running the JIT compiler, the garbage collector, and everything that make up a .NET application. - The .NET\n**base class libraries (BCL)**, which are all the libraries shipped as part of .NET. - Your .NET\n**application**, which is the code written by you, which may reference other .NET libraries, as well as libraries that make up the BCL.\n\nHowever, there's also a whole \"loading\" process that has to happen to get the .NET runtime running!\n\nAt a high level, when you run a .NET application using `dotnet myapp.dll`\n\n, your app goes through the following chain of components:\n\n- The\n`dotnet`\n\napp is a \"multiplexer\" (muxer) application that decides what you're trying to run. `hostfxr`\n\nis a native library responsible for finding the correct .NET runtime to load.`hostpolicy`\n\nis a native library responsible for*starting*the correct .NET runtime.\n\nI explore each of these components in a little more detail in this post, but for a deeper dive (on the first two at least), I recommend [Steve Gordon's posts looking at the internals](https://www.stevejgordon.co.uk/a-brief-introduction-to-the-dotnet-muxer).\n\n[The ](#the-dotnet-muxer)`dotnet`\n\nmuxer\n\n`dotnet`\n\nmuxerThe `dotnet`\n\nmuxer is the entrypoint for most of the work you do as a .NET developer. Whether you're doing development with `dotnet build`\n\nand `dotnet publish`\n\n, or actually running an application using `dotnet MyApp.dll`\n\n, the `dotnet`\n\nmuxer is your entrypoint.\n\nOn Windows, the `dotnet`\n\nmuxer is the executable that's installed by default at *C:\\Program Files\\dotnet\\dotnet.exe*. There's a single entrypoint here, even if you have multiple versions of the .NET runtime or .NET SDK installed on your machine.\n\nWhen you install a new version of the SDK or runtime, you'll typically get a new version of the muxer, but there's still only one.\n\nThe muxer is really just responsible for one thing: loading the `hostfxr`\n\nlibrary and invoking it. That said, it still does a *bit* of preliminary validation. Calling `dotnet`\n\nwithout any arguments doesn't make any sense, so if you simply run `dotnet.exe`\n\n, [the muxer itself](https://github.com/dotnet/runtime/blob/f169b52556bc4769b4260b7a85c05c6f78911097/src/native/corehost/corehost.cpp#L187) prints some basic usage information:\n\n```\nUsage: dotnet [path-to-application]\nUsage: dotnet [commands]\n\npath-to-application:\n  The path to an application .dll file to execute.\n\ncommands:\n  -h|--help                         Display help.\n  --info                            Display .NET information.\n  --list-runtimes [--arch <arch>]   Display the installed runtimes matching the host or specified architecture. Example architectures: arm64, x64, x86.\n  --list-sdks [--arch <arch>]       Display the installed SDKs matching the host or specified architecture. Example architectures: arm64, x64, x86.\n```\n\nThe next step is for the muxer to try to find and load the *.NET Host Framework Resolver* (`hostfxr`\n\n). This searches a subfolder *host\\fxr* next to the `dotnet`\n\nexecutable, and reads all the folder versions listed there. If, like me, you have lots of runtimes installed, you'll have lots of entries:\n\nThe muxer reads all these folders, does a SemVer comparison, and selects the highest one. Inside the folder you'll find the `hostfxr`\n\nlibrary (`hostfxr.dll`\n\non Windows, `libhostfxr.dylib`\n\non mac, and `libhostfxr.so`\n\non Linux). The muxer loads the `hostfxr`\n\nlibrary into the process.\n\nSteve Gordon walks through the code the muxer uses to do this search and loading in\n\n[his post on the hostfxr library]if you want to see the details!\n\nOnce the muxer has loaded `hostfxr`\n\n, it resolves [the hostfxr_main_startupinfo function](https://github.com/dotnet/runtime/blob/main/docs/design/features/hosting-layer-apis.md#net-core-21) and invokes it.\n\nNow, if we take a look at the tracing logs, we can see this all playing out:\n\n```\nTracing enabled @ Thu Oct 23 18:33:26 2025 GMT\n--- Invoked dotnet [version: 10.0.0-rc.2.25502.107 @Commit: 89c8f6a112d37d2ea8b77821e56d170a1bccdc5a] main = {\nC:\\Program Files\\dotnet\\dotnet.exe\nbin\\Debug\\net9.0\\myapp.dll\n}\n\n.NET root search location options: 0\nReading fx resolver directory=[C:\\Program Files\\dotnet\\host\\fxr]\nConsidering fxr version=[10.0.0-rc.2.25502.107]...\nConsidering fxr version=[2.1.30]...\nConsidering fxr version=[3.1.32]...\nConsidering fxr version=[5.0.17]...\nConsidering fxr version=[6.0.36]...\nConsidering fxr version=[7.0.20]...\nConsidering fxr version=[9.0.10]...\nConsidering fxr version=[9.0.6]...\nDetected latest fxr version=[C:\\Program Files\\dotnet\\host\\fxr\\10.0.0-rc.2.25502.107]...\n\nResolved fxr [C:\\Program Files\\dotnet\\host\\fxr\\10.0.0-rc.2.25502.107\\hostfxr.dll]...\nLoaded library from C:\\Program Files\\dotnet\\host\\fxr\\10.0.0-rc.2.25502.107\\hostfxr.dll\n\nInvoking fx resolver [C:\\Program Files\\dotnet\\host\\fxr\\10.0.0-rc.2.25502.107\\hostfxr.dll] hostfxr_main_startupinfo\nHost path: [C:\\Program Files\\dotnet\\dotnet.exe]\nDotnet path: [C:\\Program Files\\dotnet\\]\nApp path: [C:\\Program Files\\dotnet\\dotnet.dll]\n```\n\nThese logs clearly show the muxer searching the *host\\fxr* directory, finding the highest version, loading the `hostfxr.dll`\n\nlibrary, and invoking the `hostfxr_main_startupinfo`\n\nfunction.\n\nThere's a variation on the \"muxer\" as the standard entrypoint, which is the \"apphost\" model. When you publish your .NET application, you typically also get an executable produced next to your app's dll, e.g.\n\n`MyApp.exe`\n\nas well as`MyApp.dll`\n\n. This executable is essentially a modified version of the`dotnet`\n\nmuxer, with various tweaks. I'm not going to look into the apphost in this post, just know that it exists!\n\nWe've loaded the `hostfxr`\n\nlibrary, so it's time to see what that does.\n\n[The ](#the-hostfxr-library)`hostfxr`\n\nlibrary\n\n`hostfxr`\n\nlibraryThe `hostfxr`\n\nlibrary has several responsibilities:\n\n- Parse the provided arguments to decide what to execute; is this a .NET SDK command like\n`dotnet build`\n\nand`dotnet publish`\n\n, or is it an app execution like`dotnet MyApp.dll`\n\n. - If it's an SDK command, find the correct SDK to use.\n- Decide which version of the .NET runtime to load.\n- Load the\n`hostpolicy`\n\nlibrary for the selected runtime.\n\nWe'll look at how each of those steps shows up in the tracing logs below.\n\n[Parse the arguments and decide behaviour](#parse-the-arguments-and-decide-behaviour)\n\nThe first step is *conceptually* part of the muxer in that it's about deciding the intention of the caller. Are they trying to execute SDK commands, or are they trying to execute an application? It's easiest to see this playing out in the tracing logs if we run an SDK command like `dotnet --info`\n\n:\n\n```\n--- Executing in muxer mode...\nUsing the provided arguments to determine the application to execute.\nApplication '--info' is not a managed executable.\n--- Resolving .NET SDK with working dir [D:\\repos\\temp\\MyApp]\n```\n\nIn the above logs, you can see that `hostfxr`\n\nhas established that `--info`\n\nis *not* an app to run, so it redirects to the .NET SDK. On the other hand, if we had run our app using `dotnet myapp.dll`\n\nwe'd see something like this instead:\n\n```\n--- Executing in muxer mode...\nUsing the provided arguments to determine the application to execute.\nUsing dotnet root path [C:\\Program Files\\dotnet\\]\nApp runtimeconfig.json from [D:\\repos\\temp\\myapp\\bin\\Debug\\net9.0\\myapp.dll]\n```\n\nWe'll come back to the application case in a second, for now we'll stick to the SDK scenario:\n\n[Finding the SDK](#finding-the-sdk)\n\nOnce `hostfxr`\n\nhas decided that an SDK command was executed the next step is to work out *which* .NET SDK to load by reading any *global.json* files in the path:\n\n```\n--- Resolving .NET SDK with working dir [D:\\repos\\temp\\MyApp]\nProbing path [D:\\repos\\temp\\MyApp\\global.json] for global.json\nProbing path [D:\\repos\\temp\\global.json] for global.json\nProbing path [D:\\repos\\global.json] for global.json\nFound global.json [D:\\repos\\global.json]\n\n--- Resolving SDK information from global.json [D:\\repos\\global.json]\nValue 'sdk/version' is missing or null in [D:\\repos\\global.json]\nValue 'sdk/rollForward' is missing or null in [D:\\repos\\global.json]\nResolving SDKs with version = 'latest', rollForward = 'latestMajor', allowPrerelease = false\n```\n\nIn these logs we can see that `hostfxr`\n\nfound a *global.json* folder in a parent path and parsed the rules for loading an SDK. Now it can search for the available SDKs and pick the one to run:\n\n```\nSearching for SDK versions in [C:\\Program Files\\dotnet\\sdk]\nIgnoring version [10.0.100-preview.6.25358.103] because it does not match the roll-forward policy\nIgnoring version [10.0.100-rc.2.25502.107] because it does not match the roll-forward policy\nVersion [9.0.301] is a better match than [none]\nVersion [9.0.306] is a better match than [9.0.301]\nSDK path resolved to [C:\\Program Files\\dotnet\\sdk\\9.0.306]\nUsing .NET SDK dll=[C:\\Program Files\\dotnet\\sdk\\9.0.306\\dotnet.dll]\n\nUsing the provided arguments to determine the application to execute.\nUsing dotnet root path [C:\\Program Files\\dotnet\\]\nApp runtimeconfig.json from [C:\\Program Files\\dotnet\\sdk\\9.0.306\\dotnet.dll]\n```\n\nAs you can see, it's resolved to the `9.0.306`\n\nversion of the SDK and is executing the `dotnet.dll`\n\nSDK application. It's also interesting to see the final three logs, starting with `\"Using the provided arguments\"`\n\n—they're essentially the *same* logs we saw when we ran `dotnet myapp.dll`\n\n. The only difference is that in this case, the .NET app we're running is `dotnet.dll`\n\n, the .NET SDK.\n\nWe'll switch back to the console app again now, and continue with the load process.\n\n[Choosing a .NET runtime to load](#choosing-a-net-runtime-to-load)\n\nAt this point `hostfxr`\n\nknows which .NET *app* to load but it doesn't know which .NET *runtime* to load. It determines this by inspecting the *runtimeconfig.json* of the app. This file lives alongside the app and includes, among other things, the version of the runtime to use:\n\n```\n{\n  \"runtimeOptions\": {\n    \"tfm\": \"net9.0\",\n    \"framework\": {\n      \"name\": \"Microsoft.NETCore.App\",\n      \"version\": \"9.0.0\"\n    },\n    \"configProperties\": {\n      \"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization\": false\n    }\n  }\n}\n```\n\nIf we check the tracing logs, we can see `hostfxr`\n\nprobes for and finds this file, and reads the specified `framework`\n\ndetails:\n\n```\nUsing the provided arguments to determine the application to execute.\nUsing dotnet root path [C:\\Program Files\\dotnet\\]\nApp runtimeconfig.json from [D:\\repos\\temp\\myapp\\bin\\Debug\\net9.0\\myapp.dll]\n\nRuntime config is cfg=D:\\repos\\temp\\myapp\\bin\\Debug\\net9.0\\myapp.runtimeconfig.json dev=D:\\repos\\temp\\myapp\\bin\\Debug\\net9.0\\myapp.runtimeconfig.dev.json\nAttempting to read dev runtime config: D:\\repos\\temp\\myapp\\bin\\Debug\\net9.0\\myapp.runtimeconfig.dev.json\nAttempting to read runtime config: D:\\repos\\temp\\myapp\\bin\\Debug\\net9.0\\myapp.runtimeconfig.json\nRuntime config [D:\\repos\\temp\\myapp\\bin\\Debug\\net9.0\\myapp.runtimeconfig.json] is valid=[1]\n\n--- The specified framework 'Microsoft.NETCore.App', version '9.0.0', apply_patches=1, version_compatibility_range=minor is compatible with the previously referenced version '9.0.0'.\n```\n\nWith the requested version established, `hostfxr`\n\nsets about searching for which versions of the runtime are available by looking in *C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App*. It applies whatever [roll forward policies](https://learn.microsoft.com/en-us/dotnet/core/versions/selection#control-roll-forward-behavior) are configured for the app (`Minor`\n\nunless otherwise specified) and chooses the best match:\n\n```\n--- Resolving FX directory, name 'Microsoft.NETCore.App' version '9.0.0'\nSearching FX directory in [C:\\Program Files\\dotnet]\nAttempting FX roll forward starting from version='[9.0.0]', apply_patches=1, version_compatibility_range=minor, roll_to_highest_version=0, prefer_release=1\n\n'Roll forward' enabled with version_compatibility_range [minor]. Looking for the lowest release greater than or equal version to [9.0.0]\nFound version [9.0.6]\n\nApplying patch roll forward from [9.0.6] on release only\nInspecting version... [10.0.0-rc.2.25502.107]\nInspecting version... [2.1.30]\nInspecting version... [3.1.32]\nInspecting version... [5.0.17]\nInspecting version... [6.0.36]\nInspecting version... [7.0.20]\nInspecting version... [8.0.17]\nInspecting version... [8.0.21]\nInspecting version... [9.0.10]\nInspecting version... [9.0.6]\nChanging Selected FX version from [] to [C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\9.0.10]\n\nChose FX version [C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\9.0.10]\n```\n\nAs you can see above, `hostfxr`\n\nfound that `9.0.10`\n\nwas the best version match for the app. If it *couldn't* find a match for some reason, you'd see a message something like this:\n\n```\nNo match greater than or equal to [10.0.0] found.\nFramework reference didn't resolve to any available version.\nIt was not possible to find any compatible framework version\nYou must install or update .NET to run this application.\n```\n\nOnce a valid runtime version is found, `hostfxr`\n\nattempts to load the *runtimeconfig.json* for the *runtime*. This indicates if any other runtimes need to be resolved.\n\nThe runtime is actually a shared \"framework\", called\n\n`Microsoft.NETCore.App`\n\n. Frameworks can referenceotherframeworks, for example the`Microsoft.AspNetCore.App`\n\nand`Microsoft.WindowsDesktop.App`\n\n\"frameworks\" can reference the`Microsoft.NETCore.App`\n\nframework. You can also create your own frameworks if you want! Everything is resolved recursively at this point.\n\n```\nRuntime config is cfg=C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\9.0.10\\Microsoft.NETCore.App.runtimeconfig.json dev=C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\9.0.10\\Microsoft.NETCore.App.runtimeconfig.dev.json\n\nAttempting to read dev runtime config: C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\9.0.10\\Microsoft.NETCore.App.runtimeconfig.dev.json\nAttempting to read runtime config: C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\9.0.10\\Microsoft.NETCore.App.runtimeconfig.json\nRuntime config [C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\9.0.10\\Microsoft.NETCore.App.runtimeconfig.json] is valid=[1]\n\n--- Summary of all frameworks:\n     framework:'Microsoft.NETCore.App', lowest requested version='9.0.0', found version='9.0.10', effective reference version='9.0.0' apply_patches=1, version_compatibility_range=minor, roll_to_highest_version=0, folder=C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\9.0.10\n\nExecuting as a framework-dependent app as per config file [D:\\repos\\temp\\myapp\\bin\\Debug\\net9.0\\myapp.runtimeconfig.json]\n```\n\nOnce all the frameworks are loaded (just the `Microsoft.NETCore.App`\n\nruntime in this case) we move onto the final responsibility of `hostfxr`\n\n, loading `hostpolicy`\n\n.\n\n[Loading ](#loading-hostpolicy)`hostpolicy`\n\n`hostpolicy`\n\nOnce the .NET runtime is resolved, `hostfxr`\n\nneeds to load the `hostpolicy`\n\nlibrary for the specific chosen version of the runtime. It does this by reading the *deps.json* file of the chosen runtime and looking for a library called something like `runtime.win-x64.Microsoft.NETCore.DotNetHostPolicy`\n\n. If it doesn't find that entry (it didn't in the example below) then it just looks for it in the root framework folder:\n\n```\n--- Resolving hostpolicy.dll version from deps json [C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\9.0.10\\Microsoft.NETCore.App.deps.json]\nDependency manifest C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\9.0.10\\Microsoft.NETCore.App.deps.json does not contain an entry for runtime.win-x64.Microsoft.NETCore.DotNetHostPolicy\n\nThe expected hostpolicy.dll directory is [C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\9.0.10]\nLoaded library from C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\9.0.10\\hostpolicy.dll\n```\n\nAnd as you can see from the final line, `hostfxr`\n\nfound `hostpolicy.dll`\n\nand loaded it, so it's time to look at the `hostpolicy`\n\nbehaviour.\n\n[The ](#the-hostpolicy-library)`hostpolicy`\n\nlibrary\n\n`hostpolicy`\n\nlibraryThe main responsibilities of `hostpolicy`\n\nare:\n\n- Building the Trusted Platform Assemblies list based on the application and framework\n*deps.json*. - Setting up the context switches to run the application.\n- Launching the .NET runtime to run your application.\n\n[Building the Trusted Platform Assemblies list](#building-the-trusted-platform-assemblies-list)\n\nAfter printing a few logs that I'm going to skip over for the purposes of this post, we start to get a *lot* of logs printed. I truncate them to just a few entries below, just enough to give a taste of what's going on:\n\n```\nLoading deps file... [C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\9.0.10\\Microsoft.NETCore.App.deps.json]: is_framework_dependent=0, use_fallback_graph=0\n\nProcessing package Microsoft.NETCore.App.Runtime.win-x64/9.0.10\n  Adding runtime assets\n    System.Private.CoreLib.dll assemblyVersion=9.0.0.0 fileVersion=9.0.1025.47515\n    Microsoft.VisualBasic.dll assemblyVersion=10.0.0.0 fileVersion=9.0.1025.47515\n    Microsoft.Win32.Primitives.dll assemblyVersion=9.0.0.0 fileVersion=9.0.1025.47515\n    mscorlib.dll assemblyVersion=4.0.0.0 fileVersion=9.0.1025.47515\n    netstandard.dll assemblyVersion=2.1.0.0 fileVersion=9.0.1025.47515\n    System.AppContext.dll assemblyVersion=9.0.0.0 fileVersion=9.0.1025.47515\n    System.Buffers.dll assemblyVersion=9.0.0.0 fileVersion=9.0.1025.47515\n    System.ComponentModel.DataAnnotations.dll assemblyVersion=4.0.0.0 fileVersion=9.0.1025.47515\n# ...\n\n  Adding native assets\n    clrjit.dll assemblyVersion= fileVersion=9.0.1025.47515\n    coreclr.dll assemblyVersion= fileVersion=9.0.1025.47515\n    createdump.exe assemblyVersion= fileVersion=9.0.1025.47515\n    System.IO.Compression.Native.dll assemblyVersion= fileVersion=9.0.1025.47515\n# ...\n\nReconciling library Microsoft.NETCore.App.Runtime.win-x64/9.0.10\n  package: Microsoft.NETCore.App.Runtime.win-x64, version: 9.0.10\n  Adding runtime assets\n    Entry 0 for asset name: System.Private.CoreLib, relpath: System.Private.CoreLib.dll, assemblyVersion 9.0.0.0, fileVersion 9.0.1025.47515\n    Entry 1 for asset name: Microsoft.VisualBasic, relpath: Microsoft.VisualBasic.dll, assemblyVersion 10.0.0.0, fileVersion 9.0.1025.47515\n    Entry 2 for asset name: Microsoft.Win32.Primitives, relpath: Microsoft.Win32.Primitives.dll, assemblyVersion 9.0.0.0, fileVersion 9.0.1025.47515\n    Entry 3 for asset name: mscorlib, relpath: mscorlib.dll, assemblyVersion 4.0.0.0, fileVersion 9.0.1025.47515\n# ...\n\n  Adding native assets\n    Entry 0 for asset name: clretwrc, relpath: clretwrc.dll, assemblyVersion , fileVersion 9.0.1025.47515\n    Entry 1 for asset name: clrgc, relpath: clrgc.dll, assemblyVersion , fileVersion 9.0.1025.47515\n    Entry 2 for asset name: clrgcexp, relpath: clrgcexp.dll, assemblyVersion , fileVersion 9.0.1025.47515\n#...\n```\n\nIn the above logs, `hostpolicy`\n\nhas read the *deps.json* file for the chosen runtime, and is loading all the libraries it lists. You can see these files all listed if you open the *deps.json* file yourself, for example:\n\n```\n{\n  \"runtimeTarget\": {\n    \"name\": \".NETCoreApp,Version=v9.0/win-x64\",\n    \"signature\": \"\"\n  },\n  \"compilationOptions\": {},\n  \"targets\": {\n    \".NETCoreApp,Version=v9.0\": {},\n    \".NETCoreApp,Version=v9.0/win-x64\": {\n      \"Microsoft.NETCore.App.Runtime.win-x64/9.0.10\": {\n        \"runtime\": {\n          \"System.Private.CoreLib.dll\": {\n            \"assemblyVersion\": \"9.0.0.0\",\n            \"fileVersion\": \"9.0.1025.47515\"\n          },\n          \"Microsoft.VisualBasic.dll\": {\n            \"assemblyVersion\": \"10.0.0.0\",\n            \"fileVersion\": \"9.0.1025.47515\"\n          },\n//...\n```\n\nAfter processing the framework *deps.json* file, `hostpolicy`\n\nmoves onto your *apps* deps.json file, which is likely much simpler. In the simple console app case it will only contain a reference to the app dll itself:\n\n```\nProcessing package myapp/1.0.0\n  Adding runtime assets\n    myapp.dll assemblyVersion= fileVersion=\n\nReconciling library myapp/1.0.0\n  project: myapp, version: 1.0.0\n  Adding runtime assets\n    Entry 0 for asset name: myapp, relpath: myapp.dll, assemblyVersion , fileVersion\n```\n\nWith the list of assets created, `hostpolicy`\n\nsets about building up the Trusted Platform Assemblies (TPA) list. As per [this glossary](https://github.com/dotnet/runtime/blob/1e09fc169a2c4d0c54c483967b845b03d11215d5/docs/project/glossary.md):\n\nTrusted Platform Assemblies used to be a special set of assemblies that comprised the platform assemblies, when it was originally designed. As of today, it is simply the set of assemblies known to constitute the application.\n\nSo `hostpolicy`\n\nsimply walks through all those assemblies it discovered, and adds them to the TPA:\n\n```\n-- Probe configurations:\n  probe type=app\n  probe type=framework dir=[C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\9.0.10] fx_level=1\n\nAdding tpa entry: D:\\repos\\temp\\myapp\\bin\\Debug\\net9.0\\myapp.dll, AssemblyVersion: , FileVersion: \n\nProcessing TPA for deps entry [myapp, 1.0.0, myapp.dll] with fx level: 0\n  Using probe config: type=app\n    Local path query D:\\repos\\temp\\myapp\\bin\\Debug\\net9.0\\myapp.dll (skipped file existence check)\n    Probed deps dir and matched 'D:\\repos\\temp\\myapp\\bin\\Debug\\net9.0\\myapp.dll'\n\nProcessing TPA for deps entry [Microsoft.NETCore.App.Runtime.win-x64, 9.0.10, System.Private.CoreLib.dll] with fx level: 1\n  Using probe config: type=app\n    Skipping... not app asset\n  Using probe config: type=framework dir=[C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\9.0.10] fx_level=1\n    Local path query C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\9.0.10\\System.Private.CoreLib.dll (skipped file existence check)\n    Probed deps json and matched 'C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\9.0.10\\System.Private.CoreLib.dll'\n\nAdding tpa entry: C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\9.0.10\\System.Private.CoreLib.dll, AssemblyVersion: 9.0.0.0, FileVersion: 9.0.1025.47515\n#...\n```\n\nThat goes on for another 1000 lines, even in a basic console app, so we'll skip ahead 😅\n\n[Creating the context switches](#creating-the-context-switches)\n\nThe next lines written by `hostpolicy`\n\nin the trace log are:\n\n```\nProperty FX_DEPS_FILE = C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\9.0.10\\Microsoft.NETCore.App.deps.json\nProperty TRUSTED_PLATFORM_ASSEMBLIES = C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\9.0.10\\System.Security.Cryptography.X509Certificates.dll;C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\9.0.10\\Microsoft.CSharp.dll; # TRUNCATED!\nProperty NATIVE_DLL_SEARCH_DIRECTORIES = C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\9.0.10\\;\nProperty PLATFORM_RESOURCE_ROOTS = \nProperty APP_CONTEXT_BASE_DIRECTORY = D:\\repos\\temp\\myapp\\bin\\Debug\\net9.0\\\nProperty APP_CONTEXT_DEPS_FILES = D:\\repos\\temp\\myapp\\bin\\Debug\\net9.0\\myapp.deps.json;C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\9.0.10\\Microsoft.NETCore.App.deps.json\nProperty PROBING_DIRECTORIES = \nProperty RUNTIME_IDENTIFIER = win-x64\nProperty System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization = false\nProperty HOST_RUNTIME_CONTRACT = 0x1cd836084d8\n```\n\nThis shows the context properties which will be passed to the runtime when it's loaded. As you can see, it's primarily a set of configuration values loaded from the the environment, containing various details about paths to files used to initialize the runtime. It also contains the `configProperties`\n\nfrom the app's *runtimeconfig.json*, such as the `EnableUnsafeBinaryFormatterSerialization`\n\nsetting.\n\nNote that I truncated the\n\n`TRUSTED_PLATFORM_ASSEMBLIES`\n\nproperty as it's a list of paths toallthe assemblies in the TPA\n\nAnd *finally* `hostpolicy`\n\nloads the `coreclr.dll`\n\n.NET runtime and launches it!\n\n```\nCoreCLR path = 'C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\9.0.10\\coreclr.dll', CoreCLR dir = 'C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\9.0.10\\'\nLoaded library from C:\\Program Files\\dotnet\\shared\\Microsoft.NETCore.App\\9.0.10\\coreclr.dll\n\nLaunch host: C:\\Program Files\\dotnet\\dotnet.exe, app: D:\\repos\\temp\\myapp\\bin\\Debug\\net9.0\\myapp.dll, argc: 0, args:\n```\n\nAnd there we have it, from muxer, to `hostfxr`\n\n, to `hostpolicy.dll`\n\nto `coreclr.dll`\n\nand a running app! If you're running into difficulties early in the NET app booting process, then consider enabling tracing to see exactly what's going on.\n\n[Summary](#summary)\n\nIn this post I showed how you can enable host tracing by setting `COREHOST_TRACE=1`\n\nand setting `COREHOST_TRACEFILE`\n\nto a file path. I then ran a very simple app and explored the host tracing logs it produces. We then saw how the dotnet muxer is the entrypoint for the app, which locates and loads `hostfxr`\n\n. `hostfxr`\n\nis then responsible for finding the correct .NET runtime to load and for loading `hostpolicy.dll`\n\n. Finally `hostpolicy.dll`\n\nboots the .NET runtime and runs your application.", "url": "https://wpnews.pro/news/exploring-the-net-boot-process-via-host-tracing", "canonical_source": "https://andrewlock.net/exploring-the-dotnet-boot-process-via-host-tracing/", "published_at": "2025-11-25 10:00:00+00:00", "updated_at": "2026-05-23 21:41:25.486672+00:00", "lang": "en", "topics": ["developer-tools"], "entities": [".NET", "OpenTelemetry", "APM"], "alternates": {"html": "https://wpnews.pro/news/exploring-the-net-boot-process-via-host-tracing", "markdown": "https://wpnews.pro/news/exploring-the-net-boot-process-via-host-tracing.md", "text": "https://wpnews.pro/news/exploring-the-net-boot-process-via-host-tracing.txt", "jsonld": "https://wpnews.pro/news/exploring-the-net-boot-process-via-host-tracing.jsonld"}}