The SDK and the dotnet CLI
Drive the build, run, test and publish loop with the dotnet CLI, and pin the SDK with global.json so every machine builds the same way.
dotnet new webapi -o src/Orders.Api
dotnet build
dotnet run --project src/Orders.Api
dotnet test
dotnet publish src/Orders.Api -o artifacts/orders-apiThe SDK is the build toolchain: the dotnet CLI, MSBuild, the Roslyn compilers, NuGet and the project templates. Most CLI commands are thin front ends over MSBuild targets, so dotnet build on a laptop and in CI run the same targets. SDK versions read as major.minor.feature-band plus patch: 10.0.402 is the 4xx feature band of the 10.0 SDK, patch 02. Feature bands add tooling within a major version; patches ship alongside the monthly runtime patches.
The everyday commands
| Command | Does | Default worth knowing |
|---|---|---|
dotnet new webapi | Creates a project from a template | Minimal APIs plus OpenAPI; --use-controllers for controllers. Also web (empty), worker, classlib, sln |
dotnet build | Restores and compiles | Debug configuration, output in bin/Debug/net10.0 |
dotnet run | Builds and starts the app | Applies the first profile in Properties/launchSettings.json |
dotnet watch | Runs and hot-reloads on save | Development only; edits it cannot apply need a restart |
dotnet test | Builds and runs test projects | Runs every test project in the solution |
dotnet publish | Produces deployable output | Release configuration by default since .NET 8; framework-dependent |
The webapi, web and worker templates differ only in what they scaffold; all three sit on the same host. Check which xUnit major version the built-in xunit template references before keeping it: this course uses the xunit.v3 package.
Which SDK runs: global.json
The dotnet muxer walks up from the current directory to the first global.json. Without one it uses the newest installed SDK, which on a laptop may be a preview of the next release. With one, rollForward decides how strict it is. The default, latestPatch, accepts only the same feature band (10.0.4xx), so a CI image that moves to 10.0.5xx fails with "A compatible .NET SDK was not found". latestFeature accepts any newer 10.0 band and never jumps to 11, which is what most teams want. latestMajor lets a new major SDK in silently, with new analyzers that can break a warnings-as-errors build.
dotnet run is not production
dotnet run builds Debug and applies the launch profile, which sets ASPNETCORE_ENVIRONMENT=Development and the URLs. Published apps never read launchSettings.json: they start as Production, on port 8080 in the official container images or http://localhost:5000 elsewhere. Bugs that only appear in production (missing configuration, the developer exception page hiding an error format, user secrets not loaded) reproduce locally with dotnet run --no-launch-profile. Arguments after -- go to the app, not to the CLI.
Diagnosing a machine
dotnet --version prints the SDK that global.json selects in the current folder, --list-sdks and --list-runtimes show what is installed, and dotnet --info prints both plus the OS, architecture and RID. Paste --info into every build-failure ticket.
Example
# One solution with an API, a worker and a test project, from an empty repository folder.
dotnet new globaljson --sdk-version 10.0.402 --roll-forward latestFeature
dotnet new gitignore
dotnet new sln -n Shop # Shop.slnx with the .NET 10 SDK
dotnet new webapi -o src/Orders.Api # minimal APIs + OpenAPI
dotnet new worker -o src/Orders.Worker # queue consumer, no HTTP server
dotnet new xunit -o tests/Orders.Api.Tests
dotnet sln add src/Orders.Api src/Orders.Worker tests/Orders.Api.Tests
dotnet reference add src/Orders.Api/Orders.Api.csproj --project tests/Orders.Api.Tests
# Inner loop
dotnet build # Debug, every project in Shop.slnx
dotnet test # builds first, then runs the tests
dotnet watch --project src/Orders.Api # hot reload while editing
# Run it the way production will: no launch profile, Production environment
dotnet run --project src/Orders.Api --no-launch-profile -e ASPNETCORE_ENVIRONMENT=Production
# Deployable output (Release is the default for publish)
dotnet publish src/Orders.Api -o artifacts/orders-api
dotnet artifacts/orders-api/Orders.Api.dllWhen to use it
- Pin a repository to the 10.0.4xx SDK band so a laptop with an 11.0 preview installed builds exactly what CI builds.
- Reproduce a production-only bug locally by running the API with --no-launch-profile and ASPNETCORE_ENVIRONMENT=Production.
- Scaffold an orders solution with an API, a worker and a test project in a dozen commands, ready for CI.
More examples
global.json that pins the SDK band
Commit this at the repository root. <code>version</code> is the minimum; <code>latestFeature</code> lets a developer with 10.0.5xx installed build without editing the file, while a machine that only has the .NET 11 SDK gets a clear error instead of silently compiling with new analyzers and a different default C# version. <code>allowPrerelease: false</code> stops a preview SDK on a laptop from being chosen.
{
"sdk": {
"version": "10.0.402",
// Accept 10.0.402 or any newer 10.0 feature band, never an 11.0 SDK.
"rollForward": "latestFeature",
"allowPrerelease": false
}
}launchSettings.json: why local runs are Development
The template generates this file under <code>Properties/</code>. <code>dotnet run</code>, Visual Studio and Rider apply the selected profile; <code>dotnet publish</code> does not copy it and the container never sees it. Keep only developer conveniences here (URL, environment) and never put settings the app needs in production into it, or the deployed app will behave differently from the one you tested.
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5228",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}Diagnosing which SDK and runtime a machine uses
When a build fails on one machine and not another, compare <code>dotnet --version</code> run inside the repository folder: it honours <code>global.json</code>, so it shows the SDK that actually builds. If no compatible SDK is installed the CLI names the <code>global.json</code> file it read, which catches a stray file in a parent directory.
# In the repository folder: the SDK that global.json selects here
dotnet --version
# Everything installed, to compare a laptop with the CI image
dotnet --list-sdks
dotnet --list-runtimes
# Full picture for a ticket: SDK, runtimes, OS, architecture, RID, global.json path
dotnet --info
# Quieter CI logs: no banner, no telemetry
export DOTNET_NOLOGO=1
export DOTNET_CLI_TELEMETRY_OPTOUT=1
dotnet build -c Release -warnaserror
Discussion