Ü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.csprojDer 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 aus2. 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.dllDiese .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-arm64erzeugt. - Auf einem Intel-Linux-System wird Code für
linux-x64erzeugt. - Auf Windows kann Code für
win-x64erzeugt 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.pdbMyApp / 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:
./MyAppbzw.
MyApp.exestatt:
dotnet MyApp.dllMyApp.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.0odernet10.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:
| Eintrag | Bedeutung |
|---|---|
OutputType | Gibt an, ob eine ausführbare Anwendung oder eine Library gebaut wird |
TargetFramework | Gibt an, für welche .NET-Version gebaut wird |
PackageReference | Verweist auf NuGet-Packages |
RuntimeIdentifier | Gibt 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 restoreDabei 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.jsonDiese Datei beschreibt den aufgelösten Dependency Graph.
Vereinfacht:
restore = Dependencies finden, herunterladen und vorbereitenWichtig: Viele andere Kommandos führen restore automatisch aus, wenn es nötig ist, zum Beispiel:
dotnet build
dotnet run
dotnet test
dotnet publishDu musst dotnet restore daher oft nicht manuell ausführen.
6. dotnet build
dotnet build kompiliert das Projekt.
Typischer Befehl:
dotnet buildOder als Release-Build:
dotnet build -c Releaseflowchart 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.pdbVereinfacht:
build = Source Code in ausführbare Build-Artefakte übersetzenWann 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 ReleaseDer 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 erzeugen8. Unterschied zwischen build und publish
| Thema | dotnet build | dotnet publish |
|---|---|---|
| Hauptzweck | Projekt kompilieren | Anwendung bereitstellen |
| Typische Verwendung | Entwicklung, Tests, CI | Deployment, Weitergabe, Container |
| Output | Build-Ordner | Publish-Ordner |
| Pfad | bin/Release/net10.0/ | bin/Release/net10.0/publish/ |
| Runtime enthalten? | Normalerweise nein | Optional bei self-contained |
| Optimiert für Deployment? | Nicht primär | Ja |
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 ReleaseOder explizit:
dotnet publish -c Release --self-contained falseTypischer Output:
MyApp
MyApp.dll
MyApp.runtimeconfig.json
MyApp.deps.json
MyApp.pdbVorteile
- 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 trueBeispiel für Intel/AMD Linux:
dotnet publish -c Release -r linux-x64 --self-contained trueBeispiel für Windows x64:
dotnet publish -c Release -r win-x64 --self-contained trueVorteile
- 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
| Frage | Framework-dependent | Self-contained |
|---|---|---|
| Enthält die App die .NET Runtime? | Nein | Ja |
| Muss .NET auf dem Zielsystem installiert sein? | Ja | Nein |
| Output-Größe | Kleiner | Größer |
| Plattformabhängig? | Kann portabler sein | Immer für eine konkrete Runtime-ID |
| Typische Verwendung | Server, Entwicklerumgebungen, Container mit Runtime-Image | Desktop-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-arm64Du brauchst einen RID besonders dann, wenn du für eine konkrete Plattform publishen möchtest.
Beispiel:
dotnet publish -c Release -r osx-arm64Ab 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 trueWenn du framework-dependent mit einer konkreten Runtime-ID willst:
dotnet publish -c Release -r osx-arm64 --self-contained false13. Beispiel: kompletter Ablauf
Angenommen, du hast dieses Projekt:
MyApp/
├── MyApp.csproj
└── Program.csSchritt 1: Dependencies wiederherstellen
dotnet restoreOutput-Idee:
MyApp/
├── obj/
│ └── project.assets.jsonSchritt 2: Build ausführen
dotnet build -c ReleaseOutput-Idee:
MyApp/
├── bin/
│ └── Release/
│ └── net10.0/
│ ├── MyApp
│ ├── MyApp.dll
│ ├── MyApp.runtimeconfig.json
│ ├── MyApp.deps.json
│ └── MyApp.pdbSchritt 3: Publish erstellen
dotnet publish -c ReleaseOutput-Idee:
MyApp/
├── bin/
│ └── Release/
│ └── net10.0/
│ └── publish/
│ ├── MyApp
│ ├── MyApp.dll
│ ├── MyApp.runtimeconfig.json
│ ├── MyApp.deps.json
│ └── MyApp.pdbBei 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 Libraries14. 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 buildRelease-Build
dotnet build -c ReleaseFramework-dependent publish
dotnet publish -c Release --self-contained falseSelf-contained publish für macOS Apple Silicon
dotnet publish -c Release -r osx-arm64 --self-contained trueSelf-contained publish für Linux x64
dotnet publish -c Release -r linux-x64 --self-contained trueSelf-contained publish für Windows x64
dotnet publish -c Release -r win-x64 --self-contained trueSingle-file publish
dotnet publish -c Release -r osx-arm64 --self-contained true -p:PublishSingleFile=trueDabei 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 mitliefernNoch kürzer:
restore = Was brauche ich?
build = Kompiliere mein Projekt.
publish = Erzeuge das, was ich weitergeben/deployen kann.