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.
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.
"The .NET CLI is the
dotnetcommand 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 fromglobal.json, or the latest installed if there isn't one.
dotnet newcreates a project, solution or file from a template;dotnet new listshows what's installed anddotnet new installadds more, like a company template pack.dotnet buildrestores NuGet packages first and then compiles intobin/Debug/net10.0.dotnet runrestores and builds if needed, then runs the project, and anything after--goes to the app as arguments.dotnet cleandeletes 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 likedotnet efcome from .NET tools rather than the SDK itself. The mistake I see is expectingdotnet cleanto fix package problems: it only removes build outputs for one configuration, so deletingbinandobjis the bigger hammer. Anddotnet runis 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."
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 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.dotnet new gitignore, dotnet new editorconfig, dotnet new globaljson, dotnet new nugetconfig.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.
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.
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.
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.
The interviewer checks basic CLI fluency and whether you understand what sits underneath.
Green flags
-- for app arguments and that run is for development.Red flags
clean fixes everything. 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.
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:.
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.
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.
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.
dotnet clean and what it leaves behindDuring 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.
| 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 |
| 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 |
dotnet build-server shutdown stops them if they hold stale state.--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.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.
dotnet build twice and compare the times.bin/Release/net10.0/ and open the .runtimeconfig.json and .deps.json files.dotnet clean (no -c) and notice the Release output is still there; then run dotnet clean -c Release.dotnet run --project src/Orders.Cli -- --region australiaeast, then try it without -- and read the error.-bl and open msbuild.binlog in the MSBuild Structured Log Viewer.hello.cs containing Console.WriteLine("G'day"); and run dotnet run hello.cs.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}");
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.
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.
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.
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.
Last updated 1 Nov 2026.