The .NET CLI I Use Before I Open the IDE

· 10 min read

An IDE can make a repository feel ready before the repository is actually ready. It may select an SDK for you, restore packages in the background, build only the project you opened, or hide a test and formatting failure behind a notification.

The .NET CLI gives a clean checkout a more explicit preflight. This post is for .NET developers starting work in an existing repository or checking a small sample before opening an IDE. The promise is modest: inspect the selected SDK, find the solution, restore once, build, test, and verify formatting with commands that can be run again without changing application source.

The examples target the .NET SDK 10.0.400 and net10.0. They assume an SDK-style repository with a solution or project file. They do not cover IDE setup, package upgrades, CI design, deployment, integration-test infrastructure, or making a repository production-ready.

Start at the repository root

The first failure to avoid is running a correct command from the wrong directory. In a Git checkout, move to the repository root before looking for a solution:

cd "$(git rev-parse --show-toplevel)"

If the directory is not a Git checkout, change to the directory that contains the repository’s solution or project files instead. The rest of this post uses Bash syntax; the .NET commands themselves also work from PowerShell.

Check the SDK that will actually run

These commands answer different questions:

dotnet --version
dotnet --info
dotnet --list-sdks
  • dotnet --version shows the SDK selected for the current directory.
  • dotnet --info shows the selected SDK, host, runtimes, operating system, and other environment details.
  • dotnet --list-sdks lists SDKs installed on the machine.

The list of installed SDKs is not the same thing as the SDK selected for the repository. A global.json file can select an SDK version and define how version roll-forward works. Check the repository’s file before assuming that the newest entry from --list-sdks is the right one:

if [ -f global.json ]; then
    cat global.json
else
    echo "No global.json at the repository root"
fi

The .NET SDK also searches parent directories for global.json. The useful check is therefore dotnet --version from the repository root, not just a visual inspection of the installed SDK list.

For a team repository, commit global.json when a specific SDK selection is part of the build contract. Do not copy the version from this post blindly; use a feature band that the repository supports and that your build environment can install.

Discover the solution before building it

A repository can contain more than one solution, a solution plus sample projects, or no solution at all. List the candidates without searching generated build output:

find . -type f \
    \( -name '*.slnx' -o -name '*.sln' -o \
       -name '*.csproj' -o -name '*.fsproj' -o -name '*.vbproj' \) \
    -not -path './.git/*' \
    -not -path '*/bin/*' \
    -not -path '*/obj/*' |
    sort

Choose the solution that represents the work you are about to do. Do not silently select the first match when a repository has several:

solution="path/to/App.slnx"
dotnet sln "$solution" list

The current .NET 10 SDK can create and inspect XML solution files with the .slnx extension, while existing .sln files remain valid inputs. If the repository uses an older SDK or an older solution format, use the file that is already checked in rather than converting it just to follow this example.

If there is no solution, run the same workflow against the project that is the intended entry point:

project="src/App/App.csproj"
dotnet restore "$project"
dotnet build "$project" --configuration Release --no-restore
dotnet test "$project" --configuration Release --no-build
dotnet format "$project" --no-restore --verify-no-changes

dotnet restore and dotnet build can search the current directory when no file is supplied, but an explicit path is easier to review and safer when the checkout contains multiple projects.

A tiny sample to make the workflow concrete

The preflight is easier to understand with one library and one test project. The sample has no database, cloud service, or production credentials:

mkdir CliPreflight
cd CliPreflight

dotnet new sln --name CliPreflight --format slnx --no-update-check
dotnet new classlib --framework net10.0 \
    --name CliPreflight.Core \
    --output src/CliPreflight.Core \
    --no-restore
dotnet new xunit --framework net10.0 \
    --name CliPreflight.Tests \
    --output tests/CliPreflight.Tests \
    --no-restore

dotnet sln CliPreflight.slnx add \
    src/CliPreflight.Core/CliPreflight.Core.csproj \
    tests/CliPreflight.Tests/CliPreflight.Tests.csproj
dotnet add tests/CliPreflight.Tests/CliPreflight.Tests.csproj \
    reference src/CliPreflight.Core/CliPreflight.Core.csproj

The --no-restore switches on the template commands keep project creation separate from dependency restoration. Add a global.json at the sample root if you want to make the SDK selection explicit:

{
  "sdk": {
    "version": "10.0.400",
    "rollForward": "latestPatch",
    "allowPrerelease": false
  }
}

Replace that version with one supported by your repository and build agents. The sample’s library contains one small behavior:

namespace CliPreflight.Core;

public static class Greeting
{
    public static string Create(string name)
    {
        return string.IsNullOrWhiteSpace(name)
            ? "Hello, developer!"
            : $"Hello, {name.Trim()}!";
    }
}

The test project verifies the behavior without requiring an external service:

using CliPreflight.Core;

namespace CliPreflight.Tests;

public class GreetingTests
{
    [Fact]
    public void Create_trims_the_name()
    {
        Assert.Equal("Hello, Ada!", Greeting.Create(" Ada "));
    }
}

The resulting shape is intentionally boring:

CliPreflight/
├── CliPreflight.slnx
├── global.json
├── src/
│   └── CliPreflight.Core/
│       ├── CliPreflight.Core.csproj
│       └── Greeting.cs
└── tests/
    └── CliPreflight.Tests/
        ├── CliPreflight.Tests.csproj
        └── UnitTest1.cs

The template-generated project files also contain the test framework package references. Their exact versions come from the installed template and should be treated as dependency data, not as a promise that every future template will generate the same versions.

Run the preflight in a deliberate order

Once solution points to the selected solution, run:

dotnet restore "$solution"
dotnet build "$solution" --configuration Release --no-restore
dotnet test "$solution" --configuration Release --no-build
dotnet format "$solution" --no-restore --verify-no-changes

The order makes each boundary visible:

  1. Restore resolves the projects’ dependencies and creates the assets needed by later commands.
  2. Build compiles the complete selected solution in Release mode. --no-restore prevents a hidden second restore.
  3. Test runs the test assemblies currently present. --no-build also avoids an implicit build and restore, which makes the prerequisite explicit; it does not prove that the assemblies are fresh, so run it immediately after the intended build.
  4. Format verification reads the repository’s formatting and analyzer configuration. --verify-no-changes checks whether formatting would change files and exits unsuccessfully if it would; it does not silently rewrite the checkout.

These commands are safe to rerun at the source level, but they are not side-effect free. Restore writes package and assets information, build writes bin and obj, tests can create result files or modify a test database, and formatting without --verify-no-changes can edit source files. “Safe to rerun” means that the normal preflight does not delete source or deploy anything; inspect the repository’s test setup before running it.

A clean-checkout checklist

For an existing repository, this is the short version:

cd "$(git rev-parse --show-toplevel)"

dotnet --version
dotnet --info
dotnet --list-sdks

solution="path/to/App.slnx"
dotnet sln "$solution" list
dotnet restore "$solution"
dotnet build "$solution" --configuration Release --no-restore
dotnet test "$solution" --configuration Release --no-build
dotnet format "$solution" --no-restore --verify-no-changes

The solution path is intentionally explicit. Discovery tells you what exists; choosing the path is a decision about what this checkout represents. If a repository has separate application and test solutions, run the appropriate preflight for each rather than pretending that one solution covers the other.

Failure modes that are useful signals

The selected SDK is not installed

dotnet --list-sdks is an inventory, not an installation command. If dotnet --version fails because global.json requests an unavailable SDK, install the requested SDK or agree on a repository change. Do not remove global.json just to make a local command pass; that replaces a visible version problem with a hidden machine dependency.

Restore cannot reach a package source

Private feeds, expired credentials, offline package caches, and source outages can all fail restore. Check the repository’s NuGet.config and the configured source policy. Do not put a feed token in a command, a project file, or a committed configuration file. Use the approved credential provider or secret mechanism, and do not use --ignore-failed-sources as a way to declare an incomplete dependency graph healthy.

--no-restore or --no-build fails

That is often the intended signal. Run restore first, then build, then test. The speed flags are safe only when the prerequisite completed for the same solution, configuration, framework, and runtime. If the project targets several frameworks, make those dimensions explicit instead of treating one successful target as complete validation.

There are several solution files

The CLI cannot know whether App.slnx, Samples.sln, or a test-only solution is the right entry point. Use find to inventory them and pass the selected path to every command. A short command that tests the wrong solution is worse than a longer command that states its scope.

Formatting verification fails

A non-zero result from dotnet format --verify-no-changes means the repository’s configured formatting or analyzer rules would make a change. Review the diff, decide whether the change belongs in the current branch, and run the formatter deliberately if it does. Do not turn a preflight check into an unreviewed mass rewrite.

Tests need services that are not in the checkout

The CLI can start a test process, but it cannot supply a database, queue, identity provider, or cloud account that the test assumes. Separate fast deterministic tests from integration tests, document prerequisites, and make the external endpoint and cleanup behavior explicit. A green local unit-test run is not evidence that an unavailable integration environment is healthy.

Security, cost, and trade-offs

  • Treat the build as code execution. A project can contain MSBuild targets, restore hooks, test setup, and package dependencies. Do not run an unknown checkout with production credentials or access to production endpoints.
  • Keep secrets out of diagnostics. dotnet --info is useful to attach to a bug report, but review output before sharing it. Never paste access tokens or private feed credentials into command history or logs.
  • Restore has a network and supply-chain boundary. Prefer the repository’s approved package sources and lock-file policy. A successful restore proves that dependencies resolved; it does not prove that every dependency is appropriate for the application.
  • Tests can have a real cost. A test suite may create cloud resources, call metered APIs, or leave data behind. Use a local or explicitly isolated test configuration for a preflight.
  • Release is a useful default, not a universal policy. It catches compilation issues in the configuration that is often deployed, while Debug can be the right choice for a development-only workflow.
  • Solution-wide validation is slower than project validation. It gives better coverage of project references and test projects, but a large repository may need targeted feedback first and the full solution check in CI.
  • Skipping restore and build saves time but spends certainty. Keep --no-restore and --no-build when the preceding steps are part of the same run; remove them when you are diagnosing a stale or incomplete checkout.

This preflight proves that a selected SDK can resolve, compile, test, and format the selected scope. It does not prove security, performance, deployment, or production readiness.

What I verified

On 2026-09-07, I exercised the sample with .NET SDK 10.0.400:

  • dotnet sln CliPreflight.slnx list found the library and test projects.
  • dotnet restore CliPreflight.slnx completed successfully.
  • dotnet build CliPreflight.slnx --configuration Release --no-restore completed with zero warnings and zero errors.
  • dotnet test CliPreflight.slnx --configuration Release --no-build ran one test, which passed.
  • dotnet format CliPreflight.slnx --no-restore --verify-no-changes completed without reporting a formatting change.

Those are results from this small sample, not a claim about an arbitrary repository. Run the same sequence against the checkout you intend to change.

Primary documentation

The version-sensitive command behavior and options are documented by Microsoft:

The CLI is not a replacement for understanding the repository. It is a way to make the first questions—“which SDK?”, “what do I build?”, and “what already passes?”—answerable before an IDE adds another layer of state.