The Build Process in Detail
By the end of this lesson
Trace what happens from source files to deployable output.
dotnet build is not one action. It is a sequence of build steps, defined by the SDK and driven by MSBuild, that happens to be spelled as a single word.
Knowing the steps is worth the twenty minutes because build failures name them. A message about a package feed is not a compile error. A message about a duplicate output file is not a code error. Once you can place the message in the sequence, most build problems stop being mysterious.
What happens between your source files and the output folder:
Evaluate the project
MSBuild reads your project file, the SDK targets it imports, and any Directory.Build.props above it. This produces the full set of properties and the list of files to compile. A property you set in the wrong place has already had its effect, or not, by the end of this step.
Restore
Resolves the dependency graph and writes the results into obj, including project.assets.json, which records the exact assemblies the compiler should reference. Failures here mention packages, versions or feeds.
Compile
The C# compiler reads your source and the reference assemblies and emits one assembly containing intermediate language and metadata. This is where syntax and type errors appear, and it is the only step that reads your code.
Generate the run-time files
The SDK writes a .deps.json listing every assembly the application expects to load, and a .runtimeconfig.json recording which framework version it wants and any runtime options. Without these the runtime cannot work out what to load.
Copy and write output
Your assembly, the assemblies from referenced projects and packages, the generated files, debug symbols and any content marked for copying are written to bin under the configuration and target framework. On Windows you also get a small .exe launcher.
What you find in bin/Release/net10.0, and what each file is for:
- Anvi.Employees.Api.dll
- Your compiled code as intermediate language plus metadata. This is the assembly, and it is what actually runs.
- Anvi.Employees.Api.exe
- A small native launcher, on platforms that produce one. It finds the runtime and hands it the .dll. It contains none of your code, which surprises people who expect the .exe to be the program.
- Anvi.Employees.Api.runtimeconfig.json
- Which framework version the application asks for, plus runtime options such as garbage collector mode. This is the file that produces a version mismatch message when the machine cannot satisfy it.
- Anvi.Employees.Api.deps.json
- The manifest of expected dependencies. Delete it and the application will usually fail to find assemblies that are sitting right beside it.
- Anvi.Employees.Api.pdb
- Debug symbols: the mapping from intermediate language back to your source lines. Without it, exception stack traces have no file names or line numbers.
Debug and Release are two different compilations of the same source, and the differences are larger than the names suggest:
| Debug | Release | |
|---|---|---|
| Optimisations | Off. Locals are kept alive, methods are not inlined, and the generated code follows your source closely | On. Methods are inlined, dead code removed, locals discarded when no longer needed |
| Debugging experience | Stepping matches what you wrote; every variable is inspectable | Stepping can jump around and variables may be optimised away |
| Conditional code | The DEBUG symbol is defined, so #if DEBUG blocks and Debug.Assert calls are included | Those blocks and assertions are not compiled in at all |
| Performance | Meaningfully slower, and unpredictably so | Representative of production |
| Use it for | Writing and debugging code | Anything you deploy, and any measurement you intend to believe |
build and publish are not the same operation, and publish has two shapes. This is the table to reread before your first deployment:
| Framework-dependent publish | Self-contained publish | |
|---|---|---|
| Contains | Your assemblies, package assemblies, and the generated run-time files | All of that plus the entire runtime and base class library |
| Needs installed on the target | A matching major-version runtime | Nothing |
| Rough output size | Small — measured in single-digit megabytes for a typical API | Large — tens of megabytes before trimming |
| Runtime security patches | Arrive with the machine or base image update | Require you to rebuild and redeploy |
| Needs a target platform identifier | No — the output is portable | Yes, such as linux-x64 or win-x64 |
| Typical use | Containers on a runtime base image, and servers you administer | Tools handed to other people, and machines with no prerequisites |
# Development build. Fast, debuggable, not deployable
dotnet build
# What a deployment uses: optimised, with only the files needed to run
dotnet publish src/Anvi.Employees.Api -c Release -o ./artifacts/api
# Self-contained for a named platform, so the target needs no runtime
dotnet publish src/Anvi.Employees.Api -c Release -r linux-x64 --self-contained true -o ./artifacts/standalone
# Ahead-of-time compiled to native code for the same platform, for start-up time
dotnet publish src/Anvi.Employees.Api -c Release -r linux-x64 -p:PublishReadyToRun=true
# Compare the two output folders, file count and total size
dotnet build -c Release- publish defaults to Release configuration, while build and run default to Debug. That difference alone explains several confusing bug reports.
- -r names the platform. It is required for a self-contained build because the runtime being copied in is platform-specific.
- PublishReadyToRun pre-compiles intermediate language to native code ahead of time, so the first call to each method does not wait for compilation. It improves start-up and increases output size, and it does not remove the need for the runtime.
- The publish folder is deliberately narrower than bin: it drops build-time artifacts and keeps what the application needs to run. Look at both once and the distinction stops being abstract.
Summary
- A build is a sequence — evaluate, restore, compile, generate run-time files, copy output — and error messages name their stage
- The compiler emits intermediate language and metadata into one assembly; the .exe on Windows is a launcher, not the program
- .deps.json lists expected assemblies and .runtimeconfig.json names the framework version, which is how the runtime knows what to load
- Debug disables optimisations and includes #if DEBUG code, so every deployment and every measurement uses Release
- publish produces deployable output, either framework-dependent and small or self-contained and independent
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Try it yourself
Read both output folders
Run dotnet build -c Release on the employees API, then dotnet publish -c Release -o ./artifacts/api. Open both folders side by side.
Find the .deps.json and .runtimeconfig.json in each. Open the runtimeconfig file and read it — it is short. Which framework version does it name?
Show solution
The publish folder is the narrower of the two: it holds what the application needs at run time and drops build-time artifacts. That is the whole reason to deploy publish output rather than bin.
The runtimeconfig file names the framework and version your application will ask for at start-up. This is the concrete link between the TargetFramework you set in the project file and the version mismatch message a server produces. Seeing the request written down in a file makes that error much easier to reason about.
Think about it
Where did the base class library go?
A framework-dependent publish of the employees API produces maybe a dozen assemblies. Your code calls String, List, DateTime, File and HttpClient, and none of those appear as files in the folder.
Where are they, and what does that tell you about how the runtime loads code?
Show solution
They are in the shared framework installed on the machine. A framework-dependent application resolves those assemblies from the installed runtime rather than carrying its own copies, which is why the output is small and why the runtime is a prerequisite.
The runtime works out what to load from the .deps.json and .runtimeconfig.json files: one lists expected dependencies, the other names the framework version to satisfy them from. That is the mechanism behind both the small output and the mismatch error.
It also explains the self-contained trade directly. A self-contained publish copies those framework assemblies into your folder, so nothing needs installing and nothing outside your deployment can patch them.
Saved in this browser only.