Überblick: Wie das Builden einer C#/.NET-Anwendung funktioniert


Dieses Dokument erklärt, was beim Erstellen einer C#-Anwendung passiert: von .cs-Dateien über IL-Code bis zur Ausführung durch die .NET Runtime und den JIT-Compiler. Außerdem werden die wichtigsten CLI-Kommandos dotnet restore, dotnet build und dotnet publish erklärt.


1. Die Grundidee

Eine C#-Anwendung besteht zuerst aus Source Code, also Dateien wie:

Program.cs
Calculator.cs
MyApp.csproj

Der C#-Code wird nicht direkt als macOS-, Linux- oder Windows-Maschinencode gespeichert. Stattdessen wird er zuerst in eine Zwischensprache übersetzt: IL-Code.

IL steht für Intermediate Language.

Das ist ein plattformunabhängiger Code, der von der .NET Runtime verstanden wird.


flowchart LR
    A["C# Source Code<br/>.cs-Dateien"] --> B["C# Compiler<br/>Roslyn"]
    B --> C[".NET Assembly<br/>.dll mit IL-Code"]
    C --> D[".NET Runtime<br/>CoreCLR"]
    D --> E["JIT Compiler"]
    E --> F["Nativer Maschinencode<br/>für macOS/Linux/Windows"]

Vereinfacht gesagt:

C# Source Code
    ↓ wird kompiliert
IL-Code in einer .dll
    ↓ wird zur Laufzeit geladen
JIT kompiliert IL zu Maschinencode

CPU führt das Programm aus

2. Source Code, IL-Code, Runtime und JIT


C# Source Code

Das ist der Code, den du schreibst.

Beispiel:

Console.WriteLine("Hallo Welt!");

Dieser Code ist für Menschen lesbar, aber die CPU kann ihn nicht direkt ausführen.


IL-Code

Beim Build wird dein C# Source Code in IL-Code übersetzt.

Der IL-Code liegt meistens in einer Datei mit der Endung .dll.

Beispiel:

MyApp.dll

Diese .dll ist eine .NET Assembly. Sie enthält unter anderem:

  • den IL-Code deiner Anwendung
  • Metadaten über Klassen, Methoden und Typen
  • Informationen, die die .NET Runtime zum Laden der Anwendung braucht

Wichtig: Eine .dll in .NET ist nicht einfach nur eine klassische native Bibliothek. Sie ist oft der eigentliche Hauptteil deiner Anwendung.


.NET Runtime

Die .NET Runtime ist die Umgebung, die deine Anwendung ausführt.


Die Runtime kümmert sich unter anderem um:

  • Laden deiner Assemblys
  • Speicherverwaltung
  • Garbage Collection
  • Typprüfung
  • Exception Handling
  • Zugriff auf die Base Class Library
  • JIT-Kompilierung

Die Runtime ist also der Teil, der weiß, wie man IL-Code ausführt.


JIT Compiler

JIT steht für Just-In-Time.

Der JIT Compiler übersetzt IL-Code während der Programmausführung in echten Maschinencode für die aktuelle Plattform.


Beispiel:

  • Auf einem Apple-Silicon-Mac wird Code für osx-arm64 erzeugt.
  • Auf einem Intel-Linux-System wird Code für linux-x64 erzeugt.
  • Auf Windows kann Code für win-x64 erzeugt werden.

Der Vorteil:

Eine .NET Assembly kann grundsätzlich plattformunabhängig sein.
Die endgültige Übersetzung passiert erst auf dem Zielsystem.

sequenceDiagram
    participant User as Benutzer
    participant AppHost as Executable/App Host
    participant Runtime as .NET Runtime
    participant DLL as MyApp.dll
    participant JIT as JIT Compiler
    participant CPU as CPU

    User->>AppHost: Startet ./MyApp
    AppHost->>Runtime: Lade passende .NET Runtime
    Runtime->>DLL: Lade Assembly mit IL-Code
    Runtime->>JIT: Methode wird benötigt
    JIT->>CPU: Erzeuge nativen Maschinencode
    CPU->>User: Programm läuft

3. Wichtige Dateien beim Build

Bei einer einfachen C# Console Application kann ein dotnet build -c Release zum Beispiel diese Dateien erzeugen:

MyApp
MyApp.dll
MyApp.runtimeconfig.json
MyApp.deps.json
MyApp.pdb

MyApp / MyApp.exe

Das ist ein ausführbares Programm.

Es ist meistens ein kleiner App Host. Er startet die .NET Runtime und sagt ihr, welche .dll ausgeführt werden soll.


Du kannst dann schreiben:

./MyApp

bzw.

MyApp.exe

statt:

dotnet MyApp.dll

MyApp.dll

Das ist die wichtigste Datei deiner Anwendung.

Sie enthält den kompilierten IL-Code und die Metadaten deiner Anwendung.


MyApp.runtimeconfig.json

Diese Datei beschreibt, welche Runtime deine Anwendung erwartet.

Sie enthält zum Beispiel Informationen wie:

  • Target Framework, zum Beispiel net8.0, net9.0 oder net10.0
  • benötigtes Shared Framework, zum Beispiel Microsoft.NETCore.App
  • Runtime-Einstellungen

MyApp.deps.json

Diese Datei beschreibt die Dependencies deiner Anwendung.

Sie sagt der Runtime zum Beispiel:

  • welche Assemblys gebraucht werden
  • welche NuGet-Packages beteiligt sind
  • welche Versionen verwendet werden sollen
  • welche runtime-spezifischen Dateien geladen werden müssen

MyApp.pdb

Die .pdb-Datei enthält Debug-Symbole.

Sie hilft beim Debugging und bei Stack Traces mit Zeilennummern.

Die Anwendung läuft meistens auch ohne .pdb, aber Fehlersuche und Diagnose sind mit .pdb deutlich angenehmer.


4. Die Rolle der .csproj-Datei

Die .csproj-Datei ist die Projektdatei.

Sie beschreibt, wie dein Projekt gebaut werden soll.


Beispiel:

<Project Sdk="Microsoft.NET.Sdk">
 
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
 
</Project>

Wichtige Informationen darin sind zum Beispiel:

EintragBedeutung
OutputTypeGibt an, ob eine ausführbare Anwendung oder eine Library gebaut wird
TargetFrameworkGibt an, für welche .NET-Version gebaut wird
PackageReferenceVerweist auf NuGet-Packages
RuntimeIdentifierGibt optional eine Zielplattform an, zum Beispiel osx-arm64

5. dotnet restore

dotnet restore bereitet das Projekt vor, indem es Dependencies auflöst.


Typischer Befehl:

dotnet restore

Dabei passiert vor allem:

flowchart TD
    A["Projektdatei<br/>.csproj"] --> B["PackageReference lesen"]
    B --> C["NuGet-Quellen prüfen"]
    C --> D["Packages herunterladen<br/>falls noch nicht vorhanden"]
    D --> E["Dependency Graph erzeugen"]
    E --> F["obj/project.assets.json"]

Was produziert restore?

restore erzeugt normalerweise keine fertige Anwendung.

Stattdessen entstehen Hilfsdateien im obj-Ordner, besonders:

obj/project.assets.json

Diese Datei beschreibt den aufgelösten Dependency Graph.

Vereinfacht:

restore = Dependencies finden, herunterladen und vorbereiten

Wichtig: Viele andere Kommandos führen restore automatisch aus, wenn es nötig ist, zum Beispiel:

dotnet build
dotnet run
dotnet test
dotnet publish

Du musst dotnet restore daher oft nicht manuell ausführen.


6. dotnet build

dotnet build kompiliert das Projekt.

Typischer Befehl:

dotnet build

Oder als Release-Build:

dotnet build -c Release

flowchart TD
    A["Source Code<br/>.cs-Dateien"] --> B["Compiler"]
    C["Dependencies<br/>aus restore"] --> B
    B --> D["Assembly<br/>MyApp.dll mit IL-Code"]
    B --> E["Debug-Symbole<br/>MyApp.pdb"]
    B --> F["Runtime-Dateien<br/>runtimeconfig.json, deps.json"]
    B --> G["Optionaler App Host<br/>MyApp / MyApp.exe"]

Was produziert build?

Der Build-Output liegt typischerweise in:

bin/Debug/<target-framework>/

oder:

bin/Release/<target-framework>/

Beispiel:

bin/Release/net10.0/

Dort findest du zum Beispiel:

MyApp
MyApp.dll
MyApp.runtimeconfig.json
MyApp.deps.json
MyApp.pdb

Vereinfacht:

build = Source Code in ausführbare Build-Artefakte übersetzen

Wann verwendet man build?

build verwendet man vor allem für:

  • lokales Entwickeln
  • Prüfen, ob der Code kompiliert
  • CI-Pipelines
  • Tests vor dem Deployment

build ist aber nicht unbedingt der beste Output, den du direkt an Benutzer weitergibst. Dafür ist meistens publish gedacht.


7. dotnet publish

dotnet publish erstellt einen Output, den man deployen oder weitergeben kann.

Typischer Befehl:

dotnet publish -c Release

Der Publish-Output liegt typischerweise in:

bin/Release/<target-framework>/publish/

Oder bei runtime-spezifischem Publishing zum Beispiel in:

bin/Release/net10.0/osx-arm64/publish/

Dabei passiert:

flowchart TD
    A["restore"] --> B["build"]
    B --> C["Publish-Schritt"]
    C --> D["Deployment-Ordner<br/>publish/"]
    D --> E["App-Dateien"]
    D --> F["benötigte Libraries"]
    D --> G["Runtime-Dateien"]
    D --> H["optional: .NET Runtime<br/>bei self-contained"]

Was produziert publish?

publish erzeugt einen Ordner, der zum Verteilen der Anwendung gedacht ist.

Beispiel:

bin/Release/net10.0/publish/

Dieser Ordner enthält die Dateien, die du auf einen Server, in einen Container oder auf einen anderen Rechner kopieren würdest.

Vereinfacht:

publish = Deployment-fertigen Ordner erzeugen

8. Unterschied zwischen build und publish

Themadotnet builddotnet publish
HauptzweckProjekt kompilierenAnwendung bereitstellen
Typische VerwendungEntwicklung, Tests, CIDeployment, Weitergabe, Container
OutputBuild-OrdnerPublish-Ordner
Pfadbin/Release/net10.0/bin/Release/net10.0/publish/
Runtime enthalten?Normalerweise neinOptional bei self-contained
Optimiert für Deployment?Nicht primärJa

Merksatz:

build beantwortet: 
Kann mein Projekt kompiliert werden?
 
publish beantwortet: 
Welche Dateien brauche ich zum Ausliefern?

9. Framework-dependent Publishing

Bei framework-dependent Publishing enthält deine Anwendung nicht die .NET Runtime selbst.

Die Zielmaschine muss eine passende .NET Runtime installiert haben.


Beispiel:

dotnet publish -c Release

Oder explizit:

dotnet publish -c Release --self-contained false

Typischer Output:

MyApp
MyApp.dll
MyApp.runtimeconfig.json
MyApp.deps.json
MyApp.pdb

Vorteile

  • kleinerer Output
  • Updates der Runtime können zentral auf dem System erfolgen
  • gut geeignet für Server oder Container, bei denen die Runtime bereits vorhanden ist

Nachteile

  • Zielsystem braucht die passende .NET Runtime
  • Anwendung startet nicht, wenn die Runtime fehlt
  • Runtime-Versionen müssen beachtet werden
flowchart LR
    A["Deine App<br/>kleiner Output"] --> C["Zielsystem"]
    B["Installierte .NET Runtime<br/>auf Zielsystem"] --> C
    C --> D["App läuft"]

10. Self-contained Publishing

Bei self-contained Publishing enthält der Publish-Ordner deine Anwendung und die benötigte .NET Runtime.


Beispiel für Apple Silicon macOS:

dotnet publish -c Release -r osx-arm64 --self-contained true

Beispiel für Intel/AMD Linux:

dotnet publish -c Release -r linux-x64 --self-contained true

Beispiel für Windows x64:

dotnet publish -c Release -r win-x64 --self-contained true

Vorteile

  • Zielsystem braucht keine installierte .NET Runtime
  • du kontrollierst genau, welche Runtime-Version mitgeliefert wird
  • gut für Enduser-Programme oder Systeme, die du nicht administrierst

Nachteile

  • deutlich größerer Output
  • du musst für jede Zielplattform separat publishen
  • Runtime-Sicherheitsupdates kommen nicht automatisch vom Zielsystem, sondern du musst neu publishen
flowchart LR
    A["Deine App"] --> C["Publish-Ordner"]
    B[".NET Runtime"] --> C
    C --> D["Zielsystem ohne installierte Runtime"]
    D --> E["App läuft"]

11. Framework-dependent vs. self-contained

FrageFramework-dependentSelf-contained
Enthält die App die .NET Runtime?NeinJa
Muss .NET auf dem Zielsystem installiert sein?JaNein
Output-GrößeKleinerGrößer
Plattformabhängig?Kann portabler seinImmer für eine konkrete Runtime-ID
Typische VerwendungServer, Entwicklerumgebungen, Container mit Runtime-ImageDesktop-Apps, Tools für fremde Rechner, kontrollierte Deployments

Merksatz:

framework-dependent = 
Die App hängt von einer installierten Runtime ab.
 
self-contained = 
Die App bringt ihre Runtime selbst mit.

12. Runtime Identifier, kurz RID

Ein Runtime Identifier beschreibt die Zielplattform.

Beispiele:

osx-arm64
osx-x64
linux-x64
win-x64
win-arm64

Du brauchst einen RID besonders dann, wenn du für eine konkrete Plattform publishen möchtest.

Beispiel:

dotnet publish -c Release -r osx-arm64

Ab modernen .NET-Versionen bedeutet -r osx-arm64 nicht automatisch, dass die Anwendung self-contained ist. Wenn du self-contained willst, schreibe es ausdrücklich dazu:

dotnet publish -c Release -r osx-arm64 --self-contained true

Wenn du framework-dependent mit einer konkreten Runtime-ID willst:

dotnet publish -c Release -r osx-arm64 --self-contained false

13. Beispiel: kompletter Ablauf

Angenommen, du hast dieses Projekt:

MyApp/
├── MyApp.csproj
└── Program.cs

Schritt 1: Dependencies wiederherstellen

dotnet restore

Output-Idee:

MyApp/
├── obj/
│   └── project.assets.json

Schritt 2: Build ausführen

dotnet build -c Release

Output-Idee:

MyApp/
├── bin/
│   └── Release/
│       └── net10.0/
│           ├── MyApp
│           ├── MyApp.dll
│           ├── MyApp.runtimeconfig.json
│           ├── MyApp.deps.json
│           └── MyApp.pdb

Schritt 3: Publish erstellen

dotnet publish -c Release

Output-Idee:

MyApp/
├── bin/
│   └── Release/
│       └── net10.0/
│           └── publish/
│               ├── MyApp
│               ├── MyApp.dll
│               ├── MyApp.runtimeconfig.json
│               ├── MyApp.deps.json
│               └── MyApp.pdb

Bei self-contained sieht der Ordner größer aus, weil zusätzlich Runtime-Dateien enthalten sind:

publish/
├── MyApp
├── MyApp.dll
├── MyApp.runtimeconfig.json
├── MyApp.deps.json
├── viele .NET Runtime-Dateien
└── weitere native Libraries

14. Gesamtbild

flowchart TD
    A["Program.cs<br/>C# Source Code"] --> B["dotnet restore<br/>Dependencies vorbereiten"]
    B --> C["dotnet build<br/>Kompilieren"]
    C --> D["MyApp.dll<br/>IL-Code"]
    C --> E["runtimeconfig.json"]
    C --> F["deps.json"]
    C --> G["pdb"]
    C --> H["App Host<br/>MyApp / MyApp.exe"]

    D --> I["dotnet publish<br/>Deployment-Ordner erzeugen"]
    E --> I
    F --> I
    G --> I
    H --> I

    I --> J["framework-dependent<br/>klein, Runtime muss installiert sein"]
    I --> K["self-contained<br/>größer, Runtime ist dabei"]

15. Häufige Kommandos

Debug-Build

dotnet build

Release-Build

dotnet build -c Release

Framework-dependent publish

dotnet publish -c Release --self-contained false

Self-contained publish für macOS Apple Silicon

dotnet publish -c Release -r osx-arm64 --self-contained true

Self-contained publish für Linux x64

dotnet publish -c Release -r linux-x64 --self-contained true

Self-contained publish für Windows x64

dotnet publish -c Release -r win-x64 --self-contained true

Single-file publish

dotnet publish -c Release -r osx-arm64 --self-contained true -p:PublishSingleFile=true

Dabei versucht .NET, die Anwendung und viele benötigte Dateien in eine einzige ausführbare Datei zu packen.


16. Kurze Zusammenfassung

restore
    Dependencies auflösen und vorbereiten
 
build
    Source Code kompilieren
    IL-Code in .dll erzeugen
    Hilfsdateien wie runtimeconfig.json und deps.json erzeugen
 
publish
    Deployment-fertigen Ordner erzeugen
    optional Runtime mitliefern

Noch kürzer:

restore = Was brauche ich?
build   = Kompiliere mein Projekt.
publish = Erzeuge das, was ich weitergeben/deployen kann.