I used to wonder why we use a “slow and bloated” framework when languages like C produce tiny binaries and run so efficiently. I never really knew what happened under the hood of every .NET app I created, and each new release hides even more details behind abstraction.
I promised myself that, to love my job, I must first appreciate the tech I work with. This article is my attempt to understand how .NET and its runtime work. Hope you enjoy it! 😊
We all know C# is a compiled language so there is are 2 phases in it Build Phase and Run Phase
Let’s start with what happens during the build phase.
Calm before the storm - the so-called Build phase 🤣
Internally, MSBuild is invoked when we enter dotnet build
in CMD:
- SDK selection MSBuild checks the SDK specified in myapp.csproj, for example:
<Project Sdk="Microsoft.NET.Sdk.Web" />
<!-- or -->
<Project Sdk="Microsoft.NET.Sdk" />
SDKs live under:
C:\Program Files\dotnet\sdk\<version>\Sdks
Load
.props
files
These set environment variables, global properties, and defaults (output path, configuration, etc.).-
Gathers all project items which includes
-
*.cs
source files - Referenced assemblies (e.g.,
System.Net.Http.dll
) these are the using libraries which use in our c# code, each using statement corresponds to a.dll
file which is placed inC:\Program Files\dotnet\shared\Microsoft.NETCore.App\6.0.36
- NuGet packages (e.g., StackExchange.Redis)
-
-
Load
.targets
files
These define what actual build steps to run. these can include predefined sequece of steps like-
Restore
-
PrepareResources
-
Compile
-
GenerateAssemblyInfo
-
CopyFilesToOutputDirectory
Build
-
-
During above sequence we have Compile step
During compile step MSBUILD will use roslyn compiler to compile .cs file into IL ( .dll files)- compiler will enforce CLS rules
- compiler also maps all language specific types to CTS-defined types ( e.g., int -> System.Int32 )
- Both CLS ( Common language specification ) and CTS ( Common Type System ) plays vital role in produced IL( Intermediate Language ) code.
Output
MSBuild drops the generated.dll
, optional.exe
,*.pdb
,*.deps.json
, etc. into:
.\bin\Debug\net8.0\
The Actual strom - The Run Phase 🍃
dotnet run
Does two things:
-
dotnet build
( will be skipped if already done and no changes are detected ) -
dotnet myapp.dll
( actual execution of your application starts here )
Note:
So far, we have compiled our code into .dll
and .exe
files along with other dependencies.
- These files are also called IL (Intermediate Language) code.
- They cannot be directly executed on Windows, Linux, or any OS.
Why?
Because they are not machine code.
You may ask:
What is the purpose of this extra step? Why not directly convert the source code into machine code?
There are several reasons why this was implemented:
- Cross-platform: IL runs anywhere the CLR exists.
- Runtime optimisation: JIT tailors code to the actual CPU.
- Security & verification: CLR checks IL for type safety and memory violations.
- Language interoperability: C#, F#, VB.NET, etc., all compile to the same IL.
Enable verbose host tracing
Before we proceed further its better if we enable below flag for detailed CMD traces.
set COREHOST_TRACE=1
Execution starts when dotnet myapp.dll
is called :
As part of execution below command is executed internally.
dotnet exec --additionalprobingpath C:\Users\YourName\.nuget\packages C:\Users\YourName\Desktop\myapp\bin\Debug\net8.0\myapp.dll
so what it does ?
It kicks of an Execution Sequence called CoreHost chain :
-
dotnet.exe ( located in "C:\Program Files\dotnet\dotnet.exe" ) is called.
- This acts as the entry point of CoreHost chain.
- From here directly calls hostfxr.dll file
-
Hostfxr.dll is called!
- resolved runtime sdk
- Uses
--additionalprobingpath
to locate NuGet packages - Chooses appropriate framework
-
hostpolicy.dll is called :
- loads dependecy based on
.deps.json
file (found along with myapp.dll bost build ). - Resolves and loads all runtime + app assemblies.
basically loads below things
- our app dll (myapp.dll)
- Reference library if any ( Newtonsoft.Json.dll )
- .Net runtime assemblies like ( System.Console.dll )
- Other Dependency files mentioned in *.deps.json file.
- Finally once all these are done, coreclr.dll is called.
- loads dependecy based on
-
coreclr.dll :
- The fricking start of the clr itself.
lets talk about what clr is and why it is important.
As discussed before, the CLR (Common Language Runtime) acts as a runtime platform or container environment capable of processing and executing IL code. It also provides several essential runtime services:
Memory management via Garbage Collection
Conversion of IL to machine code using the JIT compiler, which optimizes based on the runtime machine
Type safety verification through the CTS (Common Type System)
Once all these steps are completed the app will be up and running 🥳.
Top comments (0)