Solutions and .slnx

This section documents MSBuild 18.9, the .NET SDK 10.0.402 (with its dotnet CLI) and NuGet 7.9, as published in the MSBuild, .NET and NuGet documentation on learn.microsoft.com, which is the reference these pages are written and verified against. Material about .NET 11 / NuGet 8.0 is still in preview and is always flagged as such — never presented as baseline.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The C# Reference bibliography lists the reference material consulted while preparing these pages.

A solution groups projects so that tools can build, test and open them together. It is not an MSBuild project: MSBuild translates it into one in memory (a metaproject) and builds every project it lists. Projects do not need a solution — dotnet build src/Ledgerline works on its own, and ProjectReference items define the real dependency graph — but IDEs open solutions, CI builds them, and the repository root is easier to work with when one file lists everything. This page uses .NET SDK 10.0.402 and the Ledgerline repository. The introduction is in Namespaces, Assemblies and Projects — Solutions and global.json.

.sln and .slnx

The .sln format dates from Visual Studio .NET: a line-oriented text file with project-type GUIDs, a GUID per project and a Global section that maps every solution configuration and platform to every project. It is hard to read and merge. .slnx is its XML replacement: only paths, solution folders and the configurations that differ from the defaults. MSBuild builds .slnx since 17.12, and since .NET 10 dotnet new sln creates .slnx (in .NET 9 and earlier it created .sln). Ledgerline uses it:

<Solution>
  <Folder Name="/samples/">
    <Project Path="samples/Ledgerline.Report/Ledgerline.Report.csproj" />
  </Folder>
  <Folder Name="/src/">
    <Project Path="src/Ledgerline.Build/Ledgerline.Build.csproj" />
    <Project Path="src/Ledgerline.Cli/Ledgerline.Cli.csproj" />
    <Project Path="src/Ledgerline.Templates/Ledgerline.Templates.csproj" />
    <Project Path="src/Ledgerline/Ledgerline.csproj" />
  </Folder>
  <Folder Name="/tests/">
    <Project Path="tests/Ledgerline.Tests/Ledgerline.Tests.csproj" />
  </Folder>
</Solution>

The same two projects in the classic format, created with dotnet new sln --format sln (only the start of the file):

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ledgerline", "src\Ledgerline\Ledgerline.csproj", "{1375A7E0-9D8E-481E-A5E2-B3BE15F9BECF}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ledgerline.Cli", "src\Ledgerline.Cli\Ledgerline.Cli.csproj", "{C8B61180-0FEC-43D6-8183-EB92B53187EB}"
EndProject
Global
…

Check that every tool in the team’s chain opens .slnx before switching (current Visual Studio, Rider and VS Code C# Dev Kit releases do); scripts and CI templates that parse .sln text themselves need updating.

dotnet sln

dotnet sln [<SOLUTION_FILE>] <command> edits a solution. Without a file argument it uses the single .sln, .slnx or .slnf in the current folder and fails (or prompts) when there are several.

Command What it does

dotnet new sln [-n <name>] [-o <dir>] [--format sln|slnx]

Creates a solution (.slnx by default since .NET 10).

dotnet sln add <PROJECT_PATH>... [--in-root] [-s|--solution-folder <PATH>]

Adds projects. By default, folders in the project path become solution folders (src/Ledgerline → /src/); --in-root puts the project at the root, -s names the folder. Shell globs work: dotnet sln add src/**/*.csproj.

dotnet sln remove <PROJECT_PATH|PROJECT_NAME>...

Removes projects, by path or by name (without the extension).

dotnet sln list

Lists the projects (works on .slnf since SDK 9.0.3xx).

dotnet sln migrate

Writes an .slnx next to an .sln with the same content. Fails if the .slnx already exists.

dotnet sln list
Project(s)
----------
samples/Ledgerline.Report/Ledgerline.Report.csproj
src/Ledgerline.Build/Ledgerline.Build.csproj
src/Ledgerline.Cli/Ledgerline.Cli.csproj
src/Ledgerline.Templates/Ledgerline.Templates.csproj
src/Ledgerline/Ledgerline.csproj
tests/Ledgerline.Tests/Ledgerline.Tests.csproj

Migrating an existing .sln is one command; the generated .slnx keeps the folders and records the platforms the old file declared:

dotnet sln Legacy.sln migrate
cat Legacy.slnx
.slnx file ~/ledgerline/mig/Legacy.slnx generated.
<Solution>
  <Configurations>
    <Platform Name="Any CPU" />
    <Platform Name="x64" />
    <Platform Name="x86" />
  </Configurations>
  <Folder Name="/src/">
    <Project Path="../src/Ledgerline.Cli/Ledgerline.Cli.csproj" />
    <Project Path="../src/Ledgerline/Ledgerline.csproj" />
  </Folder>
</Solution>

After migrating, delete the .sln (or keep it only while some tool still needs it): two solution files in the root make every bare dotnet build fail, see MSB1011: more than one project or solution file.

The noun-first commands that edit projects (dotnet reference add, dotnet package add) are on Solutions, References and Packages.

Solution filters (.slnf)

A solution filter is a JSON file that names a solution and a subset of its projects. Visual Studio loads only those projects, and MSBuild builds only those, which keeps large repositories fast to open:

{
  "solution": {
    "path": "Ledgerline.slnx",
    "projects": [
      "src/Ledgerline/Ledgerline.csproj",
      "tests/Ledgerline.Tests/Ledgerline.Tests.csproj"
    ]
  }
}
dotnet sln Ledgerline.Libraries.slnf list
dotnet build Ledgerline.Libraries.slnf -tl:off -v:m
Project(s)
----------
src/Ledgerline/Ledgerline.csproj
tests/Ledgerline.Tests/Ledgerline.Tests.csproj
  Ledgerline -> ~/ledgerline/src/Ledgerline/bin/Debug/net10.0/Ledgerline.dll
  Ledgerline -> ~/ledgerline/src/Ledgerline/bin/Debug/net8.0/Ledgerline.dll
  Ledgerline -> ~/ledgerline/src/Ledgerline/bin/Debug/netstandard2.0/Ledgerline.dll
  Ledgerline.Tests -> ~/ledgerline/tests/Ledgerline.Tests/bin/Debug/net10.0/Ledgerline.Tests.dll
Build succeeded.

Visual Studio writes the paths with backslashes, which JSON requires to be doubled (src\\Ledgerline\\Ledgerline.csproj); forward slashes, as above, work on every platform. MSBuild "automatically follows dependencies": a referenced project that is not in the filter is still built through the ProjectReference. CLI support is partial on the baseline: dotnet sln list reads .slnf since SDK 9.0.3xx, but creating a filter (dotnet new slnf) and adding or removing projects in one (dotnet sln <file>.slnf add|remove) arrive only in .NET 11, which is a preview; with SDK 10.0.402 the file is written by hand or by Visual Studio.

How MSBuild builds a solution

dotnet build Ledgerline.slnx (or msbuild Ledgerline.slnx) parses the solution, generates an in-memory metaproject with one MSBuild task call per project and the requested target (Build, Clean, Restore, Pack…), and builds it. Directory.Build.props and Directory.Build.targets are not imported into the metaproject; the solution-level equivalents are:

  • Directory.Solution.props and Directory.Solution.targets, found by walking up from the solution file;

  • before.<solution>.sln.targets / after.<solution>.sln.targets next to the solution (for .slnx, the before.<solution>.slnx.targets names work since MSBuild 17.14);

  • ImportDirectorySolutionProps/ImportDirectorySolutionTargets to switch the lookup off and DirectorySolutionPropsPath/DirectorySolutionTargetsPath to point elsewhere.

<Project>
  <Target Name="LedgerlineSolutionSummary" AfterTargets="Build">
    <Message Importance="high" Text="Solution $(SolutionName) built in configuration $(Configuration)." />
  </Target>
</Project>
dotnet build Ledgerline.slnx -tl:off -v:m | grep Solution
dotnet build Ledgerline.Libraries.slnf -tl:off -v:m | grep Solution
  Solution Ledgerline built in configuration Debug.
  Solution Ledgerline built in configuration Debug.

The filter build runs the same metaproject (with fewer projects), so $(SolutionName) is the underlying solution’s. These hooks apply only to command-line builds; Visual Studio does not build a metaproject. The documentation therefore recommends putting customisation in Directory.Build.props/.targets, which every project imports wherever it is built from.

MSB1011: more than one project or solution file

When dotnet build (or msbuild) gets no file argument, it looks in the current folder for exactly one project or solution file. Leaving an old .sln next to the new .slnx is the usual cause of:

With Legacy.sln created next to Ledgerline.slnx:

dotnet build -tl:off
MSBUILD : error MSB1011: Specify which project or solution file to use because this folder contains more than one project or solution file.

Name the file (dotnet build Ledgerline.slnx) or remove the extra one. The rules have exceptions: a solution next to a project of the same name, or next to several projects, is chosen without an error; a solution next to a single project with a different name is an error. Behaviour beyond the documented cases is easy to trip over — with a Journals.csproj and a batching.proj in one folder, MSBuild 18.9 silently picked batching.proj — so name the file in scripts.

Cross-tool equivalents

Task Equivalents

Create a solution

dotnet new sln; Visual Studio File > New > Project > Blank Solution

Convert .sln to .slnx

dotnet sln migrate; Visual Studio File > Save Solution As… with the .slnx type

Build a subset

dotnet build x.slnf, msbuild x.slnf; Visual Studio Unload project/Save As Solution Filter

Solution-level hook

Directory.Solution.targets (command line only); Visual Studio has none

Common mistakes

  • Two solution files (or a solution and a project) in the same folder — MSB1011 for every argument-less command.

  • Expecting Directory.Build.props to affect the solution build — it affects the projects, not the metaproject; use Directory.Solution.props.

  • Relying on solution-level build order or dependencies — they only exist in solution builds; declare ProjectReference items instead, so a single project builds correctly too.

  • Parsing .sln text in scripts — breaks after migrating; use dotnet sln list.

Release notes

  • MSBuild 17.12: .slnx solutions build from the command line.

  • SDK 9.0.3xx: dotnet sln list accepts .slnf.

  • MSBuild 17.14: before.<name>.slnx.targets/after.<name>.slnx.targets.

  • .NET 10: dotnet new sln creates .slnx by default.

  • .NET 11 (preview): dotnet new slnf; dotnet sln add/remove on solution filters.