Skip to main content
ANVISoftware Solutions
Lesson 6 of 12Intermediate18 min

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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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:

 DebugRelease
OptimisationsOff. Locals are kept alive, methods are not inlined, and the generated code follows your source closelyOn. Methods are inlined, dead code removed, locals discarded when no longer needed
Debugging experienceStepping matches what you wrote; every variable is inspectableStepping can jump around and variables may be optimised away
Conditional codeThe DEBUG symbol is defined, so #if DEBUG blocks and Debug.Assert calls are includedThose blocks and assertions are not compiled in at all
PerformanceMeaningfully slower, and unpredictably soRepresentative of production
Use it forWriting and debugging codeAnything 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 publishSelf-contained publish
ContainsYour assemblies, package assemblies, and the generated run-time filesAll of that plus the entire runtime and base class library
Needs installed on the targetA matching major-version runtimeNothing
Rough output sizeSmall — measured in single-digit megabytes for a typical APILarge — tens of megabytes before trimming
Runtime security patchesArrive with the machine or base image updateRequire you to rebuild and redeploy
Needs a target platform identifierNo — the output is portableYes, such as linux-x64 or win-x64
Typical useContainers on a runtime base image, and servers you administerTools handed to other people, and machines with no prerequisites
Producing each kind of output
Shell
# 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.