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

The dotnet CLI

By the end of this lesson

Create, build, test, run and publish projects from the command line.

The dotnet command is the front door to the build system. Your editor's build button calls it. Your continuous integration pipeline calls it. A container build calls it. There is no separate, more official route that the tooling uses behind your back.

That is the argument for learning it even if you normally press a button. When a build fails in the pipeline and passes on your machine, the only way to compare the two is to know what the pipeline ran. A log full of dotnet commands is unreadable until the commands mean something to you.

It is also the fastest way to work on a machine you do not own, over a remote connection, or inside a container.

Scaffolding the employees solution from nothing
Shell
# A solution file: a list of projects, used by editors and by the build
dotnet new sln -n Anvi.Employees

# Three projects: the API, the domain library, and the tests
dotnet new webapi -o src/Anvi.Employees.Api
dotnet new classlib -o src/Anvi.Employees.Core
dotnet new xunit -o tests/Anvi.Employees.Tests

# Register them with the solution
dotnet sln add src/Anvi.Employees.Api src/Anvi.Employees.Core tests/Anvi.Employees.Tests

# Wire up the references between projects
dotnet add src/Anvi.Employees.Api reference src/Anvi.Employees.Core
dotnet add tests/Anvi.Employees.Tests reference src/Anvi.Employees.Core

# Add a package to one project, not to the solution
dotnet add src/Anvi.Employees.Api package Microsoft.Extensions.Hosting

# Build everything the solution knows about
dotnet build Anvi.Employees.sln
  • dotnet new takes a template name and -o for the output folder. Run dotnet new list to see what is installed, which includes console, classlib, webapi, worker, xunit and several others.
  • A solution file holds no build logic. It lists projects so that one command can act on all of them and so editors know what belongs together.
  • dotnet sln add accepts several projects at once. Forgetting this step is the usual reason a new test project is invisible to dotnet test at the solution level.
  • dotnet add takes the target project first, then reference or package. Adding a package to a solution is not a thing — dependencies belong to projects, because that is where they are recorded.
  • Building the solution builds projects in dependency order. Anvi.Employees.Core compiles before the two projects that reference it, without you arranging that.

The verbs, and what each one actually does:

dotnet restore
Resolves the dependency graph and downloads packages into the local cache, writing the results into obj. Rarely run by hand, because build, run, test and publish do it first unless told not to.
dotnet build
Compiles into bin. Produces output that runs on this machine with this SDK. It is not a deployment package, even though it looks like one.
dotnet run
Builds if needed, then starts the program. A development convenience. Use --project when you are not standing in the project folder, and put program arguments after a bare double dash.
dotnet test
Builds the test projects and runs their tests. Exits non-zero when anything fails, which is how a pipeline knows to stop.
dotnet publish
Produces the folder you deploy: your assemblies, their dependencies, the configuration files marked for copying, and the files the runtime needs to launch the application.
dotnet watch
Re-runs build and run whenever a file changes. Development only. It is a file watcher wrapped around the commands above, not a different build path.
The loop you will actually spend your day in
Shell
# Compile only — the quickest check that nothing is broken
dotnet build

# Start the API without changing directory
dotnet run --project src/Anvi.Employees.Api

# Pass arguments to your program, not to the CLI
dotnet run --project src/Anvi.Employees.Tool -- --import employees.csv

# Rebuild and restart on every save
dotnet watch --project src/Anvi.Employees.Api

# Run one group of tests while working on them
dotnet test --filter "FullyQualifiedName~LeaveRequest"

# Release configuration, which is what a deployment uses
dotnet build -c Release
  • The bare double dash separates CLI arguments from your program's arguments. Without it, dotnet tries to interpret --import itself and reports an unknown option.
  • --filter narrows a test run by name. It saves real time on a suite that takes minutes, and it is the flag most people discover far too late.
  • -c is short for --configuration. Debug is the default for build and run; Release is the default for publish. Mixing them up is the subject of the build lesson.
What a pipeline runs, and why the flags look defensive
Shell
# Restore once, explicitly, so a failure here is reported as a restore failure
dotnet restore Anvi.Employees.sln

# Build without restoring again
dotnet build Anvi.Employees.sln -c Release --no-restore

# Test the assemblies that were just built, rather than rebuilding them
dotnet test Anvi.Employees.sln -c Release --no-build

# Publish the same build to a known folder
dotnet publish src/Anvi.Employees.Api -c Release --no-build -o ./artifacts/api
  • Each step is separate so that a failure names its own stage. A single command that restores, builds and tests gives you one red step and a log to read; four commands give you the answer in the step name.
  • --no-restore and --no-build stop later steps repeating earlier work. That is partly speed, but mainly certainty: the assemblies you test are the assemblies you published, not a second compilation that happened to produce the same thing.
  • The configuration must match across the steps. Build in Release and publish in Debug and the --no-build flag fails, because the output it expects to find is not there. That error message is confusing the first time and obvious afterwards.
  • Nothing here needs a CI product to run. Paste these four lines into a terminal and you have reproduced the pipeline locally, which is the fastest way to debug one.

Summary

  • The CLI is the same build system your editor and your pipeline use, so the commands are worth knowing
  • dotnet new scaffolds, sln add registers projects, add reference and add package record dependencies in the project file
  • build compiles for this machine; publish produces the folder you deploy
  • Pipelines split restore, build, test and publish so failures name their stage, and use --no-restore and --no-build so later steps reuse earlier output
  • Program arguments go after a bare double dash, and --project saves you changing directory

Practice

Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.

Try it yourself

Build the solution from a blank folder

Using only the command line, create the three-project employees solution from the first example: API, domain library and tests, with the references wired up.

Then run dotnet build on the solution and read the output order. Which project compiled first, and why?

Show solution

Anvi.Employees.Core compiles first. The build works out the order from the project references — the API and the tests both depend on it, so it has to exist before they can compile against it.

This is worth seeing once because it explains a class of confusing failures. An error in a library project causes the projects that reference it to report missing types, and the useful message is the first one, not the last twenty.

Think about it

Why does CI pass --no-build to the test step?

A pipeline runs build then test, and the test step is given --no-build. A colleague argues this is a fragile optimisation and wants to remove it.

Make the case for keeping it, and name the condition under which removing it would be the right call.

Show solution

The strongest argument is not speed, it is identity. With --no-build, the tests run against the exact assemblies the build produced and the publish step will ship. Without it, the test step compiles again, and you are testing a different set of files that you believe to be identical.

That belief is usually correct and occasionally not: a source file changed between steps, a different configuration was passed, an analyzer behaved differently. Each of those produces a pipeline that tests one thing and ships another.

Removing it is reasonable when the test step runs on a different agent that does not have the build output, because then there is nothing to reuse. At that point you either publish the build as an artifact and restore it, or accept the recompile deliberately.

Saved in this browser only.