📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What is the .NET CLI, and what do dotnet new, dotnet build, dotnet run and dotnet clean do?

The dotnet driver and its everyday commands: creating projects and solutions from templates, building, running and cleaning, and how the CLI relates to the SDK, MSBuild and the IDEs.

Beginner Theory Beginner developer-tooling dotnet-build dotnet-cli dotnet-new dotnet-run

🎯 Key points

  • `dotnet` is a driver: it runs apps and dispatches SDK commands, which mostly run MSBuild targets
  • `dotnet new` creates projects, solutions and files from templates (`dotnet new list`, `dotnet new install`)
  • `dotnet build` restores packages, then compiles to bin/<Configuration>/<TFM>
  • `dotnet run` restores and builds if needed, then runs the project; arguments after `--` go to the app
  • `dotnet clean` deletes the outputs a previous build produced for that configuration
  • Commands work the same on Windows, macOS, Linux and CI, which is why CI uses them
  • The SDK version used is chosen by global.json or the latest installed

⏱ 30-second answer

The .NET CLI is the dotnet command that ships with the SDK. It runs apps and drives the SDK's build system, which is MSBuild underneath. dotnet new creates a project, solution or file from a template. dotnet build restores NuGet packages and compiles the project into bin/Debug/net10.0. dotnet run builds if needed and then runs the app, and anything after -- is passed to the app as arguments. dotnet clean deletes the outputs of a previous build. The same commands work on every OS and in CI, and IDEs call the same SDK underneath.

🗣 Full answer (senior)

How I would answer this in an interview

"The .NET CLI is the dotnet command that ships with the SDK. It's a driver: it runs apps, and it dispatches SDK commands, which mostly run MSBuild targets underneath. The SDK version it uses comes from global.json, or the latest installed if there isn't one.

dotnet new creates a project, solution or file from a template; dotnet new list shows what's installed and dotnet new install adds more, like a company template pack. dotnet build restores NuGet packages first and then compiles into bin/Debug/net10.0. dotnet run restores and builds if needed, then runs the project, and anything after -- goes to the app as arguments. dotnet clean deletes the outputs a previous build produced for that configuration.

The same commands work on Windows, macOS, Linux and CI, which is why pipelines use them, and in CI I'd restore once and chain build, test and publish with --no-restore. Extra commands like dotnet ef come from .NET tools rather than the SDK itself. The mistake I see is expecting dotnet clean to fix package problems: it only removes build outputs for one configuration, so deleting bin and obj is the bigger hammer. And dotnet run is for development; production runs the published output. Because the CLI is the contract between developers, IDEs and CI, I make sure everything important can be done from the command line."


The driver

dotnet does two jobs: dotnet MyApp.dll runs a framework-dependent app, and dotnet <command> runs an SDK command. Most SDK commands are thin wrappers that run MSBuild targets: build runs the Build target, clean runs Clean, pack runs Pack. The SDK version is chosen per folder: global.json if present, otherwise the latest installed (dotnet --version shows which).

dotnet new

  • dotnet new list shows installed templates; dotnet new install <package> adds more (for example a company template pack).
  • dotnet new console -n Orders.Cli -o src/Orders.Cli -f net10.0 creates a console project. -n names it, -o sets the output folder, and -f net10.0 sets the target framework moniker so the project targets .NET 10 (it becomes <TargetFramework>net10.0</TargetFramework> in the .csproj). Without -f, the template uses the newest framework the installed SDK supports.
  • dotnet new sln -n Orders creates a solution file. A solution is just a list of projects that IDEs and the CLI build together. The .NET 10 SDK creates the newer .slnx format by default: an XML solution file that is much shorter and easier to merge than the classic .sln text format. --format sln gives the classic format if your tools still need it. dotnet sln add adds projects to either kind.
  • Item templates: dotnet new gitignore, dotnet new editorconfig, dotnet new globaljson, dotnet new nugetconfig.

dotnet build

Restores packages (unless --no-restore), then compiles every project and its references into bin/<Configuration>/<TFM>/, with intermediate files in obj/. Common options: -c Release, -f net10.0 (build only that target framework of a multi-targeted project), -bl for a binary log, -p:Property=Value to set an MSBuild property.

dotnet run

Runs one project. It restores and builds first unless --no-build, uses the first suitable launch profile in Properties/launchSettings.json (--launch-profile or --no-launch-profile to change that), and passes arguments after -- to your app: dotnet run --project src/Orders.Cli -- --region australiaeast. It is a development command; production runs the published output.

dotnet clean

Removes the files that a previous build of that configuration and framework wrote, using the list MSBuild recorded in obj/. It does not delete the restore output, other configurations or files you added by hand. When things are truly confused, deleting bin/ and obj/ is the bigger hammer.

Production example

A team's onboarding guide was twelve steps of IDE clicks. Replacing it with dotnet new templates from an internal template pack plus dotnet build and dotnet run meant the same steps worked on Windows, macOS and the CI agents, and new services started from an approved structure.

🔍 What the interviewer is checking

The interviewer checks basic CLI fluency and whether you understand what sits underneath.

Green flags

  • Knows the commands and their key options.
  • Mentions implicit restore, MSBuild and output folders.
  • Knows -- for app arguments and that run is for development.

Red flags

  • Only knows how to build from the IDE.
  • Thinks clean fixes everything.
  • Unaware SDK version selection exists.

📚 Learn in depth

1. One executable, two jobs

 dotnet <something>
     |
     v
 muxer (dotnet / dotnet.exe, first on PATH)
     |
     +-- "something" is a .dll path  --> HOST mode
     |      dotnet Orders.Cli.dll
     |      reads Orders.Cli.runtimeconfig.json, finds the runtime,
     |      loads the app. No SDK needed.
     |
     +-- "something" is a command    --> SDK mode
            dotnet build / new / run / test / publish / tool ...
            hostfxr picks an SDK (global.json, else newest installed)
            and runs the command from dotnet/sdk/<version>/

A production server running a framework-dependent app only needs the runtime. A developer machine or CI agent needs the SDK, which includes a runtime plus the CLI, MSBuild, the compiler and templates.

2. How SDK commands map to MSBuild

Most commands are thin wrappers around MSBuild targets in the project:

Command Runs Restores first?
dotnet restore Restore target (It is the restore)
dotnet build Build target Yes, unless --no-restore
dotnet clean Clean target No
dotnet run Build, then starts the app Yes, unless --no-restore / --no-build
dotnet test Build, then the test runner Yes
dotnet pack Pack target Yes
dotnet publish Publish target Yes
dotnet new Template engine, not MSBuild n/a

That is why dotnet build -p:Configuration=Release and dotnet build -c Release are the same thing, and why anything you can do in a .csproj you can also pass on the command line with -p:.

3. What dotnet build does, step by step

 dotnet build -c Release
   |
   +-- 1. resolve the SDK               <-- global.json / newest installed
   |
   +-- 2. implicit restore
   |      read PackageReference items, resolve the dependency graph,
   |      download to ~/.nuget/packages, write obj/project.assets.json
   |
   +-- 3. evaluate the project
   |      .csproj + Directory.Build.props/.targets + SDK props/targets
   |      --> properties and items
   |
   +-- 4. run the target graph
   |      ResolveReferences --> CoreCompile (csc) --> copy outputs
   |      (skips targets whose inputs haven't changed: incremental)
   |
   v
 bin/Release/net10.0/
   Orders.Cli.dll                    your code (IL)
   Orders.Cli.pdb                    symbols
   Orders.Cli.deps.json              dependency list for the host
   Orders.Cli.runtimeconfig.json     which runtime version to use
   Orders.Cli(.exe)                  apphost launcher
   Orders.Domain.dll                 referenced project
 obj/Release/net10.0/                intermediate files, file lists

The second build is much faster because MSBuild compares input and output timestamps and skips work that is up to date, and the compiler server is already running.

4. dotnet new in more depth

Command Purpose
dotnet new list Installed templates, optionally filtered: dotnet new list web
dotnet new search <term> Search NuGet.org for template packages
dotnet new install <package> Install a template pack, such as a company pack
dotnet new uninstall List or remove installed packs
dotnet new <template> --help Template-specific options
dotnet new console -n Orders.Cli -o src/Orders.Cli -f net10.0 Create a project with a name, folder and target framework
dotnet new sln -n Orders Create a solution (.slnx by default with the .NET 10 SDK)
dotnet new gitignore, editorconfig, globaljson, nugetconfig Common repo files

Without -f, a template targets the newest framework the installed SDK supports, so pass it explicitly in scripts.

5. dotnet run in more depth

 dotnet run --project src/Orders.Cli -c Release -- --region australiaeast
            |                         |          |  |
            which project             config     |  arguments for YOUR app
                                                 separator
Behaviour Detail
Build first Restores and builds unless --no-build (and --no-restore)
Configuration Debug by default; -c Release to change
Multi-targeted project Needs -f net10.0 to choose which framework to run
Launch profiles Uses the first Project profile in Properties/launchSettings.json; --launch-profile <name> or --no-launch-profile
Working directory The current directory, not the project folder
Exit code Returns your app's exit code, so scripts can check it
File-based apps (.NET 10) dotnet run app.cs runs a single C# file without a project

dotnet run is a development command. It checks for changes and may rebuild on every start, so production runs the published output instead.

6. dotnet clean and what it leaves behind

During a build, MSBuild writes a list of every file it produced to obj/<Configuration>/<TFM>/<Project>.csproj.FileListAbsolute.txt. dotnet clean deletes the files in that list.

Removed by dotnet clean Not removed
Outputs of the last build for that configuration and framework Other configurations (clean without -c Release leaves Release)
Copied references in bin/ Restore output (obj/project.assets.json, *.nuget.g.props)
Intermediate compiler outputs The NuGet global packages folder
Files you or a custom script added

For a stubborn problem, deleting bin/ and obj/ (and running dotnet build-server shutdown) is the bigger hammer. For package problems, dotnet nuget locals all --clear clears NuGet caches.

7. Useful options on most commands

Option Effect
-c Release Configuration
-f net10.0 Target framework
-r linux-x64 Runtime identifier
-p:Name=Value Set an MSBuild property
-v n / -v d Verbosity: normal or detailed
-bl Write msbuild.binlog, a full record of the build for the MSBuild Structured Log Viewer
--no-restore / --no-build Skip earlier steps that already ran
-tl:off Turn off the terminal logger

8. .NET 10 CLI changes worth knowing

Change Detail
File-based apps dotnet run app.cs, with #:package and #:property directives at the top of the file; dotnet project convert app.cs turns it into a project
.slnx by default New solutions use the XML format; dotnet sln migrate converts an existing .sln
Noun-first command forms dotnet package add, dotnet reference add alongside the older dotnet add package
One-shot tools dotnet tool exec (or dnx) runs a .NET tool without installing it
Terminal logger The compact live output (default since .NET 9) in interactive terminals; CI output stays plain

9. Performance

  • MSBuild node reuse and the Roslyn compiler server keep processes warm between builds. dotnet build-server shutdown stops them if they hold stale state.
  • In CI, restore once and pass --no-restore to later commands, and --no-build to test and publish after a build.
  • -bl is the fastest way to see why a build is slow or why a target ran.

10. .NET 11 awareness

Treat CLI changes in .NET 11 previews as awareness only. Check the SDK release notes before relying on a new command or default in scripts.

11. Try it yourself

  1. Run the commands in the code example.
  2. Run dotnet build twice and compare the times.
  3. Look in bin/Release/net10.0/ and open the .runtimeconfig.json and .deps.json files.
  4. Run dotnet clean (no -c) and notice the Release output is still there; then run dotnet clean -c Release.
  5. Run dotnet run --project src/Orders.Cli -- --region australiaeast, then try it without -- and read the error.
  6. Build with -bl and open msbuild.binlog in the MSBuild Structured Log Viewer.
  7. Create hello.cs containing Console.WriteLine("G'day"); and run dotnet run hello.cs.

💻 Code

Create a solution with a console app and a library, then build, run and clean:

dotnet new sln -n Orders
dotnet new console -n Orders.Cli -o src/Orders.Cli -f net10.0
dotnet new classlib -n Orders.Domain -o src/Orders.Domain -f net10.0
dotnet sln add src/Orders.Cli src/Orders.Domain
dotnet add src/Orders.Cli reference src/Orders.Domain

dotnet build -c Release
dotnet run --project src/Orders.Cli -- --region australiaeast
dotnet clean -c Release

src/Orders.Cli/Program.cs:

var region = args.SkipWhile(a => a != "--region").Skip(1).FirstOrDefault() ?? "australiaeast";
Console.WriteLine($"Orders CLI running for region {region} on {System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription}");

⚠ Common mistakes

  • Mistake: thinking dotnet run is how production runs the app. It checks for changes, may rebuild, uses launch profiles and runs from the SDK, none of which belongs on a server. Instead: run the published output in production: dotnet Orders.Cli.dll or the native executable from dotnet publish.

  • Mistake: forgetting the -- before app arguments. Without it, the CLI treats --region as its own option and fails or ignores it. Instead: put CLI options first, then --, then your app's arguments.

  • Mistake: expecting dotnet clean to fix restore or package problems. It only deletes build outputs for one configuration and doesn't touch restore output or NuGet caches. Instead: delete bin/ and obj/, run dotnet restore --force, or clear caches with dotnet nuget locals all --clear.

  • Mistake: running dotnet clean without the configuration you built. dotnet clean defaults to Debug, so Release outputs stay behind. Instead: pass the same -c (and -f) you used to build.

  • Mistake: not knowing which SDK is running. A different SDK on a CI agent or a colleague's machine gives different warnings and results. Instead: run dotnet --version (and dotnet --info) in the repo folder, and pin the SDK in global.json.

  • Mistake: relying on template defaults in scripts. Without -f, a template targets the newest framework the installed SDK supports, so the same script creates different projects on different machines. Instead: always pass -f net10.0 (and other options) explicitly in scripts and documentation.

  • Mistake: assuming the .NET 10 SDK changes existing solutions. Only new solutions created with dotnet new sln default to .slnx; an existing .sln stays as it is. Instead: convert deliberately with dotnet sln migrate, after checking every tool in your pipeline can read .slnx.

  • Mistake: building Debug in CI and Release locally, or the other way round. Optimisations, conditional code and warnings differ, so results don't compare. Instead: use the same configuration everywhere you compare results, and Release for anything you ship.

  • Mistake: running dotnet run on a multi-targeted project without -f. The CLI can't choose a framework and fails. Instead: pass -f net10.0 (or whichever framework you want to run).

  • Mistake: committing bin/ and obj/. They are machine-specific build outputs that cause merge conflicts and stale builds. Instead: add a .gitignore with dotnet new gitignore.

🏛 Senior insight: trade-offs

The CLI is the contract between developers, IDEs and CI. A senior engineer makes sure everything important can be done from the command line, because then it can be scripted, reproduced and automated, and IDE differences stop mattering. They standardise project creation with an internal dotnet new template pack (logging, health checks, analyser settings, Dockerfile already in place), which spreads good defaults faster than any wiki page.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate mentions that dotnet run also builds the project. Why: Checks whether the candidate understands that commands chain implicit restore and build, and how CI uses that.
    How to answer

    Restore works out the full NuGet package graph, downloads packages to the global packages folder and writes obj/project.assets.json. Build compiles against those assets into bin/<Configuration>/<TFM>. Test runs the test projects against the built assemblies. Publish produces a deployable folder with the app, its dependencies and runtime config, in Release by default since .NET 8. Each later command restores and builds implicitly, so in CI I restore once and pass --no-restore and --no-build. That way every step uses exactly the same outputs and nothing is repeated. The configuration has to match across steps.

  2. When: Candidate mentions dotnet ef or another command that isn't built into the SDK. Why: Sees whether the candidate knows the CLI is extensible and how teams pin extra tooling.
    How to answer

    A .NET tool is a console app shipped as a special NuGet package. Global tools (dotnet tool install -g) are installed per user and land on your PATH. Local tools are pinned per repository in a manifest, .config/dotnet-tools.json, created with dotnet new tool-manifest. Anyone who clones the repo runs dotnet tool restore and gets exactly the same versions. For team and CI work I prefer local tools, because the version lives in source control next to the code that depends on it.

↩ Usually asked after

More in .NET CLI

Last updated 1 Nov 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me